Feature: E-Mail-Verifikation + Forum öffentlich lesbar + Launch-Vorbereitung
- Forum ohne requiresAuth: öffentlich lesbar, Schreiben weiter via API-Guard
- E-Mail-Verifikation: Token bei Registrierung, support@-Mail, /verify-email/{token}
- Verifikations-Banner (orange, dismissible) wenn email_verified=0
- Grüner Haken / "Nicht bestätigt"-Chip in Settings
- POST /auth/resend-verification für Chip und Banner
- DB-Migration: users.verification_token TEXT
- SW by-v575, APP_VER 552
This commit is contained in:
parent
e79290edb7
commit
b9ee67b8dd
7 changed files with 137 additions and 11 deletions
|
|
@ -6,6 +6,7 @@ import string
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, Depends
|
||||
from fastapi.responses import RedirectResponse
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from database import db
|
||||
from auth import (
|
||||
|
|
@ -16,7 +17,28 @@ from username_blocklist import is_username_blocked
|
|||
from ratelimit import check as rl_check
|
||||
|
||||
router = APIRouter()
|
||||
COOKIE_NAME = "by_token"
|
||||
COOKIE_NAME = "by_token"
|
||||
_APP_URL = os.getenv("APP_URL", "https://banyaro.app")
|
||||
_SMTP_READY = bool(os.getenv("SMTP_SUPPORT_USER") and os.getenv("SMTP_SUPPORT_PASS"))
|
||||
|
||||
|
||||
def _send_verification_email(email: str, name: str, token: str):
|
||||
if not _SMTP_READY:
|
||||
return
|
||||
from routes.outreach import _send_smtp
|
||||
subject = "Ban Yaro — bitte bestätige deine E-Mail-Adresse"
|
||||
body = (
|
||||
f"Hallo {name},\n\n"
|
||||
"willkommen bei Ban Yaro! Bitte bestätige deine E-Mail-Adresse mit diesem Link:\n\n"
|
||||
f"{_APP_URL}/api/auth/verify-email/{token}\n\n"
|
||||
"Der Link ist 7 Tage gültig.\n\n"
|
||||
"Falls du dich nicht bei Ban Yaro registriert hast, kannst du diese Mail ignorieren.\n\n"
|
||||
"Viele Grüße,\nDas Ban Yaro Team\nbanyaro.app"
|
||||
)
|
||||
try:
|
||||
_send_smtp(email, subject, body, "support")
|
||||
except Exception:
|
||||
pass # Nicht blockieren wenn SMTP fehlschlägt
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
|
|
@ -64,13 +86,13 @@ async def register(data: RegisterRequest, response: Response, request: Request):
|
|||
).fetchone():
|
||||
raise HTTPException(400, "Dieser Name ist bereits vergeben. Bitte wähle einen anderen.")
|
||||
code = _gen_referral_code()
|
||||
verify_token = secrets.token_urlsafe(32)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO users (email, pw_hash, name, referral_code) VALUES (?,?,?,?)",
|
||||
(data.email, hash_password(data.password), name, code)
|
||||
"INSERT INTO users (email, pw_hash, name, referral_code, verification_token) VALUES (?,?,?,?,?)",
|
||||
(data.email, hash_password(data.password), name, code, verify_token)
|
||||
)
|
||||
except Exception:
|
||||
# Fallback falls UNIQUE-Index greift (Race Condition)
|
||||
raise HTTPException(400, "Dieser Name ist bereits vergeben. Bitte wähle einen anderen.")
|
||||
user = conn.execute(
|
||||
"SELECT id, rolle FROM users WHERE email=?", (data.email,)
|
||||
|
|
@ -116,7 +138,8 @@ async def register(data: RegisterRequest, response: Response, request: Request):
|
|||
|
||||
token = create_token(user["id"], user["rolle"])
|
||||
_set_cookie(response, token)
|
||||
return {"token": token, "name": name}
|
||||
_send_verification_email(data.email, name, verify_token)
|
||||
return {"token": token, "name": name, "email_verified": 0}
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
|
|
@ -206,3 +229,37 @@ async def me(user=Depends(get_current_user)):
|
|||
data = dict(row)
|
||||
data["is_premium"] = bool(data["is_premium"])
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/verify-email/{token}")
|
||||
async def verify_email(token: str):
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id, email_verified FROM users WHERE verification_token=?", (token,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
return RedirectResponse(f"{_APP_URL}/#settings?verified=error", status_code=302)
|
||||
conn.execute(
|
||||
"UPDATE users SET email_verified=1, verification_token=NULL WHERE id=?",
|
||||
(row["id"],)
|
||||
)
|
||||
return RedirectResponse(f"{_APP_URL}/#settings?verified=1", status_code=302)
|
||||
|
||||
|
||||
@router.post("/resend-verification")
|
||||
async def resend_verification(user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT email, name, email_verified FROM users WHERE id=?", (user["id"],)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404)
|
||||
if row["email_verified"]:
|
||||
return {"ok": True, "already_verified": True}
|
||||
token = secrets.token_urlsafe(32)
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"UPDATE users SET verification_token=? WHERE id=?", (token, user["id"])
|
||||
)
|
||||
_send_verification_email(row["email"], row["name"], token)
|
||||
return {"ok": True}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue