package audit import ( "RenewCMS/internal/domain/user" "errors" "slices" "strconv" "strings" "time" ) var ErrInvalidFilter = errors.New("invalid activity filter") var ErrInvalidRetention = errors.New("invalid activity retention period") var ErrInvalidEvent = errors.New("invalid activity event") var ErrActorNotAllowed = errors.New("activity of another actor is not allowed") type ActorType string const ( ActorUser ActorType = "user" ActorAPIKey ActorType = "api_key" ActorSystem ActorType = "system" ActorAnonymous ActorType = "anonymous" ) type Category string const ( CategoryArticles Category = "articles" CategoryMedia Category = "media" CategoryUsers Category = "users" CategoryAPIKeys Category = "api_keys" CategoryAuth Category = "auth" CategorySystem Category = "system" CategorySites Category = "sites" CategoryAuthors Category = "authors" ) type Outcome string const ( OutcomeSuccess Outcome = "success" OutcomeFailed Outcome = "failed" ) type Source string const ( SourceWeb Source = "web" SourceAPIKey Source = "api_key" SourceSystem Source = "system" ) type ChangeKind string const ( ChangeText ChangeKind = "text" ChangeBody ChangeKind = "body" ) const ( ActionLogin = "auth.login" ActionLoginFailed = "auth.login_failed" ActionLogout = "auth.logout" ActionPasswordResetRequest = "auth.password_reset_requested" ActionPasswordResetDone = "auth.password_reset_completed" ActionAPIKeyCreate = "api_key.create" ActionAPIKeyRevoke = "api_key.revoke" ActionRetentionChanged = "system.activity_retention_changed" ActionCleanup = "system.activity_cleanup" ActionArticleCreate = "article.create" ActionArticleEdit = "article.edit" ActionArticlePublish = "article.publish" ActionArticleUnpublish = "article.unpublish" ActionArticleDelete = "article.delete" ActionSiteCreate = "site.create" ActionSiteEdit = "site.edit" ActionSiteSetDefault = "site.set_default" ActionSiteDelete = "site.delete" ActionAuthorCreate = "author.create" ActionAuthorEdit = "author.edit" ActionAuthorDelete = "author.delete" ActionMediaUpload = "media.upload" ActionMediaRename = "media.rename" ActionMediaDelete = "media.delete" ActionMediaMerge = "media.merge" ActionMediaPurge = "media.purge" ActionVariantCreate = "media.variant_create" ActionVariantUpdate = "media.variant_update" ActionVariantDelete = "media.variant_delete" ActionVariantEnable = "media.variant_enable" ActionVariantDisable = "media.variant_disable" ActionVariantPurge = "media.variant_purge" ActionVariantGenerateAll = "media.variant_generate_all" ActionVariantPurgeImage = "media.variant_purge_image" ActionVariantPurgeAll = "media.variant_purge_all" ActionVariantPurgeUnused = "media.variant_purge_unused" ActionUserRegister = "user.register" ActionUserVerify = "user.verify" ActionUserUpdateProfile = "user.update_profile" ActionInviteCreate = "invite.create" ActionInviteCancel = "invite.cancel" ActionInviteRenew = "invite.renew" ActionInviteAccept = "invite.accept" ActionMediaPurgeScheduled = "system.media_purge" ActionUpdateChecked = "system.update_checked" ActionUpdateDownloaded = "system.update_downloaded" ActionRestartRequested = "system.restart_requested" ) const ( TargetAPIKey = "api_key" TargetUser = "user" TargetArticle = "article" TargetImage = "image" TargetVariant = "image_variant" TargetInvite = "invite" TargetRelease = "release" TargetSite = "site" TargetAuthor = "author" ) const ( SystemActorName = "System" ReasonWrongLogin = "Wrong username or password." MaxUserAgentLength = 255 ParamRemoved = "removed" ParamFrom = "from" ParamTo = "to" ParamMaxGBFrom = "max_gb_from" ParamMaxGBTo = "max_gb_to" ParamMerged = "merged" ParamArticlesUpdated = "articles_updated" ParamFailedGroups = "failed_groups" ParamUpdateAvailable = "update_available" ParamKind = "kind" ParamPurgedFiles = "purged_files" ParamPurgedBytes = "purged_bytes" ParamFailedFiles = "failed_files" ParamOlderThanDays = "older_than_days" ParamGenerated = "generated" ParamFailed = "failed" ParamCauses = "causes" ParamWidth = "width" ParamHeight = "height" ParamFit = "fit" ParamFormat = "format" ParamQuality = "quality" ParamTag = "tag" FieldTitle = "title" FieldOnline = "online" FieldBody = "body" FieldFilename = "filename" FieldDisplayName = "display_name" FieldWidth = "width" FieldHeight = "height" FieldFit = "fit" FieldFormat = "format" FieldQuality = "quality" FieldName = "name" FieldBaseURL = "base_url" FieldDefaultRobots = "default_robots" FieldLocale = "locale" FieldSocialImage = "default_social_image" FieldSite = "site" FieldSlug = "slug" FieldBio = "bio" FieldWebsite = "website" FieldSocialLinks = "social_links" FieldAvatar = "avatar" FieldUser = "user" FieldAuthors = "authors" maxNameLength = 255 maxActionLength = 64 maxFailureReasonSize = 255 ) func ActorTypes() []ActorType { return []ActorType{ActorUser, ActorAPIKey, ActorSystem, ActorAnonymous} } func Categories() []Category { return []Category{CategoryArticles, CategoryMedia, CategoryUsers, CategoryAPIKeys, CategoryAuth, CategorySystem, CategorySites, CategoryAuthors} } func Outcomes() []Outcome { return []Outcome{OutcomeSuccess, OutcomeFailed} } func Sources() []Source { return []Source{SourceWeb, SourceAPIKey, SourceSystem} } func ChangeKinds() []ChangeKind { return []ChangeKind{ChangeText, ChangeBody} } type Actor struct { Type ActorType `json:"type"` ID *uint32 `json:"id"` Name string `json:"name"` } type Target struct { Type string `json:"type"` ID string `json:"id"` Name string `json:"name"` } type Change struct { Field string `json:"field"` Kind ChangeKind `json:"kind"` Before *string `json:"before,omitempty"` After *string `json:"after,omitempty"` Delta *int `json:"delta,omitempty"` } type Event struct { ID uint64 `json:"id"` CreatedAt time.Time `json:"created_at"` Action string `json:"action"` Category Category `json:"category"` Actor Actor `json:"actor"` Target Target `json:"target"` Outcome Outcome `json:"outcome"` FailureReason *string `json:"failure_reason"` Security bool `json:"security"` Source Source `json:"source"` IP *string `json:"ip"` UserAgent *string `json:"user_agent"` Params map[string]any `json:"params"` Changes []Change `json:"changes"` } func NewEvent(action string, category Category) Event { return Event{ Action: action, Category: category, Outcome: OutcomeSuccess, Params: map[string]any{}, Changes: []Change{}, } } func SystemActor() Actor { return Actor{Type: ActorSystem, Name: SystemActorName} } func UserActor(u user.User) Actor { id := u.ID name := strings.TrimSpace(u.DisplayName) if name == "" { name = u.Username } return Actor{Type: ActorUser, ID: &id, Name: name} } func AnonymousActor(name string) Actor { return Actor{Type: ActorAnonymous, Name: name} } func APIKeyActor(id uint32, name string) Actor { return Actor{Type: ActorAPIKey, ID: &id, Name: name} } func APIKeyTarget(id uint32, name string) Target { return Target{Type: TargetAPIKey, ID: strconv.FormatUint(uint64(id), 10), Name: name} } func (e Event) Validate() error { if e.Action == "" || len(e.Action) > maxActionLength { return ErrInvalidEvent } if !slices.Contains(Categories(), e.Category) || !slices.Contains(Outcomes(), e.Outcome) || !slices.Contains(Sources(), e.Source) || !slices.Contains(ActorTypes(), e.Actor.Type) { return ErrInvalidEvent } if e.FailureReason != nil && len(*e.FailureReason) > maxFailureReasonSize { return ErrInvalidEvent } for _, change := range e.Changes { if change.Kind != ChangeText && change.Kind != ChangeBody { return ErrInvalidEvent } } return nil } func (e Event) Truncated() Event { e.Actor.Name = truncate(e.Actor.Name, maxNameLength) e.Target.Name = truncate(e.Target.Name, maxNameLength) return e } func truncate(value string, limit int) string { runes := []rune(value) return string(runes[:min(len(runes), limit)]) }