fix: email verification flow and trial system
All checks were successful
Deploy Development / deploy (push) Successful in 35s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

Backend fixes:
- Fixed timezone-aware datetime comparison in verify_email endpoint
- Added trial_ends_at (14 days) for new registrations
- All datetime.now() calls now use timezone.utc

Frontend additions:
- Added EmailVerificationBanner component for unverified users
- Banner shows warning before trial banner in Dashboard
- Clear messaging about verification requirement

This fixes the 500 error on email verification and ensures new users
see both verification and trial status correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-21 10:20:06 +01:00
parent 49467ca6e9
commit 9fb6e27256
3 changed files with 54 additions and 7 deletions

View File

@ -7,7 +7,7 @@ import os
import secrets
import smtplib
from typing import Optional
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from email.mime.text import MIMEText
from fastapi import APIRouter, HTTPException, Header, Depends
@ -233,19 +233,20 @@ async def register(req: RegisterRequest, request: Request):
# Generate verification token
verification_token = secrets.token_urlsafe(32)
verification_expires = datetime.now() + timedelta(hours=24)
verification_expires = datetime.now(timezone.utc) + timedelta(hours=24)
# Create profile (inactive until verified)
profile_id = str(secrets.token_hex(16))
pin_hash = hash_pin(password)
trial_ends = datetime.now(timezone.utc) + timedelta(days=14) # 14-day trial
cur.execute("""
INSERT INTO profiles (
id, name, email, pin_hash, auth_type, role, tier,
email_verified, verification_token, verification_expires,
created
) VALUES (%s, %s, %s, %s, 'email', 'user', 'free', FALSE, %s, %s, CURRENT_TIMESTAMP)
""", (profile_id, name, email, pin_hash, verification_token, verification_expires))
trial_ends_at, created
) VALUES (%s, %s, %s, %s, 'email', 'user', 'free', FALSE, %s, %s, %s, CURRENT_TIMESTAMP)
""", (profile_id, name, email, pin_hash, verification_token, verification_expires, trial_ends))
# Send verification email
app_url = os.getenv("APP_URL", "https://mitai.jinkendo.de")
@ -294,7 +295,7 @@ async def verify_email(token: str):
raise HTTPException(400, "E-Mail-Adresse bereits bestätigt")
# Check if token expired
if prof['verification_expires'] and datetime.now() > prof['verification_expires']:
if prof['verification_expires'] and datetime.now(timezone.utc) > prof['verification_expires']:
raise HTTPException(400, "Verifikations-Link abgelaufen. Bitte registriere dich erneut.")
# Mark as verified and clear token
@ -306,7 +307,7 @@ async def verify_email(token: str):
# Create session (auto-login after verification)
session_token = make_token()
expires = datetime.now() + timedelta(days=30)
expires = datetime.now(timezone.utc) + timedelta(days=30)
cur.execute("""
INSERT INTO sessions (token, profile_id, expires_at, created)
VALUES (%s, %s, %s, CURRENT_TIMESTAMP)

View File

@ -0,0 +1,42 @@
export default function EmailVerificationBanner({ profile }) {
// Only show if email is not verified
if (!profile || profile.email_verified !== false) return null
return (
<div style={{
background: '#FFF4E6',
border: '2px solid #F59E0B',
borderRadius: 12,
padding: '16px 20px',
marginBottom: 20,
display: 'flex',
alignItems: 'center',
gap: 16
}}>
<div style={{
fontSize: 32,
lineHeight: 1
}}>
📧
</div>
<div style={{flex: 1}}>
<div style={{
fontWeight: 700,
fontSize: 15,
color: '#D97706',
marginBottom: 4
}}>
E-Mail-Adresse noch nicht bestätigt
</div>
<div style={{
fontSize: 13,
color: 'var(--text2)',
lineHeight: 1.4
}}>
Bitte prüfe dein Postfach und klicke auf den Bestätigungslink.
Ohne Bestätigung ist dein Account eingeschränkt.
</div>
</div>
</div>
)
}

View File

@ -9,6 +9,7 @@ import { api } from '../utils/api'
import { useProfile } from '../context/ProfileContext'
import { getBfCategory } from '../utils/calc'
import TrialBanner from '../components/TrialBanner'
import EmailVerificationBanner from '../components/EmailVerificationBanner'
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
import Markdown from '../utils/Markdown'
import dayjs from 'dayjs'
@ -316,6 +317,9 @@ export default function Dashboard() {
</div>
</div>
{/* Email Verification Banner */}
<EmailVerificationBanner profile={activeProfile}/>
{/* Trial Banner */}
<TrialBanner profile={activeProfile}/>