95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
"""Auth-Service-Logik: Allowlist, User-Anlage, Code-Erzeugung/-Versand/-Prüfung.
|
|
|
|
Trennt die fachliche Logik von den Routen. Codes werden 6-stellig numerisch
|
|
erzeugt, nur gehasht gespeichert und per E-Mail versandt.
|
|
"""
|
|
|
|
import secrets
|
|
from datetime import timedelta
|
|
|
|
from flask import current_app
|
|
from flask_mail import Message
|
|
|
|
from ..extensions import db, mail
|
|
from ..models import User, LoginCode, utcnow
|
|
|
|
|
|
def email_allowed(email: str) -> bool:
|
|
"""True, wenn die E-Mail freigeschaltet ist.
|
|
|
|
Leere Allowlist (z.B. lokale Entwicklung ohne .env) = keine Einschränkung.
|
|
In Produktion sollte ALLOWED_EMAILS gesetzt sein (Self-Registration begrenzt).
|
|
"""
|
|
allow = current_app.config.get("ALLOWED_EMAILS", [])
|
|
return (not allow) or (email.lower() in allow)
|
|
|
|
|
|
def get_or_create_user(email: str) -> User:
|
|
"""User per E-Mail holen oder passwortlos anlegen (Self-Registration)."""
|
|
email = email.lower().strip()
|
|
user = User.query.filter_by(email=email).first()
|
|
if user is None:
|
|
user = User(email=email)
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
return user
|
|
|
|
|
|
def issue_login_code(user: User) -> str:
|
|
"""Neuen Login-Code erzeugen, alte unbenutzte Codes des Users verwerfen.
|
|
|
|
Gibt den Klartext-Code zurück (nur zum sofortigen Versand) — gespeichert
|
|
wird ausschließlich der Hash.
|
|
"""
|
|
LoginCode.query.filter_by(user_id=user.id, used=False).delete()
|
|
|
|
code = f"{secrets.randbelow(1_000_000):06d}"
|
|
ttl = current_app.config.get("LOGIN_CODE_TTL_MINUTES", 10)
|
|
lc = LoginCode(user_id=user.id, expires_at=utcnow() + timedelta(minutes=ttl))
|
|
lc.set_code(code)
|
|
db.session.add(lc)
|
|
db.session.commit()
|
|
return code
|
|
|
|
|
|
def send_login_code(user: User, code: str) -> None:
|
|
"""Code per E-Mail senden (Klartext, kein HTML/Tracking).
|
|
|
|
Im Debug-Modus wird der Code zusätzlich geloggt und Versandfehler werden
|
|
verschluckt, damit lokal ohne SMTP getestet werden kann. In Produktion
|
|
wird eine Versand-Exception weitergereicht.
|
|
"""
|
|
ttl = current_app.config.get("LOGIN_CODE_TTL_MINUTES", 10)
|
|
msg = Message(
|
|
subject="Dein Login-Code für den Projekt-Hub",
|
|
recipients=[user.email],
|
|
body=(
|
|
"Hallo,\n\n"
|
|
f"dein Login-Code lautet: {code}\n\n"
|
|
f"Der Code ist {ttl} Minuten gültig und nur einmal verwendbar.\n"
|
|
"Wenn du keinen Login angefordert hast, ignoriere diese Mail.\n"
|
|
),
|
|
)
|
|
try:
|
|
mail.send(msg)
|
|
except Exception as exc: # noqa: BLE001
|
|
current_app.logger.warning("Mailversand fehlgeschlagen: %s", exc)
|
|
if not current_app.debug:
|
|
raise
|
|
|
|
if current_app.debug:
|
|
current_app.logger.info("DEV — Login-Code für %s: %s", user.email, code)
|
|
|
|
|
|
def verify_code(user: User, code: str) -> bool:
|
|
"""Neuesten unbenutzten Code des Users prüfen; bei Erfolg als used markieren."""
|
|
lc = (
|
|
LoginCode.query.filter_by(user_id=user.id, used=False)
|
|
.order_by(LoginCode.created_at.desc())
|
|
.first()
|
|
)
|
|
if lc is None or not lc.is_valid(code):
|
|
return False
|
|
lc.used = True
|
|
db.session.commit()
|
|
return True
|