101 Zeilen
3.8 KiB
Python
101 Zeilen
3.8 KiB
Python
"""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"""
|
|
<html><body><script>
|
|
localStorage.setItem('globe_token', '{jwt_token}');
|
|
window.location.href = '/';
|
|
</script></body></html>
|
|
""")
|
|
|
|
|
|
@router.get("/me")
|
|
async def get_me(user: dict = Depends(get_current_user)):
|
|
return {"id": user["id"], "email": user["email"], "username": user["username"]}
|