// app/admin/admin.go // Admin API handlers for shared-sqs management // Created: 2026-04-09 package admin import ( "encoding/json" "net/http" "strings" "time" "shared-sqs/app/models" "shared-sqs/app/tenant" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" ) // Handler — admin API handler, holds TenantStore и admin token type Handler struct { store *tenant.TenantStore adminToken string } // NewHandler — создаёт admin handler func NewHandler(store *tenant.TenantStore, adminToken string) *Handler { return &Handler{store: store, adminToken: adminToken} } // RegisterRoutes — регистрирует admin маршруты на переданном router func (h *Handler) RegisterRoutes(r *mux.Router) { adminRouter := r.PathPrefix("/admin").Subrouter() adminRouter.Use(h.bearerAuthMiddleware) adminRouter.HandleFunc("/tenants", h.createTenant).Methods("POST") adminRouter.HandleFunc("/tenants", h.listTenants).Methods("GET") adminRouter.HandleFunc("/tenants/{id}", h.getTenant).Methods("GET") adminRouter.HandleFunc("/tenants/{id}", h.deleteTenant).Methods("DELETE") adminRouter.HandleFunc("/health", h.detailedHealth).Methods("GET") } // bearerAuthMiddleware — проверяет Bearer token для admin API (Trap #12) func (h *Handler) bearerAuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") expected := "Bearer " + h.adminToken if authHeader != expected { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) return } next.ServeHTTP(w, r) }) } // createTenantRequest — тело запроса POST /admin/tenants type createTenantRequest struct { Name string `json:"name"` MaxQueues int `json:"max_queues"` } // tenantCreateResponse — ответ с secret_key (показывается ТОЛЬКО при создании) type tenantCreateResponse struct { ID string `json:"id"` Name string `json:"name"` AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` MaxQueues int `json:"max_queues"` CreatedAt time.Time `json:"created_at"` Active bool `json:"active"` } // tenantListItem — данные тенанта без secret_key (для List/Get) type tenantListItem struct { ID string `json:"id"` Name string `json:"name"` AccessKey string `json:"access_key"` MaxQueues int `json:"max_queues"` CreatedAt time.Time `json:"created_at"` Active bool `json:"active"` } // createTenant — POST /admin/tenants func (h *Handler) createTenant(w http.ResponseWriter, r *http.Request) { var req createTenantRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "invalid request body"}) return } if req.Name == "" { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "name is required"}) return } t, err := h.store.Create(req.Name, req.MaxQueues) if err != nil { log.Errorf("admin: failed to create tenant: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": "failed to create tenant"}) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(tenantCreateResponse{ ID: t.ID, Name: t.Name, AccessKey: t.AccessKey, SecretKey: t.SecretKey, MaxQueues: t.MaxQueues, CreatedAt: t.CreatedAt, Active: t.Active, }) } // listTenants — GET /admin/tenants func (h *Handler) listTenants(w http.ResponseWriter, r *http.Request) { tenants := h.store.List() items := make([]tenantListItem, 0, len(tenants)) for _, t := range tenants { items = append(items, tenantListItem{ ID: t.ID, Name: t.Name, AccessKey: t.AccessKey, MaxQueues: t.MaxQueues, CreatedAt: t.CreatedAt, Active: t.Active, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(items) } // getTenant — GET /admin/tenants/{id} func (h *Handler) getTenant(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] t, ok := h.store.GetByID(id) if !ok { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{"error": "tenant not found"}) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tenantListItem{ ID: t.ID, Name: t.Name, AccessKey: t.AccessKey, MaxQueues: t.MaxQueues, CreatedAt: t.CreatedAt, Active: t.Active, }) } // deleteTenant — DELETE /admin/tenants/{id} // Удаляет тенанта И ВСЕ его очереди из SyncQueues (Trap #11: иначе memory leak) func (h *Handler) deleteTenant(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] t, ok := h.store.GetByID(id) if !ok { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]string{"error": "tenant not found"}) return } // Удаляем все очереди тенанта из SyncQueues prefix := t.AccessKey + ":" models.SyncQueues.Lock() for key := range models.SyncQueues.Queues { if strings.HasPrefix(key, prefix) { delete(models.SyncQueues.Queues, key) } } models.SyncQueues.Unlock() h.store.Delete(id) w.WriteHeader(http.StatusNoContent) } // adminHealthDetail — ответ GET /admin/health type adminHealthDetail struct { Status string `json:"status"` TenantCount int `json:"tenant_count"` QueueCount int `json:"queue_count"` MessageCount int `json:"message_count"` } // detailedHealth — GET /admin/health func (h *Handler) detailedHealth(w http.ResponseWriter, r *http.Request) { tenants := h.store.List() models.SyncQueues.RLock() queueCount := len(models.SyncQueues.Queues) msgCount := 0 for _, q := range models.SyncQueues.Queues { msgCount += len(q.Messages) } models.SyncQueues.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(adminHealthDetail{ Status: "ok", TenantCount: len(tenants), QueueCount: queueCount, MessageCount: msgCount, }) }