Updated functionalities 2
This commit is contained in:
+3
-2
@@ -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
@@ -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) ──────────────
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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})
|
||||
Reference in New Issue
Block a user