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
+7 -2
View File
@@ -4,7 +4,7 @@ routes/admin_dashboard.py — Admin dashboard: today's completion stats.
import logging
from flask import Blueprint, render_template
from models import get_admin_dashboard_stats
from models import get_admin_dashboard_stats, get_missed_shifts_today
from utils.decorators import admin_required
logger = logging.getLogger("routes.admin_dashboard")
@@ -19,4 +19,9 @@ def dashboard():
except Exception as e:
logger.error(f"Dashboard stats error: {e}")
stats = {"user_stats": [], "total_sites": 0, "total_users": 0, "active_today": 0}
return render_template("admin/dashboard.html", stats=stats)
try:
missed = get_missed_shifts_today()
except Exception as e:
logger.error(f"Missed shifts error: {e}")
missed = []
return render_template("admin/dashboard.html", stats=stats, missed=missed)
+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"))
+41 -2
View File
@@ -8,9 +8,10 @@ from flask import (Blueprint, render_template, request, redirect, url_for,
from models import (
get_all_bids, get_bid, create_bid, update_bid, delete_bid,
get_bid_updates, add_bid_update, delete_bid_update, BID_STATUSES,
log_action,
log_action, get_bids_due_soon, get_admin_emails,
)
from utils.decorators import login_required
from utils.decorators import login_required, admin_required
from utils.email import send_email
logger = logging.getLogger("routes.bid_tracker")
bid_tracker_bp = Blueprint("bid_tracker", __name__, url_prefix="/bids")
@@ -237,3 +238,41 @@ def delete_update_json(update_id):
except Exception as e:
logger.error(f"delete_update_json error: {e}")
return jsonify({"error": str(e)}), 500
@bid_tracker_bp.route("/remind", methods=["POST"])
@admin_required
def send_reminders():
"""Send an email digest of bids due within 7 days to all admin users with emails."""
user = session["user"]
bids = get_bids_due_soon(days=7)
recipients = get_admin_emails()
if not bids:
flash("No active bids due within 7 days.", "info")
return redirect(url_for("bid_tracker.bids_list"))
if not recipients:
flash("No admin email addresses configured. Add them via Admin → Users.", "warning")
return redirect(url_for("bid_tracker.bids_list"))
lines = [f"Bid Deadline Reminder — {len(bids)} bid(s) due within 7 days:\n"]
for b in bids:
due = str(b["due_date"])[:10] if b.get("due_date") else "N/A"
line = f"{b['title']} — Due: {due} — Status: {b['status']}"
if b.get("solicitation_number"):
line += f" — Sol#: {b['solicitation_number']}"
lines.append(line)
lines.append("\nLog in to Website Checker for full details.")
body = "\n".join(lines)
try:
send_email(recipients, "Website Checker — Upcoming Bid Deadlines", body)
log_action(user["id"], "BID_REMIND", "bid_tracker", None,
f"Bid reminder email sent to {len(recipients)} admin(s), {len(bids)} bid(s) listed.")
flash(f"Reminder digest sent to {len(recipients)} recipient(s).", "success")
except Exception as e:
logger.error(f"send_reminders error: {e}")
flash(f"Failed to send email: {e}", "danger")
return redirect(url_for("bid_tracker.bids_list"))
+23
View File
@@ -3,10 +3,13 @@ routes/user_dashboard.py — Regular user: shift check dashboard.
"""
import logging
import time
import requests as _http_req
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
from models import (
get_today_checks, mark_website_checked, unmark_website_checked,
update_check_note, get_user_active_shifts, get_website_credentials,
get_website_url,
)
from utils.decorators import login_required
@@ -91,3 +94,23 @@ def view_credentials(website_id):
except Exception as e:
logger.error(f"view_credentials error: {e}")
return jsonify({"error": str(e)}), 500
@user_dashboard_bp.route("/health/<int:website_id>")
@login_required
def health_check(website_id):
"""Server-side HEAD probe for a website's reachability."""
url = get_website_url(website_id)
if not url:
return jsonify({"status": "not_found"}), 404
try:
t0 = time.time()
resp = _http_req.head(url, timeout=6, allow_redirects=True,
headers={"User-Agent": "WebsiteChecker/1.0 (health-check)"})
ms = int((time.time() - t0) * 1000)
ok = resp.status_code < 400
return jsonify({"status": "ok" if ok else "error", "ms": ms})
except _http_req.exceptions.Timeout:
return jsonify({"status": "timeout", "ms": 6000})
except Exception:
return jsonify({"status": "unreachable", "ms": None})