import logging from flask import request from app import db from app.models import ActivityLog logger = logging.getLogger(__name__) def _get_real_ip(): """ Return the genuine client IP address when Flask sits behind an nginx reverse proxy. nginx forwards the original client IP in two headers: - X-Forwarded-For: , , , ... - X-Real-IP: request.remote_addr is always 127.0.0.1 in a proxied setup, so we must read the headers instead. We take the leftmost (first) value in X-Forwarded-For because that is the original client; subsequent values are intermediate proxies appended by each hop. Falls back to X-Real-IP, then remote_addr as a last resort. """ try: xff = request.headers.get('X-Forwarded-For') if xff: # Strip any port appended by some proxies and take first entry return xff.split(',')[0].strip() xri = request.headers.get('X-Real-IP') if xri: return xri.strip() return request.remote_addr except RuntimeError: return None def log_action(user_id, action, entity_type=None, entity_id=None, details=None): """ Persist an activity log entry for create / edit / delete actions. Parameters ---------- user_id : int | None – the acting user (None for system actions) action : str – e.g. 'ticket_create', 'ticket_update', 'comment_delete' entity_type : str | None – 'ticket', 'comment', 'user', etc. entity_id : int | None – primary key of the affected entity details : str | None – free-form JSON or human-readable details Transaction note ---------------- This function deliberately does NOT call db.session.commit(). The entry is added to the current session and committed by the caller alongside its own business objects. This ensures the log entry is only persisted when the parent operation succeeds — an independent commit here would leave orphaned log entries for operations that were subsequently rolled back. """ ip = _get_real_ip() try: entry = ActivityLog( user_id = user_id, action = action, entity_type = entity_type, entity_id = entity_id, details = details, ip_address = ip, ) db.session.add(entry) # flush to surface constraint violations early without committing db.session.flush() logger.info( f'[ACTIVITY] action={action} entity={entity_type}:{entity_id} ' f'user_id={user_id} ip={ip}' ) except Exception as exc: db.session.rollback() logger.error(f'[ACTIVITY LOG ERROR] {exc}') def log_ticket_history(ticket, field_name, old_value, new_value, changed_by_id): """Record a granular field-level change on a ticket. Transaction note ---------------- Like log_action, this function does NOT commit — the caller is responsible for committing the session after all field changes have been recorded. """ from app.models import TicketHistory try: entry = TicketHistory( ticket_id = ticket.id, changed_by = changed_by_id, field_name = field_name, old_value = str(old_value) if old_value is not None else None, new_value = str(new_value) if new_value is not None else None, ) db.session.add(entry) db.session.flush() logger.info( f'[TICKET HISTORY] ticket_id={ticket.id} field={field_name} ' f'"{old_value}" -> "{new_value}" by user_id={changed_by_id}' ) except Exception as exc: db.session.rollback() logger.error(f'[TICKET HISTORY ERROR] {exc}')