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

105 lines
3.2 KiB
Python

"""Auth-Routen: /login (Code anfordern), /verify (Code prüfen), /logout."""
from urllib.parse import urlparse
from flask import (
render_template,
redirect,
url_for,
flash,
session,
request,
current_app,
)
from flask_login import login_user, logout_user, login_required, current_user
from ..extensions import limiter
from ..models import User
from . import bp
from .forms import EmailForm, CodeForm
from .service import (
email_allowed,
get_or_create_user,
issue_login_code,
send_login_code,
verify_code,
)
def _rate_key() -> str:
"""Rate-Limit pro E-Mail (Fallback: IP), damit Code-Spam begrenzt ist."""
return (request.form.get("email") or request.remote_addr or "").lower()
def _is_safe_next(target: str | None) -> bool:
"""Open-Redirect verhindern: nur lokale, relative Pfade erlauben."""
if not target:
return False
parsed = urlparse(target)
return not parsed.netloc and not parsed.scheme and target.startswith("/")
@bp.route("/login", methods=["GET", "POST"])
@limiter.limit(
lambda: current_app.config["LOGIN_CODE_RATE_LIMIT"],
methods=["POST"],
key_func=_rate_key,
)
def login():
if current_user.is_authenticated:
return redirect(url_for("projects.index"))
form = EmailForm()
if form.validate_on_submit():
email = form.email.data.lower().strip()
if not email_allowed(email):
flash("Diese E-Mail-Adresse ist nicht freigeschaltet.", "error")
return render_template("auth/login.html", form=form)
user = get_or_create_user(email)
code = issue_login_code(user)
try:
send_login_code(user, code)
except Exception: # noqa: BLE001
flash("Code konnte nicht gesendet werden. Bitte später erneut versuchen.", "error")
return render_template("auth/login.html", form=form)
session["auth_email"] = email
flash("Wir haben dir einen Code geschickt. Bitte gib ihn unten ein.", "info")
return redirect(url_for("auth.verify"))
return render_template("auth/login.html", form=form)
@bp.route("/verify", methods=["GET", "POST"])
def verify():
if current_user.is_authenticated:
return redirect(url_for("projects.index"))
email = session.get("auth_email")
if not email:
flash("Bitte zuerst deine E-Mail-Adresse eingeben.", "info")
return redirect(url_for("auth.login"))
form = CodeForm()
if form.validate_on_submit():
user = User.query.filter_by(email=email).first()
if user and verify_code(user, form.code.data.strip()):
login_user(user)
session.pop("auth_email", None)
flash("Eingeloggt. Willkommen zurück!", "success")
next_url = request.args.get("next")
return redirect(next_url if _is_safe_next(next_url) else url_for("projects.index"))
flash("Code ungültig oder abgelaufen. Bitte erneut versuchen.", "error")
return render_template("auth/verify.html", form=form, email=email)
@bp.route("/logout")
@login_required
def logout():
logout_user()
session.pop("auth_email", None)
flash("Du wurdest abgemeldet.", "info")
return redirect(url_for("auth.login"))