fix(auth): polish login/register/reset pages for consistency and accessibility
CI and Deploy / test (push) Successful in 2m9s
CI and Deploy / deploy (push) Successful in 39s

- LoginPage: add client-side email/password validation (inline error +
  helperText, matching the 2FA components' established pattern), and a
  proper register-mode toggle with a "Confirm password" field. The
  brief asked for confirm-password on registration but the page only
  had one shared password field; a toggle (mirroring the existing
  Tabs-for-mode pattern already used for Google/Microsoft) keeps this
  from cluttering the login form for returning users.
- Fix a real bug in ResetPasswordPage: it didn't use the app's
  getApiErrorMessage helper, so a non-string error response body would
  render as "[object Object]" in the toast. Also add a confirm-password
  field and matching client-side validation for parity with register.
- ForgotPasswordPage: add proper email format validation instead of
  only checking for non-empty.
- Add matching i18n keys (en/no) for every new validation message.

Verified live end-to-end against a running backend: register-mode
toggle, confirm-password mismatch blocking submission client-side,
and a full registration completing and landing on the dashboard.
This commit is contained in:
cesnimda
2026-07-13 08:34:17 +02:00
parent 706b3ec699
commit b8b7987c58
4 changed files with 160 additions and 35 deletions
@@ -16,6 +16,8 @@ export default function ForgotPasswordPage() {
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [emailError, setEmailError] = useState<string | undefined>(undefined);
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
useEffect(() => {
const params = new URLSearchParams(window.location.search);
@@ -46,10 +48,16 @@ export default function ForgotPasswordPage() {
component="form"
onSubmit={(e) => {
e.preventDefault();
if (!email.trim()) {
toast(t("passwordResetEnterEmail"), "info");
const trimmed = email.trim();
if (!trimmed) {
setEmailError(t("emailRequired"));
return;
}
if (!EMAIL_PATTERN.test(trimmed)) {
setEmailError(t("invalidEmail"));
return;
}
setEmailError(undefined);
setLoading(true);
api
.post("/auth/request-password-reset", { email: email.trim() })
@@ -65,7 +73,17 @@ export default function ForgotPasswordPage() {
sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}
>
{submitted ? <Alert severity="success">{t("passwordResetRequestSent")}</Alert> : null}
<TextField label={t("profileEmail")} value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" fullWidth />
<TextField
label={t("profileEmail")}
type="email"
value={email}
onChange={(e) => { setEmail(e.target.value); setEmailError(undefined); }}
autoComplete="email"
autoFocus
error={Boolean(emailError)}
helperText={emailError}
fullWidth
/>
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1, mt: 1 }}>
<Button type="button" variant="outlined" onClick={() => navigate("/login")} disabled={loading}>