package services import ( "cmbone/internal/models" "strings" "time" ) const ( auditActionSettingsUpdate = "settings.update" auditActionImageTaskCreate = "image_task.create" appLogLevelDebug = "debug" appLogLevelInfo = "info" appLogLevelWarn = "warn" appLogLevelError = "error" defaultLogLimit = 50 maxLogLimit = 200 ) type AuditLogService struct { store *SuiStore } func NewAuditLogService(store *SuiStore) *AuditLogService { return &AuditLogService{store: store} } func (s *AuditLogService) Record(action string, targetType string, targetID string, detail string) error { return recordAuditLog(s.store, currentAuditActor(s.store), action, targetType, targetID, detail) } func (s *AuditLogService) List(filter models.AuditLogFilter) ([]models.AuditLog, error) { limit, offset := normalizeLimitOffset(filter.Limit, filter.Offset) query := strings.Builder{} query.WriteString(` SELECT id, actor, action, target_type, target_id, detail, created_at FROM audit_logs WHERE 1 = 1 `) args := make([]any, 0, 5) if strings.TrimSpace(filter.Actor) != "" { query.WriteString(" AND actor = ?") args = append(args, strings.TrimSpace(filter.Actor)) } if strings.TrimSpace(filter.Action) != "" { query.WriteString(" AND action = ?") args = append(args, strings.TrimSpace(filter.Action)) } if strings.TrimSpace(filter.TargetType) != "" { query.WriteString(" AND target_type = ?") args = append(args, strings.TrimSpace(filter.TargetType)) } query.WriteString(" ORDER BY id DESC LIMIT ? OFFSET ?") args = append(args, limit, offset) rows, err := s.store.DB.Query(query.String(), args...) if err != nil { return nil, err } defer rows.Close() logs := make([]models.AuditLog, 0) for rows.Next() { log, err := scanAuditLog(rows) if err != nil { return nil, err } logs = append(logs, log) } if err := rows.Err(); err != nil { return nil, err } return logs, nil } type AppLogService struct { store *SuiStore } func NewAppLogService(store *SuiStore) *AppLogService { return &AppLogService{store: store} } func (s *AppLogService) Record(level string, module string, message string, detail string) error { return recordAppLog(s.store, level, module, message, detail) } func (s *AppLogService) List(filter models.AppLogFilter) ([]models.AppLog, error) { limit, offset := normalizeLimitOffset(filter.Limit, filter.Offset) query := strings.Builder{} query.WriteString(` SELECT id, level, module, message, detail, created_at FROM app_logs WHERE 1 = 1 `) args := make([]any, 0, 4) if strings.TrimSpace(filter.Level) != "" { query.WriteString(" AND level = ?") args = append(args, normalizeAppLogLevel(filter.Level)) } if strings.TrimSpace(filter.Module) != "" { query.WriteString(" AND module = ?") args = append(args, strings.TrimSpace(filter.Module)) } query.WriteString(" ORDER BY id DESC LIMIT ? OFFSET ?") args = append(args, limit, offset) rows, err := s.store.DB.Query(query.String(), args...) if err != nil { return nil, err } defer rows.Close() logs := make([]models.AppLog, 0) for rows.Next() { log, err := scanAppLog(rows) if err != nil { return nil, err } logs = append(logs, log) } if err := rows.Err(); err != nil { return nil, err } return logs, nil } func recordAuditLog(store *SuiStore, actor string, action string, targetType string, targetID string, detail string) error { if store == nil || store.DB == nil { return nil } if strings.TrimSpace(actor) == "" { actor = "system" } _, err := store.DB.Exec(` INSERT INTO audit_logs (actor, action, target_type, target_id, detail, created_at) VALUES (?, ?, ?, ?, ?, ?) `, actor, strings.TrimSpace(action), strings.TrimSpace(targetType), strings.TrimSpace(targetID), strings.TrimSpace(detail), time.Now().UTC().Format(time.RFC3339)) return err } func recordAppLog(store *SuiStore, level string, module string, message string, detail string) error { if store == nil || store.DB == nil { return nil } _, err := store.DB.Exec(` INSERT INTO app_logs (level, module, message, detail, created_at) VALUES (?, ?, ?, ?, ?) `, normalizeAppLogLevel(level), strings.TrimSpace(module), strings.TrimSpace(message), strings.TrimSpace(detail), time.Now().UTC().Format(time.RFC3339)) return err } func currentAuditActor(store *SuiStore) string { if store == nil || store.DB == nil { return "system" } var username string err := store.DB.QueryRow(` SELECT username FROM auth_sessions WHERE active = 1 AND expires_at > ? ORDER BY id DESC LIMIT 1 `, time.Now().UTC().Format(time.RFC3339)).Scan(&username) if err != nil || strings.TrimSpace(username) == "" { return "system" } return username } func normalizeLimitOffset(limit int, offset int) (int, int) { if limit <= 0 { limit = defaultLogLimit } if limit > maxLogLimit { limit = maxLogLimit } if offset < 0 { offset = 0 } return limit, offset } func normalizeAppLogLevel(level string) string { switch strings.ToLower(strings.TrimSpace(level)) { case appLogLevelDebug: return appLogLevelDebug case appLogLevelWarn: return appLogLevelWarn case appLogLevelError: return appLogLevelError default: return appLogLevelInfo } } func scanAuditLog(scanner rowScanner) (models.AuditLog, error) { var log models.AuditLog err := scanner.Scan( &log.ID, &log.Actor, &log.Action, &log.TargetType, &log.TargetID, &log.Detail, &log.CreatedAt, ) return log, err } func scanAppLog(scanner rowScanner) (models.AppLog, error) { var log models.AppLog err := scanner.Scan( &log.ID, &log.Level, &log.Module, &log.Message, &log.Detail, &log.CreatedAt, ) return log, err }