usernames: operator reserved-username blocklist
A plain blocklist for names like @support - separate from the collectible
system, so a reservation has no owner, no price and no "bought on Fragment"
badge.
- reserved_usernames table + migration.
- Enforced in replacePeerUsernameTx (the single editable-username write point:
account.updateUsername, channels.updateUsername, @BotFather /setusername) and
in the collectible mint path; a reserved name returns USERNAME_OCCUPIED.
- admin.Service: ReserveUsername / UnreserveUsername (journalled commands) and
the ReservedUsernames listing.
- adminapi: /v1/reserved-usernames{,/reserve,/unreserve}.
- telesrv-admin panel + a "Reserved Usernames" page in the web UI (dist rebuilt).
- Postgres and in-memory store implementations; the memory registry gains an
optional reserved-name check so tests exercise the same rule.
This commit is contained in:
parent
22846e340f
commit
2bdb1ecf37
21 changed files with 843 additions and 6 deletions
|
|
@ -12,6 +12,7 @@ import (
|
|||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -100,6 +101,7 @@ func (s *server) routes() http.Handler {
|
|||
mux.Handle("GET /api/messages/groups", s.scopedRoute(permissionMessagesRead, http.HandlerFunc(s.handleGroupMessagesAPI)))
|
||||
mux.Handle("GET /api/messages/groups/detail", s.scopedRoute(permissionMessagesRead, http.HandlerFunc(s.handleGroupMessageDetailAPI)))
|
||||
mux.Handle("GET /api/collectible-usernames", s.scopedRoute(permissionUsernamesRead, http.HandlerFunc(s.handleCollectibleUsernamesAPI)))
|
||||
mux.Handle("GET /api/reserved-usernames", s.scopedRoute(permissionUsernamesRead, http.HandlerFunc(s.handleReservedUsernamesAPI)))
|
||||
mux.Handle("GET /api/collectible-usernames/{id}", s.scopedRoute(permissionUsernamesRead, http.HandlerFunc(s.handleCollectibleUsernameDetailAPI)))
|
||||
mux.Handle("GET /api/storage/stats", s.scopedRoute(permissionStorageRead, http.HandlerFunc(s.handleStorageStatsAPI)))
|
||||
mux.Handle("GET /api/storage/accounts", s.scopedRoute(permissionStorageRead, http.HandlerFunc(s.handleStorageAccountsAPI)))
|
||||
|
|
@ -156,6 +158,8 @@ func (s *server) routes() http.Handler {
|
|||
mux.Handle("POST /api/actions/delete-uncategorized-gifs", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleDeleteUncategorizedGifsAPI)))
|
||||
mux.Handle("POST /api/actions/storage-manual-purge", s.scopedRoute(permissionStorageManage, http.HandlerFunc(s.handleStorageManualPurgeAPI)))
|
||||
mux.Handle("POST /api/actions/delete-gif-catalog-entry", s.scopedRoute(permissionContentManage, http.HandlerFunc(s.handleDeleteGifCatalogEntryAPI)))
|
||||
mux.Handle("POST /api/actions/reserve-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleReserveUsernameAPI)))
|
||||
mux.Handle("POST /api/actions/unreserve-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleUnreserveUsernameAPI)))
|
||||
mux.Handle("POST /api/actions/mint-collectible-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleMintCollectibleUsernameAPI)))
|
||||
mux.Handle("POST /api/actions/transfer-collectible-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleTransferCollectibleUsernameAPI)))
|
||||
mux.Handle("POST /api/actions/revoke-collectible-username", s.scopedRoute(permissionUsernamesManage, http.HandlerFunc(s.handleRevokeCollectibleUsernameAPI)))
|
||||
|
|
@ -2438,6 +2442,69 @@ type mintCollectibleUsernameAPIRequest struct {
|
|||
PurchaseDate flexUnix `json:"purchase_date"`
|
||||
}
|
||||
|
||||
type reserveUsernameAPIRequest struct {
|
||||
CommandID string `json:"command_id"`
|
||||
Reason string `json:"reason"`
|
||||
Confirm bool `json:"confirm"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func (s *server) handleReserveUsernameAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body reserveUsernameAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.ReserveUsernameRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "reserve-username"),
|
||||
Username: body.Username,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/reserved-usernames/reserve", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
func (s *server) handleUnreserveUsernameAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body reserveUsernameAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
return
|
||||
}
|
||||
req := admin.UnreserveUsernameRequest{
|
||||
CommandMeta: s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "unreserve-username"),
|
||||
Username: body.Username,
|
||||
}
|
||||
result, err := s.callAdminAPI(r.Context(), "/v1/reserved-usernames/unreserve", req)
|
||||
writeCommandResultAPI(w, result, err)
|
||||
}
|
||||
|
||||
func (s *server) handleReservedUsernamesAPI(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
params := url.Values{}
|
||||
for _, name := range []string{"q", "limit", "offset"} {
|
||||
if v := strings.TrimSpace(q.Get(name)); v != "" {
|
||||
params.Set(name, v)
|
||||
}
|
||||
}
|
||||
apiPath := "/v1/reserved-usernames"
|
||||
if enc := params.Encode(); enc != "" {
|
||||
apiPath += "?" + enc
|
||||
}
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, s.cfg.AdminAPIURL+apiPath, nil)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusInternalServerError, "request build failed")
|
||||
return
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+s.cfg.AdminAPIToken)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
writeAPIError(w, http.StatusBadGateway, "admin api unreachable")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *server) handleMintCollectibleUsernameAPI(w http.ResponseWriter, r *http.Request) {
|
||||
var body mintCollectibleUsernameAPIRequest
|
||||
if !decodeAction(w, r, &body) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue