Files
LT_Janitorial_Quality_Control/app/utils/audit.py
T
2026-05-16 14:10:08 -04:00

98 lines
3.4 KiB
Python

"""
audit.py
--------
Centralised helper for writing AuditLog entries.
Usage (inside any route after db.session.commit()):
from app.utils.audit import log_action
log_action(
action = 'CREATE',
entity_type = 'Facility',
entity_id = facility.id,
entity_label = facility.name,
details = f'address={facility.address}',
)
``action`` should be one of the ACTION_* constants defined below.
``entity_type`` should match the model class name for consistency.
"""
import logging
from flask import request
from flask_login import current_user
from app import db
from app.models.audit import AuditLog
logger = logging.getLogger(__name__)
# ── Canonical action constants ────────────────────────────────────────────────
ACTION_CREATE = 'CREATE'
ACTION_UPDATE = 'UPDATE'
ACTION_DELETE = 'DELETE'
ACTION_LOGIN = 'LOGIN'
ACTION_LOGOUT = 'LOGOUT'
ACTION_EXPORT = 'EXPORT'
def log_action(action: str,
entity_type: str,
entity_id: int | None = None,
entity_label: str | None = None,
details: str | None = None) -> None:
"""
Write a single AuditLog row. Safe to call from any request context.
⚠️ This function calls db.session.commit() internally.
Always call it AFTER the primary db.session.commit() for the business
transaction — never before. Calling it mid-transaction will commit any
dirty ORM state accumulated in the session up to that point.
Parameters
----------
action : One of the ACTION_* constants (or a custom string ≤ 50 chars).
entity_type : Model name — 'User', 'Facility', 'Area', 'Template',
'Inspection', 'Issue', etc.
entity_id : Primary key of the affected record (optional).
entity_label : Human-readable name / description snapshot (optional).
details : Extra context string, e.g. 'status=open→resolved' (optional).
"""
try:
# Resolve actor — fall back gracefully if called outside request context
if current_user and current_user.is_authenticated:
uid = current_user.id
uname = current_user.username
urole = current_user.role
else:
uid, uname, urole = None, 'system', 'system'
# Best-effort IP extraction; respects X-Forwarded-For from Nginx
ip = None
try:
ip = (request.headers.get('X-Forwarded-For', '').split(',')[0].strip()
or request.remote_addr)
except RuntimeError:
pass # outside request context
entry = AuditLog(
user_id = uid,
username = uname,
user_role = urole,
action = action[:50],
entity_type = entity_type[:50],
entity_id = entity_id,
entity_label = (entity_label or '')[:255],
details = details,
ip_address = (ip or '')[:45],
)
db.session.add(entry)
db.session.commit()
except Exception as exc:
# Audit logging must never break the primary request flow
logger.error('AuditLog write failed: %s', exc, exc_info=True)
try:
db.session.rollback()
except Exception:
pass