Project_Manager/app/auth/service.py
2026-06-26 18:21:22 +02:00

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