package audit import ( "fmt" "time" "github.com/knowfoolery/backend/shared/errors" "github.com/knowfoolery/backend/shared/types" ) type AuditLogLevel string const ( AuditLogLevelInfo AuditLogLevel = "info" AuditLogLevelWarning AuditLogLevel = "warning" AuditLogLevelError AuditLogLevel = "error" AuditLogLevelCritical AuditLogLevel = "critical" ) type AuditActionType string const ( AuditActionUserCreated AuditActionType = "user_created" AuditActionUserUpdated AuditActionType = "user_updated" AuditActionUserDeleted AuditActionType = "user_deleted" AuditActionUserSuspended AuditActionType = "user_suspended" AuditActionUserUnsuspended AuditActionType = "user_unsuspended" AuditActionQuestionCreated AuditActionType = "question_created" AuditActionQuestionUpdated AuditActionType = "question_updated" AuditActionQuestionDeleted AuditActionType = "question_deleted" AuditActionQuestionPublished AuditActionType = "question_published" AuditActionQuestionUnpublished AuditActionType = "question_unpublished" AuditActionThemeCreated AuditActionType = "theme_created" AuditActionThemeUpdated AuditActionType = "theme_updated" AuditActionThemeDeleted AuditActionType = "theme_deleted" AuditActionLeaderboardCreated AuditActionType = "leaderboard_created" AuditActionLeaderboardUpdated AuditActionType = "leaderboard_updated" AuditActionLeaderboardDeleted AuditActionType = "leaderboard_deleted" AuditActionLeaderboardReset AuditActionType = "leaderboard_reset" AuditActionAdminLogin AuditActionType = "admin_login" AuditActionAdminLogout AuditActionType = "admin_logout" AuditActionAdminCreated AuditActionType = "admin_created" AuditActionAdminRoleChanged AuditActionType = "admin_role_changed" AuditActionSystemConfigUpdated AuditActionType = "system_config_updated" AuditActionDataExported AuditActionType = "data_exported" AuditActionDataImported AuditActionType = "data_imported" AuditActionSecurityViolation AuditActionType = "security_violation" ) type AuditLog struct { id types.AuditLogID adminID types.UserID adminName string sessionID types.AdminSessionID action AuditActionType resourceType string resourceID string level AuditLogLevel message string details map[string]interface{} ipAddress string userAgent string timestamp time.Time duration time.Duration success bool errorMessage string tags []string metadata map[string]interface{} } func NewAuditLog( id types.AuditLogID, adminID types.UserID, adminName string, sessionID types.AdminSessionID, action AuditActionType, resourceType, resourceID string, ) (*AuditLog, error) { if id == "" { return nil, errors.ErrValidationFailed("audit_log_id", "audit log ID cannot be empty") } if adminID == "" { return nil, errors.ErrInvalidPlayerID } if adminName == "" { return nil, errors.ErrValidationFailed("admin_name", "admin name cannot be empty") } if action == "" { return nil, errors.ErrValidationFailed("action", "action cannot be empty") } return &AuditLog{ id: id, adminID: adminID, adminName: adminName, sessionID: sessionID, action: action, resourceType: resourceType, resourceID: resourceID, level: AuditLogLevelInfo, details: make(map[string]interface{}), timestamp: time.Now(), success: true, tags: make([]string, 0), metadata: make(map[string]interface{}), }, nil } func (al *AuditLog) ID() types.AuditLogID { return al.id } func (al *AuditLog) AdminID() types.UserID { return al.adminID } func (al *AuditLog) AdminName() string { return al.adminName } func (al *AuditLog) SessionID() types.AdminSessionID { return al.sessionID } func (al *AuditLog) Action() AuditActionType { return al.action } func (al *AuditLog) ResourceType() string { return al.resourceType } func (al *AuditLog) ResourceID() string { return al.resourceID } func (al *AuditLog) Level() AuditLogLevel { return al.level } func (al *AuditLog) Message() string { return al.message } func (al *AuditLog) Details() map[string]interface{} { result := make(map[string]interface{}) for k, v := range al.details { result[k] = v } return result } func (al *AuditLog) IPAddress() string { return al.ipAddress } func (al *AuditLog) UserAgent() string { return al.userAgent } func (al *AuditLog) Timestamp() time.Time { return al.timestamp } func (al *AuditLog) Duration() time.Duration { return al.duration } func (al *AuditLog) Success() bool { return al.success } func (al *AuditLog) ErrorMessage() string { return al.errorMessage } func (al *AuditLog) Tags() []string { result := make([]string, len(al.tags)) copy(result, al.tags) return result } func (al *AuditLog) Metadata() map[string]interface{} { result := make(map[string]interface{}) for k, v := range al.metadata { result[k] = v } return result } func (al *AuditLog) SetLevel(level AuditLogLevel) error { switch level { case AuditLogLevelInfo, AuditLogLevelWarning, AuditLogLevelError, AuditLogLevelCritical: al.level = level return nil default: return errors.ErrValidationFailed("level", "invalid audit log level") } } func (al *AuditLog) SetMessage(message string) error { if message == "" { return errors.ErrValidationFailed("message", "message cannot be empty") } al.message = message return nil } func (al *AuditLog) AddDetail(key string, value interface{}) error { if key == "" { return errors.ErrValidationFailed("detail_key", "detail key cannot be empty") } al.details[key] = value return nil } func (al *AuditLog) RemoveDetail(key string) { delete(al.details, key) } func (al *AuditLog) SetNetworkInfo(ipAddress, userAgent string) { if ipAddress != "" { al.ipAddress = ipAddress } if userAgent != "" { al.userAgent = userAgent } } func (al *AuditLog) SetDuration(duration time.Duration) { if duration >= 0 { al.duration = duration } } func (al *AuditLog) SetSuccess(success bool) { al.success = success if success { al.errorMessage = "" if al.level == AuditLogLevelError || al.level == AuditLogLevelCritical { al.level = AuditLogLevelInfo } } } func (al *AuditLog) SetError(errorMessage string) { if errorMessage != "" { al.success = false al.errorMessage = errorMessage if al.level == AuditLogLevelInfo { al.level = AuditLogLevelError } } } func (al *AuditLog) AddTag(tag string) error { if tag == "" { return errors.ErrValidationFailed("tag", "tag cannot be empty") } for _, existingTag := range al.tags { if existingTag == tag { return nil } } al.tags = append(al.tags, tag) return nil } func (al *AuditLog) RemoveTag(tag string) { newTags := make([]string, 0, len(al.tags)) for _, existingTag := range al.tags { if existingTag != tag { newTags = append(newTags, existingTag) } } al.tags = newTags } func (al *AuditLog) HasTag(tag string) bool { for _, existingTag := range al.tags { if existingTag == tag { return true } } return false } func (al *AuditLog) SetMetadata(key string, value interface{}) error { if key == "" { return errors.ErrInvalidMetadata } al.metadata[key] = value return nil } func (al *AuditLog) RemoveMetadata(key string) { delete(al.metadata, key) } func (al *AuditLog) IsSecuritySensitive() bool { switch al.action { case AuditActionAdminLogin, AuditActionAdminLogout, AuditActionAdminCreated, AuditActionAdminRoleChanged, AuditActionSecurityViolation: return true case AuditActionUserSuspended, AuditActionUserUnsuspended: return true case AuditActionSystemConfigUpdated: return true default: return al.HasTag("security") || al.HasTag("sensitive") } } func (al *AuditLog) IsHighPriority() bool { return al.level == AuditLogLevelError || al.level == AuditLogLevelCritical || al.IsSecuritySensitive() } func (al *AuditLog) GetAge() time.Duration { return time.Since(al.timestamp) } func (al *AuditLog) FormatSummary() string { status := "SUCCESS" if !al.success { status = "FAILED" } return fmt.Sprintf("[%s] %s by %s (%s) - %s: %s", al.level, al.action, al.adminName, al.adminID, status, al.message) } func (al *AuditLog) String() string { return fmt.Sprintf("AuditLog{ID: %s, Action: %s, AdminID: %s, Resource: %s/%s, Success: %t}", al.id, al.action, al.adminID, al.resourceType, al.resourceID, al.success) } type AuditLogSnapshot struct { ID types.AuditLogID `json:"id"` AdminID types.UserID `json:"admin_id"` AdminName string `json:"admin_name"` SessionID types.AdminSessionID `json:"session_id"` Action AuditActionType `json:"action"` ResourceType string `json:"resource_type"` ResourceID string `json:"resource_id"` Level AuditLogLevel `json:"level"` Message string `json:"message"` Details map[string]interface{} `json:"details"` IPAddress string `json:"ip_address"` UserAgent string `json:"user_agent"` Timestamp time.Time `json:"timestamp"` Duration time.Duration `json:"duration"` Success bool `json:"success"` ErrorMessage string `json:"error_message"` Tags []string `json:"tags"` Metadata map[string]interface{} `json:"metadata"` } func (al *AuditLog) ToSnapshot() AuditLogSnapshot { return AuditLogSnapshot{ ID: al.id, AdminID: al.adminID, AdminName: al.adminName, SessionID: al.sessionID, Action: al.action, ResourceType: al.resourceType, ResourceID: al.resourceID, Level: al.level, Message: al.message, Details: al.Details(), IPAddress: al.ipAddress, UserAgent: al.userAgent, Timestamp: al.timestamp, Duration: al.duration, Success: al.success, ErrorMessage: al.errorMessage, Tags: al.Tags(), Metadata: al.Metadata(), } } func (al *AuditLog) FromSnapshot(snapshot AuditLogSnapshot) error { if snapshot.ID == "" { return errors.ErrValidationFailed("audit_log_id", "audit log ID cannot be empty") } if snapshot.AdminID == "" { return errors.ErrInvalidPlayerID } al.id = snapshot.ID al.adminID = snapshot.AdminID al.adminName = snapshot.AdminName al.sessionID = snapshot.SessionID al.action = snapshot.Action al.resourceType = snapshot.ResourceType al.resourceID = snapshot.ResourceID al.level = snapshot.Level al.message = snapshot.Message al.ipAddress = snapshot.IPAddress al.userAgent = snapshot.UserAgent al.timestamp = snapshot.Timestamp al.duration = snapshot.Duration al.success = snapshot.Success al.errorMessage = snapshot.ErrorMessage al.details = make(map[string]interface{}) for k, v := range snapshot.Details { al.details[k] = v } al.tags = make([]string, len(snapshot.Tags)) copy(al.tags, snapshot.Tags) al.metadata = make(map[string]interface{}) for k, v := range snapshot.Metadata { al.metadata[k] = v } return nil }