// admin_handlers.go package main import ( "encoding/json" "log" "net/http" "strconv" "time" "github.com/gorilla/websocket" ) type tokenCreateRequest struct { Name string `json:"name"` } type tokenCreateResponse struct { Token string `json:"token"` Name string `json:"name"` } func adminTokensHandler(hub *Hub, tokensPath string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: hub.mu.RLock() var list []TokenConfig for _, tc := range hub.tokens { list = append(list, *tc) } hub.mu.RUnlock() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(list) case http.MethodPost: var req tokenCreateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } if req.Name == "" { http.Error(w, "name required", http.StatusBadRequest) return } token, err := generateToken() if err != nil { log.Printf("error generating token: %v", err) http.Error(w, "internal error", http.StatusInternalServerError) return } tc := &TokenConfig{Token: token, Name: req.Name} hub.mu.Lock() hub.tokens[token] = tc if err := saveTokens(tokensPath, hub.tokens); err != nil { log.Printf("warning: failed to save tokens: %v", err) } hub.mu.Unlock() w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(tokenCreateResponse{Token: token, Name: req.Name}) default: w.WriteHeader(http.StatusMethodNotAllowed) } } } type tokenRevokeRequest struct { Token string `json:"token"` } func adminTokenRevokeHandler(hub *Hub, tokensPath string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } var req tokenRevokeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "bad json", http.StatusBadRequest) return } if req.Token == "" { http.Error(w, "token required", http.StatusBadRequest) return } var toKick []*Client hub.mu.Lock() tc, ok := hub.tokens[req.Token] if ok { tc.Revoked = true // collect clients using this token so we can kick them after unlocking for c := range hub.clients { if c.token == tc { toKick = append(toKick, c) } } if err := saveTokens(tokensPath, hub.tokens); err != nil { log.Printf("warning: failed to save tokens: %v", err) } } hub.mu.Unlock() // Kick all clients outside the lock to avoid deadlocks with unregister/readPump. for _, c := range toKick { log.Printf("kicking client %s (%s) due to token revoke", c.nick, c.token.Name) _ = c.conn.WriteControl( websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, "token revoked"), time.Now().Add(5*time.Second), ) c.conn.Close() } w.WriteHeader(http.StatusNoContent) } } func adminEventsHandler(hub *Hub) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sinceStr := r.URL.Query().Get("since") var since int64 if sinceStr != "" { if v, err := strconv.ParseInt(sinceStr, 10, 64); err == nil { since = v } } events := hub.getEventsSince(since) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(events) } }