Add authenticated login and SSO protection

This commit is contained in:
inman
2026-05-29 15:54:13 +08:00
parent e36f28a668
commit 0648874801
50 changed files with 1853 additions and 63 deletions

View File

@@ -0,0 +1,142 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import type { FormEvent } from "react";
import Image from "next/image";
import { Loader2, LogIn, RefreshCw } from "lucide-react";
import { pulseFeedback, revealChildren, runScopedMotion } from "@/lib/ui/motion";
export function AuthLoginPanel({
next,
configured,
message,
missing
}: {
next: string;
configured: boolean;
message?: string | null;
missing?: string[];
}) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [code, setCode] = useState("");
const [randomStr, setRandomStr] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const screenRef = useRef<HTMLDivElement | null>(null);
const feedbackRef = useRef<HTMLDivElement | null>(null);
const hasMissingConfig = !configured && Boolean(missing?.length);
useEffect(() => {
refreshCaptcha();
}, []);
useEffect(() => {
return runScopedMotion(screenRef, (scope) => revealChildren(scope));
}, []);
useEffect(() => {
pulseFeedback(feedbackRef.current);
}, [error, message, missing?.join(",")]);
const captchaSrc = useMemo(() => {
if (!randomStr) return "";
return `/api/auth/captcha?randomStr=${encodeURIComponent(randomStr)}`;
}, [randomStr]);
function refreshCaptcha() {
setCode("");
setRandomStr(crypto.randomUUID());
}
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!configured || submitting) return;
setSubmitting(true);
setError(null);
try {
const response = await fetch("/api/auth/password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password, code, randomStr, next })
});
const payload = await response.json();
if (!response.ok) throw new Error(payload.error || "登录失败");
window.location.assign(payload.redirectTo || next || "/create");
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
refreshCaptcha();
} finally {
setSubmitting(false);
}
}
return (
<div className="auth-page" ref={screenRef}>
<section className="auth-brand-panel" aria-label="智念AIGC平台" data-animate>
<Image className="auth-brand-logo" src="/logo/zhinian-logo.png" alt="" width={168} height={38} priority />
<h1>AIGC平台</h1>
</section>
<section className="panel auth-panel" data-animate>
<h2></h2>
{message || hasMissingConfig || error ? (
<div ref={feedbackRef}>
{message ? <div className="callout" role="alert">{message}</div> : null}
{hasMissingConfig ? (
<div className="callout" role="alert">
{missing?.join("、")}
</div>
) : null}
{error ? <div className="callout" role="alert">{error}</div> : null}
</div>
) : null}
<form className="auth-password-form" onSubmit={submit}>
<label className="field" data-animate>
<span></span>
<input
autoComplete="username"
value={username}
onChange={(event) => setUsername(event.target.value)}
disabled={!configured || submitting}
placeholder="请输入账号"
/>
</label>
<label className="field" data-animate>
<span></span>
<input
autoComplete="current-password"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
disabled={!configured || submitting}
placeholder="请输入密码"
/>
</label>
<label className="field" data-animate>
<span></span>
<div className="auth-captcha-row">
<input
autoComplete="off"
value={code}
onChange={(event) => setCode(event.target.value)}
disabled={!configured || submitting}
placeholder="请输入验证码"
/>
<button className="auth-captcha-button" type="button" onClick={refreshCaptcha} disabled={!configured || submitting} aria-label="刷新验证码" title="刷新验证码">
{captchaSrc ? <img src={captchaSrc} alt="验证码" /> : <RefreshCw size={18} />}
</button>
</div>
</label>
<button className="button primary auth-submit" type="submit" disabled={!configured || submitting || !username.trim() || !password || !code.trim()} data-animate>
{submitting ? <Loader2 className="spin" size={18} /> : <LogIn size={18} />}
</button>
</form>
</section>
</div>
);
}