Compare commits
No commits in common. "86f7a513fec6bca599db4e2cb1b0aa23b236f35d" and "888b5c3e408d586cf8e1dde7bbb73431958a33bf" have entirely different histories.
86f7a513fe
...
888b5c3e40
|
|
@ -1,25 +0,0 @@
|
||||||
-- ================================================================
|
|
||||||
-- Migration 003: Add Email Verification Fields
|
|
||||||
-- Version: v9c
|
|
||||||
-- Date: 2026-03-21
|
|
||||||
-- ================================================================
|
|
||||||
|
|
||||||
-- Add email verification columns to profiles table
|
|
||||||
ALTER TABLE profiles
|
|
||||||
ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE,
|
|
||||||
ADD COLUMN IF NOT EXISTS verification_token TEXT,
|
|
||||||
ADD COLUMN IF NOT EXISTS verification_expires TIMESTAMP WITH TIME ZONE;
|
|
||||||
|
|
||||||
-- Create index for verification token lookups
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_profiles_verification_token
|
|
||||||
ON profiles(verification_token)
|
|
||||||
WHERE verification_token IS NOT NULL;
|
|
||||||
|
|
||||||
-- Mark existing users with email as verified (grandfather clause)
|
|
||||||
UPDATE profiles
|
|
||||||
SET email_verified = TRUE
|
|
||||||
WHERE email IS NOT NULL AND email_verified IS NULL;
|
|
||||||
|
|
||||||
COMMENT ON COLUMN profiles.email_verified IS 'Whether email address has been verified';
|
|
||||||
COMMENT ON COLUMN profiles.verification_token IS 'One-time token for email verification';
|
|
||||||
COMMENT ON COLUMN profiles.verification_expires IS 'Verification token expiry (24h from creation)';
|
|
||||||
|
|
@ -110,12 +110,6 @@ class PasswordResetConfirm(BaseModel):
|
||||||
new_password: str
|
new_password: str
|
||||||
|
|
||||||
|
|
||||||
class RegisterRequest(BaseModel):
|
|
||||||
name: str
|
|
||||||
email: str
|
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
# ── Admin Models ──────────────────────────────────────────────────────────────
|
# ── Admin Models ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class AdminProfileUpdate(BaseModel):
|
class AdminProfileUpdate(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from slowapi.util import get_remote_address
|
||||||
|
|
||||||
from db import get_db, get_cursor
|
from db import get_db, get_cursor
|
||||||
from auth import hash_pin, verify_pin, make_token, require_auth
|
from auth import hash_pin, verify_pin, make_token, require_auth
|
||||||
from models import LoginRequest, PasswordResetRequest, PasswordResetConfirm, RegisterRequest
|
from models import LoginRequest, PasswordResetRequest, PasswordResetConfirm
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||||
limiter = Limiter(key_func=get_remote_address)
|
limiter = Limiter(key_func=get_remote_address)
|
||||||
|
|
@ -174,151 +174,3 @@ def password_reset_confirm(req: PasswordResetConfirm):
|
||||||
cur.execute("DELETE FROM sessions WHERE token=%s", (f"reset_{req.token}",))
|
cur.execute("DELETE FROM sessions WHERE token=%s", (f"reset_{req.token}",))
|
||||||
|
|
||||||
return {"ok": True, "message": "Passwort erfolgreich zurückgesetzt"}
|
return {"ok": True, "message": "Passwort erfolgreich zurückgesetzt"}
|
||||||
|
|
||||||
|
|
||||||
# ── Helper: Send Email ────────────────────────────────────────────────────────
|
|
||||||
def send_email(to_email: str, subject: str, body: str):
|
|
||||||
"""Send email via SMTP (reusable helper)."""
|
|
||||||
try:
|
|
||||||
smtp_host = os.getenv("SMTP_HOST")
|
|
||||||
smtp_port = int(os.getenv("SMTP_PORT", 587))
|
|
||||||
smtp_user = os.getenv("SMTP_USER")
|
|
||||||
smtp_pass = os.getenv("SMTP_PASS")
|
|
||||||
smtp_from = os.getenv("SMTP_FROM", "noreply@jinkendo.de")
|
|
||||||
|
|
||||||
if not smtp_host or not smtp_user or not smtp_pass:
|
|
||||||
print("SMTP not configured, skipping email")
|
|
||||||
return False
|
|
||||||
|
|
||||||
msg = MIMEText(body)
|
|
||||||
msg['Subject'] = subject
|
|
||||||
msg['From'] = smtp_from
|
|
||||||
msg['To'] = to_email
|
|
||||||
|
|
||||||
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
|
||||||
server.starttls()
|
|
||||||
server.login(smtp_user, smtp_pass)
|
|
||||||
server.send_message(msg)
|
|
||||||
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Email error: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# ── Registration Endpoints ────────────────────────────────────────────────────
|
|
||||||
@router.post("/register")
|
|
||||||
@limiter.limit("3/hour")
|
|
||||||
async def register(req: RegisterRequest, request: Request):
|
|
||||||
"""Self-registration with email verification."""
|
|
||||||
email = req.email.lower().strip()
|
|
||||||
name = req.name.strip()
|
|
||||||
password = req.password
|
|
||||||
|
|
||||||
# Validation
|
|
||||||
if not email or '@' not in email:
|
|
||||||
raise HTTPException(400, "Ungültige E-Mail-Adresse")
|
|
||||||
if len(password) < 8:
|
|
||||||
raise HTTPException(400, "Passwort muss mindestens 8 Zeichen lang sein")
|
|
||||||
if not name or len(name) < 2:
|
|
||||||
raise HTTPException(400, "Name muss mindestens 2 Zeichen lang sein")
|
|
||||||
|
|
||||||
with get_db() as conn:
|
|
||||||
cur = get_cursor(conn)
|
|
||||||
|
|
||||||
# Check if email already exists
|
|
||||||
cur.execute("SELECT id FROM profiles WHERE email=%s", (email,))
|
|
||||||
if cur.fetchone():
|
|
||||||
raise HTTPException(400, "E-Mail-Adresse bereits registriert")
|
|
||||||
|
|
||||||
# Generate verification token
|
|
||||||
verification_token = secrets.token_urlsafe(32)
|
|
||||||
verification_expires = datetime.now() + timedelta(hours=24)
|
|
||||||
|
|
||||||
# Create profile (inactive until verified)
|
|
||||||
profile_id = str(secrets.token_hex(16))
|
|
||||||
pin_hash = hash_pin(password)
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
# 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 {name},
|
|
||||||
|
|
||||||
willkommen bei Mitai Jinkendo!
|
|
||||||
|
|
||||||
Bitte bestätige deine E-Mail-Adresse um die Registrierung abzuschließen:
|
|
||||||
|
|
||||||
{verify_url}
|
|
||||||
|
|
||||||
Der Link ist 24 Stunden gültig.
|
|
||||||
|
|
||||||
Dein Mitai Jinkendo Team
|
|
||||||
"""
|
|
||||||
|
|
||||||
send_email(email, "Willkommen bei Mitai Jinkendo – E-Mail bestätigen", email_body)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"message": "Registrierung erfolgreich! Bitte prüfe dein E-Mail-Postfach und bestätige deine E-Mail-Adresse."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/verify/{token}")
|
|
||||||
async def verify_email(token: str):
|
|
||||||
"""Verify email address and activate account."""
|
|
||||||
with get_db() as conn:
|
|
||||||
cur = get_cursor(conn)
|
|
||||||
|
|
||||||
# Find profile with this verification token
|
|
||||||
cur.execute("""
|
|
||||||
SELECT id, name, email, email_verified, verification_expires
|
|
||||||
FROM profiles
|
|
||||||
WHERE verification_token=%s
|
|
||||||
""", (token,))
|
|
||||||
|
|
||||||
prof = cur.fetchone()
|
|
||||||
|
|
||||||
if not prof:
|
|
||||||
raise HTTPException(400, "Ungültiger Verifikations-Link")
|
|
||||||
|
|
||||||
if prof['email_verified']:
|
|
||||||
raise HTTPException(400, "E-Mail-Adresse bereits bestätigt")
|
|
||||||
|
|
||||||
# Check if token expired
|
|
||||||
if prof['verification_expires'] and datetime.now() > prof['verification_expires']:
|
|
||||||
raise HTTPException(400, "Verifikations-Link abgelaufen. Bitte registriere dich erneut.")
|
|
||||||
|
|
||||||
# Mark as verified and clear token
|
|
||||||
cur.execute("""
|
|
||||||
UPDATE profiles
|
|
||||||
SET email_verified=TRUE, verification_token=NULL, verification_expires=NULL
|
|
||||||
WHERE id=%s
|
|
||||||
""", (prof['id'],))
|
|
||||||
|
|
||||||
# Create session (auto-login after verification)
|
|
||||||
session_token = make_token()
|
|
||||||
expires = datetime.now() + timedelta(days=30)
|
|
||||||
cur.execute("""
|
|
||||||
INSERT INTO sessions (token, profile_id, expires_at, created)
|
|
||||||
VALUES (%s, %s, %s, CURRENT_TIMESTAMP)
|
|
||||||
""", (session_token, prof['id'], expires))
|
|
||||||
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"message": "E-Mail-Adresse erfolgreich bestätigt!",
|
|
||||||
"token": session_token,
|
|
||||||
"profile": {
|
|
||||||
"id": prof['id'],
|
|
||||||
"name": prof['name'],
|
|
||||||
"email": prof['email']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import { Avatar } from './pages/ProfileSelect'
|
||||||
import SetupScreen from './pages/SetupScreen'
|
import SetupScreen from './pages/SetupScreen'
|
||||||
import { ResetPassword } from './pages/PasswordRecovery'
|
import { ResetPassword } from './pages/PasswordRecovery'
|
||||||
import LoginScreen from './pages/LoginScreen'
|
import LoginScreen from './pages/LoginScreen'
|
||||||
import Register from './pages/Register'
|
|
||||||
import Verify from './pages/Verify'
|
|
||||||
import Dashboard from './pages/Dashboard'
|
import Dashboard from './pages/Dashboard'
|
||||||
import CaptureHub from './pages/CaptureHub'
|
import CaptureHub from './pages/CaptureHub'
|
||||||
import WeightScreen from './pages/WeightScreen'
|
import WeightScreen from './pages/WeightScreen'
|
||||||
|
|
@ -61,26 +59,9 @@ function AppShell() {
|
||||||
}
|
}
|
||||||
}, [session?.profile_id])
|
}, [session?.profile_id])
|
||||||
|
|
||||||
// Handle public pages (register, verify, reset-password)
|
// Handle password reset link
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const currentPath = window.location.pathname
|
const resetToken = urlParams.get('reset-password') || (window.location.pathname === '/reset-password' ? urlParams.get('token') : null)
|
||||||
|
|
||||||
// Register page
|
|
||||||
if (currentPath === '/register') return (
|
|
||||||
<div style={{minHeight:'100vh',background:'var(--bg)',padding:24}}>
|
|
||||||
<Register/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verify email page
|
|
||||||
if (currentPath === '/verify') return (
|
|
||||||
<div style={{minHeight:'100vh',background:'var(--bg)',padding:24}}>
|
|
||||||
<Verify/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// Password reset page
|
|
||||||
const resetToken = urlParams.get('reset-password') || (currentPath === '/reset-password' ? urlParams.get('token') : null)
|
|
||||||
if (resetToken) return (
|
if (resetToken) return (
|
||||||
<div style={{minHeight:'100vh',display:'flex',alignItems:'center',justifyContent:'center',
|
<div style={{minHeight:'100vh',display:'flex',alignItems:'center',justifyContent:'center',
|
||||||
background:'var(--bg)',padding:24}}>
|
background:'var(--bg)',padding:24}}>
|
||||||
|
|
|
||||||
|
|
@ -105,22 +105,6 @@ export default function LoginScreen() {
|
||||||
textAlign:'center',padding:'4px 0',textDecoration:'underline'}}>
|
textAlign:'center',padding:'4px 0',textDecoration:'underline'}}>
|
||||||
Passwort vergessen?
|
Passwort vergessen?
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div style={{
|
|
||||||
marginTop:24, paddingTop:20,
|
|
||||||
borderTop:'1px solid var(--border)',
|
|
||||||
textAlign:'center'
|
|
||||||
}}>
|
|
||||||
<span style={{color:'var(--text2)',fontSize:14}}>
|
|
||||||
Noch kein Account?{' '}
|
|
||||||
</span>
|
|
||||||
<a href="/register" style={{
|
|
||||||
color:'var(--accent)',fontWeight:600,fontSize:14,
|
|
||||||
textDecoration:'none'
|
|
||||||
}}>
|
|
||||||
Jetzt registrieren
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{textAlign:'center',marginTop:20,fontSize:11,color:'var(--text3)'}}>
|
<div style={{textAlign:'center',marginTop:20,fontSize:11,color:'var(--text3)'}}>
|
||||||
|
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { api } from '../utils/api'
|
|
||||||
|
|
||||||
export default function Register() {
|
|
||||||
const [name, setName] = useState('')
|
|
||||||
const [email, setEmail] = useState('')
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
const [passwordConfirm, setPasswordConfirm] = useState('')
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState(null)
|
|
||||||
const [success, setSuccess] = useState(false)
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!name || name.length < 2) {
|
|
||||||
setError('Name muss mindestens 2 Zeichen lang sein')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!email || !email.includes('@')) {
|
|
||||||
setError('Ungültige E-Mail-Adresse')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (password.length < 8) {
|
|
||||||
setError('Passwort muss mindestens 8 Zeichen lang sein')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (password !== passwordConfirm) {
|
|
||||||
setError('Passwörter stimmen nicht überein')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
await api.register(name, email, password)
|
|
||||||
setSuccess(true)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message || 'Registrierung fehlgeschlagen')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
maxWidth:420, margin:'0 auto', padding:'60px 20px',
|
|
||||||
textAlign:'center'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
background:'var(--accent-light)',
|
|
||||||
border:'2px solid var(--accent)',
|
|
||||||
borderRadius:16, padding:'40px 24px', marginBottom:24
|
|
||||||
}}>
|
|
||||||
<div style={{fontSize:48, marginBottom:16}}>✓</div>
|
|
||||||
<h2 style={{marginBottom:12, color:'var(--accent-dark)'}}>
|
|
||||||
Registrierung erfolgreich!
|
|
||||||
</h2>
|
|
||||||
<p style={{color:'var(--text2)', lineHeight:1.6}}>
|
|
||||||
Wir haben dir eine E-Mail mit einem Bestätigungslink gesendet.
|
|
||||||
Bitte prüfe dein Postfach und bestätige deine E-Mail-Adresse.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link to="/login" className="btn btn-primary btn-full">
|
|
||||||
Zum Login
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<p style={{fontSize:12, color:'var(--text3)', marginTop:20}}>
|
|
||||||
Keine E-Mail erhalten? Prüfe auch deinen Spam-Ordner.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{maxWidth:420, margin:'0 auto', padding:'40px 20px'}}>
|
|
||||||
<h1 style={{textAlign:'center', marginBottom:8}}>Registrierung</h1>
|
|
||||||
<p style={{textAlign:'center', color:'var(--text2)', marginBottom:32}}>
|
|
||||||
Erstelle deinen Mitai Jinkendo Account
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="card" style={{padding:24}}>
|
|
||||||
{error && (
|
|
||||||
<div style={{
|
|
||||||
padding:'12px 16px', background:'#FCEBEB',
|
|
||||||
border:'1px solid #D85A30', borderRadius:8,
|
|
||||||
color:'#D85A30', fontSize:14, marginBottom:20
|
|
||||||
}}>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="form-row">
|
|
||||||
<label className="form-label">Name *</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-input"
|
|
||||||
value={name}
|
|
||||||
onChange={e => setName(e.target.value)}
|
|
||||||
placeholder="Dein Name"
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-row">
|
|
||||||
<label className="form-label">E-Mail *</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
className="form-input"
|
|
||||||
value={email}
|
|
||||||
onChange={e => setEmail(e.target.value)}
|
|
||||||
placeholder="deine@email.de"
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-row">
|
|
||||||
<label className="form-label">Passwort *</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="form-input"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
placeholder="Mindestens 8 Zeichen"
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-row">
|
|
||||||
<label className="form-label">Passwort bestätigen *</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="form-input"
|
|
||||||
value={passwordConfirm}
|
|
||||||
onChange={e => setPasswordConfirm(e.target.value)}
|
|
||||||
placeholder="Passwort wiederholen"
|
|
||||||
disabled={loading}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-primary btn-full"
|
|
||||||
disabled={loading}
|
|
||||||
style={{marginTop:8}}
|
|
||||||
>
|
|
||||||
{loading ? 'Registriere...' : 'Registrieren'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
textAlign:'center', marginTop:24,
|
|
||||||
paddingTop:20, borderTop:'1px solid var(--border)'
|
|
||||||
}}>
|
|
||||||
<span style={{color:'var(--text2)', fontSize:14}}>
|
|
||||||
Bereits registriert?{' '}
|
|
||||||
</span>
|
|
||||||
<Link to="/login" style={{
|
|
||||||
color:'var(--accent)', fontWeight:600, fontSize:14
|
|
||||||
}}>
|
|
||||||
Zum Login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p style={{
|
|
||||||
fontSize:11, color:'var(--text3)', textAlign:'center',
|
|
||||||
marginTop:20, lineHeight:1.6
|
|
||||||
}}>
|
|
||||||
Mit der Registrierung akzeptierst du unsere Nutzungsbedingungen
|
|
||||||
und Datenschutzerklärung.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
import { useState, useEffect, useContext } from 'react'
|
|
||||||
import { useSearchParams, useNavigate } from 'react-router-dom'
|
|
||||||
import { AuthContext } from '../context/AuthContext'
|
|
||||||
import { api } from '../utils/api'
|
|
||||||
|
|
||||||
export default function Verify() {
|
|
||||||
const [searchParams] = useSearchParams()
|
|
||||||
const token = searchParams.get('token')
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { login } = useContext(AuthContext)
|
|
||||||
|
|
||||||
const [status, setStatus] = useState('loading') // loading | success | error
|
|
||||||
const [error, setError] = useState(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const verify = async () => {
|
|
||||||
if (!token) {
|
|
||||||
setStatus('error')
|
|
||||||
setError('Kein Verifikations-Token gefunden')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await api.verifyEmail(token)
|
|
||||||
|
|
||||||
// Auto-login with returned token
|
|
||||||
if (result.token) {
|
|
||||||
login(result.token, result.profile)
|
|
||||||
setStatus('success')
|
|
||||||
|
|
||||||
// Redirect to dashboard after 2 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
navigate('/dashboard')
|
|
||||||
}, 2000)
|
|
||||||
} else {
|
|
||||||
setStatus('error')
|
|
||||||
setError('Verifizierung erfolgreich, aber Login fehlgeschlagen')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setStatus('error')
|
|
||||||
setError(err.message || 'Verifizierung fehlgeschlagen')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify()
|
|
||||||
}, [token, login, navigate])
|
|
||||||
|
|
||||||
if (status === 'loading') {
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
maxWidth:420, margin:'0 auto', padding:'60px 20px',
|
|
||||||
textAlign:'center'
|
|
||||||
}}>
|
|
||||||
<div className="spinner" style={{width:48, height:48, margin:'0 auto 24px'}}/>
|
|
||||||
<h2>E-Mail wird bestätigt...</h2>
|
|
||||||
<p style={{color:'var(--text2)'}}>Einen Moment bitte</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'error') {
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
maxWidth:420, margin:'0 auto', padding:'60px 20px',
|
|
||||||
textAlign:'center'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
background:'#FCEBEB',
|
|
||||||
border:'2px solid #D85A30',
|
|
||||||
borderRadius:16, padding:'40px 24px', marginBottom:24
|
|
||||||
}}>
|
|
||||||
<div style={{fontSize:48, marginBottom:16}}>✗</div>
|
|
||||||
<h2 style={{marginBottom:12, color:'#D85A30'}}>
|
|
||||||
Verifizierung fehlgeschlagen
|
|
||||||
</h2>
|
|
||||||
<p style={{color:'var(--text2)', lineHeight:1.6}}>
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => navigate('/register')}
|
|
||||||
className="btn btn-primary btn-full"
|
|
||||||
>
|
|
||||||
Zur Registrierung
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
maxWidth:420, margin:'0 auto', padding:'60px 20px',
|
|
||||||
textAlign:'center'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
background:'var(--accent-light)',
|
|
||||||
border:'2px solid var(--accent)',
|
|
||||||
borderRadius:16, padding:'40px 24px', marginBottom:24
|
|
||||||
}}>
|
|
||||||
<div style={{fontSize:48, marginBottom:16}}>✓</div>
|
|
||||||
<h2 style={{marginBottom:12, color:'var(--accent-dark)'}}>
|
|
||||||
E-Mail bestätigt!
|
|
||||||
</h2>
|
|
||||||
<p style={{color:'var(--text2)', lineHeight:1.6}}>
|
|
||||||
Dein Account wurde erfolgreich aktiviert.
|
|
||||||
Du wirst gleich zum Dashboard weitergeleitet...
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="spinner" style={{width:32, height:32, margin:'0 auto'}}/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -142,8 +142,6 @@ export const api = {
|
||||||
adminDeleteProfile: (id) => req(`/admin/profiles/${id}`,{method:'DELETE'}),
|
adminDeleteProfile: (id) => req(`/admin/profiles/${id}`,{method:'DELETE'}),
|
||||||
adminSetPermissions: (id,d) => req(`/admin/profiles/${id}/permissions`,jput(d)),
|
adminSetPermissions: (id,d) => req(`/admin/profiles/${id}/permissions`,jput(d)),
|
||||||
changePin: (pin) => req('/auth/pin',json({pin})),
|
changePin: (pin) => req('/auth/pin',json({pin})),
|
||||||
register: (name,email,password) => req('/auth/register',json({name,email,password})),
|
|
||||||
verifyEmail: (token) => req(`/auth/verify/${token}`),
|
|
||||||
|
|
||||||
// v9c Subscription System
|
// v9c Subscription System
|
||||||
// User-facing
|
// User-facing
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user