start project

This commit is contained in:
Hemmat
2026-08-07 21:17:17 +03:30
commit ea1e1eead6
121 changed files with 8108 additions and 0 deletions
+5
View File
@@ -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
+18
View File
@@ -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")
+45
View File
@@ -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 %}