БЕЗОПАСНОСТЬ - NODEDC LAUNCHER: скрыть email публичного инвайта
This commit is contained in:
parent
d15e90b9b5
commit
90249208b8
|
|
@ -1171,7 +1171,6 @@ function isInviteExpired(invite) {
|
|||
function toPublicInvite(invite) {
|
||||
return {
|
||||
id: invite.id,
|
||||
email: invite.email,
|
||||
role: invite.role,
|
||||
expiresAt: invite.expiresAt,
|
||||
status: isInviteExpired(invite) && invite.status !== "accepted" && invite.status !== "revoked" ? "expired" : invite.status,
|
||||
|
|
@ -1190,11 +1189,16 @@ function applyInviteRegistration(data, token, payload, { commit, provisioning =
|
|||
const invite = findInviteByToken(data, token);
|
||||
const client = findById(data.clients, invite.clientId, "client");
|
||||
const now = isoNow();
|
||||
const requestedEmail = normalizeInviteRegistrationEmail(payload?.email);
|
||||
const email = invite.email.toLowerCase();
|
||||
const name = optionalString(payload?.name, email.split("@")[0]);
|
||||
const name = optionalString(payload?.name, requestedEmail.split("@")[0]);
|
||||
|
||||
validateInviteCanBeRegistered(invite);
|
||||
|
||||
if (!requestedEmail || requestedEmail !== email) {
|
||||
throw new Error("Для этой почты нет активного инвайта");
|
||||
}
|
||||
|
||||
let user = data.users.find((item) => item.email.toLowerCase() === email);
|
||||
|
||||
if (user?.authentikUserId && !provisioning) {
|
||||
|
|
@ -1260,6 +1264,10 @@ function applyInviteRegistration(data, token, payload, { commit, provisioning =
|
|||
return { invite, client, user, membership, data };
|
||||
}
|
||||
|
||||
function normalizeInviteRegistrationEmail(value) {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
function validateInviteCanBeRegistered(invite) {
|
||||
if (invite.status === "accepted") {
|
||||
throw new Error("Инвайт уже принят");
|
||||
|
|
|
|||
|
|
@ -830,20 +830,34 @@ function sanitizeNewPassword(value) {
|
|||
}
|
||||
|
||||
function sanitizeInviteRegistrationPayload(payload) {
|
||||
const email = typeof payload?.email === "string" ? payload.email.trim().toLowerCase() : "";
|
||||
const name = typeof payload?.name === "string" ? payload.name.trim() : "";
|
||||
|
||||
if (!isValidInviteRegistrationEmail(email)) {
|
||||
throw new Error("Введите почту, на которую выписан инвайт");
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new Error("Введите имя");
|
||||
}
|
||||
|
||||
return {
|
||||
email,
|
||||
name: name.slice(0, 120),
|
||||
password: sanitizeNewPassword(payload?.password),
|
||||
};
|
||||
}
|
||||
|
||||
function isValidInviteRegistrationEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
function sendInviteApiError(res, error) {
|
||||
const message = error instanceof Error ? error.message : "Инвайт недоступен";
|
||||
const status =
|
||||
message.includes("не найден")
|
||||
? 404
|
||||
: message.includes("другую почту")
|
||||
: message.includes("другую почту") || message.includes("нет активного инвайта")
|
||||
? 403
|
||||
: message.includes("истёк") || message.includes("отозван")
|
||||
? 410
|
||||
|
|
|
|||
|
|
@ -464,20 +464,22 @@ export function LauncherApp() {
|
|||
}
|
||||
|
||||
async function handleRegisterInvite(command: RegisterInviteCommand) {
|
||||
if (!inviteToken || inviteFlow?.status !== "ready") return;
|
||||
if (!inviteToken || !inviteFlow || !("payload" in inviteFlow) || !inviteFlow.payload || (inviteFlow.status !== "ready" && inviteFlow.status !== "error")) return;
|
||||
|
||||
setInviteFlow({ status: "registering", payload: inviteFlow.payload });
|
||||
const payload = inviteFlow.payload;
|
||||
|
||||
setInviteFlow({ status: "registering", payload });
|
||||
|
||||
try {
|
||||
const result = await registerInvite(inviteToken, command);
|
||||
|
||||
setData(syncLauncherServiceLinks(result.data));
|
||||
setInviteFlow({ status: "registered", payload: inviteFlow.payload, loginUrl: result.loginUrl });
|
||||
setInviteFlow({ status: "registered", payload, loginUrl: result.loginUrl });
|
||||
} catch (error) {
|
||||
setInviteFlow({
|
||||
status: "error",
|
||||
message: error instanceof Error ? error.message : "Не удалось зарегистрироваться по инвайту",
|
||||
payload: inviteFlow.payload,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -599,7 +601,6 @@ export function LauncherApp() {
|
|||
return (
|
||||
<InviteFlowScreen
|
||||
state={inviteFlow ?? { status: "loading" }}
|
||||
currentEmail={authSession?.authenticated ? authSession.user.email : null}
|
||||
isAuthenticated={Boolean(authSession?.authenticated)}
|
||||
onAccept={() => void handleAcceptInvite()}
|
||||
onRegister={(command) => void handleRegisterInvite(command)}
|
||||
|
|
@ -767,7 +768,6 @@ function resolveDefaultClientId(data: LauncherData, userId: string, requestedCli
|
|||
|
||||
function InviteFlowScreen({
|
||||
state,
|
||||
currentEmail,
|
||||
isAuthenticated,
|
||||
onAccept,
|
||||
onRegister,
|
||||
|
|
@ -776,7 +776,6 @@ function InviteFlowScreen({
|
|||
onGoHome,
|
||||
}: {
|
||||
state: InviteFlowState;
|
||||
currentEmail: string | null;
|
||||
isAuthenticated: boolean;
|
||||
onAccept: () => void;
|
||||
onRegister: (command: RegisterInviteCommand) => void;
|
||||
|
|
@ -784,30 +783,36 @@ function InviteFlowScreen({
|
|||
onSwitchAccount: () => void;
|
||||
onGoHome: () => void;
|
||||
}) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||
const payload = "payload" in state ? state.payload : undefined;
|
||||
const defaultInviteName = payload ? payload.invite.email.split("@")[0] : "";
|
||||
const emailMismatch = payload && currentEmail ? payload.invite.email.toLowerCase() !== currentEmail.toLowerCase() : false;
|
||||
const inviteStatus = payload?.invite.status;
|
||||
const isAccepting = state.status === "accepting";
|
||||
const isRegistering = state.status === "registering";
|
||||
const requiresAccountSwitch = state.status === "error" && state.message.includes("другую почту");
|
||||
const canAccept = Boolean(
|
||||
state.status === "ready" &&
|
||||
isAuthenticated &&
|
||||
!emailMismatch &&
|
||||
inviteStatus !== "accepted" &&
|
||||
inviteStatus !== "expired" &&
|
||||
inviteStatus !== "revoked"
|
||||
);
|
||||
const isTerminalInvite = inviteStatus === "accepted" || inviteStatus === "expired" || inviteStatus === "revoked";
|
||||
const passwordMismatch = Boolean(passwordConfirm && password !== passwordConfirm);
|
||||
const canRegister = Boolean(
|
||||
const canShowRegistrationForm = Boolean(
|
||||
payload &&
|
||||
state.status === "ready" &&
|
||||
!isAuthenticated &&
|
||||
!isTerminalInvite &&
|
||||
(state.status === "ready" || state.status === "registering" || state.status === "error")
|
||||
);
|
||||
const passwordMismatch = Boolean(passwordConfirm && password !== passwordConfirm);
|
||||
const normalizedEmail = email.trim();
|
||||
const canRegister = Boolean(
|
||||
canShowRegistrationForm &&
|
||||
state.status !== "registering" &&
|
||||
normalizedEmail.includes("@") &&
|
||||
name.trim() &&
|
||||
password.length >= 8 &&
|
||||
password === passwordConfirm
|
||||
);
|
||||
|
|
@ -815,16 +820,9 @@ function InviteFlowScreen({
|
|||
? [
|
||||
`Рабочая область: ${payload.client.name}`,
|
||||
`Роль: ${membershipRoleLabel(payload.invite.role)}`,
|
||||
`Почта инвайта: ${payload.invite.email}`,
|
||||
]
|
||||
: ["Проверяем приглашение и платформенную сессию"];
|
||||
const statusMessage = resolveInviteStatusMessage(state, Boolean(emailMismatch), isAuthenticated, inviteStatus);
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultInviteName || name) return;
|
||||
|
||||
setName(defaultInviteName);
|
||||
}, [defaultInviteName, name]);
|
||||
const statusMessage = resolveInviteStatusMessage(state, isAuthenticated, inviteStatus);
|
||||
|
||||
return (
|
||||
<div className="launcher-app nodedc-auth-page">
|
||||
|
|
@ -846,15 +844,25 @@ function InviteFlowScreen({
|
|||
{state.status === "error" ? <p className="nodedc-auth-card__status">{state.message}</p> : null}
|
||||
{passwordMismatch ? <p className="nodedc-auth-card__status">Пароли не совпадают.</p> : null}
|
||||
|
||||
{payload && !isAuthenticated && (state.status === "ready" || state.status === "registering") && !isTerminalInvite ? (
|
||||
{canShowRegistrationForm ? (
|
||||
<form
|
||||
className="nodedc-auth-card__form"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (!canRegister) return;
|
||||
onRegister({ name: name.trim() || defaultInviteName, password });
|
||||
onRegister({ email: normalizedEmail, name: name.trim(), password });
|
||||
}}
|
||||
>
|
||||
<label className="nodedc-auth-card__field">
|
||||
<span>Эл. почта</span>
|
||||
<input
|
||||
value={email}
|
||||
type="email"
|
||||
placeholder="email@company.ru"
|
||||
autoComplete="email"
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="nodedc-auth-card__field">
|
||||
<span>Имя</span>
|
||||
<input value={name} placeholder="Ваше имя" autoComplete="name" onChange={(event) => setName(event.target.value)} />
|
||||
|
|
@ -886,7 +894,7 @@ function InviteFlowScreen({
|
|||
Уже есть аккаунт
|
||||
</button>
|
||||
</form>
|
||||
) : emailMismatch ? (
|
||||
) : requiresAccountSwitch ? (
|
||||
<button className="button button--primary" type="button" onClick={onSwitchAccount}>
|
||||
Сменить аккаунт
|
||||
</button>
|
||||
|
|
@ -919,7 +927,7 @@ function NodeDcAuthBrandHeader() {
|
|||
);
|
||||
}
|
||||
|
||||
function resolveInviteStatusMessage(state: InviteFlowState, emailMismatch: boolean, isAuthenticated: boolean, inviteStatus?: Invite["status"]) {
|
||||
function resolveInviteStatusMessage(state: InviteFlowState, isAuthenticated: boolean, inviteStatus?: Invite["status"]) {
|
||||
if (state.status === "loading") return "Проверяем приглашение.";
|
||||
if (state.status === "accepting") return "Подключаем доступ к рабочей области.";
|
||||
if (state.status === "registering") return "Создаём аккаунт и подключаем доступ.";
|
||||
|
|
@ -927,8 +935,7 @@ function resolveInviteStatusMessage(state: InviteFlowState, emailMismatch: boole
|
|||
if (state.status === "accepted" || inviteStatus === "accepted") return "Доступ уже подключён.";
|
||||
if (inviteStatus === "expired") return "Срок действия приглашения истёк.";
|
||||
if (inviteStatus === "revoked") return "Приглашение отозвано.";
|
||||
if (emailMismatch) return "Нужно войти под почтой, на которую выписан инвайт.";
|
||||
if (!isAuthenticated) return "Создайте аккаунт для почты, на которую выписан инвайт.";
|
||||
if (!isAuthenticated) return "Введите почту, имя и пароль для регистрации по приглашению.";
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { Invite } from "../../entities/invite/types";
|
|||
import type { LauncherData } from "./mockApi";
|
||||
|
||||
export interface PublicInviteResponse {
|
||||
invite: Pick<Invite, "id" | "email" | "role" | "expiresAt" | "status">;
|
||||
invite: Pick<Invite, "id" | "role" | "expiresAt" | "status">;
|
||||
client: Pick<Client, "id" | "name" | "status">;
|
||||
}
|
||||
|
||||
|
|
@ -17,6 +17,7 @@ export interface AcceptInviteResponse {
|
|||
}
|
||||
|
||||
export interface RegisterInviteCommand {
|
||||
email: string;
|
||||
name: string;
|
||||
password: string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue