212 lines
6.3 KiB
Go
212 lines
6.3 KiB
Go
// 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,
|
||
})
|
||
}
|