Files
tessera-ctl/apps/web/src/app/(portal)/change-password/page.tsx
T
schalli 9aa87bd000 fix(quick-260921-jt4): autoFocus von vier Seitenformularen entfernt (D-02)
login/page.tsx, reset-password/page.tsx, reset-password/[token]/page.tsx,
change-password/page.tsx: alle vier sind Seitenformulare, kein Dialog —
D-02 verlangt hier das Entfernen, nicht den ref+Effekt-Ersatz.
Fokus-Klauen beim Seitenaufruf ist genau der Schaden, gegen den
noAutofocus existiert. autoComplete/required unveraendert.

Nebennutzen bei change-password: der Hinweisbereich zum erzwungenen
Passwortwechsel steht unmittelbar ueber dem Formular — bislang sprang
der Fokus daran vorbei, eine Vorlesehilfe las ihn nie vor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 14:52:40 +02:00

164 lines
5.6 KiB
TypeScript

'use client';
import { useState, useTransition, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { fetchCurrentUser, changePasswordAction } from '@/lib/auth-actions';
export default function ChangePasswordPage() {
const t = useTranslations('auth');
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [passwordMismatch, setPasswordMismatch] = useState(false);
const [isForced, setIsForced] = useState(false);
useEffect(() => {
async function checkForceChange() {
const currentUser = await fetchCurrentUser();
if (currentUser?.mustChangePassword) {
setIsForced(true);
}
}
checkForceChange();
}, []);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setError(null);
setPasswordMismatch(false);
const formData = new FormData(e.currentTarget);
const currentPassword = formData.get('currentPassword') as string;
const newPassword = formData.get('newPassword') as string;
const confirmPassword = formData.get('confirmPassword') as string;
if (newPassword !== confirmPassword) {
setPasswordMismatch(true);
return;
}
startTransition(async () => {
const result = await changePasswordAction(currentPassword, newPassword);
if (result) {
setError(result.error);
}
});
}
return (
<div className="mx-auto max-w-md py-8 px-4">
<h1 className="text-2xl font-bold text-foreground mb-6">
{t('changePassword.title')}
</h1>
{isForced && (
<div className="rounded-md bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 px-4 py-3 text-sm text-yellow-800 dark:text-yellow-200 mb-6">
{t('changePassword.forceChangeNotice')}
</div>
)}
{error && (
<div className="rounded-md bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm text-destructive mb-6">
{t(`changePassword.${error}`)}
</div>
)}
{passwordMismatch && (
<div className="rounded-md bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm text-destructive mb-6">
{t('changePassword.passwordMismatch')}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div className="space-y-2">
<label
htmlFor="currentPassword"
className="text-sm font-medium text-foreground"
>
{t('changePassword.currentPassword')}
</label>
<input
id="currentPassword"
name="currentPassword"
type="password"
required
autoComplete="current-password"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder={t('changePassword.currentPassword')}
/>
</div>
<div className="space-y-2">
<label
htmlFor="newPassword"
className="text-sm font-medium text-foreground"
>
{t('changePassword.newPassword')}
</label>
<input
id="newPassword"
name="newPassword"
type="password"
required
minLength={8}
autoComplete="new-password"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder={t('changePassword.newPassword')}
/>
</div>
<div className="space-y-2">
<label
htmlFor="confirmPassword"
className="text-sm font-medium text-foreground"
>
{t('changePassword.confirmPassword')}
</label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
required
minLength={8}
autoComplete="new-password"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
placeholder={t('changePassword.confirmPassword')}
/>
</div>
<button
type="submit"
disabled={isPending}
className="flex w-full items-center justify-center rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
>
{isPending ? (
<svg
className="animate-spin h-4 w-4"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
) : (
t('changePassword.submit')
)}
</button>
</form>
</div>
);
}