diff --git a/backend/routers/auth.py b/backend/routers/auth.py index ec67cd6..6f83e92 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -323,3 +323,74 @@ async def verify_email(token: str): "email": prof['email'] } } + + +@router.post("/resend-verification") +@limiter.limit("3/hour") +async def resend_verification(req: dict, request: Request): + """Resend verification email for unverified account.""" + email = req.get('email', '').strip().lower() + + if not email: + raise HTTPException(400, "E-Mail-Adresse erforderlich") + + with get_db() as conn: + cur = get_cursor(conn) + + # Find profile by email + cur.execute(""" + SELECT id, name, email, email_verified, verification_token, verification_expires + FROM profiles + WHERE email=%s + """, (email,)) + + prof = cur.fetchone() + + if not prof: + # Don't leak info about existing emails + return {"ok": True, "message": "Falls ein Account mit dieser E-Mail existiert, wurde eine Bestätigungs-E-Mail versendet."} + + if prof['email_verified']: + raise HTTPException(400, "E-Mail-Adresse bereits bestätigt") + + # Generate new verification token + verification_token = secrets.token_urlsafe(32) + verification_expires = datetime.now(timezone.utc) + timedelta(hours=24) + + cur.execute(""" + UPDATE profiles + SET verification_token=%s, verification_expires=%s + WHERE id=%s + """, (verification_token, verification_expires, prof['id'])) + + # Send verification email + app_url = os.getenv("APP_URL", "https://mitai.jinkendo.de") + verify_url = f"{app_url}/verify?token={verification_token}" + + email_body = f"""Hallo {prof['name']}, + +du hast eine neue Bestätigungs-E-Mail angefordert. + +Bitte bestätige deine E-Mail-Adresse, indem du auf folgenden Link klickst: + +{verify_url} + +Dieser Link ist 24 Stunden gültig. + +Falls du diese E-Mail nicht angefordert hast, kannst du sie einfach ignorieren. + +Viele Grüße +Dein Mitai Jinkendo Team +""" + + try: + send_email( + to=email, + subject="Neue Bestätigungs-E-Mail - Mitai Jinkendo", + body=email_body + ) + except Exception as e: + print(f"Failed to send verification email: {e}") + raise HTTPException(500, "E-Mail konnte nicht versendet werden") + + return {"ok": True, "message": "Bestätigungs-E-Mail wurde erneut versendet."} diff --git a/frontend/src/components/EmailVerificationBanner.jsx b/frontend/src/components/EmailVerificationBanner.jsx index 302ffec..cf65c98 100644 --- a/frontend/src/components/EmailVerificationBanner.jsx +++ b/frontend/src/components/EmailVerificationBanner.jsx @@ -1,7 +1,33 @@ +import { useState } from 'react' +import { api } from '../utils/api' + export default function EmailVerificationBanner({ profile }) { + const [resending, setResending] = useState(false) + const [success, setSuccess] = useState(false) + const [error, setError] = useState(null) + // Only show if email is not verified if (!profile || profile.email_verified !== false) return null + const handleResend = async () => { + if (!profile.email) return + + setResending(true) + setError(null) + setSuccess(false) + + try { + await api.resendVerification(profile.email) + setSuccess(true) + setTimeout(() => setSuccess(false), 5000) + } catch (err) { + setError(err.message || 'Fehler beim Versenden') + setTimeout(() => setError(null), 5000) + } finally { + setResending(false) + } + } + return (
📧
-
+
Bitte prüfe dein Postfach und klicke auf den Bestätigungslink. - Ohne Bestätigung ist dein Account eingeschränkt. + {success && ( + + ✓ Neue E-Mail versendet! + + )} + {error && ( + + ✗ {error} + + )}
+
) } diff --git a/frontend/src/pages/Verify.jsx b/frontend/src/pages/Verify.jsx index 9be07b3..81a13e7 100644 --- a/frontend/src/pages/Verify.jsx +++ b/frontend/src/pages/Verify.jsx @@ -9,8 +9,11 @@ export default function Verify() { const navigate = useNavigate() const { login } = useAuth() - const [status, setStatus] = useState('loading') // loading | success | error + const [status, setStatus] = useState('loading') // loading | success | error | expired const [error, setError] = useState(null) + const [email, setEmail] = useState('') + const [resending, setResending] = useState(false) + const [resendSuccess, setResendSuccess] = useState(false) useEffect(() => { const verify = async () => { @@ -37,14 +40,41 @@ export default function Verify() { setError('Verifizierung erfolgreich, aber Login fehlgeschlagen') } } catch (err) { - setStatus('error') - setError(err.message || 'Verifizierung fehlgeschlagen') + const errorMsg = err.message || 'Verifizierung fehlgeschlagen' + + // Check if token expired + if (errorMsg.includes('abgelaufen') || errorMsg.includes('expired')) { + setStatus('expired') + setError(errorMsg) + } else { + setStatus('error') + setError(errorMsg) + } } } verify() }, [token, login, 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 || 'E-Mail konnte nicht versendet werden') + } finally { + setResending(false) + } + } + if (status === 'loading') { return (
+
+
+

+ Verifikations-Link abgelaufen +

+

+ Dieser Link ist leider nicht mehr gültig. Bitte fordere eine neue Bestätigungs-E-Mail an. +

+ + {resendSuccess ? ( +
+
+
+ E-Mail versendet! +
+

+ Bitte prüfe dein Postfach. +

+
+ ) : ( + <> + setEmail(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleResend()} + style={{width:'100%', boxSizing:'border-box', marginBottom:12}} + autoFocus + /> + + {error && ( +
+ {error} +
+ )} + + + + )} +
+ + +
+ ) + } + if (status === 'error') { return (
navigate('/register')} className="btn btn-primary btn-full" + style={{marginBottom:12}} > Zur Registrierung + +
) } diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 0204445..5cdba29 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -144,6 +144,7 @@ export const api = { changePin: (pin) => req('/auth/pin',json({pin})), register: (name,email,password) => req('/auth/register',json({name,email,password})), verifyEmail: (token) => req(`/auth/verify/${token}`), + resendVerification: (email) => req('/auth/resend-verification',json({email})), // v9c Subscription System // User-facing