"""Auth-Router: Magic Link Login für Globe.""" import logging from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, EmailStr from auth import create_token, generate_magic_token, get_current_user from config import GLOBE_BASE_URL, MAGIC_LINK_EXPIRE_MINUTES from database import get_db from email_utils import send_magic_link_email logger = logging.getLogger("globe.auth") router = APIRouter(prefix="/auth", tags=["auth"]) class LoginRequest(BaseModel): email: EmailStr @router.post("/request-link") async def request_magic_link(req: LoginRequest, db=Depends(get_db)): """Sendet Magic Link per E-Mail.""" email = req.email.lower().strip() # User prüfen cursor = await db.execute( "SELECT id, username, email, is_active, globe_access FROM users WHERE LOWER(email) = ?", (email,), ) user = await cursor.fetchone() if not user: raise HTTPException(status_code=404, detail="Kein Account mit dieser E-Mail-Adresse gefunden.") if not user["is_active"]: raise HTTPException(status_code=403, detail="Account ist deaktiviert.") if not user["globe_access"]: raise HTTPException(status_code=403, detail="Kein Globe-Zugang. Bitte wenden Sie sich an Ihren Administrator.") # Magic Token erzeugen token = generate_magic_token() expires = datetime.now(timezone.utc) + timedelta(minutes=MAGIC_LINK_EXPIRE_MINUTES) await db.execute( """INSERT INTO magic_links (user_id, email, token, code, expires_at, purpose) VALUES (?, ?, ?, '', ?, 'globe_login')""", (user["id"], email, token, expires.isoformat()), ) await db.commit() link = f"{GLOBE_BASE_URL}/api/auth/verify?token={token}" try: await send_magic_link_email(email, link) except Exception: raise HTTPException(status_code=502, detail="E-Mail konnte nicht gesendet werden.") logger.info(f"Magic Link gesendet an {email}") return {"ok": True, "message": "Anmelde-Link wurde per E-Mail gesendet."} @router.get("/verify") async def verify_token(token: str, db=Depends(get_db)): """Verifiziert Magic Link Token, gibt JWT zurück.""" cursor = await db.execute( """SELECT ml.user_id, ml.expires_at, ml.is_used, u.username, u.email, u.is_active, u.globe_access, u.role FROM magic_links ml JOIN users u ON ml.user_id = u.id WHERE ml.token = ? AND ml.purpose = 'globe_login'""", (token,), ) row = await cursor.fetchone() if not row: raise HTTPException(status_code=400, detail="Ungültiger Link.") if row["is_used"]: raise HTTPException(status_code=400, detail="Link wurde bereits verwendet.") if datetime.fromisoformat(row["expires_at"]) < datetime.now(timezone.utc): raise HTTPException(status_code=400, detail="Link ist abgelaufen.") if not row["is_active"] or not row["globe_access"]: raise HTTPException(status_code=403, detail="Kein Zugang.") await db.execute("UPDATE magic_links SET is_used = 1 WHERE token = ?", (token,)) await db.execute("UPDATE users SET last_login_at = ? WHERE id = ?", (datetime.now(timezone.utc).isoformat(), row["user_id"])) await db.commit() jwt_token = create_token(row["user_id"], row["username"], row["email"], row["role"]) # Redirect zum Frontend mit Token als Query-Parameter from fastapi.responses import HTMLResponse return HTMLResponse(f"""
""") @router.get("/me") async def get_me(user: dict = Depends(get_current_user)): return {"id": user["id"], "email": user["email"], "username": user["username"]}