feat(auth): add two-factor and password reset flow
This commit is contained in:
parent
bb9fe11869
commit
44944efd9b
@ -9,6 +9,7 @@ import { apiAuthCaptcha, apiUserRequestPasswordReset } from "@/client/api";
|
||||
import type { ResetPasswordParams } from "@/client/model";
|
||||
import { getCaptcha } from "@/lib/auth-crypto";
|
||||
import { AUTH_FORM } from "@/css/auth/styles";
|
||||
import { t } from "@/i18n/T";
|
||||
|
||||
export function ForgotPasswordPage() {
|
||||
const [error, setError] = useState<string>("");
|
||||
@ -24,7 +25,7 @@ export function ForgotPasswordPage() {
|
||||
const result = await getCaptcha(apiAuthCaptcha, true);
|
||||
setCaptchaImage(result.base64);
|
||||
} catch {
|
||||
setError("Failed to load captcha");
|
||||
setError(t("auth.login.error.failed_to_load_captcha"));
|
||||
}
|
||||
};
|
||||
|
||||
@ -38,7 +39,7 @@ export function ForgotPasswordPage() {
|
||||
setSuccess(false);
|
||||
|
||||
if (data.captcha !== captchaText) {
|
||||
setError("Captcha verification failed");
|
||||
setError(t("auth.forgot_password.captcha_failed"));
|
||||
loadCaptcha();
|
||||
setLoading(false);
|
||||
return;
|
||||
@ -50,7 +51,7 @@ export function ForgotPasswordPage() {
|
||||
setSuccess(true);
|
||||
} catch (err) {
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -61,8 +62,8 @@ export function ForgotPasswordPage() {
|
||||
<div className={AUTH_FORM.card}>
|
||||
<div className={AUTH_FORM.cardBg}>
|
||||
<div className={AUTH_FORM.header}>
|
||||
<h2 className={AUTH_FORM.title}>Reset your password</h2>
|
||||
<p className={AUTH_FORM.subtitle}>Enter your email and we'll send you a reset link</p>
|
||||
<h2 className={AUTH_FORM.title}>{t("auth.forgot_password.title")}</h2>
|
||||
<p className={AUTH_FORM.subtitle}>{t("auth.forgot_password.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}>
|
||||
@ -75,23 +76,23 @@ export function ForgotPasswordPage() {
|
||||
{success && (
|
||||
<Alert style={{ backgroundColor: "var(--surface-elevated)", borderColor: "var(--success)" }} className="text-[var(--success)] dark:text-[var(--success)]">
|
||||
<AlertDescription>
|
||||
If an account exists with that email, you will receive a password reset link shortly.
|
||||
{t("auth.forgot_password.success_message")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register("email", {
|
||||
required: "Email is required",
|
||||
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Invalid email address" }
|
||||
required: t("auth.register.email_required"),
|
||||
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}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -102,13 +103,13 @@ export function ForgotPasswordPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="captcha"
|
||||
{...register("captcha", { required: "Captcha is required" })}
|
||||
placeholder="Enter code"
|
||||
{...register("captcha", { required: t("auth.login.captcha_required") })}
|
||||
placeholder={t("auth.login.captcha_placeholder")}
|
||||
disabled={loading || success}
|
||||
className={AUTH_FORM.input}
|
||||
onChange={(e) => setCaptchaText(e.target.value)}
|
||||
@ -119,7 +120,7 @@ export function ForgotPasswordPage() {
|
||||
alt="Captcha"
|
||||
className={AUTH_FORM.captchaImg}
|
||||
onClick={loadCaptcha}
|
||||
title="Click to refresh"
|
||||
title={t("auth.login.captcha_title")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -133,12 +134,12 @@ export function ForgotPasswordPage() {
|
||||
className={AUTH_FORM.submitBtn}
|
||||
disabled={loading || success}
|
||||
>
|
||||
{loading ? "Sending..." : "Send Reset Link"}
|
||||
{loading ? t("auth.forgot_password.sending") : t("auth.forgot_password.submit")}
|
||||
</Button>
|
||||
|
||||
<div className={AUTH_FORM.linkTextCenter}>
|
||||
<Link to="/auth/login" className={AUTH_FORM.link}>
|
||||
Back to login
|
||||
{t("auth.forgot_password.back_to_login")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -10,6 +10,7 @@ import { useLoginMutation } from "@/hooks/useAuth";
|
||||
import type { LoginParams } from "@/client/model";
|
||||
import { getCaptcha, encryptPassword } from "@/lib/auth-crypto";
|
||||
import { AUTH_FORM } from "@/css/auth/styles";
|
||||
import { t } from "@/i18n/T";
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
@ -28,7 +29,7 @@ export function LoginPage() {
|
||||
setCaptchaImage(result.base64);
|
||||
setPublicKey(result.publicKey || "");
|
||||
} 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 } } };
|
||||
if (apiErr.response?.status === 428) {
|
||||
setNeeds2FA(true);
|
||||
setError("Two-factor authentication required");
|
||||
setError(t("auth.login.error.two_factor_required"));
|
||||
} else if (apiErr.response?.status === 401) {
|
||||
setError("Invalid username or password");
|
||||
setError(t("auth.login.error.invalid_credentials"));
|
||||
} else {
|
||||
setError(apiErr.response?.data?.message || "Login failed");
|
||||
setError(apiErr.response?.data?.message || t("auth.login.error.login_failed"));
|
||||
}
|
||||
loadCaptcha();
|
||||
}
|
||||
@ -73,8 +74,8 @@ export function LoginPage() {
|
||||
<div className={AUTH_FORM.card}>
|
||||
<div className={AUTH_FORM.cardBg}>
|
||||
<div className={AUTH_FORM.header}>
|
||||
<h2 className={AUTH_FORM.title}>Welcome back!</h2>
|
||||
<p className={AUTH_FORM.subtitle}>We're so excited to see you again!</p>
|
||||
<h2 className={AUTH_FORM.title}>{t("auth.login.title")}</h2>
|
||||
<p className={AUTH_FORM.subtitle}>{t("auth.login.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}>
|
||||
@ -86,12 +87,12 @@ export function LoginPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="username"
|
||||
{...register("username", { required: "Username is required" })}
|
||||
placeholder="Username or email"
|
||||
{...register("username", { required: t("auth.login.account_required") || "Username is required" })}
|
||||
placeholder={t("auth.login.account_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -102,13 +103,13 @@ export function LoginPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
{...register("password", { required: "Password is required" })}
|
||||
placeholder="Password"
|
||||
{...register("password", { required: t("auth.login.password_required") || "Password is required" })}
|
||||
placeholder={t("auth.login.password")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -119,13 +120,13 @@ export function LoginPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="captcha"
|
||||
{...register("captcha", { required: "Captcha is required" })}
|
||||
placeholder="Enter code"
|
||||
{...register("captcha", { required: t("auth.login.captcha_required") || "Captcha is required" })}
|
||||
placeholder={t("auth.login.captcha_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -135,7 +136,7 @@ export function LoginPage() {
|
||||
alt="Captcha"
|
||||
className={AUTH_FORM.captchaImg}
|
||||
onClick={loadCaptcha}
|
||||
title="Click to refresh"
|
||||
title={t("auth.login.captcha_title")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -147,12 +148,12 @@ export function LoginPage() {
|
||||
{needs2FA && (
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="totp_code"
|
||||
{...register("totp_code")}
|
||||
placeholder="6-digit code"
|
||||
placeholder={t("auth.login.2fa_placeholder")}
|
||||
maxLength={6}
|
||||
disabled={loading}
|
||||
className={`${AUTH_FORM.input} ${AUTH_FORM.inputMono}`}
|
||||
@ -161,7 +162,7 @@ export function LoginPage() {
|
||||
)}
|
||||
|
||||
<Link to="/auth/forgot-password" className={AUTH_FORM.link}>
|
||||
Forgot your password?
|
||||
{t("auth.login.forgot_password")}
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
@ -169,13 +170,13 @@ export function LoginPage() {
|
||||
className={AUTH_FORM.submitBtn}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Logging in..." : "Log In"}
|
||||
{loading ? t("auth.login.submit_loading") : t("auth.login.submit")}
|
||||
</Button>
|
||||
|
||||
<div className={AUTH_FORM.linkText}>
|
||||
Need an account?{" "}
|
||||
{t("auth.login.need_account")}{" "}
|
||||
<Link to="/auth/register" className={AUTH_FORM.link}>
|
||||
Register
|
||||
{t("auth.login.register")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -9,6 +9,7 @@ import { apiAuthCaptcha, apiAuthRegister } from "@/client/api";
|
||||
import type { RegisterParams } from "@/client/model";
|
||||
import { getCaptcha, encryptPassword } from "@/lib/auth-crypto";
|
||||
import { AUTH_FORM } from "@/css/auth/styles";
|
||||
import { t } from "@/i18n/T";
|
||||
|
||||
export function RegisterPage() {
|
||||
const navigate = useNavigate();
|
||||
@ -25,7 +26,7 @@ export function RegisterPage() {
|
||||
setCaptchaImage(result.base64);
|
||||
setPublicKey(result.publicKey || "");
|
||||
} catch {
|
||||
setError("Failed to load captcha");
|
||||
setError(t("auth.login.error.failed_to_load_captcha"));
|
||||
}
|
||||
};
|
||||
|
||||
@ -41,7 +42,7 @@ export function RegisterPage() {
|
||||
setError("");
|
||||
|
||||
if (data.password !== data.confirmPassword) {
|
||||
setError("Passwords do not match");
|
||||
setError(t("auth.register.passwords_not_match"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@ -60,9 +61,9 @@ export function RegisterPage() {
|
||||
} catch (err) {
|
||||
const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
|
||||
if (apiErr.response?.status === 409) {
|
||||
setError("Username or email already exists");
|
||||
setError(t("auth.register.user_exists"));
|
||||
} else {
|
||||
setError(apiErr.response?.data?.message || "Registration failed");
|
||||
setError(apiErr.response?.data?.message || t("auth.register.registration_failed"));
|
||||
}
|
||||
loadCaptcha();
|
||||
} finally {
|
||||
@ -75,8 +76,8 @@ export function RegisterPage() {
|
||||
<div className={AUTH_FORM.card}>
|
||||
<div className={AUTH_FORM.cardBg}>
|
||||
<div className={AUTH_FORM.header}>
|
||||
<h2 className={AUTH_FORM.title}>Create an account</h2>
|
||||
<p className={AUTH_FORM.subtitle}>Join us today!</p>
|
||||
<h2 className={AUTH_FORM.title}>{t("auth.register.title")}</h2>
|
||||
<p className={AUTH_FORM.subtitle}>{t("auth.register.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}>
|
||||
@ -88,16 +89,16 @@ export function RegisterPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="username"
|
||||
{...register("username", {
|
||||
required: "Username is required",
|
||||
minLength: { value: 3, message: "Username must be at least 3 characters" },
|
||||
pattern: { value: /^[a-zA-Z0-9_-]+$/, message: "Username can only contain letters, numbers, _ and -" }
|
||||
required: t("auth.register.username_required"),
|
||||
minLength: { value: 3, message: t("auth.register.username_min_length") },
|
||||
pattern: { value: /^[a-zA-Z0-9_-]+$/, message: t("auth.register.username_pattern") }
|
||||
})}
|
||||
placeholder="Choose a username"
|
||||
placeholder={t("auth.register.username_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -108,16 +109,16 @@ export function RegisterPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
{...register("email", {
|
||||
required: "Email is required",
|
||||
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Invalid email address" }
|
||||
required: t("auth.register.email_required"),
|
||||
pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: t("auth.register.email_invalid") }
|
||||
})}
|
||||
placeholder="Enter your email"
|
||||
placeholder={t("auth.register.email_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -128,16 +129,16 @@ export function RegisterPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
{...register("password", {
|
||||
required: "Password is required",
|
||||
minLength: { value: 8, message: "Password must be at least 8 characters" }
|
||||
required: t("auth.login.password_required"),
|
||||
minLength: { value: 8, message: t("auth.register.password_min_length") }
|
||||
})}
|
||||
placeholder="Create a password"
|
||||
placeholder={t("auth.register.password_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -148,16 +149,16 @@ export function RegisterPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
{...register("confirmPassword", {
|
||||
required: "Please confirm your password",
|
||||
validate: value => value === password || "Passwords do not match"
|
||||
required: t("auth.register.confirm_password_required"),
|
||||
validate: value => value === password || t("auth.register.passwords_not_match")
|
||||
})}
|
||||
placeholder="Confirm your password"
|
||||
placeholder={t("auth.register.confirm_password_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -168,13 +169,13 @@ export function RegisterPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="captcha"
|
||||
{...register("captcha", { required: "Captcha is required" })}
|
||||
placeholder="Enter code"
|
||||
{...register("captcha", { required: t("auth.login.captcha_required") })}
|
||||
placeholder={t("auth.login.captcha_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -184,7 +185,7 @@ export function RegisterPage() {
|
||||
alt="Captcha"
|
||||
className={AUTH_FORM.captchaImg}
|
||||
onClick={loadCaptcha}
|
||||
title="Click to refresh"
|
||||
title={t("auth.login.captcha_title")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -198,13 +199,13 @@ export function RegisterPage() {
|
||||
className={AUTH_FORM.submitBtn}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Creating account..." : "Create Account"}
|
||||
{loading ? t("auth.register.submit_loading") : t("auth.register.submit")}
|
||||
</Button>
|
||||
|
||||
<div className={AUTH_FORM.linkText}>
|
||||
Already have an account?{" "}
|
||||
{t("auth.register.already_have_account")}{" "}
|
||||
<Link to="/auth/login" className={AUTH_FORM.link}>
|
||||
Login
|
||||
{t("auth.register.login")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -9,6 +9,7 @@ import { apiAuthCaptcha, apiUserConfirmPasswordReset } from "@/client/api";
|
||||
import type { ConfirmResetPasswordParams } from "@/client/model";
|
||||
import { getCaptcha, encryptPassword } from "@/lib/auth-crypto";
|
||||
import { AUTH_FORM } from "@/css/auth/styles";
|
||||
import { t } from "@/i18n/T";
|
||||
|
||||
export function ResetPasswordPage() {
|
||||
const navigate = useNavigate();
|
||||
@ -33,7 +34,7 @@ export function ResetPasswordPage() {
|
||||
setCaptchaImage(result.base64);
|
||||
setPublicKey(result.publicKey || "");
|
||||
} catch {
|
||||
setError("Failed to load captcha");
|
||||
setError(t("auth.login.error.failed_to_load_captcha"));
|
||||
}
|
||||
};
|
||||
|
||||
@ -46,7 +47,7 @@ export function ResetPasswordPage() {
|
||||
setError("");
|
||||
|
||||
if (data.new_password !== data.confirmPassword) {
|
||||
setError("Passwords do not match");
|
||||
setError(t("auth.register.passwords_not_match"));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@ -63,9 +64,9 @@ export function ResetPasswordPage() {
|
||||
} catch (err) {
|
||||
const apiErr = err as { response?: { status?: number; data?: { message?: string } } };
|
||||
if (apiErr.response?.status === 400) {
|
||||
setError("Invalid or expired reset token");
|
||||
setError(t("auth.reset_password.invalid_token"));
|
||||
} else {
|
||||
setError(apiErr.response?.data?.message || "Failed to reset password");
|
||||
setError(apiErr.response?.data?.message || t("auth.reset_password.reset_failed"));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -77,8 +78,8 @@ export function ResetPasswordPage() {
|
||||
<div className={AUTH_FORM.card}>
|
||||
<div className={AUTH_FORM.cardBg}>
|
||||
<div className={AUTH_FORM.header}>
|
||||
<h2 className={AUTH_FORM.title}>Set new password</h2>
|
||||
<p className={AUTH_FORM.subtitle}>Choose a strong password for your account</p>
|
||||
<h2 className={AUTH_FORM.title}>{t("auth.reset_password.title")}</h2>
|
||||
<p className={AUTH_FORM.subtitle}>{t("auth.reset_password.subtitle")}</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={AUTH_FORM.form}>
|
||||
@ -92,16 +93,16 @@ export function ResetPasswordPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="new_password"
|
||||
type="password"
|
||||
{...register("new_password", {
|
||||
required: "Password is required",
|
||||
minLength: { value: 8, message: "Password must be at least 8 characters" }
|
||||
required: t("auth.login.password_required"),
|
||||
minLength: { value: 8, message: t("auth.register.password_min_length") }
|
||||
})}
|
||||
placeholder="Enter new password"
|
||||
placeholder={t("auth.reset_password.new_password_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -112,16 +113,16 @@ export function ResetPasswordPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
{...register("confirmPassword", {
|
||||
required: "Please confirm your password",
|
||||
validate: value => value === password || "Passwords do not match"
|
||||
required: t("auth.register.confirm_password_required"),
|
||||
validate: value => value === password || t("auth.register.passwords_not_match")
|
||||
})}
|
||||
placeholder="Confirm new password"
|
||||
placeholder={t("auth.reset_password.confirm_password_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -132,13 +133,13 @@ export function ResetPasswordPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="captcha"
|
||||
{...register("captcha", { required: "Captcha is required" })}
|
||||
placeholder="Enter code"
|
||||
{...register("captcha", { required: t("auth.login.captcha_required") })}
|
||||
placeholder={t("auth.login.captcha_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -148,7 +149,7 @@ export function ResetPasswordPage() {
|
||||
alt="Captcha"
|
||||
className={AUTH_FORM.captchaImg}
|
||||
onClick={loadCaptcha}
|
||||
title="Click to refresh"
|
||||
title={t("auth.login.captcha_title")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -162,12 +163,12 @@ export function ResetPasswordPage() {
|
||||
className={AUTH_FORM.submitBtn}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Resetting..." : "Reset Password"}
|
||||
{loading ? t("auth.reset_password.resetting") : t("auth.reset_password.submit")}
|
||||
</Button>
|
||||
|
||||
<div className={AUTH_FORM.linkTextCenter}>
|
||||
<Link to="/auth/login" className={AUTH_FORM.link}>
|
||||
Back to login
|
||||
{t("auth.reset_password.back_to_login")}
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -9,6 +9,7 @@ import { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } from "@/comp
|
||||
import { api2faEnable, api2faVerify, api2faDisable, api2faStatus } from "@/client/api";
|
||||
import type { Disable2FAParams } from "@/client/model";
|
||||
import { AUTH_FORM } from "@/css/auth/styles";
|
||||
import { t } from "@/i18n/T";
|
||||
|
||||
export function TwoFactorPage() {
|
||||
const navigate = useNavigate();
|
||||
@ -27,7 +28,7 @@ export function TwoFactorPage() {
|
||||
const response = await api2faStatus();
|
||||
setIsEnabled(response.data.is_enabled || false);
|
||||
} catch {
|
||||
setError("Failed to load 2FA status");
|
||||
setError(t("auth.two_factor.error.load_failed"));
|
||||
}
|
||||
};
|
||||
|
||||
@ -47,7 +48,7 @@ export function TwoFactorPage() {
|
||||
setShowSetup(true);
|
||||
} catch (err) {
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -67,7 +68,7 @@ export function TwoFactorPage() {
|
||||
setSecret("");
|
||||
} catch (err) {
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -82,7 +83,7 @@ export function TwoFactorPage() {
|
||||
setIsEnabled(false);
|
||||
} catch (err) {
|
||||
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 {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -93,9 +94,9 @@ export function TwoFactorPage() {
|
||||
<div className={AUTH_FORM.card}>
|
||||
<div className={AUTH_FORM.cardBg}>
|
||||
<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}>
|
||||
{isEnabled ? "2FA is currently enabled" : "Add an extra layer of security"}
|
||||
{isEnabled ? t("auth.two_factor.enabled") : t("auth.two_factor.disabled")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -109,15 +110,14 @@ export function TwoFactorPage() {
|
||||
{!isEnabled && !showSetup && (
|
||||
<div className={AUTH_FORM.form}>
|
||||
<p className={AUTH_FORM.infoText}>
|
||||
Two-factor authentication adds an additional layer of security to your account by requiring
|
||||
more than just a password to log in.
|
||||
{t("auth.two_factor.description")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleEnable}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.submitBtn}
|
||||
>
|
||||
Enable 2FA
|
||||
{t("auth.two_factor.enable")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -125,14 +125,14 @@ export function TwoFactorPage() {
|
||||
{showSetup && (
|
||||
<div className={AUTH_FORM.form}>
|
||||
<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}>
|
||||
<img src={qrCode} alt="QR Code" className={AUTH_FORM.qrImage} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
value={secret}
|
||||
readOnly
|
||||
@ -141,7 +141,7 @@ export function TwoFactorPage() {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<InputOTP maxLength={6} onChange={setVerificationCode} disabled={loading}>
|
||||
<InputOTPGroup>
|
||||
@ -160,7 +160,7 @@ export function TwoFactorPage() {
|
||||
disabled={loading || verificationCode.length !== 6}
|
||||
className={AUTH_FORM.submitBtn}
|
||||
>
|
||||
Verify
|
||||
{t("auth.two_factor.submit")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@ -172,7 +172,7 @@ export function TwoFactorPage() {
|
||||
}}
|
||||
className={AUTH_FORM.submitBtnOutline}
|
||||
>
|
||||
Cancel
|
||||
{t("auth.two_factor.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -182,18 +182,18 @@ export function TwoFactorPage() {
|
||||
<form onSubmit={handleSubmit(handleDisable)} className={AUTH_FORM.form}>
|
||||
<Alert className={AUTH_FORM.successAlert}>
|
||||
<AlertDescription>
|
||||
Two-factor authentication is currently enabled on your account.
|
||||
{t("auth.two_factor.enabled_message")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="code"
|
||||
{...register("code", { required: "Code is required" })}
|
||||
placeholder="Enter 6-digit code"
|
||||
{...register("code", { required: t("auth.two_factor.code_required") })}
|
||||
placeholder={t("auth.two_factor.code_placeholder")}
|
||||
maxLength={6}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
@ -205,13 +205,13 @@ export function TwoFactorPage() {
|
||||
|
||||
<div className={AUTH_FORM.inputGroup}>
|
||||
<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>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
{...register("password", { required: "Password is required" })}
|
||||
placeholder="Enter your password"
|
||||
{...register("password", { required: t("auth.login.password_required") })}
|
||||
placeholder={t("auth.two_factor.password_placeholder")}
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.input}
|
||||
/>
|
||||
@ -226,7 +226,7 @@ export function TwoFactorPage() {
|
||||
disabled={loading}
|
||||
className={AUTH_FORM.submitBtnDestructive}
|
||||
>
|
||||
{loading ? "Disabling..." : "Disable 2FA"}
|
||||
{loading ? t("auth.two_factor.disabling") : t("auth.two_factor.disable")}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
@ -237,7 +237,7 @@ export function TwoFactorPage() {
|
||||
className="w-full h-11 transition-colors"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
Back
|
||||
{t("auth.two_factor.back")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
0
src/fonts.css
Normal file
0
src/fonts.css
Normal file
0
src/i18n/T.tsx
Normal file
0
src/i18n/T.tsx
Normal file
0
src/i18n/de.json
Normal file
0
src/i18n/de.json
Normal file
0
src/i18n/en.json
Normal file
0
src/i18n/en.json
Normal file
0
src/i18n/fr.json
Normal file
0
src/i18n/fr.json
Normal file
0
src/i18n/jp.json
Normal file
0
src/i18n/jp.json
Normal file
0
src/i18n/zh.json
Normal file
0
src/i18n/zh.json
Normal file
Loading…
Reference in New Issue
Block a user