Auth: Nur noch Magic Link, Code-Verifizierung entfernt

- /api/auth/verify-code Endpoint entfernt
- generate_magic_code() entfernt
- E-Mail: Nur noch Anmelde-Link, kein 6-stelliger Code
- Login-Seite: Zeigt nach E-Mail-Eingabe Hinweis statt Code-Feld
- Magic Link Token-Verifikation via URL bleibt bestehen
Dieser Commit ist enthalten in:
Claude Dev
2026-03-25 00:01:21 +01:00
Ursprung bd2c274dd2
Commit af08fa6b4d
5 geänderte Dateien mit 69 neuen und 183 gelöschten Zeilen

Datei anzeigen

@@ -1,6 +1,5 @@
"""Auth: JWT + Magic Link fuer Globe."""
import secrets
import string
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
@@ -72,6 +71,3 @@ async def get_current_user(
def generate_magic_token() -> str:
return secrets.token_urlsafe(48)
def generate_magic_code() -> str:
return "".join(secrets.choice(string.digits) for _ in range(6))

Datei anzeigen

@@ -5,7 +5,7 @@ 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, generate_magic_code, get_current_user
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
@@ -18,14 +18,9 @@ class LoginRequest(BaseModel):
email: EmailStr
class CodeVerifyRequest(BaseModel):
email: EmailStr
code: str
@router.post("/request-link")
async def request_magic_link(req: LoginRequest, db=Depends(get_db)):
"""Sendet Magic Link + Code per E-Mail."""
"""Sendet Magic Link per E-Mail."""
email = req.email.lower().strip()
# User pruefen
@@ -41,27 +36,26 @@ async def request_magic_link(req: LoginRequest, db=Depends(get_db)):
if not user["globe_access"]:
raise HTTPException(status_code=403, detail="Kein Globe-Zugang. Bitte wenden Sie sich an Ihren Administrator.")
# Magic Token + Code erzeugen
# Magic Token erzeugen
token = generate_magic_token()
code = generate_magic_code()
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, code, expires.isoformat()),
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, code, link)
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": "Zugangscode wurde per E-Mail gesendet."}
return {"ok": True, "message": "Anmelde-Link wurde per E-Mail gesendet."}
@router.get("/verify")
@@ -101,37 +95,6 @@ async def verify_token(token: str, db=Depends(get_db)):
""")
@router.post("/verify-code")
async def verify_code(req: CodeVerifyRequest, db=Depends(get_db)):
"""Verifiziert 6-stelligen Code, gibt JWT zurueck."""
email = req.email.lower().strip()
cursor = await db.execute(
"""SELECT ml.id, 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.code = ? AND LOWER(u.email) = ? AND ml.purpose = 'globe_login'
ORDER BY ml.created_at DESC LIMIT 1""",
(req.code, email),
)
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=400, detail="Ungueltiger Code.")
if row["is_used"]:
raise HTTPException(status_code=400, detail="Code wurde bereits verwendet.")
if datetime.fromisoformat(row["expires_at"]) < datetime.now(timezone.utc):
raise HTTPException(status_code=400, detail="Code 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 id = ?", (row["id"],))
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"])
return {"ok": True, "token": jwt_token}
@router.get("/me")
async def get_me(user: dict = Depends(get_current_user)):
return {"id": user["id"], "email": user["email"], "username": user["username"]}

Datei anzeigen

@@ -9,21 +9,21 @@ from config import SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM_EMA
logger = logging.getLogger("globe.email")
async def send_magic_link_email(to_email: str, code: str, link: str):
"""Sendet Magic Link + Code per E-Mail."""
async def send_magic_link_email(to_email: str, link: str):
"""Sendet Magic Link per E-Mail."""
html = f"""
<div style="font-family: 'Courier New', monospace; max-width: 500px; margin: 0 auto; padding: 30px; background: #0b1121; color: #e8eaf0; border-radius: 12px;">
<h2 style="color: #00ff88; font-size: 16px; letter-spacing: 2px; margin-bottom: 24px;">AEGISSIGHT GLOBE</h2>
<p style="font-size: 14px; line-height: 1.6;">Dein Zugangscode:</p>
<div style="background: rgba(0,255,136,0.08); border: 1px solid rgba(0,255,136,0.2); border-radius: 8px; padding: 20px; text-align: center; margin: 20px 0;">
<span style="font-size: 32px; font-weight: 700; letter-spacing: 8px; color: #00ff88;">{code}</span>
<p style="font-size: 14px; line-height: 1.6;">Klicke auf den Button, um dich anzumelden:</p>
<div style="text-align: center; margin: 24px 0;">
<a href="{link}" style="display: inline-block; background: #00ff88; color: #0b1121; padding: 14px 40px; border-radius: 6px; text-decoration: none; font-weight: 700; font-size: 16px; letter-spacing: 1px;">Jetzt anmelden</a>
</div>
<p style="font-size: 13px; color: rgba(255,255,255,0.6); line-height: 1.6;">
Oder klicke auf diesen Link:<br>
<a href="{link}" style="color: #00ff88; word-break: break-all;">{link}</a>
<p style="font-size: 12px; color: rgba(255,255,255,0.4); line-height: 1.6;">
Oder kopiere diesen Link in deinen Browser:<br>
<a href="{link}" style="color: #00ff88; word-break: break-all; font-size: 11px;">{link}</a>
</p>
<p style="font-size: 11px; color: rgba(255,255,255,0.3); margin-top: 24px;">
Dieser Code ist 10 Minuten gueltig. Falls du diese Anfrage nicht gesendet hast, ignoriere diese E-Mail.
Dieser Link ist 10 Minuten gueltig. Falls du diese Anfrage nicht gesendet hast, ignoriere diese E-Mail.
</p>
</div>
"""
@@ -31,8 +31,8 @@ async def send_magic_link_email(to_email: str, code: str, link: str):
msg = MIMEMultipart("alternative")
msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM_EMAIL}>"
msg["To"] = to_email
msg["Subject"] = "AegisSight Globe — Zugangscode"
msg.attach(MIMEText(f"Dein Globe-Zugangscode: {code}\n\nLink: {link}\n\nGueltig fuer 10 Minuten.", "plain"))
msg["Subject"] = "AegisSight Globe — Anmelde-Link"
msg.attach(MIMEText(f"Dein Globe-Anmeldelink:\n\n{link}\n\nGueltig fuer 10 Minuten.", "plain"))
msg.attach(MIMEText(html, "html"))
try: