06/15 Phase 1 + 2 codes

This commit is contained in:
2026-06-15 11:23:05 -04:00
commit c2064b84b4
62 changed files with 2937 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
"""Auth flows: register, login, logout, email verification, password reset."""
from datetime import datetime
from flask import (Blueprint, render_template, redirect, url_for, flash,
request, current_app, abort)
from flask_login import login_user, logout_user, login_required, current_user
from flask_babel import gettext as _
from app.extensions import db, limiter
from app.models.user import User
from app.models.plan import Plan
from app.models.enums import Role, TrustEventType
from app.utils.security import generate_token, read_token
from app.services.email import send_email
from app.services.turnstile import verify_turnstile, turnstile_enabled
from app.services.trust import record_event
from app.blueprints.auth.forms import (RegisterForm, LoginForm,
ResetRequestForm, ResetForm)
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
_VERIFY_SALT = "email-verify"
_RESET_SALT = "password-reset"
def _send_verify_email(user):
token = generate_token(user.id, _VERIFY_SALT)
link = url_for("auth.verify_email", token=token, _external=True)
send_email(user.email, _("Verify your email"),
_("Confirm your account: %(link)s", link=link))
@auth_bp.route("/register", methods=["GET", "POST"])
@limiter.limit("10 per hour", methods=["POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for("main.index"))
form = RegisterForm()
if form.validate_on_submit():
if not verify_turnstile():
flash(_("Captcha verification failed."), "danger")
return render_template("auth/register.html", form=form,
turnstile=turnstile_enabled())
existing = User.query.filter_by(email=form.email.data.lower()).first()
if existing:
flash(_("An account with that email already exists."), "danger")
return render_template("auth/register.html", form=form,
turnstile=turnstile_enabled())
free_plan = Plan.query.filter_by(slug="free").first()
user = User(
email=form.email.data.lower(),
display_name=form.display_name.data.strip(),
role=Role.free,
tier_id=free_plan.id if free_plan else None,
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
_send_verify_email(user)
flash(_("Account created. Check your email to verify."), "success")
return redirect(url_for("auth.login"))
return render_template("auth/register.html", form=form,
turnstile=turnstile_enabled())
@auth_bp.route("/login", methods=["GET", "POST"])
@limiter.limit("20 per hour", methods=["POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("main.index"))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user is None or not user.check_password(form.password.data):
flash(_("Invalid email or password."), "danger")
return render_template("auth/login.html", form=form)
if not user.is_active:
flash(_("This account is suspended."), "danger")
return render_template("auth/login.html", form=form)
login_user(user, remember=form.remember.data)
user.last_login_at = datetime.utcnow()
db.session.commit()
nxt = request.args.get("next")
if nxt and nxt.startswith("/"):
return redirect(nxt)
return redirect(url_for("main.index"))
return render_template("auth/login.html", form=form)
@auth_bp.route("/logout")
@login_required
def logout():
logout_user()
flash(_("Signed out."), "info")
return redirect(url_for("main.index"))
@auth_bp.route("/verify/<token>")
def verify_email(token):
user_id = read_token(token, _VERIFY_SALT,
current_app.config["TOKEN_VERIFY_MAX_AGE"])
if user_id is None:
flash(_("Verification link is invalid or expired."), "danger")
return redirect(url_for("auth.login"))
user = User.query.get(user_id)
if user is None:
abort(404)
if not user.email_verified:
user.email_verified = True
record_event(user, TrustEventType.verified_email, 5)
db.session.commit()
flash(_("Email verified. You're all set."), "success")
return redirect(url_for("auth.login"))
@auth_bp.route("/reset", methods=["GET", "POST"])
@limiter.limit("5 per hour", methods=["POST"])
def reset_request():
form = ResetRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user:
token = generate_token(user.id, _RESET_SALT)
link = url_for("auth.reset_password", token=token, _external=True)
send_email(user.email, _("Reset your password"),
_("Reset link: %(link)s", link=link))
# Always show success to avoid email enumeration.
flash(_("If that email exists, a reset link has been sent."), "info")
return redirect(url_for("auth.login"))
return render_template("auth/reset_request.html", form=form)
@auth_bp.route("/reset/<token>", methods=["GET", "POST"])
def reset_password(token):
user_id = read_token(token, _RESET_SALT,
current_app.config["TOKEN_RESET_MAX_AGE"])
if user_id is None:
flash(_("Reset link is invalid or expired."), "danger")
return redirect(url_for("auth.reset_request"))
user = User.query.get(user_id)
if user is None:
abort(404)
form = ResetForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash(_("Password updated. Sign in."), "success")
return redirect(url_for("auth.login"))
return render_template("auth/reset.html", form=form, token=token)