start project
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint("auth", __name__, template_folder="templates")
|
||||
|
||||
from app.blueprints.auth import routes # noqa: E402,F401 registers routes
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Login form (Chapter 12). CSRF handled automatically via form.hidden_tag()
|
||||
(Flask-WTF, Ch12's CSRF requirement)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import PasswordField, StringField, SubmitField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
# NOTE: no Email() validator — that validator requires the extra
|
||||
# `email-validator` package. Login checks the value against a stored
|
||||
# user row anyway, so a malformed email simply fails to match rather
|
||||
# than needing format validation up front; kept simple to avoid a new
|
||||
# dependency.
|
||||
email = StringField("Email", validators=[DataRequired()])
|
||||
password = PasswordField("Password", validators=[DataRequired()])
|
||||
submit = SubmitField("Log in")
|
||||
@@ -0,0 +1,45 @@
|
||||
"""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"))
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Log in — Kavosh{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-sm mx-auto mt-16 sm:mt-24">
|
||||
<div class="text-center mb-6">
|
||||
<h1 class="font-display font-bold text-2xl tracking-tight">Kavosh</h1>
|
||||
<p class="text-sm text-muted dark:text-muted-dark mt-1">Sign in to view your site's traffic</p>
|
||||
</div>
|
||||
<div class="bg-surface dark:bg-surface-dark border border-line dark:border-line-dark rounded-xl p-6 shadow-sm">
|
||||
{% for message in get_flashed_messages() %}
|
||||
<p class="text-danger dark:text-danger-dark text-sm mb-3 bg-danger/10 dark:bg-danger-dark/10 rounded-md px-3 py-2">{{ message }}</p>
|
||||
{% endfor %}
|
||||
<form method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-4">
|
||||
{{ form.email.label(class="block text-sm font-medium text-muted dark:text-muted-dark mb-1") }}
|
||||
{{ form.email(class="w-full border border-line dark:border-line-dark rounded-md px-3 py-2 bg-paper dark:bg-paper-dark text-ink dark:text-ink-dark focus:outline-none focus:ring-2 focus:ring-accent dark:focus:ring-accent-dark", autofocus=true) }}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
{{ form.password.label(class="block text-sm font-medium text-muted dark:text-muted-dark mb-1") }}
|
||||
{{ form.password(class="w-full border border-line dark:border-line-dark rounded-md px-3 py-2 bg-paper dark:bg-paper-dark text-ink dark:text-ink-dark focus:outline-none focus:ring-2 focus:ring-accent dark:focus:ring-accent-dark") }}
|
||||
</div>
|
||||
{{ form.submit(class="w-full bg-accent dark:bg-accent-dark text-white dark:text-paper-dark font-medium rounded-md px-4 py-2 hover:opacity-90 transition-opacity cursor-pointer") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user