shared-sqs: Этапы 5+6 — Admin API, auth middleware, graceful shutdown

This commit is contained in:
Naeel
2026-04-09 12:59:53 +03:00
parent 08053ca8e5
commit 073683250c
3 changed files with 351 additions and 80 deletions
+211
View File
@@ -0,0 +1,211 @@
// 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,
})
}