Updated functionalities 2

This commit is contained in:
2026-04-25 12:41:24 -04:00
parent a515df6a02
commit 111ec5c740
11 changed files with 280 additions and 11 deletions
+3 -2
View File
@@ -1,7 +1,7 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort
from flask_login import login_user, logout_user, login_required, current_user
from urllib.parse import urlparse
from app import db
from app import db, limiter
from app.models.user import User
from app.utils.forms import LoginForm, UserForm, ProfileForm
from app.utils.decorators import admin_required, supervisor_required
@@ -30,6 +30,7 @@ def _safe_next(next_url: str | None) -> str:
@bp.route('/login', methods=['GET', 'POST'])
@limiter.limit('20 per minute; 5 per second')
def login():
if current_user.is_authenticated:
return redirect(url_for('dashboard.index'))
@@ -254,7 +255,7 @@ def toggle_active(user_id):
f'account {action_label} by {current_user.username}',
)
flash(f'User {user.username} has been {action_label}.', 'success')
return redirect(request.referrer or url_for('auth.list_users'))
return redirect(_safe_next(request.referrer) or url_for('auth.list_users'))
# ── Notification Matrix ───────────────────────────────────────────────────────
+29 -5
View File
@@ -13,6 +13,7 @@ Provides a single screen to:
"""
import logging
from urllib.parse import urlparse
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort
from flask_login import login_required, current_user
from app import db
@@ -24,6 +25,17 @@ from app.utils.decorators import admin_required, supervisor_required
from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE
from app.utils.scope import get_customer_scope
def _safe_referrer(fallback: str) -> str:
"""Return request.referrer only if it is a safe relative URL, else fallback."""
ref = request.referrer
if not ref:
return fallback
parsed = urlparse(ref)
if parsed.netloc or parsed.scheme:
return fallback
return ref
logger = logging.getLogger(__name__)
bp = Blueprint('customers', __name__, url_prefix='/customers')
@@ -88,12 +100,24 @@ def index():
# All active projects for the assignment modal
projects = Project.query.filter_by(active=True).order_by(Project.name).all()
# ── Expired pending-setup invitations ─────────────────────────────────
# Surface customer accounts whose invitation token has expired but
# password_set is still False — they need a fresh invite to log in.
from app.utils.time_utils import now_eastern
expired_invitations = [
c for c in customers
if not c.password_set
and c.set_password_token_expires is not None
and c.set_password_token_expires < now_eastern()
]
return render_template(
'customers/index.html',
customers = customers,
assignment_map = assignment_map,
scope_map = scope_map,
projects = projects,
customers = customers,
assignment_map = assignment_map,
scope_map = scope_map,
projects = projects,
expired_invitations = expired_invitations,
)
@@ -467,7 +491,7 @@ def toggle_active(customer_id):
log_action(ACTION_UPDATE, 'User', customer.id, customer.username,
f'account {label} via customer_mgmt by {current_user.username}')
flash(f'Customer "{customer.username}" has been {label}.', 'success')
return redirect(request.referrer or url_for('customers.index'))
return redirect(_safe_referrer(url_for('customers.index')))
# ── AJAX: facilities for a project (used by add-assignment form) ──────────────
+2 -2
View File
@@ -66,10 +66,10 @@ def index():
open_issues_q = open_issues_q.join(
Area, Issue.area_id == Area.id
).filter(Area.facility_id.in_(customer_facility_ids))
open_issues = open_issues_q.count()
# Severity breakdown for the open issues card
# Single query — derive count from the list to avoid hitting the DB twice
open_issues_all = open_issues_q.all()
open_issues = len(open_issues_all)
severity_breakdown = {
'critical': sum(1 for i in open_issues_all if i.severity == 'critical'),
'high': sum(1 for i in open_issues_all if i.severity == 'high'),
+43 -1
View File
@@ -223,4 +223,46 @@ def check_sla():
sent = send_sla_alerts()
logger.info('SLA CHECK TRIGGERED | notifications_sent=%s', sent)
return jsonify({'ok': True, 'notifications_sent': sent})
return jsonify({'ok': True, 'notifications_sent': sent})
# ── Expired token cleanup (called by cron) ────────────────────────────────────
@bp.route('/cleanup-tokens', methods=['POST'])
@csrf.exempt
def cleanup_tokens():
"""Purge expired and revoked refresh tokens from api_refresh_tokens.
Safe to run frequently — only deletes rows where expires_at has passed
OR revoked=True. Keeps the table lean without touching live sessions.
Recommended cron schedule — nightly is sufficient:
0 3 * * * curl -s -X POST https://yourdomain.com/notifications/cleanup-tokens \\
-d "token=YOUR_DIGEST_SECRET"
"""
token = request.form.get('token') or request.args.get('token')
expected = current_app.config.get('DIGEST_SECRET')
if not expected or token != expected:
logger.warning('TOKEN CLEANUP REJECTED | bad or missing token')
abort(403)
from app.models.api_token import RefreshToken
from app.utils.time_utils import now_eastern
now = now_eastern()
deleted = (
RefreshToken.query
.filter(
db.or_(
RefreshToken.expires_at < now,
RefreshToken.revoked == True, # noqa: E712
)
)
.delete(synchronize_session=False)
)
db.session.commit()
logger.info('TOKEN CLEANUP | deleted=%s expired/revoked rows', deleted)
return jsonify({'ok': True, 'deleted': deleted})