05/24 Enhance functionalities

This commit is contained in:
2026-05-24 18:10:11 -04:00
parent 6605484927
commit fbf271ed71
15 changed files with 593 additions and 30 deletions
+64 -1
View File
@@ -4,8 +4,13 @@ routes/auth.py — Authentication routes: login, logout, change-password.
import logging
from flask import Blueprint, render_template, request, session, redirect, url_for, flash
from models import authenticate, check_login_allowed, change_password, log_action
from models import (
authenticate, check_login_allowed, change_password, log_action,
get_user_by_email, create_password_reset_token,
get_password_reset_user, consume_password_reset_token,
)
from utils.decorators import login_required
from utils.email import send_email
logger = logging.getLogger("routes.auth")
auth_bp = Blueprint("auth", __name__)
@@ -95,3 +100,61 @@ def ping():
"""Keep-alive endpoint for the session timeout warning in app.js."""
session.modified = True
return "", 204
@auth_bp.route("/forgot-password", methods=["GET", "POST"])
def forgot_password():
if "user" in session:
return redirect(url_for("index"))
if request.method == "GET":
return render_template("forgot_password.html")
email = request.form.get("email", "").strip().lower()
if email:
user = get_user_by_email(email)
if user and user.get("email"):
try:
token = create_password_reset_token(user["id"])
reset_url = url_for("auth.reset_password", token=token, _external=True)
body = (
f"Hi {user.get('full_name') or user['username']},\n\n"
f"A password reset was requested for your Website Checker account.\n"
f"Click the link below to set a new password (valid for 1 hour):\n\n"
f"{reset_url}\n\n"
f"If you did not request this, you can safely ignore this email.\n"
)
send_email(user["email"], "Website Checker — Password Reset", body)
except Exception as e:
logger.error(f"Password reset email error for '{email}': {e}")
# Always show the same message to prevent user enumeration
flash("If that email is registered, a reset link has been sent.", "info")
return render_template("forgot_password.html")
@auth_bp.route("/reset-password/<token>", methods=["GET", "POST"])
def reset_password(token):
user = get_password_reset_user(token)
if not user:
flash("This reset link is invalid or has expired.", "danger")
return redirect(url_for("auth.forgot_password"))
if request.method == "GET":
return render_template("reset_password.html", token=token)
password = request.form.get("password", "")
confirm = request.form.get("confirm_password", "")
if len(password) < 8:
flash("Password must be at least 8 characters.", "danger")
return render_template("reset_password.html", token=token)
if password != confirm:
flash("Passwords do not match.", "danger")
return render_template("reset_password.html", token=token)
if consume_password_reset_token(token, password):
logger.info(f"Password reset successfully for user id={user['id']}.")
flash("Password reset successfully. Please log in.", "success")
return redirect(url_for("auth.login"))
flash("Reset link expired or already used. Please request a new one.", "danger")
return redirect(url_for("auth.forgot_password"))