feat(frontend): VerifyEmail + ForgotPassword + ResetPassword + AcceptInvite pages

This commit is contained in:
2026-05-20 23:09:04 +02:00
parent 4e15d8b59d
commit 1850cd78f5
4 changed files with 174 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { authApi } from '../../api/auth.js';
import { ApiClientError } from '../../api/client.js';
import { useAuth } from '../../stores/authStore.js';
import { AuthLayout, Field } from './Login.js';
export function AcceptInvitePage() {
const [params] = useSearchParams();
const token = params.get('token') ?? '';
const navigate = useNavigate();
const refreshMe = useAuth((s) => s.refreshMe);
const [displayName, setDisplayName] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true); setError(null);
try {
await authApi.acceptInvite({ token, displayName, password });
await refreshMe();
navigate('/', { replace: true });
} catch (err) {
setError(err instanceof ApiClientError ? err.message : 'Accepteren mislukt.');
} finally { setBusy(false); }
}
return (
<AuthLayout title="Account aanmaken">
{!token && <p className="text-sm text-danger-700">Geen token in URL.</p>}
<form onSubmit={submit} className="space-y-4">
<Field label="Naam">
<input required className="input-field" value={displayName} onChange={(e) => setDisplayName(e.target.value)} autoComplete="name" />
</Field>
<Field label="Wachtwoord (min. 8 tekens)">
<input type="password" required minLength={8} className="input-field" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password" />
</Field>
{error && <p className="rounded-xl bg-danger-50 p-3 text-sm text-danger-700 dark:bg-danger-400/10 dark:text-danger-400">{error}</p>}
<button type="submit" className="btn-primary w-full py-3" disabled={busy || !token}>{busy ? 'Bezig…' : 'Account aanmaken'}</button>
</form>
</AuthLayout>
);
}

View File

@@ -0,0 +1,40 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { authApi } from '../../api/auth.js';
import { AuthLayout, Field } from './Login.js';
export function ForgotPasswordPage() {
const [email, setEmail] = useState('');
const [busy, setBusy] = useState(false);
const [sent, setSent] = useState(false);
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
try { await authApi.forgotPassword({ email }); }
finally { setBusy(false); setSent(true); }
}
if (sent) {
return (
<AuthLayout title="Check je mail 📬">
<p className="text-sm">Als <strong>{email}</strong> bekend is, hebben we een reset-link gestuurd. De link is 1 uur geldig.</p>
<Link to="/login" className="mt-6 inline-block text-sm text-brand-600 hover:underline">Terug naar inloggen</Link>
</AuthLayout>
);
}
return (
<AuthLayout title="Wachtwoord vergeten">
<form onSubmit={submit} className="space-y-4">
<Field label="E-mailadres">
<input type="email" required className="input-field" value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" />
</Field>
<button type="submit" className="btn-primary w-full py-3" disabled={busy}>{busy ? 'Bezig…' : 'Stuur reset-link'}</button>
</form>
<div className="mt-6 text-sm">
<Link to="/login" className="text-brand-600 hover:underline">Terug naar inloggen</Link>
</div>
</AuthLayout>
);
}

View File

@@ -0,0 +1,46 @@
import { useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { authApi } from '../../api/auth.js';
import { ApiClientError } from '../../api/client.js';
import { AuthLayout, Field } from './Login.js';
export function ResetPasswordPage() {
const [params] = useSearchParams();
const token = params.get('token') ?? '';
const navigate = useNavigate();
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState(false);
async function submit(e: React.FormEvent) {
e.preventDefault();
setBusy(true); setError(null);
try { await authApi.resetPassword({ token, password }); setDone(true); }
catch (err) { setError(err instanceof ApiClientError ? err.message : 'Reset mislukt.'); }
finally { setBusy(false); }
}
if (done) {
return (
<AuthLayout title="Wachtwoord ingesteld ✅">
<p className="text-sm">Je kunt nu inloggen met je nieuwe wachtwoord.</p>
<button className="btn-primary mt-6 w-full" onClick={() => navigate('/login')}>Naar inloggen</button>
</AuthLayout>
);
}
return (
<AuthLayout title="Nieuw wachtwoord">
{!token && <p className="text-sm text-danger-700">Geen token in URL.</p>}
<form onSubmit={submit} className="space-y-4">
<Field label="Nieuw wachtwoord (min. 8 tekens)">
<input type="password" required minLength={8} className="input-field" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password" />
</Field>
{error && <p className="rounded-xl bg-danger-50 p-3 text-sm text-danger-700 dark:bg-danger-400/10 dark:text-danger-400">{error}</p>}
<button type="submit" className="btn-primary w-full py-3" disabled={busy || !token}>{busy ? 'Bezig…' : 'Opslaan'}</button>
</form>
<Link to="/login" className="mt-6 block text-sm text-brand-600 hover:underline">Terug naar inloggen</Link>
</AuthLayout>
);
}

View File

@@ -0,0 +1,43 @@
import { useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { authApi } from '../../api/auth.js';
import { ApiClientError } from '../../api/client.js';
import { AuthLayout } from './Login.js';
export function VerifyEmailPage() {
const [params] = useSearchParams();
const token = params.get('token');
const [state, setState] = useState<'pending' | 'ok' | 'err'>('pending');
const [message, setMessage] = useState('');
useEffect(() => {
if (!token) { setState('err'); setMessage('Token ontbreekt.'); return; }
(async () => {
try {
await authApi.verifyEmail({ token });
setState('ok');
} catch (e) {
setState('err');
setMessage(e instanceof ApiClientError ? e.message : 'Verificatie mislukt.');
}
})();
}, [token]);
return (
<AuthLayout title="E-mailverificatie">
{state === 'pending' && <p>Bezig met verifiëren</p>}
{state === 'ok' && (
<>
<p className="text-sm">Je e-mailadres is bevestigd </p>
<Link to="/login" className="btn-primary mt-6 inline-flex">Naar inloggen</Link>
</>
)}
{state === 'err' && (
<>
<p className="text-sm text-danger-700 dark:text-danger-400">{message}</p>
<Link to="/login" className="mt-6 inline-block text-sm text-brand-600 hover:underline">Terug naar inloggen</Link>
</>
)}
</AuthLayout>
);
}