Investment Plans workspace
Open raw ↗
"""Immutable audit logger for HAD Digital MVP.

Every action is timestamped and attributed.
The audit_log table is append-only (no UPDATE or DELETE allowed in application code).
"""

from database import get_db


def log_action(
    user_id: int = None,
    username: str = None,
    action: str = "",
    resource_type: str = None,
    resource_id: int = None,
    details: str = None,
    ip_address: str = None,
    user_agent: str = None,
):
    """Log an auditable action.
    
    Args:
        user_id: ID of the user performing the action (None for system actions)
        username: Username for display (auto-resolved from user_id if not provided)
        action: The action being performed (e.g., 'login', 'submit_report', 'acknowledge_alert')
        resource_type: Type of resource being acted upon (e.g., 'patient', 'report', 'alert')
        resource_id: ID of the resource
        details: Human-readable details of the action
        ip_address: Client IP address
        user_agent: Client user agent string
    """
    db = get_db()

    # Auto-resolve username if not provided
    if username is None and user_id is not None:
        user = db.fetchone("SELECT username FROM users WHERE id = ?", (user_id,))
        if user:
            username = user["username"]

    db.execute(
        """INSERT INTO audit_log 
           (user_id, username, action, resource_type, resource_id, details, ip_address, user_agent)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
        (user_id, username, action, resource_type, resource_id, details, ip_address, user_agent),
    )
    db.commit()


def get_audit_log(
    user_id: int = None,
    action: str = None,
    resource_type: str = None,
    resource_id: int = None,
    limit: int = 100,
    offset: int = 0,
) -> list[dict]:
    """Query the audit log with optional filters.
    
    Returns entries in reverse chronological order (newest first).
    """
    db = get_db()
    conditions = []
    params = []

    if user_id is not None:
        conditions.append("user_id = ?")
        params.append(user_id)
    if action is not None:
        conditions.append("action = ?")
        params.append(action)
    if resource_type is not None:
        conditions.append("resource_type = ?")
        params.append(resource_type)
    if resource_id is not None:
        conditions.append("resource_id = ?")
        params.append(resource_id)

    where_clause = " AND ".join(conditions) if conditions else "1=1"

    rows = db.fetchall(
        f"SELECT * FROM audit_log WHERE {where_clause} ORDER BY timestamp DESC LIMIT ? OFFSET ?",
        tuple(params) + (limit, offset),
    )
    return [dict(r) for r in rows]


def get_audit_count(
    user_id: int = None,
    action: str = None,
    resource_type: str = None,
) -> int:
    """Count audit log entries matching the filters."""
    db = get_db()
    conditions = []
    params = []

    if user_id is not None:
        conditions.append("user_id = ?")
        params.append(user_id)
    if action is not None:
        conditions.append("action = ?")
        params.append(action)
    if resource_type is not None:
        conditions.append("resource_type = ?")
        params.append(resource_type)

    where_clause = " AND ".join(conditions) if conditions else "1=1"
    row = db.fetchone(
        f"SELECT COUNT(*) as cnt FROM audit_log WHERE {where_clause}",
        tuple(params),
    )
    return row["cnt"] if row else 0