import { registrationNotice } from "@dada/shared-contracts"; import { useEffect, useId, useRef, useState, type FormEvent, type KeyboardEvent } from "react"; import "./user-auth.css"; type AuthMode = "login" | "register"; type SendState = "idle" | "sending" | "sent" | "error"; interface ErrorEnvelopeBody { error?: { details?: { field_errors?: Array<{ message_key?: string }> }; message_key?: string; }; } const messageByKey: Record = { "auth.account.suspended": "账号已暂停,请联系管理员。", "auth.challenge.expired": "验证码已过期,请重新获取。", "auth.challenge.invalid": "验证码不正确,请检查后重试。", "auth.challenge.resend_too_soon": "请等待倒计时结束后重新获取验证码。", "auth.challenge.too_many_attempts": "尝试次数过多,请稍后再试。", "auth.login.admin_required": "此邮箱需从管理员登录入口进入。", "auth.login.registration_required": "该邮箱尚未注册,请切换到注册。", "auth.email.already_registered": "该邮箱已注册,请切换到登录。", "auth.invite.disabled": "邀请码已停用,请更换邀请码。", "auth.invite.exhausted": "邀请码使用次数已耗尽,请更换邀请码。", "auth.invite.expired": "邀请码已过期,请更换邀请码。", "auth.invite.not_found": "邀请码无效,请检查后重试。", "auth.privacy.consent_required": "请阅读并同意《内测使用与隐私告知》。", "auth.privacy.notice_version_invalid": "告知版本已更新,请重新阅读后同意。", "auth.profile.invalid": "请检查创作署名和社交 ID。", "auth.registration.login_required": "该邮箱已注册,请切换到登录。", "auth.registration.stage_limit_reached": "本轮内测名额已满,请返回登录。", "auth.service.unavailable": "邮件服务暂时不可用,请稍后重试。", }; function errorMessage(body: ErrorEnvelopeBody) { const key = body.error?.details?.field_errors?.[0]?.message_key ?? body.error?.message_key; return key ? (messageByKey[key] ?? "请求未完成,请检查后重试。") : "请求未完成,请检查后重试。"; } function maskedEmail(email: string) { const [local = "", domain = ""] = email.split("@", 2); const visible = local.slice(0, Math.min(2, local.length)); return `${visible}${"*".repeat(Math.max(3, local.length - visible.length))}@${domain}`; } function maskedRegistrationEmail(email: string) { const [local = "", domain = ""] = email.split("@", 2); return `${local.slice(0, 1)}***@${domain.slice(0, 1)}***`; } function maskedInvite(inviteCode: string) { return `••••${inviteCode.slice(-3)}`; } export function UserAuthPage() { const emailId = useId(); const codeId = useId(); const inviteId = useId(); const creatorNameId = useId(); const socialId = useId(); const loginTab = useRef(null); const registerTab = useRef(null); const noticeButton = useRef(null); const noticeCloseButton = useRef(null); const noticeDialog = useRef(null); const [mode, setMode] = useState("login"); const [email, setEmail] = useState(""); const [code, setCode] = useState(""); const [inviteCode, setInviteCode] = useState(""); const [creatorName, setCreatorName] = useState(""); const [socialHandle, setSocialHandle] = useState(""); const [privacyConsentAccepted, setPrivacyConsentAccepted] = useState(false); const [noticeOpen, setNoticeOpen] = useState(false); const [registrationId, setRegistrationId] = useState(); const [sendState, setSendState] = useState("idle"); const [countdown, setCountdown] = useState(0); const [error, setError] = useState(); const [localTestAvailable, setLocalTestAvailable] = useState(false); const [localTestError, setLocalTestError] = useState(); const [localTestSubmitting, setLocalTestSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false); const emailValid = /^[^@\s]+@[^@\s]+$/.test(email); const registrationReady = Boolean( registrationId && /^[0-9]{6}$/.test(code) && creatorName.trim() && socialHandle.trim() && privacyConsentAccepted, ); useEffect(() => { if (countdown <= 0) return; const timer = window.setInterval(() => setCountdown((value) => Math.max(0, value - 1)), 1_000); return () => window.clearInterval(timer); }, [countdown]); useEffect(() => { const controller = new AbortController(); void fetch("/api/v1/auth/local-test", { credentials: "same-origin", signal: controller.signal }) .then(async (response) => { if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return; const body = await response.json() as { available?: boolean }; if (body.available === true) setLocalTestAvailable(true); }) .catch(() => undefined); return () => controller.abort(); }, []); useEffect(() => { if (!noticeOpen) return; const previousOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; noticeCloseButton.current?.focus(); return () => { document.body.style.overflow = previousOverflow; }; }, [noticeOpen]); function resetRegistrationDetails() { setCode(""); setCreatorName(""); setSocialHandle(""); setPrivacyConsentAccepted(false); setNoticeOpen(false); } function switchMode(nextMode: AuthMode) { if (nextMode === mode) return; setMode(nextMode); resetRegistrationDetails(); setInviteCode(""); setRegistrationId(undefined); setSendState("idle"); setCountdown(0); setError(undefined); } function modifyRegistrationEntry() { resetRegistrationDetails(); setRegistrationId(undefined); setSendState("idle"); setCountdown(0); setError(undefined); window.requestAnimationFrame(() => document.getElementById(inviteId)?.focus()); } function closeNotice() { setNoticeOpen(false); window.requestAnimationFrame(() => noticeButton.current?.focus()); } function handleNoticeKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { event.preventDefault(); closeNotice(); return; } if (event.key === "Tab") { const focusable = Array.from( noticeDialog.current?.querySelectorAll("button, [href], input, [tabindex]:not([tabindex='-1'])") ?? [], ).filter((element) => !element.hasAttribute("disabled")); const first = focusable[0]; const last = focusable.at(-1); if (!first || !last) return; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } } function switchTab(nextMode: AuthMode) { switchMode(nextMode); window.requestAnimationFrame(() => (nextMode === "login" ? loginTab : registerTab).current?.focus()); } async function sendCode() { if (!emailValid || sendState === "sending" || countdown > 0) return; setSendState("sending"); setError(undefined); const endpoint = mode === "login" ? "/api/v1/auth/login/send" : "/api/v1/auth/register/send"; const payload = mode === "login" ? { email } : { email, invite_code: inviteCode }; try { const response = await fetch(endpoint, { body: JSON.stringify(payload), credentials: "same-origin", headers: { "Content-Type": "application/json" }, method: "POST", }); const body = await response.json() as ErrorEnvelopeBody & { registration_id?: string }; if (!response.ok || !body.registration_id) { setSendState("error"); setError(errorMessage(body)); return; } setRegistrationId(body.registration_id); setSendState("sent"); setCountdown(60); } catch { setSendState("error"); setError(messageByKey["auth.service.unavailable"]); } } async function login(event: FormEvent) { event.preventDefault(); if (!registrationId || !/^[0-9]{6}$/.test(code) || submitting) return; setSubmitting(true); setError(undefined); try { const response = await fetch("/api/v1/auth/login/complete", { body: JSON.stringify({ registration_id: registrationId, verification_code: code }), credentials: "same-origin", headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", ""), }, method: "POST", }); const body = await response.json() as ErrorEnvelopeBody; if (!response.ok) { setError(errorMessage(body)); return; } window.location.assign("/app"); } catch { setError("登录请求未完成,请重试。"); } finally { setSubmitting(false); } } async function completeRegistration(event: FormEvent) { event.preventDefault(); if (!registrationReady || !registrationId || submitting) return; setSubmitting(true); setError(undefined); try { const response = await fetch("/api/v1/auth/register/complete", { body: JSON.stringify({ creator_name: creatorName, privacy_consent_accepted: privacyConsentAccepted, privacy_notice_version: registrationNotice.version, registration_id: registrationId, social_id: socialHandle, verification_code: code, }), credentials: "same-origin", headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", ""), }, method: "POST", }); const body = await response.json() as ErrorEnvelopeBody; if (!response.ok) { setError(errorMessage(body)); return; } window.location.assign("/app"); } catch { setError("注册请求未完成,请重试。"); } finally { setSubmitting(false); } } async function enterLocalTest() { if (localTestSubmitting) return; setLocalTestSubmitting(true); setLocalTestError(undefined); try { const response = await fetch("/api/v1/auth/local-test", { credentials: "same-origin", method: "POST", }); if (!response.ok) { setLocalTestError("本机测试会话未能建立,请重试。"); return; } window.location.assign("/app"); } catch { setLocalTestError("本机测试会话未能建立,请重试。"); } finally { setLocalTestSubmitting(false); } } return ( <>
DADA
LOCAL CREATIVE SYSTEM / WINDOWS P0-A 在本机展开你的创作
管理员登录
{localTestAvailable ? (
{localTestError ?

{localTestError}

: null}
) : null}
{mode === "login" ? (

邮箱验证码登录

setEmail(event.target.value)} placeholder="请输入邮箱" type="email" value={email} />
setCode(event.target.value.replace(/\D/g, ""))} placeholder="6 位验证码" value={code} />
{sendState === "sent" ? (

验证码已发送至 {maskedEmail(email)}

) : null} {error ?

{error}

: null}
) : (

{registrationId ? "完善注册资料" : "邀请码注册"}

{!registrationId ? ( <> setInviteCode(event.target.value)} value={inviteCode} /> setEmail(event.target.value)} placeholder="请输入邮箱" type="email" value={email} /> ) : ( <>
验证码已发送
邀请码 · 已验证 {maskedInvite(inviteCode)}
邮箱 · 已验证 {maskedRegistrationEmail(email)}
setCode(event.target.value.replace(/\D/g, ""))} placeholder="输入 6 位验证码" value={code} /> setCreatorName(event.target.value)} placeholder="成品中显示的名称" value={creatorName} /> setSocialHandle(event.target.value)} placeholder="成品中显示的账号文本" value={socialHandle} />
《内测使用与隐私告知》
{!privacyConsentAccepted ?

完成注册前必须阅读并勾选同意。

: null}
)} {error ?

{error}

: null}
)}
测试数据仅保存在本机,不自动备份,也不会迁移到正式系统。
{noticeOpen ? (

版本 {registrationNotice.version} · 生效日期 {registrationNotice.effectiveAt}

{registrationNotice.title}

{registrationNotice.sections.map((section) => (

{section.title}

{section.body}

))}
) : null} ); }