fix(delete): review follow-ups — admin Deleted filter, guest gate, dialog spacing
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m46s

- Admin /users gains a Deleted scope (between People and Robots); every other scope now
  hides tombstoned accounts (deleted_at filter in UserFilter).
- Admin delete-user is offered for any non-deleted account (drop the not-guest gate, so a
  stale is_guest account can still be tombstoned).
- Harden EmailService.ConfirmCode to ClearGuest — defence-in-depth so no confirmed-email
  path leaves is_guest set (the currently-live paths already do).
- Space the delete/change dialog's action row from its input field.
Integration tests: the Deleted filter scoping + ConfirmCode guest promotion.
This commit is contained in:
Ilia Denisov
2026-07-03 13:59:43 +02:00
parent c2a8426b74
commit 3faca690dd
8 changed files with 113 additions and 9 deletions
+5
View File
@@ -206,6 +206,11 @@ func (s *EmailService) ConfirmCode(ctx context.Context, accountID uuid.UUID, ema
if err := s.store.confirmEmailIdentity(ctx, conf.id, accountID, addr, s.now()); err != nil {
return Account{}, err
}
// Binding the first confirmed email promotes a guest to a durable account, matching the
// link and deeplink flows (defence-in-depth: no confirmed-email path leaves is_guest set).
if err := s.store.ClearGuest(ctx, accountID); err != nil {
return Account{}, err
}
return s.store.GetByID(ctx, accountID)
}
+12 -4
View File
@@ -26,12 +26,14 @@ type UserListItem struct {
}
// UserFilter narrows the admin user list: Robots selects robot accounts (otherwise the
// non-robot "people"); NameMask and ExternalIDMask are glob masks ('*' = any run, '?' =
// one char) matched case-insensitively against the display name / any identity's external
// id; EmailExact is a strict (exact) match against an account's email identity. An empty
// value means no filter on that field.
// non-robot "people"); Deleted selects tombstoned accounts (every other scope hides them);
// NameMask and ExternalIDMask are glob masks ('*' = any run, '?' = one char) matched
// case-insensitively against the display name / any identity's external id; EmailExact is a
// strict (exact) match against an account's email identity. An empty value means no filter
// on that field.
type UserFilter struct {
Robots bool
Deleted bool
NameMask string
ExternalIDMask string
EmailExact string
@@ -57,6 +59,12 @@ func (s *Store) IsRobot(ctx context.Context, accountID uuid.UUID) (bool, error)
func userListWhere(f UserFilter) (string, []any) {
args := []any{f.Robots}
where := robotExists + ` = $1`
// The Deleted scope shows tombstoned accounts; every other scope hides them.
if f.Deleted {
where += ` AND a.deleted_at IS NOT NULL`
} else {
where += ` AND a.deleted_at IS NULL`
}
if name := LikePattern(f.NameMask); name != "" {
args = append(args, name)
where += fmt.Sprintf(` AND a.display_name ILIKE $%d ESCAPE '\'`, len(args))