fix: email verification flow and trial system
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:
parent
49467ca6e9
commit
9fb6e27256
|
|
@ -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)
|
||||
|
|
|
|||
42
frontend/src/components/EmailVerificationBanner.jsx
Normal file
42
frontend/src/components/EmailVerificationBanner.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}/>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user