05/24 Enhance functionalities
This commit is contained in:
@@ -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
@@ -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
@@ -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"))
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user