refactor(api/v2): dedup the admin user-mutation handlers

The patch-admin, patch-status and delete-user handlers each repeated the same
session open/load/commit/rollback scaffold. Extract it into adminMutateUser,
which owns the transaction and takes a closure for each handler's distinct
guard-and-write step.
This commit is contained in:
kolaente 2026-06-11 20:49:03 +02:00 committed by kolaente
parent 5579daa452
commit 5b3ee89edd
1 changed files with 44 additions and 57 deletions

View File

@ -138,28 +138,18 @@ func adminUsersPatchAdmin(_ context.Context, in *struct {
return nil, translateDomainError(models.ErrInvalidData{Message: "is_admin is required"}) return nil, translateDomainError(models.ErrInvalidData{Message: "is_admin is required"})
} }
s := db.NewSession() target, err := adminMutateUser(in.ID, func(s *xorm.Session, target *user.User) error {
defer s.Close() if !*in.Body.IsAdmin {
if err := user.GuardLastAdmin(s, target); err != nil {
target, err := adminLoadUser(s, in.ID) return err
if err != nil { }
return nil, translateDomainError(err)
}
if !*in.Body.IsAdmin {
if err := user.GuardLastAdmin(s, target); err != nil {
_ = s.Rollback()
return nil, translateDomainError(err)
} }
} target.IsAdmin = *in.Body.IsAdmin
_, err := s.ID(target.ID).Cols("is_admin").Update(target)
target.IsAdmin = *in.Body.IsAdmin return err
if _, err := s.ID(target.ID).Cols("is_admin").Update(target); err != nil { })
_ = s.Rollback() if err != nil {
return nil, translateDomainError(err) return nil, err
}
if err := s.Commit(); err != nil {
return nil, translateDomainError(err)
} }
return adminUserResponse(target) //nolint:contextcheck // see adminUserResponse. return adminUserResponse(target) //nolint:contextcheck // see adminUserResponse.
@ -177,28 +167,17 @@ func adminUsersPatchStatus(_ context.Context, in *struct {
return nil, translateDomainError(models.ErrInvalidData{Message: "invalid status"}) return nil, translateDomainError(models.ErrInvalidData{Message: "invalid status"})
} }
s := db.NewSession() target, err := adminMutateUser(in.ID, func(s *xorm.Session, target *user.User) error {
defer s.Close() // Any non-Active status blocks login, so moving an admin out of Active is equivalent to demotion.
if target.IsAdmin && newStatus != user.StatusActive {
target, err := adminLoadUser(s, in.ID) if err := user.GuardLastAdmin(s, target); err != nil {
if err != nil { return err
return nil, translateDomainError(err) }
}
// Any non-Active status blocks login, so moving an admin out of Active is equivalent to demotion.
if target.IsAdmin && newStatus != user.StatusActive {
if err := user.GuardLastAdmin(s, target); err != nil {
_ = s.Rollback()
return nil, translateDomainError(err)
} }
} return user.SetUserStatus(s, target, newStatus)
})
if err := user.SetUserStatus(s, target, newStatus); err != nil { if err != nil {
_ = s.Rollback() return nil, err
return nil, translateDomainError(err)
}
if err := s.Commit(); err != nil {
return nil, translateDomainError(err)
} }
// Refresh locally since GetUserByID refuses disabled accounts. // Refresh locally since GetUserByID refuses disabled accounts.
@ -218,33 +197,41 @@ func adminUsersDelete(_ context.Context, in *struct {
return nil, translateDomainError(models.ErrInvalidData{Message: "invalid mode, expected 'now' or 'scheduled'"}) return nil, translateDomainError(models.ErrInvalidData{Message: "invalid mode, expected 'now' or 'scheduled'"})
} }
_, err := adminMutateUser(in.ID, func(s *xorm.Session, target *user.User) error {
if err := user.GuardLastAdmin(s, target); err != nil {
return err
}
if mode == "now" {
return models.DeleteUser(s, target)
}
return user.RequestDeletion(s, target)
})
if err != nil {
return nil, err
}
return &emptyBody{}, nil
}
// adminMutateUser opens a session, loads the user by ID, runs mutate against it,
// then commits — owning the transaction so each handler only supplies its
// distinct guard-and-write step. mutate must not commit or rollback. Errors
// (load, mutate, commit) are translated to RFC 9457 responses.
func adminMutateUser(id int64, mutate func(s *xorm.Session, target *user.User) error) (*user.User, error) {
s := db.NewSession() s := db.NewSession()
defer s.Close() defer s.Close()
target, err := adminLoadUser(s, in.ID) target, err := adminLoadUser(s, id)
if err != nil { if err != nil {
return nil, translateDomainError(err) return nil, translateDomainError(err)
} }
if err := mutate(s, target); err != nil {
if err := user.GuardLastAdmin(s, target); err != nil {
_ = s.Rollback() _ = s.Rollback()
return nil, translateDomainError(err) return nil, translateDomainError(err)
} }
if mode == "now" {
err = models.DeleteUser(s, target)
} else {
err = user.RequestDeletion(s, target)
}
if err != nil {
_ = s.Rollback()
return nil, translateDomainError(err)
}
if err := s.Commit(); err != nil { if err := s.Commit(); err != nil {
return nil, translateDomainError(err) return nil, translateDomainError(err)
} }
return &emptyBody{}, nil return target, nil
} }
// adminLoadUser fetches a user by ID, returning ErrUserDoesNotExist for an // adminLoadUser fetches a user by ID, returning ErrUserDoesNotExist for an