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.
This commit is contained in:
kolaente 2026-06-11 21:39:08 +02:00 committed by kolaente
parent f33cde82e2
commit b3bcab1f72
7 changed files with 59 additions and 113 deletions

View File

@ -45,27 +45,15 @@ func RegisterEventForAudit[T any, PT interface {
events.Event events.Event
}](toEntry func(PT) *Entry) { }](toEntry func(PT) *Entry) {
name := PT(new(T)).Name() 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 { events.RegisterListener(name, &auditListener{handle: func(msg *message.Message) error {
if !license.IsFeatureEnabled(license.FeatureAuditLogs) { if !license.IsFeatureEnabled(license.FeatureAuditLogs) {
return nil // license is runtime-mutable — checked per event, not at registration return nil // license is runtime-mutable — checked per event, not at registration
} }
entry, err := toEntry(msg.Payload) e := PT(new(T)) // fresh instance per message — handlers run concurrently
if err != nil { if err := json.Unmarshal(msg.Payload, e); err != nil {
return err return err
} }
entry := toEntry(e)
if entry == nil { if entry == nil {
return nil return nil
} }

View File

@ -18,7 +18,6 @@ package models
import ( import (
"code.vikunja.io/api/pkg/user" "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 // ProjectUpdatedEvent represents an event where a project has been updated
type ProjectUpdatedEvent struct { type ProjectUpdatedEvent struct {
Project *Project `json:"project"` Project *Project `json:"project"`
Doer web.Auth `json:"doer"` Doer *user.User `json:"doer"`
} }
// Name defines the name for ProjectUpdatedEvent // 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 // ProjectDeletedEvent represents an event where a project has been deleted
type ProjectDeletedEvent struct { type ProjectDeletedEvent struct {
Project *Project `json:"project"` Project *Project `json:"project"`
Doer web.Auth `json:"doer"` Doer *user.User `json:"doer"`
} }
// Name defines the name for ProjectDeletedEvent // Name defines the name for ProjectDeletedEvent
@ -258,7 +257,7 @@ func (p *ProjectDeletedEvent) Name() string {
type ProjectSharedWithUserEvent struct { type ProjectSharedWithUserEvent struct {
Project *Project `json:"project"` Project *Project `json:"project"`
User *user.User `json:"user"` User *user.User `json:"user"`
Doer web.Auth `json:"doer"` Doer *user.User `json:"doer"`
} }
// Name defines the name for ProjectSharedWithUserEvent // 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 // ProjectSharedWithTeamEvent represents an event where a project has been shared with a team
type ProjectSharedWithTeamEvent struct { type ProjectSharedWithTeamEvent struct {
Project *Project `json:"project"` Project *Project `json:"project"`
Team *Team `json:"team"` Team *Team `json:"team"`
Doer web.Auth `json:"doer"` Doer *user.User `json:"doer"`
} }
// Name defines the name for ProjectSharedWithTeamEvent // Name defines the name for ProjectSharedWithTeamEvent
@ -308,8 +307,8 @@ func (t *TeamMemberRemovedEvent) Name() string {
// TeamCreatedEvent represents a TeamCreatedEvent event // TeamCreatedEvent represents a TeamCreatedEvent event
type TeamCreatedEvent struct { type TeamCreatedEvent struct {
Team *Team `json:"team"` Team *Team `json:"team"`
Doer web.Auth `json:"doer"` Doer *user.User `json:"doer"`
} }
// Name defines the name for TeamCreatedEvent // Name defines the name for TeamCreatedEvent
@ -319,8 +318,8 @@ func (t *TeamCreatedEvent) Name() string {
// TeamDeletedEvent represents a TeamDeletedEvent event // TeamDeletedEvent represents a TeamDeletedEvent event
type TeamDeletedEvent struct { type TeamDeletedEvent struct {
Team *Team `json:"team"` Team *Team `json:"team"`
Doer web.Auth `json:"doer"` Doer *user.User `json:"doer"`
} }
// Name defines the name for TeamDeletedEvent // Name defines the name for TeamDeletedEvent

View File

@ -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 { func auditActorFromUser(u *user.User) audit.Actor {
if u == nil { if u == nil {
return audit.SystemActor() return audit.SystemActor()
@ -281,95 +264,51 @@ func registerEventsForAuditLogging() {
Target: audit.ProjectTarget(e.Project.ID), Target: audit.ProjectTarget(e.Project.ID),
} }
}) })
audit.RegisterEventNameForAudit((&ProjectUpdatedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) { audit.RegisterEventForAudit(func(e *ProjectUpdatedEvent) *audit.Entry {
e := &struct {
Project *Project `json:"project"`
Doer *auditDoerRef `json:"doer"`
}{}
if err := json.Unmarshal(payload, e); err != nil {
return nil, err
}
return &audit.Entry{ return &audit.Entry{
Action: audit.ActionProjectUpdated, Action: audit.ActionProjectUpdated,
Actor: auditActorFromDoerRef(e.Doer), Actor: auditActorFromUser(e.Doer),
Target: audit.ProjectTarget(e.Project.ID), 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{ return &audit.Entry{
Action: audit.ActionProjectDeleted, Action: audit.ActionProjectDeleted,
Actor: auditActorFromDoerRef(e.Doer), Actor: auditActorFromUser(e.Doer),
Target: audit.ProjectTarget(e.Project.ID), 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{ return &audit.Entry{
Action: audit.ActionProjectSharedWithUser, Action: audit.ActionProjectSharedWithUser,
Actor: auditActorFromDoerRef(e.Doer), Actor: auditActorFromUser(e.Doer),
Target: audit.ProjectTarget(e.Project.ID), Target: audit.ProjectTarget(e.Project.ID),
Metadata: map[string]any{"user_id": e.User.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{ return &audit.Entry{
Action: audit.ActionProjectSharedWithTeam, Action: audit.ActionProjectSharedWithTeam,
Actor: auditActorFromDoerRef(e.Doer), Actor: auditActorFromUser(e.Doer),
Target: audit.ProjectTarget(e.Project.ID), Target: audit.ProjectTarget(e.Project.ID),
Metadata: map[string]any{"team_id": e.Team.ID}, Metadata: map[string]any{"team_id": e.Team.ID},
}, nil }
}) })
// Teams // Teams
audit.RegisterEventNameForAudit((&TeamCreatedEvent{}).Name(), func(payload []byte) (*audit.Entry, error) { audit.RegisterEventForAudit(func(e *TeamCreatedEvent) *audit.Entry {
e := &struct {
Team *Team `json:"team"`
Doer *auditDoerRef `json:"doer"`
}{}
if err := json.Unmarshal(payload, e); err != nil {
return nil, err
}
return &audit.Entry{ return &audit.Entry{
Action: audit.ActionTeamCreated, Action: audit.ActionTeamCreated,
Actor: auditActorFromDoerRef(e.Doer), Actor: auditActorFromUser(e.Doer),
Target: audit.TeamTarget(e.Team.ID), 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{ return &audit.Entry{
Action: audit.ActionTeamDeleted, Action: audit.ActionTeamDeleted,
Actor: auditActorFromDoerRef(e.Doer), Actor: auditActorFromUser(e.Doer),
Target: audit.TeamTarget(e.Team.ID), Target: audit.TeamTarget(e.Team.ID),
}, nil }
}) })
audit.RegisterEventForAudit(func(e *TeamMemberAddedEvent) *audit.Entry { audit.RegisterEventForAudit(func(e *TeamMemberAddedEvent) *audit.Entry {
return &audit.Entry{ return &audit.Entry{

View File

@ -1217,9 +1217,13 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
return err return err
} }
doer, err := GetUserOrLinkShareUser(s, auth)
if err != nil {
return err
}
events.DispatchOnCommit(s, &ProjectUpdatedEvent{ events.DispatchOnCommit(s, &ProjectUpdatedEvent{
Project: project, Project: project,
Doer: auth, Doer: doer,
}) })
l, err := GetProjectSimpleByID(s, project.ID) l, err := GetProjectSimpleByID(s, project.ID)
@ -1448,9 +1452,13 @@ func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
return return
} }
doer, err := GetUserOrLinkShareUser(s, a)
if err != nil {
return err
}
events.DispatchOnCommit(s, &ProjectDeletedEvent{ events.DispatchOnCommit(s, &ProjectDeletedEvent{
Project: fullProject, Project: fullProject,
Doer: a, Doer: doer,
}) })
childProjects := []*Project{} childProjects := []*Project{}

View File

@ -109,10 +109,14 @@ func (tl *TeamProject) Create(s *xorm.Session, a web.Auth) (err error) {
return err return err
} }
doer, err := GetUserOrLinkShareUser(s, a)
if err != nil {
return err
}
events.DispatchOnCommit(s, &ProjectSharedWithTeamEvent{ events.DispatchOnCommit(s, &ProjectSharedWithTeamEvent{
Project: l, Project: l,
Team: team, Team: team,
Doer: a, Doer: doer,
}) })
err = updateProjectLastUpdated(s, l) err = updateProjectLastUpdated(s, l)

View File

@ -115,10 +115,14 @@ func (lu *ProjectUser) Create(s *xorm.Session, a web.Auth) (err error) {
return err return err
} }
doer, err := GetUserOrLinkShareUser(s, a)
if err != nil {
return err
}
events.DispatchOnCommit(s, &ProjectSharedWithUserEvent{ events.DispatchOnCommit(s, &ProjectSharedWithUserEvent{
Project: l, Project: l,
User: u, User: u,
Doer: a, Doer: doer,
}) })
err = updateProjectLastUpdated(s, l) err = updateProjectLastUpdated(s, l)

View File

@ -222,7 +222,7 @@ func (t *Team) CreateNewTeam(s *xorm.Session, a web.Auth, firstUserShouldBeAdmin
events.DispatchOnCommit(s, &TeamCreatedEvent{ events.DispatchOnCommit(s, &TeamCreatedEvent{
Team: t, Team: t,
Doer: a, Doer: doer,
}) })
return nil return nil
} }
@ -360,9 +360,13 @@ func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) {
return return
} }
doer, err := GetUserOrLinkShareUser(s, a)
if err != nil {
return err
}
events.DispatchOnCommit(s, &TeamDeletedEvent{ events.DispatchOnCommit(s, &TeamDeletedEvent{
Team: t, Team: t,
Doer: a, Doer: doer,
}) })
return nil return nil
} }