package services import ( "context" "encoding/json" "fmt" "time" "veza-backend-api/internal/database" "github.com/google/uuid" "go.uber.org/zap" ) // AuditService gère les logs d'audit type AuditService struct { db *database.Database logger *zap.Logger } // AuditLog représente un log d'audit type AuditLog struct { ID uuid.UUID `json:"id" db:"id"` UserID *uuid.UUID `json:"user_id" db:"user_id"` Action string `json:"action" db:"action"` Resource string `json:"resource" db:"resource"` ResourceID *uuid.UUID `json:"resource_id" db:"resource_id"` IPAddress string `json:"ip_address" db:"ip_address"` UserAgent string `json:"user_agent" db:"user_agent"` Metadata json.RawMessage `json:"metadata" db:"metadata"` Timestamp time.Time `json:"timestamp" db:"timestamp"` } // AuditLogCreateRequest données pour créer un log d'audit type AuditLogCreateRequest struct { UserID *uuid.UUID `json:"user_id"` Action string `json:"action"` Resource string `json:"resource"` ResourceID *uuid.UUID `json:"resource_id"` IPAddress string `json:"ip_address"` UserAgent string `json:"user_agent"` Metadata map[string]interface{} `json:"metadata"` } // AuditLogSearchRequest paramètres de recherche // BE-API-034: Enhanced with better filtering and pagination type AuditLogSearchRequest struct { UserID *uuid.UUID `json:"user_id"` Action string `json:"action"` Resource string `json:"resource"` ResourceID *uuid.UUID `json:"resource_id"` IPAddress string `json:"ip_address"` UserAgent string `json:"user_agent"` StartDate *time.Time `json:"start_date"` EndDate *time.Time `json:"end_date"` Limit int `json:"limit"` Offset int `json:"offset"` Page int `json:"page"` // Alternative to offset (page-based pagination) } // AuditStats statistiques d'audit type AuditStats struct { Action string `json:"action" db:"action"` Resource string `json:"resource" db:"resource"` ActionCount int64 `json:"action_count" db:"action_count"` UniqueUsers int64 `json:"unique_users" db:"unique_users"` UniqueIPs int64 `json:"unique_ips" db:"unique_ips"` } // SuspiciousActivity activité suspecte détectée type SuspiciousActivity struct { UserID *uuid.UUID `json:"user_id" db:"user_id"` IPAddress string `json:"ip_address" db:"ip_address"` ActionCount int64 `json:"action_count" db:"action_count"` UniqueActions int64 `json:"unique_actions" db:"unique_actions"` RiskScore int `json:"risk_score" db:"risk_score"` } // NewAuditService crée un nouveau service d'audit func NewAuditService(db *database.Database, logger *zap.Logger) *AuditService { return &AuditService{ db: db, logger: logger, } } // LogAction enregistre une action d'audit func (as *AuditService) LogAction(ctx context.Context, req *AuditLogCreateRequest) error { // Convertir les métadonnées en JSON metadataJSON, err := json.Marshal(req.Metadata) if err != nil { as.logger.Error("Failed to marshal audit metadata", zap.Error(err), zap.String("action", req.Action), ) return fmt.Errorf("failed to marshal audit metadata: %w", err) } // Insérer le log d'audit query := ` INSERT INTO audit_logs (id, user_id, action, resource, resource_id, ip_address, user_agent, metadata, timestamp) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ` _, err = as.db.ExecContext(ctx, query, uuid.New(), req.UserID, req.Action, req.Resource, req.ResourceID, req.IPAddress, req.UserAgent, metadataJSON, time.Now(), ) if err != nil { as.logger.Error("Failed to log audit action", zap.Error(err), zap.String("action", req.Action), zap.String("resource", req.Resource), ) return fmt.Errorf("failed to log audit action: %w", err) } as.logger.Debug("Audit action logged", zap.String("action", req.Action), zap.String("resource", req.Resource), zap.String("user_id", req.UserID.String()), ) return nil } // LogLogin enregistre une tentative de connexion func (as *AuditService) LogLogin(ctx context.Context, userID *uuid.UUID, success bool, ipAddress, userAgent string, metadata map[string]interface{}) error { action := "login_failed" if success { action = "login_success" } req := &AuditLogCreateRequest{ UserID: userID, Action: action, Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: metadata, } return as.LogAction(ctx, req) } // LogLogout enregistre une déconnexion func (as *AuditService) LogLogout(ctx context.Context, userID uuid.UUID, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: &userID, Action: "logout", Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{}, } return as.LogAction(ctx, req) } // LogUpload enregistre un upload de fichier func (as *AuditService) LogUpload(ctx context.Context, userID uuid.UUID, resourceID uuid.UUID, fileName string, fileSize int64, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: &userID, Action: "upload", Resource: "track", ResourceID: &resourceID, IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{ "file_name": fileName, "file_size": fileSize, }, } return as.LogAction(ctx, req) } // LogPermissionChange enregistre un changement de permission func (as *AuditService) LogPermissionChange(ctx context.Context, userID uuid.UUID, targetUserID uuid.UUID, oldPermissions, newPermissions []string, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: &userID, Action: "permission_change", Resource: "user", ResourceID: &targetUserID, IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{ "old_permissions": oldPermissions, "new_permissions": newPermissions, }, } return as.LogAction(ctx, req) } // LogDeletion enregistre une suppression func (as *AuditService) LogDeletion(ctx context.Context, userID uuid.UUID, resource string, resourceID uuid.UUID, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: &userID, Action: "delete", Resource: resource, ResourceID: &resourceID, IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{}, } return as.LogAction(ctx, req) } // BE-SEC-013: LogPasswordChange enregistre un changement de mot de passe func (as *AuditService) LogPasswordChange(ctx context.Context, userID uuid.UUID, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: &userID, Action: "password_change", Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{}, } return as.LogAction(ctx, req) } // BE-SEC-013: LogPasswordResetRequest enregistre une demande de réinitialisation de mot de passe func (as *AuditService) LogPasswordResetRequest(ctx context.Context, userID *uuid.UUID, email string, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: userID, Action: "password_reset_request", Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{ "email": email, }, } return as.LogAction(ctx, req) } // BE-SEC-013: LogPasswordReset enregistre une réinitialisation de mot de passe func (as *AuditService) LogPasswordReset(ctx context.Context, userID uuid.UUID, success bool, ipAddress, userAgent string) error { action := "password_reset_failed" if success { action = "password_reset_success" } req := &AuditLogCreateRequest{ UserID: &userID, Action: action, Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{}, } return as.LogAction(ctx, req) } // BE-SEC-013: LogTwoFactorEnabled enregistre l'activation de 2FA func (as *AuditService) LogTwoFactorEnabled(ctx context.Context, userID uuid.UUID, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: &userID, Action: "2fa_enabled", Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{}, } return as.LogAction(ctx, req) } // BE-SEC-013: LogTwoFactorDisabled enregistre la désactivation de 2FA func (as *AuditService) LogTwoFactorDisabled(ctx context.Context, userID uuid.UUID, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: &userID, Action: "2fa_disabled", Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{}, } return as.LogAction(ctx, req) } // BE-SEC-013: LogTwoFactorVerification enregistre une vérification 2FA func (as *AuditService) LogTwoFactorVerification(ctx context.Context, userID uuid.UUID, success bool, ipAddress, userAgent string) error { action := "2fa_verification_failed" if success { action = "2fa_verification_success" } req := &AuditLogCreateRequest{ UserID: &userID, Action: action, Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{}, } return as.LogAction(ctx, req) } // BE-SEC-013: LogAccessDenied enregistre un accès refusé func (as *AuditService) LogAccessDenied(ctx context.Context, userID *uuid.UUID, resource string, resourceID *uuid.UUID, reason string, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: userID, Action: "access_denied", Resource: resource, ResourceID: resourceID, IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{ "reason": reason, }, } return as.LogAction(ctx, req) } // BE-SEC-013: LogRoleChange enregistre un changement de rôle func (as *AuditService) LogRoleChange(ctx context.Context, userID uuid.UUID, targetUserID uuid.UUID, oldRole, newRole string, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: &userID, Action: "role_change", Resource: "user", ResourceID: &targetUserID, IPAddress: ipAddress, UserAgent: userAgent, Metadata: map[string]interface{}{ "old_role": oldRole, "new_role": newRole, }, } return as.LogAction(ctx, req) } // BE-SEC-013: LogAccountLocked enregistre un verrouillage de compte func (as *AuditService) LogAccountLocked(ctx context.Context, email string, reason string, lockedUntil *time.Time, ipAddress, userAgent string) error { metadata := map[string]interface{}{ "email": email, "reason": reason, } if lockedUntil != nil { metadata["locked_until"] = lockedUntil.Format(time.RFC3339) } req := &AuditLogCreateRequest{ UserID: nil, // May not have userID if account locked before login Action: "account_locked", Resource: "user", IPAddress: ipAddress, UserAgent: userAgent, Metadata: metadata, } return as.LogAction(ctx, req) } // BE-SEC-013: LogSecurityEvent enregistre un événement de sécurité générique func (as *AuditService) LogSecurityEvent(ctx context.Context, userID *uuid.UUID, action string, resource string, resourceID *uuid.UUID, details map[string]interface{}, ipAddress, userAgent string) error { req := &AuditLogCreateRequest{ UserID: userID, Action: action, Resource: resource, ResourceID: resourceID, IPAddress: ipAddress, UserAgent: userAgent, Metadata: details, } return as.LogAction(ctx, req) } // SearchLogs recherche des logs d'audit func (as *AuditService) SearchLogs(ctx context.Context, req *AuditLogSearchRequest) ([]*AuditLog, error) { // Construire la requête dynamiquement query := ` SELECT id, user_id, action, resource, resource_id, ip_address, user_agent, metadata, timestamp FROM audit_logs WHERE 1=1 ` args := []interface{}{} argIndex := 1 if req.UserID != nil { query += fmt.Sprintf(" AND user_id = $%d", argIndex) args = append(args, *req.UserID) argIndex++ } if req.Action != "" { query += fmt.Sprintf(" AND action = $%d", argIndex) args = append(args, req.Action) argIndex++ } if req.Resource != "" { query += fmt.Sprintf(" AND resource = $%d", argIndex) args = append(args, req.Resource) argIndex++ } if req.StartDate != nil { query += fmt.Sprintf(" AND timestamp >= $%d", argIndex) args = append(args, *req.StartDate) argIndex++ } if req.EndDate != nil { query += fmt.Sprintf(" AND timestamp <= $%d", argIndex) args = append(args, *req.EndDate) argIndex++ } query += " ORDER BY timestamp DESC" if req.Limit > 0 { query += fmt.Sprintf(" LIMIT $%d", argIndex) args = append(args, req.Limit) argIndex++ } if req.Offset > 0 { query += fmt.Sprintf(" OFFSET $%d", argIndex) args = append(args, req.Offset) } rows, err := as.db.QueryContext(ctx, query, args...) if err != nil { as.logger.Error("Failed to search audit logs", zap.Error(err), ) return nil, fmt.Errorf("failed to search audit logs: %w", err) } defer rows.Close() var logs []*AuditLog for rows.Next() { var log AuditLog err := rows.Scan( &log.ID, &log.UserID, &log.Action, &log.Resource, &log.ResourceID, &log.IPAddress, &log.UserAgent, &log.Metadata, &log.Timestamp, ) if err != nil { as.logger.Error("Failed to scan audit log", zap.Error(err), ) continue } logs = append(logs, &log) } return logs, nil } // CountLogs compte le nombre total de logs correspondant aux critères de recherche // BE-API-034: Added total count for pagination func (as *AuditService) CountLogs(ctx context.Context, req *AuditLogSearchRequest) (int64, error) { query := ` SELECT COUNT(*) FROM audit_logs WHERE 1=1 ` args := []interface{}{} argIndex := 1 if req.UserID != nil { query += fmt.Sprintf(" AND user_id = $%d", argIndex) args = append(args, *req.UserID) argIndex++ } if req.Action != "" { query += fmt.Sprintf(" AND action = $%d", argIndex) args = append(args, req.Action) argIndex++ } if req.Resource != "" { query += fmt.Sprintf(" AND resource = $%d", argIndex) args = append(args, req.Resource) argIndex++ } if req.ResourceID != nil { query += fmt.Sprintf(" AND resource_id = $%d", argIndex) args = append(args, *req.ResourceID) argIndex++ } if req.IPAddress != "" { query += fmt.Sprintf(" AND ip_address = $%d", argIndex) args = append(args, req.IPAddress) argIndex++ } if req.UserAgent != "" { query += fmt.Sprintf(" AND user_agent LIKE $%d", argIndex) args = append(args, "%"+req.UserAgent+"%") argIndex++ } if req.StartDate != nil { query += fmt.Sprintf(" AND timestamp >= $%d", argIndex) args = append(args, *req.StartDate) argIndex++ } if req.EndDate != nil { query += fmt.Sprintf(" AND timestamp <= $%d", argIndex) args = append(args, *req.EndDate) argIndex++ } var count int64 err := as.db.QueryRowContext(ctx, query, args...).Scan(&count) if err != nil { as.logger.Error("Failed to count audit logs", zap.Error(err), ) return 0, fmt.Errorf("failed to count audit logs: %w", err) } return count, nil } // GetStats récupère les statistiques d'audit func (as *AuditService) GetStats(ctx context.Context, startDate, endDate time.Time) ([]*AuditStats, error) { query := ` SELECT action, resource, COUNT(*) as action_count, COUNT(DISTINCT user_id) as unique_users, COUNT(DISTINCT ip_address) as unique_ips FROM audit_logs WHERE timestamp BETWEEN $1 AND $2 GROUP BY action, resource ORDER BY action_count DESC ` rows, err := as.db.QueryContext(ctx, query, startDate, endDate) if err != nil { as.logger.Error("Failed to get audit stats", zap.Error(err), ) return nil, fmt.Errorf("failed to get audit stats: %w", err) } defer rows.Close() var stats []*AuditStats for rows.Next() { var stat AuditStats err := rows.Scan( &stat.Action, &stat.Resource, &stat.ActionCount, &stat.UniqueUsers, &stat.UniqueIPs, ) if err != nil { as.logger.Error("Failed to scan audit stat", zap.Error(err), ) continue } stats = append(stats, &stat) } return stats, nil } // DetectSuspiciousActivity détecte les activités suspectes func (as *AuditService) DetectSuspiciousActivity(ctx context.Context, hours int) ([]*SuspiciousActivity, error) { query := ` WITH user_activity AS ( SELECT user_id, ip_address, COUNT(*) as action_count, COUNT(DISTINCT action) as unique_actions FROM audit_logs WHERE timestamp >= NOW() - INTERVAL '%d hours' GROUP BY user_id, ip_address ) SELECT user_id, ip_address, action_count, unique_actions, CASE WHEN action_count > 1000 THEN 100 WHEN action_count > 500 THEN 80 WHEN action_count > 100 THEN 60 WHEN action_count > 50 THEN 40 WHEN action_count > 20 THEN 20 ELSE 0 END as risk_score FROM user_activity WHERE action_count > 20 ORDER BY risk_score DESC, action_count DESC ` rows, err := as.db.QueryContext(ctx, fmt.Sprintf(query, hours)) if err != nil { as.logger.Error("Failed to detect suspicious activity", zap.Error(err), ) return nil, fmt.Errorf("failed to detect suspicious activity: %w", err) } defer rows.Close() var activities []*SuspiciousActivity for rows.Next() { var activity SuspiciousActivity err := rows.Scan( &activity.UserID, &activity.IPAddress, &activity.ActionCount, &activity.UniqueActions, &activity.RiskScore, ) if err != nil { as.logger.Error("Failed to scan suspicious activity", zap.Error(err), ) continue } activities = append(activities, &activity) } return activities, nil } // CleanupOldLogs nettoie les anciens logs d'audit func (as *AuditService) CleanupOldLogs(ctx context.Context, retentionDays int) (int64, error) { query := ` DELETE FROM audit_logs WHERE timestamp < NOW() - INTERVAL '%d days' ` result, err := as.db.ExecContext(ctx, fmt.Sprintf(query, retentionDays)) if err != nil { as.logger.Error("Failed to cleanup old audit logs", zap.Error(err), ) return 0, fmt.Errorf("failed to cleanup old audit logs: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return 0, fmt.Errorf("failed to get rows affected: %w", err) } as.logger.Info("Old audit logs cleaned up", zap.Int64("deleted_count", rowsAffected), zap.Int("retention_days", retentionDays), ) return rowsAffected, nil } // GetUserActivity récupère l'activité d'un utilisateur func (as *AuditService) GetUserActivity(ctx context.Context, userID uuid.UUID, limit int) ([]*AuditLog, error) { req := &AuditLogSearchRequest{ UserID: &userID, Limit: limit, } return as.SearchLogs(ctx, req) } // GetIPActivity récupère l'activité d'une IP func (as *AuditService) GetIPActivity(ctx context.Context, ipAddress string, limit int) ([]*AuditLog, error) { query := ` SELECT id, user_id, action, resource, resource_id, ip_address, user_agent, metadata, timestamp FROM audit_logs WHERE ip_address = $1 ORDER BY timestamp DESC LIMIT $2 ` rows, err := as.db.QueryContext(ctx, query, ipAddress, limit) if err != nil { as.logger.Error("Failed to get IP activity", zap.Error(err), zap.String("ip_address", ipAddress), ) return nil, fmt.Errorf("failed to get IP activity: %w", err) } defer rows.Close() var logs []*AuditLog for rows.Next() { var log AuditLog err := rows.Scan( &log.ID, &log.UserID, &log.Action, &log.Resource, &log.ResourceID, &log.IPAddress, &log.UserAgent, &log.Metadata, &log.Timestamp, ) if err != nil { as.logger.Error("Failed to scan audit log", zap.Error(err), ) continue } logs = append(logs, &log) } return logs, nil }