БЕЗОПАСНОСТЬ - NODEDC LAUNCHER: скрыть email публичного инвайта

This commit is contained in:
DCCONSTRUCTIONS
2026-05-05 17:50:32 +03:00
parent d15e90b9b5
commit 90249208b8
4 changed files with 61 additions and 31 deletions
+34 -27
View File
@@ -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;
}
+2 -1
View File
@@ -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;
}