From a26c6704fc052e5c850995cd3b44dc3ebdb7dbb7 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 11 Jun 2026 21:39:08 +0200 Subject: [PATCH] refactor(events): use a concrete doer on project and team events ProjectUpdated/Deleted, ProjectSharedWith* and TeamCreated/Deleted carried an interface-typed Doer that could not be unmarshaled, forcing the audit registrations to decode anonymous mirror structs. Hydrate the doer via GetUserOrLinkShareUser at the dispatch sites like the task events already do, register the events directly and drop the untyped audit registration path. Webhook payloads for these events now serialize link share doers as their pseudo-user (negative id) instead of the raw link share object, consistent with task events. --- pkg/audit/listener.go | 18 ++----- pkg/models/events.go | 25 +++++----- pkg/models/listeners.go | 97 +++++++------------------------------ pkg/models/project.go | 12 ++++- pkg/models/project_team.go | 6 ++- pkg/models/project_users.go | 6 ++- pkg/models/teams.go | 8 ++- 7 files changed, 59 insertions(+), 113 deletions(-) diff --git a/pkg/audit/listener.go b/pkg/audit/listener.go index c0454512a..599a9b385 100644 --- a/pkg/audit/listener.go +++ b/pkg/audit/listener.go @@ -45,27 +45,15 @@ func RegisterEventForAudit[T any, PT interface { events.Event }](toEntry func(PT) *Entry) { name := PT(new(T)).Name() - RegisterEventNameForAudit(name, func(payload []byte) (*Entry, error) { - e := PT(new(T)) // fresh instance per message — handlers run concurrently - if err := json.Unmarshal(payload, e); err != nil { - return nil, err - } - return toEntry(e), nil - }) -} - -// RegisterEventNameForAudit is the untyped variant for events which cannot be -// unmarshaled into their Go struct directly (e.g. interface-typed Doer -// fields); the mapping decodes the raw payload itself. -func RegisterEventNameForAudit(name string, toEntry func(payload []byte) (*Entry, error)) { events.RegisterListener(name, &auditListener{handle: func(msg *message.Message) error { if !license.IsFeatureEnabled(license.FeatureAuditLogs) { return nil // license is runtime-mutable — checked per event, not at registration } - entry, err := toEntry(msg.Payload) - if err != nil { + e := PT(new(T)) // fresh instance per message — handlers run concurrently + if err := json.Unmarshal(msg.Payload, e); err != nil { return err } + entry := toEntry(e) if entry == nil { return nil } diff --git a/pkg/models/events.go b/pkg/models/events.go index 1996f54b8..b938345f4 100644 --- a/pkg/models/events.go +++ b/pkg/models/events.go @@ -18,7 +18,6 @@ package models import ( "code.vikunja.io/api/pkg/user" - "code.vikunja.io/api/pkg/web" ) ///////////////// @@ -230,8 +229,8 @@ func (l *ProjectCreatedEvent) Name() string { // ProjectUpdatedEvent represents an event where a project has been updated type ProjectUpdatedEvent struct { - Project *Project `json:"project"` - Doer web.Auth `json:"doer"` + Project *Project `json:"project"` + Doer *user.User `json:"doer"` } // Name defines the name for ProjectUpdatedEvent @@ -241,8 +240,8 @@ func (p *ProjectUpdatedEvent) Name() string { // ProjectDeletedEvent represents an event where a project has been deleted type ProjectDeletedEvent struct { - Project *Project `json:"project"` - Doer web.Auth `json:"doer"` + Project *Project `json:"project"` + Doer *user.User `json:"doer"` } // Name defines the name for ProjectDeletedEvent @@ -258,7 +257,7 @@ func (p *ProjectDeletedEvent) Name() string { type ProjectSharedWithUserEvent struct { Project *Project `json:"project"` User *user.User `json:"user"` - Doer web.Auth `json:"doer"` + Doer *user.User `json:"doer"` } // Name defines the name for ProjectSharedWithUserEvent @@ -268,9 +267,9 @@ func (p *ProjectSharedWithUserEvent) Name() string { // ProjectSharedWithTeamEvent represents an event where a project has been shared with a team type ProjectSharedWithTeamEvent struct { - Project *Project `json:"project"` - Team *Team `json:"team"` - Doer web.Auth `json:"doer"` + Project *Project `json:"project"` + Team *Team `json:"team"` + Doer *user.User `json:"doer"` } // Name defines the name for ProjectSharedWithTeamEvent @@ -308,8 +307,8 @@ func (t *TeamMemberRemovedEvent) Name() string { // TeamCreatedEvent represents a TeamCreatedEvent event type TeamCreatedEvent struct { - Team *Team `json:"team"` - Doer web.Auth `json:"doer"` + Team *Team `json:"team"` + Doer *user.User `json:"doer"` } // Name defines the name for TeamCreatedEvent @@ -319,8 +318,8 @@ func (t *TeamCreatedEvent) Name() string { // TeamDeletedEvent represents a TeamDeletedEvent event type TeamDeletedEvent struct { - Team *Team `json:"team"` - Doer web.Auth `json:"doer"` + Team *Team `json:"team"` + Doer *user.User `json:"doer"` } // Name defines the name for TeamDeletedEvent diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index e29bb2369..e50631ae1 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -88,23 +88,6 @@ func RegisterListeners() { } } -// auditDoerRef decodes the doer of events whose Doer field is an interface -// and thus can't be unmarshaled into the event struct directly. -type auditDoerRef struct { - ID int64 `json:"id"` - Hash string `json:"hash"` // only set when the doer is a link share -} - -func auditActorFromDoerRef(d *auditDoerRef) audit.Actor { - if d == nil { - return audit.SystemActor() - } - if d.Hash != "" { - return audit.LinkShareActor(d.ID) - } - return audit.ActorFromDoerID(d.ID) -} - func auditActorFromUser(u *user.User) audit.Actor { if u == nil { return audit.SystemActor() @@ -281,95 +264,51 @@ func registerEventsForAuditLogging() { Target: audit.ProjectTarget(e.Project.ID), } }) - audit.RegisterEventNameForAudit((&ProjectUpdatedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) { - e := &struct { - Project *Project `json:"project"` - Doer *auditDoerRef `json:"doer"` - }{} - if err := json.Unmarshal(payload, e); err != nil { - return nil, err - } + audit.RegisterEventForAudit(func(e *ProjectUpdatedEvent) *audit.Entry { return &audit.Entry{ Action: audit.ActionProjectUpdated, - Actor: auditActorFromDoerRef(e.Doer), + Actor: auditActorFromUser(e.Doer), Target: audit.ProjectTarget(e.Project.ID), - }, nil - }) - audit.RegisterEventNameForAudit((&ProjectDeletedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) { - e := &struct { - Project *Project `json:"project"` - Doer *auditDoerRef `json:"doer"` - }{} - if err := json.Unmarshal(payload, e); err != nil { - return nil, err } + }) + audit.RegisterEventForAudit(func(e *ProjectDeletedEvent) *audit.Entry { return &audit.Entry{ Action: audit.ActionProjectDeleted, - Actor: auditActorFromDoerRef(e.Doer), + Actor: auditActorFromUser(e.Doer), Target: audit.ProjectTarget(e.Project.ID), - }, nil - }) - audit.RegisterEventNameForAudit((&ProjectSharedWithUserEvent{}).Name(), func(payload []byte) (*audit.Entry, error) { - e := &struct { - Project *Project `json:"project"` - User *user.User `json:"user"` - Doer *auditDoerRef `json:"doer"` - }{} - if err := json.Unmarshal(payload, e); err != nil { - return nil, err } + }) + audit.RegisterEventForAudit(func(e *ProjectSharedWithUserEvent) *audit.Entry { return &audit.Entry{ Action: audit.ActionProjectSharedWithUser, - Actor: auditActorFromDoerRef(e.Doer), + Actor: auditActorFromUser(e.Doer), Target: audit.ProjectTarget(e.Project.ID), Metadata: map[string]any{"user_id": e.User.ID}, - }, nil - }) - audit.RegisterEventNameForAudit((&ProjectSharedWithTeamEvent{}).Name(), func(payload []byte) (*audit.Entry, error) { - e := &struct { - Project *Project `json:"project"` - Team *Team `json:"team"` - Doer *auditDoerRef `json:"doer"` - }{} - if err := json.Unmarshal(payload, e); err != nil { - return nil, err } + }) + audit.RegisterEventForAudit(func(e *ProjectSharedWithTeamEvent) *audit.Entry { return &audit.Entry{ Action: audit.ActionProjectSharedWithTeam, - Actor: auditActorFromDoerRef(e.Doer), + Actor: auditActorFromUser(e.Doer), Target: audit.ProjectTarget(e.Project.ID), Metadata: map[string]any{"team_id": e.Team.ID}, - }, nil + } }) // Teams - audit.RegisterEventNameForAudit((&TeamCreatedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) { - e := &struct { - Team *Team `json:"team"` - Doer *auditDoerRef `json:"doer"` - }{} - if err := json.Unmarshal(payload, e); err != nil { - return nil, err - } + audit.RegisterEventForAudit(func(e *TeamCreatedEvent) *audit.Entry { return &audit.Entry{ Action: audit.ActionTeamCreated, - Actor: auditActorFromDoerRef(e.Doer), + Actor: auditActorFromUser(e.Doer), Target: audit.TeamTarget(e.Team.ID), - }, nil - }) - audit.RegisterEventNameForAudit((&TeamDeletedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) { - e := &struct { - Team *Team `json:"team"` - Doer *auditDoerRef `json:"doer"` - }{} - if err := json.Unmarshal(payload, e); err != nil { - return nil, err } + }) + audit.RegisterEventForAudit(func(e *TeamDeletedEvent) *audit.Entry { return &audit.Entry{ Action: audit.ActionTeamDeleted, - Actor: auditActorFromDoerRef(e.Doer), + Actor: auditActorFromUser(e.Doer), Target: audit.TeamTarget(e.Team.ID), - }, nil + } }) audit.RegisterEventForAudit(func(e *TeamMemberAddedEvent) *audit.Entry { return &audit.Entry{ diff --git a/pkg/models/project.go b/pkg/models/project.go index 23fc9f6ca..019afe792 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -1217,9 +1217,13 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje return err } + doer, err := GetUserOrLinkShareUser(s, auth) + if err != nil { + return err + } events.DispatchOnCommit(s, &ProjectUpdatedEvent{ Project: project, - Doer: auth, + Doer: doer, }) l, err := GetProjectSimpleByID(s, project.ID) @@ -1448,9 +1452,13 @@ func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) { return } + doer, err := GetUserOrLinkShareUser(s, a) + if err != nil { + return err + } events.DispatchOnCommit(s, &ProjectDeletedEvent{ Project: fullProject, - Doer: a, + Doer: doer, }) childProjects := []*Project{} diff --git a/pkg/models/project_team.go b/pkg/models/project_team.go index 0c9fb6908..4f3ed9c42 100644 --- a/pkg/models/project_team.go +++ b/pkg/models/project_team.go @@ -109,10 +109,14 @@ func (tl *TeamProject) Create(s *xorm.Session, a web.Auth) (err error) { return err } + doer, err := GetUserOrLinkShareUser(s, a) + if err != nil { + return err + } events.DispatchOnCommit(s, &ProjectSharedWithTeamEvent{ Project: l, Team: team, - Doer: a, + Doer: doer, }) err = updateProjectLastUpdated(s, l) diff --git a/pkg/models/project_users.go b/pkg/models/project_users.go index 58ef71c38..1470dd1bb 100644 --- a/pkg/models/project_users.go +++ b/pkg/models/project_users.go @@ -115,10 +115,14 @@ func (lu *ProjectUser) Create(s *xorm.Session, a web.Auth) (err error) { return err } + doer, err := GetUserOrLinkShareUser(s, a) + if err != nil { + return err + } events.DispatchOnCommit(s, &ProjectSharedWithUserEvent{ Project: l, User: u, - Doer: a, + Doer: doer, }) err = updateProjectLastUpdated(s, l) diff --git a/pkg/models/teams.go b/pkg/models/teams.go index 98c87161c..e1ac8887c 100644 --- a/pkg/models/teams.go +++ b/pkg/models/teams.go @@ -222,7 +222,7 @@ func (t *Team) CreateNewTeam(s *xorm.Session, a web.Auth, firstUserShouldBeAdmin events.DispatchOnCommit(s, &TeamCreatedEvent{ Team: t, - Doer: a, + Doer: doer, }) return nil } @@ -360,9 +360,13 @@ func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) { return } + doer, err := GetUserOrLinkShareUser(s, a) + if err != nil { + return err + } events.DispatchOnCommit(s, &TeamDeletedEvent{ Team: t, - Doer: a, + Doer: doer, }) return nil }