From b86710903b89548d0b7f8a739360a56706ea2784 Mon Sep 17 00:00:00 2001 From: kolaente Date: Wed, 10 Jun 2026 21:32:14 +0200 Subject: [PATCH] fix: dispatch pending events after user creation commits The register handler, local/LDAP login and the OIDC callback all queue the user.created event via DispatchOnCommit but never called DispatchPending, so the event was silently dropped and its queue entry leaked. Flush after commit and discard on rollback. --- pkg/modules/auth/openid/openid.go | 9 +++++++++ pkg/routes/api/shared/auth.go | 10 +++++++++- pkg/routes/api/v1/login.go | 5 +++++ pkg/routes/api/v1/user_register.go | 2 +- pkg/routes/api/v2/auth_public.go | 4 ++-- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/pkg/modules/auth/openid/openid.go b/pkg/modules/auth/openid/openid.go index 381570f42..b1fa3961a 100644 --- a/pkg/modules/auth/openid/openid.go +++ b/pkg/modules/auth/openid/openid.go @@ -27,6 +27,7 @@ import ( "strings" "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/auth" @@ -187,6 +188,9 @@ func HandleCallback(c *echo.Context) error { s := db.NewSession() defer s.Close() + // Discards events queued during a rolled-back transaction (e.g. user + // creation); a no-op once DispatchPending has run. + defer events.CleanupPending(s) // Check if we have seen this user before u, err := getOrCreateUser(s, cl, provider, idToken) @@ -212,6 +216,9 @@ func HandleCallback(c *echo.Context) error { if err := enforceTOTPIfRequired(s, u, cb.TOTPPasscode); err != nil { if commitErr := s.Commit(); commitErr != nil { log.Errorf("Error committing session after failed OIDC TOTP attempt for user %d: %v", u.ID, commitErr) + } else { + // The user creation above was committed, so its events are real. + events.DispatchPending(c.Request().Context(), s) } if user.IsErrInvalidTOTPPasscode(err) { user.HandleFailedTOTPAuth(u) @@ -233,6 +240,8 @@ func HandleCallback(c *echo.Context) error { return err } + events.DispatchPending(c.Request().Context(), s) + // Create token return auth.NewUserAuthTokenResponse(u, c, false) } diff --git a/pkg/routes/api/shared/auth.go b/pkg/routes/api/shared/auth.go index d11a1cdbb..925a533d8 100644 --- a/pkg/routes/api/shared/auth.go +++ b/pkg/routes/api/shared/auth.go @@ -17,8 +17,11 @@ package shared import ( + "context" + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/models" @@ -39,9 +42,12 @@ type UserRegister struct { // busts the cached user-count metric so the registration shows up immediately. // The caller is responsible for the registration-enabled gate and input // validation; both v1 and v2 share this body. -func RegisterUser(in *UserRegister) (*user.User, error) { +func RegisterUser(ctx context.Context, in *UserRegister) (*user.User, error) { s := db.NewSession() defer s.Close() + // Discards events queued during a rolled-back transaction; a no-op once + // DispatchPending has run. + defer events.CleanupPending(s) newUser, err := models.RegisterUser(s, &user.User{ Username: in.Username, @@ -59,6 +65,8 @@ func RegisterUser(in *UserRegister) (*user.User, error) { return nil, err } + events.DispatchPending(ctx, s) + // Bust the cached user count so the new registration shows up in metrics // immediately instead of after the regular cache expiry. if config.MetricsEnabled.GetBool() { diff --git a/pkg/routes/api/v1/login.go b/pkg/routes/api/v1/login.go index 6c7eb686a..7385ed1f6 100644 --- a/pkg/routes/api/v1/login.go +++ b/pkg/routes/api/v1/login.go @@ -53,6 +53,9 @@ func Login(c *echo.Context) (err error) { s := db.NewSession() defer s.Close() + // Discards events queued during a rolled-back transaction (e.g. LDAP user + // creation); a no-op once DispatchPending has run. + defer events.CleanupPending(s) var user *user2.User if config.AuthLdapEnabled.GetBool() { @@ -127,6 +130,8 @@ func Login(c *echo.Context) (err error) { return err } + events.DispatchPending(c.Request().Context(), s) + // Create token return auth.NewUserAuthTokenResponse(user, c, u.LongToken) } diff --git a/pkg/routes/api/v1/user_register.go b/pkg/routes/api/v1/user_register.go index 1c70df765..e9a90dc2f 100644 --- a/pkg/routes/api/v1/user_register.go +++ b/pkg/routes/api/v1/user_register.go @@ -63,7 +63,7 @@ func RegisterUser(c *echo.Context) error { return c.JSON(http.StatusBadRequest, models.Message{Message: "No or invalid user model provided."}) } - newUser, err := shared.RegisterUser(userIn) + newUser, err := shared.RegisterUser(c.Request().Context(), userIn) if err != nil { return err } diff --git a/pkg/routes/api/v2/auth_public.go b/pkg/routes/api/v2/auth_public.go index 5791b7c3f..c41fb162d 100644 --- a/pkg/routes/api/v2/auth_public.go +++ b/pkg/routes/api/v2/auth_public.go @@ -127,8 +127,8 @@ func registerLocalAuthRoutes(api huma.API) { }, authConfirmEmail) } -func authRegister(_ context.Context, in *struct{ Body shared.UserRegister }) (*registerUserBody, error) { - newUser, err := shared.RegisterUser(&in.Body) +func authRegister(ctx context.Context, in *struct{ Body shared.UserRegister }) (*registerUserBody, error) { + newUser, err := shared.RegisterUser(ctx, &in.Body) if err != nil { return nil, translateDomainError(err) }