""" 審計日誌服務 自動記錄所有 CRUD 操作,符合 ISO 要求 """ from typing import Optional, Dict, Any from datetime import datetime from sqlalchemy.orm import Session from fastapi import Request from app.models.audit_log import AuditLog class AuditService: """審計日誌服務類別""" @staticmethod def log( db: Session, action: str, resource_type: str, performed_by: str, resource_id: Optional[int] = None, details: Optional[Dict[str, Any]] = None, ip_address: Optional[str] = None, ) -> AuditLog: """ 創建審計日誌 Args: db: 資料庫 Session action: 操作類型 (create/update/delete/login/logout) resource_type: 資源類型 (employee/identity/department/etc) performed_by: 操作者 SSO 帳號 resource_id: 資源 ID details: 詳細變更內容 (dict) ip_address: IP 位址 Returns: AuditLog: 創建的審計日誌物件 """ # TODO: 從 JWT token 或 session 獲取 tenant_id,目前暫時使用預設值 1 tenant_id = 1 audit_log = AuditLog( tenant_id=tenant_id, action=action, resource_type=resource_type, resource_id=resource_id, performed_by=performed_by, details=details, ip_address=ip_address, ) db.add(audit_log) db.commit() db.refresh(audit_log) return audit_log @staticmethod def log_create( db: Session, resource_type: str, resource_id: int, performed_by: str, details: Dict[str, Any], ip_address: Optional[str] = None, ) -> AuditLog: """記錄創建操作""" return AuditService.log( db=db, action="create", resource_type=resource_type, resource_id=resource_id, performed_by=performed_by, details=details, ip_address=ip_address, ) @staticmethod def log_update( db: Session, resource_type: str, resource_id: int, performed_by: str, old_values: Dict[str, Any], new_values: Dict[str, Any], ip_address: Optional[str] = None, ) -> AuditLog: """ 記錄更新操作 Args: old_values: 舊值 new_values: 新值 """ details = { "old": old_values, "new": new_values, "changed_fields": list(set(old_values.keys()) & set(new_values.keys())) } return AuditService.log( db=db, action="update", resource_type=resource_type, resource_id=resource_id, performed_by=performed_by, details=details, ip_address=ip_address, ) @staticmethod def log_delete( db: Session, resource_type: str, resource_id: int, performed_by: str, details: Dict[str, Any], ip_address: Optional[str] = None, ) -> AuditLog: """記錄刪除操作""" return AuditService.log( db=db, action="delete", resource_type=resource_type, resource_id=resource_id, performed_by=performed_by, details=details, ip_address=ip_address, ) @staticmethod def log_login( db: Session, username: str, ip_address: Optional[str] = None, success: bool = True, ) -> AuditLog: """記錄登入操作""" return AuditService.log( db=db, action="login", resource_type="authentication", performed_by=username, details={"success": success}, ip_address=ip_address, ) @staticmethod def log_logout( db: Session, username: str, ip_address: Optional[str] = None, ) -> AuditLog: """記錄登出操作""" return AuditService.log( db=db, action="logout", resource_type="authentication", performed_by=username, ip_address=ip_address, ) @staticmethod def get_client_ip(request: Request) -> Optional[str]: """ 從 Request 獲取客戶端 IP 優先順序: 1. X-Forwarded-For (代理服務器) 2. X-Real-IP (Nginx) 3. request.client.host (直接連接) """ # 檢查 X-Forwarded-For forwarded_for = request.headers.get("X-Forwarded-For") if forwarded_for: # 取第一個 IP (客戶端真實 IP) return forwarded_for.split(",")[0].strip() # 檢查 X-Real-IP real_ip = request.headers.get("X-Real-IP") if real_ip: return real_ip # 直接連接 if request.client: return request.client.host return None @staticmethod def model_to_dict(obj, exclude_fields: Optional[set] = None) -> Dict[str, Any]: """ 將 SQLAlchemy Model 轉換為 dict (用於審計日誌) Args: obj: SQLAlchemy Model 物件 exclude_fields: 排除的欄位集合 Returns: dict: 模型的 dict 表示 """ if exclude_fields is None: exclude_fields = {"created_at", "updated_at", "_sa_instance_state"} result = {} for column in obj.__table__.columns: if column.name not in exclude_fields: value = getattr(obj, column.name) # 處理 datetime if isinstance(value, datetime): value = value.isoformat() result[column.name] = value return result @staticmethod def log_action( db: Session, action: str, resource_type: str, resource_id: Optional[int] = None, details: Optional[Dict[str, Any]] = None, request: Optional[Request] = None, performed_by: str = "system", ) -> AuditLog: """ 通用操作記錄 (permissions.py 使用的介面) Args: db: 資料庫 Session action: 操作類型 resource_type: 資源類型 resource_id: 資源 ID details: 詳細內容 request: FastAPI Request 物件 (用於取得 IP) performed_by: 操作者 """ ip_address = None if request is not None: ip_address = AuditService.get_client_ip(request) return AuditService.log( db=db, action=action, resource_type=resource_type, resource_id=resource_id, performed_by=performed_by, details=details, ip_address=ip_address, ) # 全域審計服務實例 audit_service = AuditService()