"""Single-admin auth routes (Chapter 12 / Chapter 11's route table).""" from __future__ import annotations from flask import flash, redirect, render_template, request, url_for from flask_login import current_user, login_required, login_user, logout_user from app.blueprints.auth import bp from app.blueprints.auth.forms import LoginForm from app.extensions import db, limiter, login_manager from app.models.user import User @login_manager.user_loader def load_user(user_id: str) -> User | None: return db.session.get(User, int(user_id)) @bp.route("/login", methods=["GET", "POST"]) @limiter.limit("10 per minute") # Ch12: rate limiting on /login at minimum def login(): if current_user.is_authenticated: return redirect(url_for("overview.overview")) form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data.strip().lower()).first() if user is not None and user.check_password(form.password.data): login_user(user) next_url = request.args.get("next") or url_for("overview.overview") return redirect(next_url) flash("Invalid email or password.") return render_template("auth/login.html", form=form) @bp.post("/logout") @login_required def logout(): # NOTE (flagged): Ch11's route table lists "/login, /logout" under a # shared "GET/POST" column. Logout is POST-only here — a state-changing # action behind a plain GET is a CSRF-adjacent anti-pattern Ch12's own # CSRF requirement argues against; login stays GET (show form) + POST # (submit), matching the table as-is. logout_user() return redirect(url_for("auth.login"))