"""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"))