feat(auth): add two-factor and password reset flow

This commit is contained in:
ZhenYi 2026-05-17 16:36:26 +08:00
parent bb9fe11869
commit 44944efd9b
12 changed files with 115 additions and 111 deletions

View File

@ -9,6 +9,7 @@ import { apiAuthCaptcha, apiUserRequestPasswordReset } from "@/client/api";
import type { ResetPasswordParams } from "@/client/model"; import type { ResetPasswordParams } from "@/client/model";
import { getCaptcha } from "@/lib/auth-crypto"; import { getCaptcha } from "@/lib/auth-crypto";
import { AUTH_FORM } from "@/css/auth/styles"; import { AUTH_FORM } from "@/css/auth/styles";
import { t } from "@/i18n/T";
export function ForgotPasswordPage() { export function ForgotPasswordPage() {
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
@ -24,7 +25,7 @@ export function ForgotPasswordPage() {
const result = await getCaptcha(apiAuthCaptcha, true); const result = await getCaptcha(apiAuthCaptcha, true);
setCaptchaImage(result.base64); setCaptchaImage(result.base64);
} catch { } catch {
setError("Failed to load captcha"); setError(t("auth.login.error.failed_to_load_captcha"));
} }
}; };
@ -38,7 +39,7 @@ export function ForgotPasswordPage() {
setSuccess(false); setSuccess(false);
if (data.captcha !== captchaText) { if (data.captcha !== captchaText) {
setError("Captcha verification failed"); setError(t("auth.forgot_password.captcha_failed"));
loadCaptcha(); loadCaptcha();
setLoading(false); setLoading(false);
return; return;
@ -50,7 +51,7 @@ export function ForgotPasswordPage() {
setSuccess(true); setSuccess(true);
} catch (err) { } catch (err) {
const apiErr = err as { response?: { status?: number; data?: { message?: string } } }; const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
setError(apiErr.response?.data?.message || "Failed to send reset email"); setError(apiErr.response?.data?.message || t("auth.forgot_password.send_failed"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -61,8 +62,8 @@ export function ForgotPasswordPage() {
<div className={AUTH_FORM.card}> <div className={AUTH_FORM.card}>
<div className={AUTH_FORM.cardBg}> <div className={AUTH_FORM.cardBg}>
<div className={AUTH_FORM.header}> <div className={AUTH_FORM.header}>
<h2 className={AUTH_FORM.title}>Reset your password</h2> <h2 className={AUTH_FORM.title}>{t("auth.forgot_password.title")}</h2>
<p className={AUTH_FORM.subtitle}>Enter your email and we'll send you a reset link</p> <p className={AUTH_FORM.subtitle}>{t("auth.forgot_password.subtitle")}</p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}> <form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}>
@ -75,23 +76,23 @@ export function ForgotPasswordPage() {
{success && ( {success && (
<Alert style={{ backgroundColor: "var(--surface-elevated)", borderColor: "var(--success)" }} className="text-[var(--success)] dark:text-[var(--success)]"> <Alert style={{ backgroundColor: "var(--surface-elevated)", borderColor: "var(--success)" }} className="text-[var(--success)] dark:text-[var(--success)]">
<AlertDescription> <AlertDescription>
If an account exists with that email, you will receive a password reset link shortly. {t("auth.forgot_password.success_message")}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="email" className={AUTH_FORM.inputLabel}> <Label htmlFor="email" className={AUTH_FORM.inputLabel}>
Email <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.register.email")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="email" id="email"
type="email" type="email"
{...register("email", { {...register("email", {
required: "Email is required", required: t("auth.register.email_required"),
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Invalid email address" } pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: t("auth.register.email_invalid") }
})} })}
placeholder="Enter your email" placeholder={t("auth.forgot_password.email_placeholder")}
disabled={loading || success} disabled={loading || success}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -102,13 +103,13 @@ export function ForgotPasswordPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="captcha" className={AUTH_FORM.inputLabel}> <Label htmlFor="captcha" className={AUTH_FORM.inputLabel}>
Verification <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.verification")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id="captcha" id="captcha"
{...register("captcha", { required: "Captcha is required" })} {...register("captcha", { required: t("auth.login.captcha_required") })}
placeholder="Enter code" placeholder={t("auth.login.captcha_placeholder")}
disabled={loading || success} disabled={loading || success}
className={AUTH_FORM.input} className={AUTH_FORM.input}
onChange={(e) => setCaptchaText(e.target.value)} onChange={(e) => setCaptchaText(e.target.value)}
@ -119,7 +120,7 @@ export function ForgotPasswordPage() {
alt="Captcha" alt="Captcha"
className={AUTH_FORM.captchaImg} className={AUTH_FORM.captchaImg}
onClick={loadCaptcha} onClick={loadCaptcha}
title="Click to refresh" title={t("auth.login.captcha_title")}
/> />
)} )}
</div> </div>
@ -133,12 +134,12 @@ export function ForgotPasswordPage() {
className={AUTH_FORM.submitBtn} className={AUTH_FORM.submitBtn}
disabled={loading || success} disabled={loading || success}
> >
{loading ? "Sending..." : "Send Reset Link"} {loading ? t("auth.forgot_password.sending") : t("auth.forgot_password.submit")}
</Button> </Button>
<div className={AUTH_FORM.linkTextCenter}> <div className={AUTH_FORM.linkTextCenter}>
<Link to="/auth/login" className={AUTH_FORM.link}> <Link to="/auth/login" className={AUTH_FORM.link}>
Back to login {t("auth.forgot_password.back_to_login")}
</Link> </Link>
</div> </div>
</form> </form>

View File

@ -10,6 +10,7 @@ import { useLoginMutation } from "@/hooks/useAuth";
import type { LoginParams } from "@/client/model"; import type { LoginParams } from "@/client/model";
import { getCaptcha, encryptPassword } from "@/lib/auth-crypto"; import { getCaptcha, encryptPassword } from "@/lib/auth-crypto";
import { AUTH_FORM } from "@/css/auth/styles"; import { AUTH_FORM } from "@/css/auth/styles";
import { t } from "@/i18n/T";
export function LoginPage() { export function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -28,7 +29,7 @@ export function LoginPage() {
setCaptchaImage(result.base64); setCaptchaImage(result.base64);
setPublicKey(result.publicKey || ""); setPublicKey(result.publicKey || "");
} catch { } catch {
setError("Failed to load captcha"); setError(t("auth.login.error.failed_to_load_captcha"));
} }
}; };
@ -56,11 +57,11 @@ export function LoginPage() {
const apiErr = err as { response?: { status?: number; data?: { message?: string } } }; const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
if (apiErr.response?.status === 428) { if (apiErr.response?.status === 428) {
setNeeds2FA(true); setNeeds2FA(true);
setError("Two-factor authentication required"); setError(t("auth.login.error.two_factor_required"));
} else if (apiErr.response?.status === 401) { } else if (apiErr.response?.status === 401) {
setError("Invalid username or password"); setError(t("auth.login.error.invalid_credentials"));
} else { } else {
setError(apiErr.response?.data?.message || "Login failed"); setError(apiErr.response?.data?.message || t("auth.login.error.login_failed"));
} }
loadCaptcha(); loadCaptcha();
} }
@ -73,8 +74,8 @@ export function LoginPage() {
<div className={AUTH_FORM.card}> <div className={AUTH_FORM.card}>
<div className={AUTH_FORM.cardBg}> <div className={AUTH_FORM.cardBg}>
<div className={AUTH_FORM.header}> <div className={AUTH_FORM.header}>
<h2 className={AUTH_FORM.title}>Welcome back!</h2> <h2 className={AUTH_FORM.title}>{t("auth.login.title")}</h2>
<p className={AUTH_FORM.subtitle}>We're so excited to see you again!</p> <p className={AUTH_FORM.subtitle}>{t("auth.login.subtitle")}</p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}> <form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}>
@ -86,12 +87,12 @@ export function LoginPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="username" className={AUTH_FORM.inputLabel}> <Label htmlFor="username" className={AUTH_FORM.inputLabel}>
Account <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.account")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="username" id="username"
{...register("username", { required: "Username is required" })} {...register("username", { required: t("auth.login.account_required") || "Username is required" })}
placeholder="Username or email" placeholder={t("auth.login.account_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -102,13 +103,13 @@ export function LoginPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="password" className={AUTH_FORM.inputLabel}> <Label htmlFor="password" className={AUTH_FORM.inputLabel}>
Password <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.password")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="password" id="password"
type="password" type="password"
{...register("password", { required: "Password is required" })} {...register("password", { required: t("auth.login.password_required") || "Password is required" })}
placeholder="Password" placeholder={t("auth.login.password")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -119,13 +120,13 @@ export function LoginPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="captcha" className={AUTH_FORM.inputLabel}> <Label htmlFor="captcha" className={AUTH_FORM.inputLabel}>
Verification <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.verification")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id="captcha" id="captcha"
{...register("captcha", { required: "Captcha is required" })} {...register("captcha", { required: t("auth.login.captcha_required") || "Captcha is required" })}
placeholder="Enter code" placeholder={t("auth.login.captcha_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -135,7 +136,7 @@ export function LoginPage() {
alt="Captcha" alt="Captcha"
className={AUTH_FORM.captchaImg} className={AUTH_FORM.captchaImg}
onClick={loadCaptcha} onClick={loadCaptcha}
title="Click to refresh" title={t("auth.login.captcha_title")}
/> />
)} )}
</div> </div>
@ -147,12 +148,12 @@ export function LoginPage() {
{needs2FA && ( {needs2FA && (
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="totp_code" className={AUTH_FORM.inputLabel}> <Label htmlFor="totp_code" className={AUTH_FORM.inputLabel}>
2FA Code <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.2fa_code")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="totp_code" id="totp_code"
{...register("totp_code")} {...register("totp_code")}
placeholder="6-digit code" placeholder={t("auth.login.2fa_placeholder")}
maxLength={6} maxLength={6}
disabled={loading} disabled={loading}
className={`${AUTH_FORM.input} ${AUTH_FORM.inputMono}`} className={`${AUTH_FORM.input} ${AUTH_FORM.inputMono}`}
@ -161,7 +162,7 @@ export function LoginPage() {
)} )}
<Link to="/auth/forgot-password" className={AUTH_FORM.link}> <Link to="/auth/forgot-password" className={AUTH_FORM.link}>
Forgot your password? {t("auth.login.forgot_password")}
</Link> </Link>
<Button <Button
@ -169,13 +170,13 @@ export function LoginPage() {
className={AUTH_FORM.submitBtn} className={AUTH_FORM.submitBtn}
disabled={loading} disabled={loading}
> >
{loading ? "Logging in..." : "Log In"} {loading ? t("auth.login.submit_loading") : t("auth.login.submit")}
</Button> </Button>
<div className={AUTH_FORM.linkText}> <div className={AUTH_FORM.linkText}>
Need an account?{" "} {t("auth.login.need_account")}{" "}
<Link to="/auth/register" className={AUTH_FORM.link}> <Link to="/auth/register" className={AUTH_FORM.link}>
Register {t("auth.login.register")}
</Link> </Link>
</div> </div>
</form> </form>

View File

@ -9,6 +9,7 @@ import { apiAuthCaptcha, apiAuthRegister } from "@/client/api";
import type { RegisterParams } from "@/client/model"; import type { RegisterParams } from "@/client/model";
import { getCaptcha, encryptPassword } from "@/lib/auth-crypto"; import { getCaptcha, encryptPassword } from "@/lib/auth-crypto";
import { AUTH_FORM } from "@/css/auth/styles"; import { AUTH_FORM } from "@/css/auth/styles";
import { t } from "@/i18n/T";
export function RegisterPage() { export function RegisterPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -25,7 +26,7 @@ export function RegisterPage() {
setCaptchaImage(result.base64); setCaptchaImage(result.base64);
setPublicKey(result.publicKey || ""); setPublicKey(result.publicKey || "");
} catch { } catch {
setError("Failed to load captcha"); setError(t("auth.login.error.failed_to_load_captcha"));
} }
}; };
@ -41,7 +42,7 @@ export function RegisterPage() {
setError(""); setError("");
if (data.password !== data.confirmPassword) { if (data.password !== data.confirmPassword) {
setError("Passwords do not match"); setError(t("auth.register.passwords_not_match"));
setLoading(false); setLoading(false);
return; return;
} }
@ -60,9 +61,9 @@ export function RegisterPage() {
} catch (err) { } catch (err) {
const apiErr = err as { response?: { status?: number; data?: { message?: string } } }; const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
if (apiErr.response?.status === 409) { if (apiErr.response?.status === 409) {
setError("Username or email already exists"); setError(t("auth.register.user_exists"));
} else { } else {
setError(apiErr.response?.data?.message || "Registration failed"); setError(apiErr.response?.data?.message || t("auth.register.registration_failed"));
} }
loadCaptcha(); loadCaptcha();
} finally { } finally {
@ -75,8 +76,8 @@ export function RegisterPage() {
<div className={AUTH_FORM.card}> <div className={AUTH_FORM.card}>
<div className={AUTH_FORM.cardBg}> <div className={AUTH_FORM.cardBg}>
<div className={AUTH_FORM.header}> <div className={AUTH_FORM.header}>
<h2 className={AUTH_FORM.title}>Create an account</h2> <h2 className={AUTH_FORM.title}>{t("auth.register.title")}</h2>
<p className={AUTH_FORM.subtitle}>Join us today!</p> <p className={AUTH_FORM.subtitle}>{t("auth.register.subtitle")}</p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}> <form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}>
@ -88,16 +89,16 @@ export function RegisterPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="username" className={AUTH_FORM.inputLabel}> <Label htmlFor="username" className={AUTH_FORM.inputLabel}>
Username <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.register.username")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="username" id="username"
{...register("username", { {...register("username", {
required: "Username is required", required: t("auth.register.username_required"),
minLength: { value: 3, message: "Username must be at least 3 characters" }, minLength: { value: 3, message: t("auth.register.username_min_length") },
pattern: { value: /^[a-zA-Z0-9_-]+$/, message: "Username can only contain letters, numbers, _ and -" } pattern: { value: /^[a-zA-Z0-9_-]+$/, message: t("auth.register.username_pattern") }
})} })}
placeholder="Choose a username" placeholder={t("auth.register.username_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -108,16 +109,16 @@ export function RegisterPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="email" className={AUTH_FORM.inputLabel}> <Label htmlFor="email" className={AUTH_FORM.inputLabel}>
Email <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.register.email")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="email" id="email"
type="email" type="email"
{...register("email", { {...register("email", {
required: "Email is required", required: t("auth.register.email_required"),
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Invalid email address" } pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: t("auth.register.email_invalid") }
})} })}
placeholder="Enter your email" placeholder={t("auth.register.email_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -128,16 +129,16 @@ export function RegisterPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="password" className={AUTH_FORM.inputLabel}> <Label htmlFor="password" className={AUTH_FORM.inputLabel}>
Password <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.password")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="password" id="password"
type="password" type="password"
{...register("password", { {...register("password", {
required: "Password is required", required: t("auth.login.password_required"),
minLength: { value: 8, message: "Password must be at least 8 characters" } minLength: { value: 8, message: t("auth.register.password_min_length") }
})} })}
placeholder="Create a password" placeholder={t("auth.register.password_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -148,16 +149,16 @@ export function RegisterPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="confirmPassword" className={AUTH_FORM.inputLabel}> <Label htmlFor="confirmPassword" className={AUTH_FORM.inputLabel}>
Confirm Password <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.register.confirm_password")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="confirmPassword" id="confirmPassword"
type="password" type="password"
{...register("confirmPassword", { {...register("confirmPassword", {
required: "Please confirm your password", required: t("auth.register.confirm_password_required"),
validate: value => value === password || "Passwords do not match" validate: value => value === password || t("auth.register.passwords_not_match")
})} })}
placeholder="Confirm your password" placeholder={t("auth.register.confirm_password_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -168,13 +169,13 @@ export function RegisterPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="captcha" className={AUTH_FORM.inputLabel}> <Label htmlFor="captcha" className={AUTH_FORM.inputLabel}>
Verification <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.verification")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id="captcha" id="captcha"
{...register("captcha", { required: "Captcha is required" })} {...register("captcha", { required: t("auth.login.captcha_required") })}
placeholder="Enter code" placeholder={t("auth.login.captcha_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -184,7 +185,7 @@ export function RegisterPage() {
alt="Captcha" alt="Captcha"
className={AUTH_FORM.captchaImg} className={AUTH_FORM.captchaImg}
onClick={loadCaptcha} onClick={loadCaptcha}
title="Click to refresh" title={t("auth.login.captcha_title")}
/> />
)} )}
</div> </div>
@ -198,13 +199,13 @@ export function RegisterPage() {
className={AUTH_FORM.submitBtn} className={AUTH_FORM.submitBtn}
disabled={loading} disabled={loading}
> >
{loading ? "Creating account..." : "Create Account"} {loading ? t("auth.register.submit_loading") : t("auth.register.submit")}
</Button> </Button>
<div className={AUTH_FORM.linkText}> <div className={AUTH_FORM.linkText}>
Already have an account?{" "} {t("auth.register.already_have_account")}{" "}
<Link to="/auth/login" className={AUTH_FORM.link}> <Link to="/auth/login" className={AUTH_FORM.link}>
Login {t("auth.register.login")}
</Link> </Link>
</div> </div>
</form> </form>

View File

@ -9,6 +9,7 @@ import { apiAuthCaptcha, apiUserConfirmPasswordReset } from "@/client/api";
import type { ConfirmResetPasswordParams } from "@/client/model"; import type { ConfirmResetPasswordParams } from "@/client/model";
import { getCaptcha, encryptPassword } from "@/lib/auth-crypto"; import { getCaptcha, encryptPassword } from "@/lib/auth-crypto";
import { AUTH_FORM } from "@/css/auth/styles"; import { AUTH_FORM } from "@/css/auth/styles";
import { t } from "@/i18n/T";
export function ResetPasswordPage() { export function ResetPasswordPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -33,7 +34,7 @@ export function ResetPasswordPage() {
setCaptchaImage(result.base64); setCaptchaImage(result.base64);
setPublicKey(result.publicKey || ""); setPublicKey(result.publicKey || "");
} catch { } catch {
setError("Failed to load captcha"); setError(t("auth.login.error.failed_to_load_captcha"));
} }
}; };
@ -46,7 +47,7 @@ export function ResetPasswordPage() {
setError(""); setError("");
if (data.new_password !== data.confirmPassword) { if (data.new_password !== data.confirmPassword) {
setError("Passwords do not match"); setError(t("auth.register.passwords_not_match"));
setLoading(false); setLoading(false);
return; return;
} }
@ -63,9 +64,9 @@ export function ResetPasswordPage() {
} catch (err) { } catch (err) {
const apiErr = err as { response?: { status?: number; data?: { message?: string } } }; const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
if (apiErr.response?.status === 400) { if (apiErr.response?.status === 400) {
setError("Invalid or expired reset token"); setError(t("auth.reset_password.invalid_token"));
} else { } else {
setError(apiErr.response?.data?.message || "Failed to reset password"); setError(apiErr.response?.data?.message || t("auth.reset_password.reset_failed"));
} }
} finally { } finally {
setLoading(false); setLoading(false);
@ -77,8 +78,8 @@ export function ResetPasswordPage() {
<div className={AUTH_FORM.card}> <div className={AUTH_FORM.card}>
<div className={AUTH_FORM.cardBg}> <div className={AUTH_FORM.cardBg}>
<div className={AUTH_FORM.header}> <div className={AUTH_FORM.header}>
<h2 className={AUTH_FORM.title}>Set new password</h2> <h2 className={AUTH_FORM.title}>{t("auth.reset_password.title")}</h2>
<p className={AUTH_FORM.subtitle}>Choose a strong password for your account</p> <p className={AUTH_FORM.subtitle}>{t("auth.reset_password.subtitle")}</p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}> <form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}>
@ -92,16 +93,16 @@ export function ResetPasswordPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="new_password" className={AUTH_FORM.inputLabel}> <Label htmlFor="new_password" className={AUTH_FORM.inputLabel}>
New Password <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.password")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="new_password" id="new_password"
type="password" type="password"
{...register("new_password", { {...register("new_password", {
required: "Password is required", required: t("auth.login.password_required"),
minLength: { value: 8, message: "Password must be at least 8 characters" } minLength: { value: 8, message: t("auth.register.password_min_length") }
})} })}
placeholder="Enter new password" placeholder={t("auth.reset_password.new_password_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -112,16 +113,16 @@ export function ResetPasswordPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="confirmPassword" className={AUTH_FORM.inputLabel}> <Label htmlFor="confirmPassword" className={AUTH_FORM.inputLabel}>
Confirm Password <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.register.confirm_password")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="confirmPassword" id="confirmPassword"
type="password" type="password"
{...register("confirmPassword", { {...register("confirmPassword", {
required: "Please confirm your password", required: t("auth.register.confirm_password_required"),
validate: value => value === password || "Passwords do not match" validate: value => value === password || t("auth.register.passwords_not_match")
})} })}
placeholder="Confirm new password" placeholder={t("auth.reset_password.confirm_password_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -132,13 +133,13 @@ export function ResetPasswordPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="captcha" className={AUTH_FORM.inputLabel}> <Label htmlFor="captcha" className={AUTH_FORM.inputLabel}>
Verification <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.verification")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
id="captcha" id="captcha"
{...register("captcha", { required: "Captcha is required" })} {...register("captcha", { required: t("auth.login.captcha_required") })}
placeholder="Enter code" placeholder={t("auth.login.captcha_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -148,7 +149,7 @@ export function ResetPasswordPage() {
alt="Captcha" alt="Captcha"
className={AUTH_FORM.captchaImg} className={AUTH_FORM.captchaImg}
onClick={loadCaptcha} onClick={loadCaptcha}
title="Click to refresh" title={t("auth.login.captcha_title")}
/> />
)} )}
</div> </div>
@ -162,12 +163,12 @@ export function ResetPasswordPage() {
className={AUTH_FORM.submitBtn} className={AUTH_FORM.submitBtn}
disabled={loading} disabled={loading}
> >
{loading ? "Resetting..." : "Reset Password"} {loading ? t("auth.reset_password.resetting") : t("auth.reset_password.submit")}
</Button> </Button>
<div className={AUTH_FORM.linkTextCenter}> <div className={AUTH_FORM.linkTextCenter}>
<Link to="/auth/login" className={AUTH_FORM.link}> <Link to="/auth/login" className={AUTH_FORM.link}>
Back to login {t("auth.reset_password.back_to_login")}
</Link> </Link>
</div> </div>
</form> </form>

View File

@ -9,6 +9,7 @@ import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "@/comp
import { api2faEnable, api2faVerify, api2faDisable, api2faStatus } from "@/client/api"; import { api2faEnable, api2faVerify, api2faDisable, api2faStatus } from "@/client/api";
import type { Disable2FAParams } from "@/client/model"; import type { Disable2FAParams } from "@/client/model";
import { AUTH_FORM } from "@/css/auth/styles"; import { AUTH_FORM } from "@/css/auth/styles";
import { t } from "@/i18n/T";
export function TwoFactorPage() { export function TwoFactorPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -27,7 +28,7 @@ export function TwoFactorPage() {
const response = await api2faStatus(); const response = await api2faStatus();
setIsEnabled(response.data.is_enabled || false); setIsEnabled(response.data.is_enabled || false);
} catch { } catch {
setError("Failed to load 2FA status"); setError(t("auth.two_factor.error.load_failed"));
} }
}; };
@ -47,7 +48,7 @@ export function TwoFactorPage() {
setShowSetup(true); setShowSetup(true);
} catch (err) { } catch (err) {
const apiErr = err as { response?: { status?: number; data?: { message?: string } } }; const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
setError(apiErr.response?.data?.message || "Failed to enable 2FA"); setError(apiErr.response?.data?.message || t("auth.two_factor.error.enable_failed"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -67,7 +68,7 @@ export function TwoFactorPage() {
setSecret(""); setSecret("");
} catch (err) { } catch (err) {
const apiErr = err as { response?: { status?: number; data?: { message?: string } } }; const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
setError(apiErr.response?.data?.message || "Invalid verification code"); setError(apiErr.response?.data?.message || t("auth.two_factor.error.invalid_code"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -82,7 +83,7 @@ export function TwoFactorPage() {
setIsEnabled(false); setIsEnabled(false);
} catch (err) { } catch (err) {
const apiErr = err as { response?: { status?: number; data?: { message?: string } } }; const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
setError(apiErr.response?.data?.message || "Failed to disable 2FA"); setError(apiErr.response?.data?.message || t("auth.two_factor.error.disable_failed"));
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -93,9 +94,9 @@ export function TwoFactorPage() {
<div className={AUTH_FORM.card}> <div className={AUTH_FORM.card}>
<div className={AUTH_FORM.cardBg}> <div className={AUTH_FORM.cardBg}>
<div className={AUTH_FORM.header}> <div className={AUTH_FORM.header}>
<h2 className={AUTH_FORM.title}>Two-Factor Authentication</h2> <h2 className={AUTH_FORM.title}>{t("auth.two_factor.title")}</h2>
<p className={AUTH_FORM.subtitle}> <p className={AUTH_FORM.subtitle}>
{isEnabled ? "2FA is currently enabled" : "Add an extra layer of security"} {isEnabled ? t("auth.two_factor.enabled") : t("auth.two_factor.disabled")}
</p> </p>
</div> </div>
@ -109,15 +110,14 @@ export function TwoFactorPage() {
{!isEnabled && !showSetup && ( {!isEnabled && !showSetup && (
<div className={AUTH_FORM.form}> <div className={AUTH_FORM.form}>
<p className={AUTH_FORM.infoText}> <p className={AUTH_FORM.infoText}>
Two-factor authentication adds an additional layer of security to your account by requiring {t("auth.two_factor.description")}
more than just a password to log in.
</p> </p>
<Button <Button
onClick={handleEnable} onClick={handleEnable}
disabled={loading} disabled={loading}
className={AUTH_FORM.submitBtn} className={AUTH_FORM.submitBtn}
> >
Enable 2FA {t("auth.two_factor.enable")}
</Button> </Button>
</div> </div>
)} )}
@ -125,14 +125,14 @@ export function TwoFactorPage() {
{showSetup && ( {showSetup && (
<div className={AUTH_FORM.form}> <div className={AUTH_FORM.form}>
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label className={AUTH_FORM.inputLabel}>Scan QR Code</Label> <Label className={AUTH_FORM.inputLabel}>{t("auth.two_factor.scan_qr_code")}</Label>
<div className={AUTH_FORM.qrContainer}> <div className={AUTH_FORM.qrContainer}>
<img src={qrCode} alt="QR Code" className={AUTH_FORM.qrImage} /> <img src={qrCode} alt="QR Code" className={AUTH_FORM.qrImage} />
</div> </div>
</div> </div>
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label className={AUTH_FORM.inputLabel}>Or enter this code manually</Label> <Label className={AUTH_FORM.inputLabel}>{t("auth.two_factor.or_enter_manually")}</Label>
<Input <Input
value={secret} value={secret}
readOnly readOnly
@ -141,7 +141,7 @@ export function TwoFactorPage() {
</div> </div>
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label className={AUTH_FORM.inputLabel}>Verification Code</Label> <Label className={AUTH_FORM.inputLabel}>{t("auth.two_factor.verification_code")}</Label>
<div className="flex justify-center"> <div className="flex justify-center">
<InputOTP maxLength={6} onChange={setVerificationCode} disabled={loading}> <InputOTP maxLength={6} onChange={setVerificationCode} disabled={loading}>
<InputOTPGroup> <InputOTPGroup>
@ -160,7 +160,7 @@ export function TwoFactorPage() {
disabled={loading || verificationCode.length !== 6} disabled={loading || verificationCode.length !== 6}
className={AUTH_FORM.submitBtn} className={AUTH_FORM.submitBtn}
> >
Verify {t("auth.two_factor.submit")}
</Button> </Button>
<Button <Button
@ -172,7 +172,7 @@ export function TwoFactorPage() {
}} }}
className={AUTH_FORM.submitBtnOutline} className={AUTH_FORM.submitBtnOutline}
> >
Cancel {t("auth.two_factor.cancel")}
</Button> </Button>
</div> </div>
</div> </div>
@ -182,18 +182,18 @@ export function TwoFactorPage() {
<form onSubmit={handleSubmit(handleDisable)} className={AUTH_FORM.form}> <form onSubmit={handleSubmit(handleDisable)} className={AUTH_FORM.form}>
<Alert className={AUTH_FORM.successAlert}> <Alert className={AUTH_FORM.successAlert}>
<AlertDescription> <AlertDescription>
Two-factor authentication is currently enabled on your account. {t("auth.two_factor.enabled_message")}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="code" className={AUTH_FORM.inputLabel}> <Label htmlFor="code" className={AUTH_FORM.inputLabel}>
Verification Code <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.two_factor.verification_code")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="code" id="code"
{...register("code", { required: "Code is required" })} {...register("code", { required: t("auth.two_factor.code_required") })}
placeholder="Enter 6-digit code" placeholder={t("auth.two_factor.code_placeholder")}
maxLength={6} maxLength={6}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
@ -205,13 +205,13 @@ export function TwoFactorPage() {
<div className={AUTH_FORM.inputGroup}> <div className={AUTH_FORM.inputGroup}>
<Label htmlFor="password" className={AUTH_FORM.inputLabel}> <Label htmlFor="password" className={AUTH_FORM.inputLabel}>
Password <span className={AUTH_FORM.inputLabelAccent}>*</span> {t("auth.login.password")} <span className={AUTH_FORM.inputLabelAccent}>*</span>
</Label> </Label>
<Input <Input
id="password" id="password"
type="password" type="password"
{...register("password", { required: "Password is required" })} {...register("password", { required: t("auth.login.password_required") })}
placeholder="Enter your password" placeholder={t("auth.two_factor.password_placeholder")}
disabled={loading} disabled={loading}
className={AUTH_FORM.input} className={AUTH_FORM.input}
/> />
@ -226,7 +226,7 @@ export function TwoFactorPage() {
disabled={loading} disabled={loading}
className={AUTH_FORM.submitBtnDestructive} className={AUTH_FORM.submitBtnDestructive}
> >
{loading ? "Disabling..." : "Disable 2FA"} {loading ? t("auth.two_factor.disabling") : t("auth.two_factor.disable")}
</Button> </Button>
</form> </form>
)} )}
@ -237,7 +237,7 @@ export function TwoFactorPage() {
className="w-full h-11 transition-colors" className="w-full h-11 transition-colors"
style={{ color: "var(--text-muted)" }} style={{ color: "var(--text-muted)" }}
> >
Back {t("auth.two_factor.back")}
</Button> </Button>
</div> </div>
</div> </div>

0
src/fonts.css Normal file
View File

0
src/i18n/T.tsx Normal file
View File

0
src/i18n/de.json Normal file
View File

0
src/i18n/en.json Normal file
View File

0
src/i18n/fr.json Normal file
View File

0
src/i18n/jp.json Normal file
View File

0
src/i18n/zh.json Normal file
View File