feat(auth): 注册协议 - 增加同意校验并预留影策协议正文
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { type FormEvent, useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { App, Button, Divider, Input } from "antd";
|
||||
import { App, Button, Checkbox, Divider, Input, Modal } from "antd";
|
||||
import { ArrowRight, Info, LockKeyhole, Mail, ShieldCheck, TriangleAlert, UserRound } from "lucide-react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
|
||||
@@ -9,6 +9,9 @@ import { ApiError } from "@/services/api/request";
|
||||
|
||||
type AuthSettings = Awaited<ReturnType<typeof getAuthSettings>>;
|
||||
|
||||
// TODO: 由维护者填写正式的影策服务协议条款后再发布。
|
||||
const SERVICE_AGREEMENT: { title: string; content: string }[] = [];
|
||||
|
||||
export default function RegisterPage() {
|
||||
const navigate = useNavigate();
|
||||
const [params] = useSearchParams();
|
||||
@@ -20,6 +23,8 @@ export default function RegisterPage() {
|
||||
const [displayName, setDisplayName] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [agreementAccepted, setAgreementAccepted] = useState(false);
|
||||
const [agreementOpen, setAgreementOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [sendingCode, setSendingCode] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
@@ -68,6 +73,10 @@ export default function RegisterPage() {
|
||||
const submit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
if (registering.current || registerCountdown > 0) return;
|
||||
if (!agreementAccepted) {
|
||||
message.warning("请先同意影策服务协议");
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
message.error("两次输入的密码不一致");
|
||||
return;
|
||||
@@ -75,7 +84,7 @@ export default function RegisterPage() {
|
||||
registering.current = true;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await register({ username, email, emailCode, displayName, password });
|
||||
await register({ username, email, emailCode, displayName, password, acceptedTerms: agreementAccepted });
|
||||
const { applyUserSession } = await import("@/lib/user-session");
|
||||
await applyUserSession(await getAuthSession());
|
||||
if (!settings?.firstUser) window.sessionStorage.setItem("infinite-canvas:model-setup-guide", "1");
|
||||
@@ -189,7 +198,16 @@ export default function RegisterPage() {
|
||||
</AuthField>
|
||||
</div>
|
||||
|
||||
<Button type="primary" htmlType="submit" size="large" block loading={submitting} disabled={disabled || registerCountdown > 0} icon={<ArrowRight className="size-4" />} iconPlacement="end">
|
||||
<div className="flex items-start gap-2 text-xs leading-5 text-white/55">
|
||||
<Checkbox checked={agreementAccepted} onChange={(event) => setAgreementAccepted(event.target.checked)}>
|
||||
<span className="text-xs leading-5 text-white/55">我已阅读并同意</span>
|
||||
</Checkbox>
|
||||
<button type="button" className="-ml-1 text-xs leading-5 text-blue-300/85 transition hover:text-blue-200" onClick={() => setAgreementOpen(true)}>
|
||||
《影策服务协议》
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button type="primary" htmlType="submit" size="large" block loading={submitting} disabled={disabled || registerCountdown > 0 || !agreementAccepted} icon={<ArrowRight className="size-4" />} iconPlacement="end">
|
||||
{registerCountdown > 0 ? `${registerCountdown} 秒后可重试` : "创建账号"}
|
||||
</Button>
|
||||
{settings?.linuxdoEnabled ? (
|
||||
@@ -197,11 +215,22 @@ export default function RegisterPage() {
|
||||
<Divider plain className="!border-white/10 !text-white/30">
|
||||
或
|
||||
</Divider>
|
||||
<Button size="large" block icon={<LinuxDOIcon />} href={linuxDOLoginURL(next)}>
|
||||
<Button size="large" block disabled={!agreementAccepted} icon={<LinuxDOIcon />} href={agreementAccepted ? linuxDOLoginURL(next, true) : undefined}>
|
||||
使用 Linux.do 注册 / 登录
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Modal className="workspace-modal workspace-modal-compact" title="影策服务协议" open={agreementOpen} onCancel={() => setAgreementOpen(false)} footer={null} destroyOnHidden>
|
||||
<div className="max-h-96 space-y-4 overflow-y-auto pr-1 text-sm leading-6 text-foreground/68">
|
||||
{SERVICE_AGREEMENT.length === 0 ? <p className="m-0">服务协议内容待补充。</p> : null}
|
||||
{SERVICE_AGREEMENT.map((item) => (
|
||||
<section key={item.title}>
|
||||
<h3 className="m-0 text-sm font-semibold text-foreground">{item.title}</h3>
|
||||
<p className="mt-1.5 mb-0">{item.content}</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -371,9 +371,10 @@ export function getAuthSettings() {
|
||||
return http.get<{ firstUser: boolean; registrationEnabled: boolean; linuxdoEnabled: boolean; emailEnabled: boolean; emailCodeRequired: boolean }>("/auth/settings");
|
||||
}
|
||||
|
||||
export function linuxDOLoginURL(next: string) {
|
||||
export function linuxDOLoginURL(next: string, acceptedTerms?: boolean) {
|
||||
const base = String(apiBaseURL).replace(/\/$/, "");
|
||||
return `${base}/auth/linuxdo/start?next=${encodeURIComponent(next)}`;
|
||||
const termsQuery = acceptedTerms === undefined ? "" : `&acceptedTerms=${acceptedTerms ? "true" : "false"}`;
|
||||
return `${base}/auth/linuxdo/start?next=${encodeURIComponent(next)}${termsQuery}`;
|
||||
}
|
||||
|
||||
export function getAuthSession() {
|
||||
@@ -426,7 +427,7 @@ export function resetPassword(input: { email: string; emailCode: string; passwor
|
||||
return http.post<{ reset: boolean }>("/auth/password-reset", input);
|
||||
}
|
||||
|
||||
export function register(input: { username: string; email?: string; emailCode?: string; displayName?: string; password: string }) {
|
||||
export function register(input: { username: string; email?: string; emailCode?: string; displayName?: string; password: string; acceptedTerms: boolean }) {
|
||||
return http.post<{ user: LocalUser }>("/auth/register", input);
|
||||
}
|
||||
|
||||
|
||||
41
web/test/auth-registration-agreement.test.ts
Normal file
41
web/test/auth-registration-agreement.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
describe("注册服务协议", () => {
|
||||
test("确认密码后展示协议入口,默认未同意且保留注册限流", () => {
|
||||
const source = readFileSync(resolve(import.meta.dir, "../src/pages/auth/register.tsx"), "utf8");
|
||||
const passwordIndex = source.indexOf('label="确认密码"');
|
||||
const agreementIndex = source.indexOf("Checkbox checked={agreementAccepted}");
|
||||
const submitIndex = source.indexOf('htmlType="submit"');
|
||||
|
||||
expect(passwordIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(agreementIndex).toBeGreaterThan(passwordIndex);
|
||||
expect(submitIndex).toBeGreaterThan(agreementIndex);
|
||||
expect(source).toContain("[agreementAccepted, setAgreementAccepted] = useState(false)");
|
||||
expect(source).toContain("disabled={disabled || registerCountdown > 0 || !agreementAccepted}");
|
||||
expect(source).toContain("if (registering.current || registerCountdown > 0) return;");
|
||||
expect(source).toContain("disabled={!agreementAccepted}");
|
||||
});
|
||||
|
||||
test("普通注册与 Linux.do 注册传递同意状态,登录入口保留原合同", () => {
|
||||
const page = readFileSync(resolve(import.meta.dir, "../src/pages/auth/register.tsx"), "utf8");
|
||||
const login = readFileSync(resolve(import.meta.dir, "../src/pages/auth/login.tsx"), "utf8");
|
||||
const api = readFileSync(resolve(import.meta.dir, "../src/services/api/auth.ts"), "utf8");
|
||||
|
||||
expect(page).toContain("acceptedTerms: agreementAccepted");
|
||||
expect(page).toContain("agreementAccepted ? linuxDOLoginURL(next, true) : undefined");
|
||||
expect(page).toContain("请先同意影策服务协议");
|
||||
expect(login).toContain("linuxDOLoginURL(next)");
|
||||
expect(api).toContain("acceptedTerms: boolean");
|
||||
expect(api).toContain("acceptedTerms?: boolean");
|
||||
});
|
||||
|
||||
test("协议使用影策标题并提供正文承载区域", () => {
|
||||
const page = readFileSync(resolve(import.meta.dir, "../src/pages/auth/register.tsx"), "utf8");
|
||||
|
||||
expect(page).toContain('title="影策服务协议"');
|
||||
expect(page).toContain("SERVICE_AGREEMENT.map");
|
||||
expect(page).toContain("服务协议内容待补充。");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user