Files
sless/shared-sqs/app/admin/admin.go
T

212 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
})
}