105 lines
3.2 KiB
Python
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"))
|