- Added EmailVerificationBanner component to notify users about unverified email status and provide a resend verification option. - Introduced VerifyPage for handling email verification via a token in the URL, including success and error handling. - Updated LoginPage and AccountSettingsPage to allow users to resend verification emails directly from these pages. - Enhanced API utility with new functions for verifying emails and resending verification requests. - Updated routing to include the new verification page and improved link structure for verification links.
183 lines
6.3 KiB
JavaScript
183 lines
6.3 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { useSearchParams, useNavigate } from 'react-router-dom'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import api from '../utils/api'
|
|
|
|
/**
|
|
* E-Mail-Bestätigung (?token=) — gleiches Muster wie Mitai: SPA ruft /api/auth/verify/{token} auf.
|
|
*/
|
|
export default function VerifyPage() {
|
|
const [searchParams] = useSearchParams()
|
|
const token = searchParams.get('token')
|
|
const navigate = useNavigate()
|
|
const { checkAuth } = useAuth()
|
|
|
|
const [status, setStatus] = useState('loading')
|
|
const [error, setError] = useState(null)
|
|
const [email, setEmail] = useState('')
|
|
const [resending, setResending] = useState(false)
|
|
const [resendSuccess, setResendSuccess] = useState(false)
|
|
const [hasStarted, setHasStarted] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (hasStarted) return
|
|
if (!token?.trim()) {
|
|
setStatus('error')
|
|
setError('Kein Verifikations-Token in der URL')
|
|
return
|
|
}
|
|
setHasStarted(true)
|
|
|
|
const run = async () => {
|
|
try {
|
|
const result = await api.verifyEmail(token.trim())
|
|
if (result.token) {
|
|
localStorage.setItem('authToken', result.token)
|
|
await checkAuth()
|
|
}
|
|
setStatus('success')
|
|
setTimeout(() => navigate('/', { replace: true }), 1500)
|
|
} catch (err) {
|
|
const msg = err.message || 'Verifizierung fehlgeschlagen'
|
|
|
|
if (msg.includes('bereits bestätigt') || msg.includes('bereits verwendet')) {
|
|
setStatus('already_verified')
|
|
setError(msg)
|
|
setTimeout(() => navigate('/login', { replace: true }), 3500)
|
|
} else if (msg.includes('abgelaufen')) {
|
|
setStatus('expired')
|
|
setError(msg)
|
|
} else {
|
|
setStatus('error')
|
|
setError(msg)
|
|
}
|
|
}
|
|
}
|
|
run()
|
|
}, [token, hasStarted, checkAuth, navigate])
|
|
|
|
const handleResend = async () => {
|
|
if (!email.trim()) {
|
|
setError('Bitte E-Mail-Adresse eingeben')
|
|
return
|
|
}
|
|
setResending(true)
|
|
setError(null)
|
|
try {
|
|
await api.resendVerification(email.trim().toLowerCase())
|
|
setResendSuccess(true)
|
|
} catch (err) {
|
|
setError(err.message || 'Versand fehlgeschlagen')
|
|
} finally {
|
|
setResending(false)
|
|
}
|
|
}
|
|
|
|
if (status === 'loading') {
|
|
return (
|
|
<div style={{ maxWidth: 420, margin: '0 auto', padding: '3rem 1rem', textAlign: 'center' }}>
|
|
<div className="spinner" style={{ width: 48, height: 48, margin: '0 auto 1rem' }} />
|
|
<h2 style={{ fontSize: '1.25rem' }}>E-Mail wird bestätigt…</h2>
|
|
<p style={{ color: 'var(--text2)' }}>Einen Moment bitte.</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (status === 'expired') {
|
|
return (
|
|
<div style={{ maxWidth: 420, margin: '0 auto', padding: '2rem 1rem' }}>
|
|
<div className="card" style={{ marginBottom: '1rem' }}>
|
|
<h2 style={{ fontSize: '1.15rem', marginBottom: '0.5rem' }}>Link abgelaufen</h2>
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.9rem', lineHeight: 1.5 }}>
|
|
{error || 'Bitte fordere eine neue Bestätigungs-E-Mail an.'}
|
|
</p>
|
|
{resendSuccess ? (
|
|
<p style={{ color: 'var(--accent)', fontWeight: 600, marginTop: '1rem' }}>E-Mail unterwegs — Postfach prüfen.</p>
|
|
) : (
|
|
<>
|
|
<label className="form-label" style={{ marginTop: '1rem' }}>
|
|
Registrierte E-Mail
|
|
</label>
|
|
<input
|
|
type="email"
|
|
className="form-input"
|
|
placeholder="du@beispiel.de"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
autoComplete="email"
|
|
/>
|
|
{error && !resendSuccess && (
|
|
<p style={{ color: 'var(--danger)', fontSize: '0.85rem', marginTop: '0.5rem' }}>{error}</p>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary btn-full"
|
|
style={{ marginTop: '0.85rem' }}
|
|
disabled={resending || !email.trim()}
|
|
onClick={handleResend}
|
|
>
|
|
{resending ? 'Sende…' : 'Erneut senden'}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate('/login')}
|
|
style={{
|
|
background: 'none',
|
|
border: 'none',
|
|
color: 'var(--text3)',
|
|
textDecoration: 'underline',
|
|
cursor: 'pointer',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
Zum Login
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (status === 'already_verified') {
|
|
return (
|
|
<div style={{ maxWidth: 420, margin: '0 auto', padding: '2rem 1rem', textAlign: 'center' }}>
|
|
<div className="card">
|
|
<h2 style={{ fontSize: '1.15rem' }}>Schon erledigt</h2>
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.92rem', marginTop: '0.5rem' }}>
|
|
{error || 'E-Mail bereits bestätigt — du kannst dich anmelden.'}
|
|
</p>
|
|
<p style={{ fontSize: '0.82rem', color: 'var(--text3)', marginTop: '0.75rem' }}>Weiterleitung zum Login…</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (status === 'error') {
|
|
return (
|
|
<div style={{ maxWidth: 420, margin: '0 auto', padding: '2rem 1rem', textAlign: 'center' }}>
|
|
<div className="card" style={{ borderColor: 'var(--danger)' }}>
|
|
<h2 style={{ fontSize: '1.15rem', color: 'var(--danger)' }}>Konnte nicht bestätigen</h2>
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.92rem', marginTop: '0.5rem' }}>{error}</p>
|
|
<button type="button" className="btn btn-primary btn-full" style={{ marginTop: '1rem' }} onClick={() => navigate('/login')}>
|
|
Zum Login
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* success */
|
|
return (
|
|
<div style={{ maxWidth: 420, margin: '0 auto', padding: '2rem 1rem', textAlign: 'center' }}>
|
|
<div className="card">
|
|
<h2 style={{ fontSize: '1.15rem', color: 'var(--accent-dark)', marginBottom: '0.5rem' }}>
|
|
Bestätigung erfolgreich
|
|
</h2>
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.92rem' }}>Du wirst weitergeleitet…</p>
|
|
<div className="spinner" style={{ width: 32, height: 32, margin: '1rem auto 0' }} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|