feat: 实现前端认证系统,WebSocket 连接携带 JWT token
- 新增 AuthContext/AuthProvider:登录/注册状态管理,JWT 自动刷新 - 新增 AuthPage 组件:登录/注册表单,支持模式切换和前端校验 - 新增 api.ts:封装 auth REST API 客户端(register/login/refresh/logout) - WebSocket 连接时拼接 ?token=<jwt>,重连自动携带 - useVisionSession 接受 accessToken 参数并透传 - App.tsx 包裹 AuthProvider,未登录时显示登录页 - storage.ts 新增 token/user 的 localStorage 存储 - i18n 新增中/英/日三语 auth 翻译 - App.css 新增 auth 页面和用户 badge 样式
This commit is contained in:
130
frontend/src/components/AuthPage/index.tsx
Normal file
130
frontend/src/components/AuthPage/index.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
// ============================================================
|
||||
// AuthPage — 登录 / 注册页面
|
||||
// 职责:用户认证表单,支持登录和注册模式切换
|
||||
// ============================================================
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useAuth } from "../../lib/auth";
|
||||
import { useI18n } from "../../lib/i18n";
|
||||
|
||||
type AuthMode = "login" | "register";
|
||||
|
||||
export function AuthPage() {
|
||||
const { login, register } = useAuth();
|
||||
const { t } = useI18n();
|
||||
const [mode, setMode] = useState<AuthMode>("login");
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
// 前端校验
|
||||
if (username.length < 3 || username.length > 64) {
|
||||
setError(t("auth.error.usernameLength"));
|
||||
return;
|
||||
}
|
||||
if (password.length < 8 || password.length > 72) {
|
||||
setError(t("auth.error.passwordLength"));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
const fn = mode === "login" ? login : register;
|
||||
const result = await fn(username, password);
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error);
|
||||
}
|
||||
},
|
||||
[username, password, mode, login, register, t]
|
||||
);
|
||||
|
||||
const toggleMode = useCallback(() => {
|
||||
setMode((m) => (m === "login" ? "register" : "login"));
|
||||
setError("");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="auth-page">
|
||||
<div className="auth-card">
|
||||
<div className="auth-card__header">
|
||||
<h1 className="auth-card__logo">CamTalk</h1>
|
||||
<p className="auth-card__subtitle">{t("auth.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<form className="auth-form" onSubmit={handleSubmit}>
|
||||
<div className="auth-form__tabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`auth-tab ${mode === "login" ? "auth-tab--active" : ""}`}
|
||||
onClick={() => { setMode("login"); setError(""); }}
|
||||
>
|
||||
{t("auth.login")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`auth-tab ${mode === "register" ? "auth-tab--active" : ""}`}
|
||||
onClick={() => { setMode("register"); setError(""); }}
|
||||
>
|
||||
{t("auth.register")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="auth-form__fields">
|
||||
<label className="auth-field">
|
||||
<span className="auth-field__label">{t("auth.username")}</span>
|
||||
<input
|
||||
type="text"
|
||||
className="auth-field__input"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t("auth.username.placeholder")}
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="auth-field">
|
||||
<span className="auth-field__label">{t("auth.password")}</span>
|
||||
<input
|
||||
type="password"
|
||||
className="auth-field__input"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t("auth.password.placeholder")}
|
||||
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error && <div className="auth-form__error">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="auth-form__submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting
|
||||
? t("auth.submitting")
|
||||
: mode === "login"
|
||||
? t("auth.login")
|
||||
: t("auth.register")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="auth-card__footer">
|
||||
{mode === "login" ? t("auth.noAccount") : t("auth.hasAccount")}
|
||||
<button className="auth-card__link" onClick={toggleMode}>
|
||||
{mode === "login" ? t("auth.register") : t("auth.login")}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export function useWebSocketManager() {
|
||||
return {
|
||||
status,
|
||||
lastMessage,
|
||||
connect: () => wsClient.connect(),
|
||||
connect: (token?: string) => wsClient.connect(token),
|
||||
disconnect: () => wsClient.disconnect(),
|
||||
send: wsClient.send.bind(wsClient),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user