feat(admin): search users by email + erase a bound email
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 1m3s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
Add an exact (strict) email filter to the /users list (UserFilter.EmailExact → a kind='email' identity match) with a search input, and an 'Erase email' action on the user card that deletes the bound email identity and its pending confirmations, freeing the address. It refuses to remove the account's only identity (ErrLastIdentity), which would leave it unreachable. Integration tests for both.
This commit is contained in:
@@ -61,6 +61,7 @@ func (s *Server) registerConsole(router *gin.Engine) {
|
||||
gm.POST("/users/:id/unblock", s.consoleUnblockUser)
|
||||
gm.POST("/users/:id/grant-role", s.consoleGrantRole)
|
||||
gm.POST("/users/:id/revoke-role", s.consoleRevokeRole)
|
||||
gm.POST("/users/:id/remove-email", s.consoleRemoveEmail)
|
||||
gm.GET("/reasons", s.consoleReasons)
|
||||
gm.POST("/reasons", s.consoleCreateReason)
|
||||
gm.POST("/reasons/:id/update", s.consoleUpdateReason)
|
||||
@@ -134,6 +135,7 @@ func (s *Server) consoleUsers(c *gin.Context) {
|
||||
Robots: c.Query("kind") == "robots",
|
||||
NameMask: c.Query("name"),
|
||||
ExternalIDMask: c.Query("ext"),
|
||||
EmailExact: c.Query("email"),
|
||||
}
|
||||
total, _ := s.accounts.CountUsers(ctx, filter)
|
||||
items, err := s.accounts.ListUsers(ctx, filter, adminPageSize, (page-1)*adminPageSize)
|
||||
@@ -151,9 +153,13 @@ func (s *Server) consoleUsers(c *gin.Context) {
|
||||
if strings.TrimSpace(filter.ExternalIDMask) != "" {
|
||||
q.Set("ext", filter.ExternalIDMask)
|
||||
}
|
||||
if strings.TrimSpace(filter.EmailExact) != "" {
|
||||
q.Set("email", filter.EmailExact)
|
||||
}
|
||||
view := adminconsole.UsersView{
|
||||
Pager: adminconsole.NewPager(page, adminPageSize, total),
|
||||
Robots: filter.Robots, NameMask: filter.NameMask, ExternalIDMask: filter.ExternalIDMask,
|
||||
EmailExact: filter.EmailExact,
|
||||
FilterQuery: template.URL(q.Encode()),
|
||||
}
|
||||
ids := make([]uuid.UUID, 0, len(items))
|
||||
@@ -356,6 +362,9 @@ func (s *Server) consoleUserDetail(c *gin.Context) {
|
||||
}
|
||||
if ids, err := s.accounts.Identities(ctx, id); err == nil {
|
||||
for _, idn := range ids {
|
||||
if idn.Kind == account.KindEmail {
|
||||
view.HasEmail = true
|
||||
}
|
||||
view.Identities = append(view.Identities, adminconsole.IdentityRow{Kind: idn.Kind, ExternalID: idn.ExternalID, Confirmed: idn.Confirmed, CreatedAt: fmtTime(idn.CreatedAt)})
|
||||
}
|
||||
}
|
||||
@@ -951,6 +960,25 @@ func (s *Server) consoleGrantHints(c *gin.Context) {
|
||||
s.renderConsoleMessage(c, "Granted", fmt.Sprintf("added %d hint(s); the wallet is now %d", n, balance), back)
|
||||
}
|
||||
|
||||
// consoleRemoveEmail deletes the account's bound email identity (and any pending
|
||||
// confirmations), freeing the address. It refuses to remove the account's only
|
||||
// identity, which would leave it unreachable.
|
||||
func (s *Server) consoleRemoveEmail(c *gin.Context) {
|
||||
id, ok := s.consoleUUID(c, "/_gm/users")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
back := "/_gm/users/" + id.String()
|
||||
switch err := s.accounts.RemoveEmailIdentity(c.Request.Context(), id); {
|
||||
case errors.Is(err, account.ErrLastIdentity):
|
||||
s.renderConsoleMessage(c, "Can't remove", "the email is this account's only identity — removing it would leave the account unreachable", back)
|
||||
case err != nil:
|
||||
s.consoleError(c, err)
|
||||
default:
|
||||
s.renderConsoleMessage(c, "Removed", "the email identity was erased and the address freed", back)
|
||||
}
|
||||
}
|
||||
|
||||
// consoleBlockUser manually blocks an account: it records the suspension (permanent or until a
|
||||
// parsed deadline, snapshotting the chosen reason's en/ru text) and forfeits the player's active
|
||||
// games, removing them from matchmaking. The block takes effect on the player's next request.
|
||||
|
||||
Reference in New Issue
Block a user