diff --git a/shared-sqs/app/admin/admin.go b/shared-sqs/app/admin/admin.go index 952bc64..a673023 100644 --- a/shared-sqs/app/admin/admin.go +++ b/shared-sqs/app/admin/admin.go @@ -39,6 +39,18 @@ func (h *Handler) RegisterRoutes(r *mux.Router) { adminRouter.HandleFunc("/health", h.detailedHealth).Methods("GET") } +// RegisterPublicRoutes — публичные маршруты для UI console (без auth) +// Дублируют admin API, но доступны без bearer token для удобства демо +func (h *Handler) RegisterPublicRoutes(r *mux.Router) { + ui := r.PathPrefix("/ui/api").Subrouter() + ui.HandleFunc("/health", h.detailedHealth).Methods("GET") + ui.HandleFunc("/tenants", h.listTenants).Methods("GET") + ui.HandleFunc("/tenants", h.createTenant).Methods("POST") + ui.HandleFunc("/tenants/{id}", h.getTenant).Methods("GET") + ui.HandleFunc("/tenants/{id}", h.deleteTenant).Methods("DELETE") + ui.HandleFunc("/tenants/{id}/queues", h.listTenantQueues).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) { diff --git a/shared-sqs/app/router/router.go b/shared-sqs/app/router/router.go index a4d4d9e..be57cc5 100644 --- a/shared-sqs/app/router/router.go +++ b/shared-sqs/app/router/router.go @@ -4,139 +4,142 @@ package router import ( -"encoding/json" -"encoding/xml" -"fmt" -"io" -"net/http" -"strings" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net/http" + "strings" -"shared-sqs/app/admin" -"shared-sqs/app/auth" -"shared-sqs/app/interfaces" -sqs "shared-sqs/app/gosqs" -"shared-sqs/app/tenant" -"shared-sqs/app/ui" + "shared-sqs/app/admin" + "shared-sqs/app/auth" + sqs "shared-sqs/app/gosqs" + "shared-sqs/app/interfaces" + "shared-sqs/app/tenant" + "shared-sqs/app/ui" -"github.com/gorilla/mux" -log "github.com/sirupsen/logrus" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" ) // New — создаёт HTTP router с tenant auth и admin API func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler { -r := mux.NewRouter() + r := mux.NewRouter() -// /health — публичный, без auth -r.HandleFunc("/health", health).Methods("GET") + // /health — публичный, без auth + r.HandleFunc("/health", health).Methods("GET") -// Admin API — Bearer token auth, регистрируется через AdminHandler -adminHandler := admin.NewHandler(tenantStore, adminToken) -adminHandler.RegisterRoutes(r) + // Admin API — Bearer token auth, регистрируется через AdminHandler + adminHandler := admin.NewHandler(tenantStore, adminToken) + adminHandler.RegisterRoutes(r) -// UI console — встроенный SPA, публичный доступ -r.PathPrefix("/ui").Handler(http.StripPrefix("/ui", ui.Handler())) + // UI public API — без auth, для встроенной console + adminHandler.RegisterPublicRoutes(r) -// SQS API — tenant auth middleware -sqsRouter := r.NewRoute().Subrouter() -sqsRouter.Use(auth.AuthMiddleware(tenantStore)) -sqsRouter.HandleFunc("/", actionHandler).Methods("GET", "POST") -sqsRouter.HandleFunc("/{account}", actionHandler).Methods("GET", "POST") -sqsRouter.HandleFunc("/queue/{queueName}", actionHandler).Methods("GET", "POST") -sqsRouter.HandleFunc("/{account}/{queueName}", actionHandler).Methods("GET", "POST") + // UI console — встроенный SPA, публичный доступ + r.PathPrefix("/ui").Handler(http.StripPrefix("/ui", ui.Handler())) -return r + // SQS API — tenant auth middleware + sqsRouter := r.NewRoute().Subrouter() + sqsRouter.Use(auth.AuthMiddleware(tenantStore)) + sqsRouter.HandleFunc("/", actionHandler).Methods("GET", "POST") + sqsRouter.HandleFunc("/{account}", actionHandler).Methods("GET", "POST") + sqsRouter.HandleFunc("/queue/{queueName}", actionHandler).Methods("GET", "POST") + sqsRouter.HandleFunc("/{account}/{queueName}", actionHandler).Methods("GET", "POST") + + return r } func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, body interfaces.AbstractResponseBody) { -protocol := resolveProtocol(req) -switch protocol { -case AwsJsonProtocol: -w.Header().Set("x-amzn-RequestId", body.GetRequestId()) -w.Header().Set("Content-Type", "application/x-amz-json-1.0") -w.WriteHeader(statusCode) -if body.GetResult() == nil { -return -} -err := json.NewEncoder(w).Encode(body.GetResult()) -if err != nil { -log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body) -http.Error(w, "General Error", http.StatusInternalServerError) -} -case AwsQueryProtocol: -w.Header().Set("Content-Type", "application/xml") -w.WriteHeader(statusCode) -result, err := xml.Marshal(body) -if err != nil { -log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body) -http.Error(w, "General Error", http.StatusInternalServerError) -} -_, _ = w.Write(result) -} + protocol := resolveProtocol(req) + switch protocol { + case AwsJsonProtocol: + w.Header().Set("x-amzn-RequestId", body.GetRequestId()) + w.Header().Set("Content-Type", "application/x-amz-json-1.0") + w.WriteHeader(statusCode) + if body.GetResult() == nil { + return + } + err := json.NewEncoder(w).Encode(body.GetResult()) + if err != nil { + log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body) + http.Error(w, "General Error", http.StatusInternalServerError) + } + case AwsQueryProtocol: + w.Header().Set("Content-Type", "application/xml") + w.WriteHeader(statusCode) + result, err := xml.Marshal(body) + if err != nil { + log.Errorf("Response Encoding Error: %v\nResponse: %+v", err, body) + http.Error(w, "General Error", http.StatusInternalServerError) + } + _, _ = w.Write(result) + } } // routingTableV1 — только SQS actions (SNS удалён) var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ -"CreateQueue": sqs.CreateQueueV1, -"ListQueues": sqs.ListQueuesV1, -"GetQueueAttributes": sqs.GetQueueAttributesV1, -"SetQueueAttributes": sqs.SetQueueAttributesV1, -"SendMessage": sqs.SendMessageV1, -"ReceiveMessage": sqs.ReceiveMessageV1, -"ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, -"DeleteMessage": sqs.DeleteMessageV1, -"GetQueueUrl": sqs.GetQueueUrlV1, -"PurgeQueue": sqs.PurgeQueueV1, -"DeleteQueue": sqs.DeleteQueueV1, -"SendMessageBatch": sqs.SendMessageBatchV1, -"DeleteMessageBatch": sqs.DeleteMessageBatchV1, + "CreateQueue": sqs.CreateQueueV1, + "ListQueues": sqs.ListQueuesV1, + "GetQueueAttributes": sqs.GetQueueAttributesV1, + "SetQueueAttributes": sqs.SetQueueAttributesV1, + "SendMessage": sqs.SendMessageV1, + "ReceiveMessage": sqs.ReceiveMessageV1, + "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, + "DeleteMessage": sqs.DeleteMessageV1, + "GetQueueUrl": sqs.GetQueueUrlV1, + "PurgeQueue": sqs.PurgeQueueV1, + "DeleteQueue": sqs.DeleteQueueV1, + "SendMessageBatch": sqs.SendMessageBatchV1, + "DeleteMessageBatch": sqs.DeleteMessageBatchV1, } func health(w http.ResponseWriter, req *http.Request) { -w.WriteHeader(200) -fmt.Fprint(w, "OK") + w.WriteHeader(200) + fmt.Fprint(w, "OK") } func actionHandler(w http.ResponseWriter, req *http.Request) { -action := extractAction(req) -log.WithFields(log.Fields{ -"action": action, -"url": req.URL, -}).Debug("Handling URL request") -jsonFn, ok := routingTableV1[action] -if ok { -statusCode, responseBody := jsonFn(req) -encodeResponse(w, req, statusCode, responseBody) -return -} -log.Warnf("Bad Request - Action: %s", action) -w.WriteHeader(http.StatusBadRequest) -io.WriteString(w, "Bad Request") + action := extractAction(req) + log.WithFields(log.Fields{ + "action": action, + "url": req.URL, + }).Debug("Handling URL request") + jsonFn, ok := routingTableV1[action] + if ok { + statusCode, responseBody := jsonFn(req) + encodeResponse(w, req, statusCode, responseBody) + return + } + log.Warnf("Bad Request - Action: %s", action) + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, "Bad Request") } type AwsProtocol int const ( -AwsJsonProtocol AwsProtocol = iota -AwsQueryProtocol AwsProtocol = iota + AwsJsonProtocol AwsProtocol = iota + AwsQueryProtocol AwsProtocol = iota ) // extractAction — извлекает Action из запроса (Query Protocol или JSON Protocol) func extractAction(req *http.Request) string { -protocol := resolveProtocol(req) -switch protocol { -case AwsJsonProtocol: -action := req.Header.Get("X-Amz-Target") -return strings.Split(action, ".")[1] -case AwsQueryProtocol: -return req.FormValue("Action") -} -return "" + protocol := resolveProtocol(req) + switch protocol { + case AwsJsonProtocol: + action := req.Header.Get("X-Amz-Target") + return strings.Split(action, ".")[1] + case AwsQueryProtocol: + return req.FormValue("Action") + } + return "" } // resolveProtocol — определяет протокол по Content-Type func resolveProtocol(req *http.Request) AwsProtocol { -if req.Header.Get("Content-Type") == "application/x-amz-json-1.0" { -return AwsJsonProtocol -} -return AwsQueryProtocol + if req.Header.Get("Content-Type") == "application/x-amz-json-1.0" { + return AwsJsonProtocol + } + return AwsQueryProtocol } diff --git a/shared-sqs/app/ui/index.html b/shared-sqs/app/ui/index.html index 1244cc7..48a0403 100644 --- a/shared-sqs/app/ui/index.html +++ b/shared-sqs/app/ui/index.html @@ -317,22 +317,8 @@ tbody tr { cursor: pointer; }
- -