fix(auth): polish login/register/reset pages for consistency and accessibility
- 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:
@@ -4,7 +4,7 @@ import { Alert, Box, Button, Paper, TextField, Typography } from "@mui/material"
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { api } from "../api";
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
@@ -16,7 +16,9 @@ export default function ResetPasswordPage() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [token, setToken] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<{ newPassword?: string; confirmPassword?: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -54,6 +56,12 @@ export default function ResetPasswordPage() {
|
||||
toast(t("missingResetLinkInfo"), "error");
|
||||
return;
|
||||
}
|
||||
const errors: { newPassword?: string; confirmPassword?: string } = {};
|
||||
if (!newPassword || newPassword.length < 8) errors.newPassword = t("passwordTooShort");
|
||||
if (confirmPassword !== newPassword) errors.confirmPassword = t("passwordsDoNotMatch");
|
||||
setFieldErrors(errors);
|
||||
if (Object.keys(errors).length > 0) return;
|
||||
|
||||
setLoading(true);
|
||||
api
|
||||
.post("/auth/reset-password", { email, token, newPassword })
|
||||
@@ -62,16 +70,34 @@ export default function ResetPasswordPage() {
|
||||
navigate("/login", { replace: true });
|
||||
})
|
||||
.catch((e2: any) => {
|
||||
const msg = e2?.response?.data || e2?.message || t("resetFailed");
|
||||
toast(String(msg), "error");
|
||||
toast(getApiErrorMessage(e2, t("resetFailed")), "error");
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}}
|
||||
sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}
|
||||
>
|
||||
{missingResetInfo ? <Alert severity="warning">{t("missingResetLinkInfo")}</Alert> : null}
|
||||
<TextField label={t("profileEmail")} value={email} onChange={(e) => setEmail(e.target.value)} disabled={!missingResetInfo} fullWidth />
|
||||
<TextField label={t("profileNewPassword")} type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} fullWidth />
|
||||
<TextField label={t("profileEmail")} type="email" value={email} onChange={(e) => setEmail(e.target.value)} disabled={!missingResetInfo} fullWidth />
|
||||
<TextField
|
||||
label={t("profileNewPassword")}
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => { setNewPassword(e.target.value); setFieldErrors((f) => ({ ...f, newPassword: undefined, confirmPassword: undefined })); }}
|
||||
autoComplete="new-password"
|
||||
error={Boolean(fieldErrors.newPassword)}
|
||||
helperText={fieldErrors.newPassword || t("passwordTooShort")}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label={t("confirmPassword")}
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => { setConfirmPassword(e.target.value); setFieldErrors((f) => ({ ...f, confirmPassword: undefined })); }}
|
||||
autoComplete="new-password"
|
||||
error={Boolean(fieldErrors.confirmPassword)}
|
||||
helperText={fieldErrors.confirmPassword}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1, mt: 1 }}>
|
||||
<Button type="button" variant="outlined" onClick={() => navigate("/login")} disabled={loading}>
|
||||
|
||||
Reference in New Issue
Block a user