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 secrets
|
||||||
import smtplib
|
import smtplib
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Header, Depends
|
from fastapi import APIRouter, HTTPException, Header, Depends
|
||||||
|
|
@ -233,19 +233,20 @@ async def register(req: RegisterRequest, request: Request):
|
||||||
|
|
||||||
# Generate verification token
|
# Generate verification token
|
||||||
verification_token = secrets.token_urlsafe(32)
|
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)
|
# Create profile (inactive until verified)
|
||||||
profile_id = str(secrets.token_hex(16))
|
profile_id = str(secrets.token_hex(16))
|
||||||
pin_hash = hash_pin(password)
|
pin_hash = hash_pin(password)
|
||||||
|
trial_ends = datetime.now(timezone.utc) + timedelta(days=14) # 14-day trial
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO profiles (
|
INSERT INTO profiles (
|
||||||
id, name, email, pin_hash, auth_type, role, tier,
|
id, name, email, pin_hash, auth_type, role, tier,
|
||||||
email_verified, verification_token, verification_expires,
|
email_verified, verification_token, verification_expires,
|
||||||
created
|
trial_ends_at, created
|
||||||
) VALUES (%s, %s, %s, %s, 'email', 'user', 'free', FALSE, %s, %s, CURRENT_TIMESTAMP)
|
) VALUES (%s, %s, %s, %s, 'email', 'user', 'free', FALSE, %s, %s, %s, CURRENT_TIMESTAMP)
|
||||||
""", (profile_id, name, email, pin_hash, verification_token, verification_expires))
|
""", (profile_id, name, email, pin_hash, verification_token, verification_expires, trial_ends))
|
||||||
|
|
||||||
# Send verification email
|
# Send verification email
|
||||||
app_url = os.getenv("APP_URL", "https://mitai.jinkendo.de")
|
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")
|
raise HTTPException(400, "E-Mail-Adresse bereits bestätigt")
|
||||||
|
|
||||||
# Check if token expired
|
# 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.")
|
raise HTTPException(400, "Verifikations-Link abgelaufen. Bitte registriere dich erneut.")
|
||||||
|
|
||||||
# Mark as verified and clear token
|
# Mark as verified and clear token
|
||||||
|
|
@ -306,7 +307,7 @@ async def verify_email(token: str):
|
||||||
|
|
||||||
# Create session (auto-login after verification)
|
# Create session (auto-login after verification)
|
||||||
session_token = make_token()
|
session_token = make_token()
|
||||||
expires = datetime.now() + timedelta(days=30)
|
expires = datetime.now(timezone.utc) + timedelta(days=30)
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO sessions (token, profile_id, expires_at, created)
|
INSERT INTO sessions (token, profile_id, expires_at, created)
|
||||||
VALUES (%s, %s, %s, CURRENT_TIMESTAMP)
|
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 { useProfile } from '../context/ProfileContext'
|
||||||
import { getBfCategory } from '../utils/calc'
|
import { getBfCategory } from '../utils/calc'
|
||||||
import TrialBanner from '../components/TrialBanner'
|
import TrialBanner from '../components/TrialBanner'
|
||||||
|
import EmailVerificationBanner from '../components/EmailVerificationBanner'
|
||||||
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
|
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
|
||||||
import Markdown from '../utils/Markdown'
|
import Markdown from '../utils/Markdown'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
@ -316,6 +317,9 @@ export default function Dashboard() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Email Verification Banner */}
|
||||||
|
<EmailVerificationBanner profile={activeProfile}/>
|
||||||
|
|
||||||
{/* Trial Banner */}
|
{/* Trial Banner */}
|
||||||
<TrialBanner profile={activeProfile}/>
|
<TrialBanner profile={activeProfile}/>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user