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
}](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
}

View File

@ -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

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 {
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{

View File

@ -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{}

View File

@ -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)

View File

@ -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)

View File

@ -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
}