229 lines
5.5 KiB
Go
229 lines
5.5 KiB
Go
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
|
||
|
|
}
|