Files
tessera-ctl/apps/web/src/app/(portal)/change-password/page.tsx
T
schalli 30e4bc3a4f style(web): Seiten auf Mosaik-Muster umgestellt
Einheitliche Seitenkoepfe mit Modul-Kachel, Knopf- und Linkklassen
statt gelber Schrift, Karten ohne harte Rahmen, Tabellenkoepfe ohne
Grossbuchstaben, Marktplatz mit Fluent-Reitern, Filterpillen und
Kachel-Karten, Einstellungs-/Verwaltungsnavigation mit Auswahlpille,
leere Zustaende mit Symbol im Kreis, Proxmox ohne leuchtende Schatten.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 14:49:01 +02:00

164 lines
5.5 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-semibold 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="btn btn-primary w-full"
>
{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>
);
}