feat(admin): grant hints to a user's wallet from the console
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m5s
Add an "Add hints" form on the admin user card that additively tops up a player's hint wallet (1-100 per grant). The grant is raise-only by construction (an additive UPDATE never lowers the balance) and stays correct under a concurrent in-game spend; a per-grant cap bounds a fat-finger, since the console can never reduce a wallet. The in-game hint policy is unchanged and already correct: a game offers the per-seat allowance plus the wallet, spending the allowance first and the wallet only after (covered by TestHintPolicy).
This commit is contained in:
@@ -28,6 +28,10 @@ import (
|
||||
// adminPageSize is the page size of the admin console's paginated lists.
|
||||
const adminPageSize = 50
|
||||
|
||||
// maxHintGrant caps a single operator hint grant. Grants are additive and can never lower a
|
||||
// wallet, so a fat-fingered grant cannot be undone through this form; the cap bounds one mistake.
|
||||
const maxHintGrant = 100
|
||||
|
||||
// registerConsole mounts the server-rendered admin console under /_gm. The gateway
|
||||
// puts HTTP Basic-Auth in front of /_gm and reverse-proxies it verbatim; the
|
||||
// backend trusts the gateway (as for all of /api) and adds only a same-origin guard
|
||||
@@ -51,6 +55,7 @@ func (s *Server) registerConsole(router *gin.Engine) {
|
||||
gm.GET("/users/:id", s.consoleUserDetail)
|
||||
gm.POST("/users/:id/message", s.consoleUserMessage)
|
||||
gm.POST("/users/:id/clear-high-rate-flag", s.consoleClearHighRateFlag)
|
||||
gm.POST("/users/:id/grant-hints", s.consoleGrantHints)
|
||||
gm.POST("/users/:id/block", s.consoleBlockUser)
|
||||
gm.POST("/users/:id/unblock", s.consoleUnblockUser)
|
||||
gm.GET("/reasons", s.consoleReasons)
|
||||
@@ -263,8 +268,8 @@ func (s *Server) consoleUserDetail(c *gin.Context) {
|
||||
view := adminconsole.UserDetailView{
|
||||
ID: acc.ID.String(), DisplayName: acc.DisplayName, Language: acc.PreferredLanguage,
|
||||
TimeZone: acc.TimeZone, Guest: acc.IsGuest, NotificationsInAppOnly: acc.NotificationsInAppOnly,
|
||||
PaidAccount: acc.PaidAccount, HintBalance: acc.HintBalance, CreatedAt: fmtTime(acc.CreatedAt),
|
||||
HasStats: !acc.IsGuest, ConnectorEnabled: s.connector != nil,
|
||||
PaidAccount: acc.PaidAccount, HintBalance: acc.HintBalance, HintGrantMax: maxHintGrant,
|
||||
CreatedAt: fmtTime(acc.CreatedAt), HasStats: !acc.IsGuest, ConnectorEnabled: s.connector != nil,
|
||||
}
|
||||
if acc.MergedInto != uuid.Nil {
|
||||
view.MergedInto = acc.MergedInto.String()
|
||||
@@ -774,6 +779,28 @@ func (s *Server) consoleClearHighRateFlag(c *gin.Context) {
|
||||
s.renderConsoleMessage(c, "Cleared", "high-rate flag cleared", "/_gm/users/"+id.String())
|
||||
}
|
||||
|
||||
// consoleGrantHints adds hints to a user's wallet. The grant is additive (raise-only): it tops a
|
||||
// player up and can never lower what they already hold, so blocking a reduction is inherent rather
|
||||
// than a separate guard. A single grant is bounded by maxHintGrant.
|
||||
func (s *Server) consoleGrantHints(c *gin.Context) {
|
||||
id, ok := s.consoleUUID(c, "/_gm/users")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
back := "/_gm/users/" + id.String()
|
||||
n, err := strconv.Atoi(trimForm(c, "amount"))
|
||||
if err != nil || n < 1 || n > maxHintGrant {
|
||||
s.renderConsoleMessage(c, "Invalid amount", fmt.Sprintf("enter a whole number of hints to add, between 1 and %d", maxHintGrant), back)
|
||||
return
|
||||
}
|
||||
balance, err := s.accounts.GrantHints(c.Request.Context(), id, n)
|
||||
if err != nil {
|
||||
s.consoleError(c, err)
|
||||
return
|
||||
}
|
||||
s.renderConsoleMessage(c, "Granted", fmt.Sprintf("added %d hint(s); the wallet is now %d", n, balance), 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