46 lines
1.7 KiB
Python
46 lines
1.7 KiB
Python
"""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"))
|