feat(frontend): VerifyEmail + ForgotPassword + ResetPassword + AcceptInvite pages
This commit is contained in:
45
packages/frontend/src/pages/auth/AcceptInvite.tsx
Normal file
45
packages/frontend/src/pages/auth/AcceptInvite.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
40
packages/frontend/src/pages/auth/ForgotPassword.tsx
Normal file
40
packages/frontend/src/pages/auth/ForgotPassword.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
46
packages/frontend/src/pages/auth/ResetPassword.tsx
Normal file
46
packages/frontend/src/pages/auth/ResetPassword.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
43
packages/frontend/src/pages/auth/VerifyEmail.tsx
Normal file
43
packages/frontend/src/pages/auth/VerifyEmail.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user