From 579bf97e171c3d24c1e7f6ff21a07eb1fc420a3b Mon Sep 17 00:00:00 2001 From: Naeel Date: Sun, 19 Apr 2026 08:25:23 +0300 Subject: [PATCH] =?UTF-8?q?console=20v0.5.0:=20auth=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20deck=20API,=20login=20overlay,=20logout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- console/main.go | 98 +++++++++++++++++++++++++++++++++++++++---- console/ui/index.html | 95 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 183 insertions(+), 10 deletions(-) diff --git a/console/main.go b/console/main.go index 47b0efa..d60fd97 100644 --- a/console/main.go +++ b/console/main.go @@ -40,6 +40,12 @@ var ( const defaultSATokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" +var deckAPIs = map[string]string{ + "prod": "https://deck-api.ngcloud.ru/api/v1", + "dev": "https://deck-api-dev.ngcloud.ru/api/v1", + "test": "https://deck-api-test.ngcloud.ru/api/v1", +} + type server struct { dyn dynamic.Interface ns string @@ -54,6 +60,7 @@ type server struct { tokenMu sync.Mutex cachedJWT string tokenExpAt time.Time + tokenCache sync.Map } type createFunctionRequest struct { @@ -138,12 +145,32 @@ func main() { mux.HandleFunc("/api/httptriggers", s.handleList(httpTrigGVR)) mux.HandleFunc("/api/timetriggers", s.handleList(timeTrigGVR)) - mux.HandleFunc("/console/api/environments", s.handleList(environmentGVR)) - mux.HandleFunc("/console/api/packages", s.handleList(packageGVR)) - mux.HandleFunc("/console/api/functions", s.handleFunctionsRoot) - mux.HandleFunc("/console/api/functions/", s.handleFunctionsAction) - mux.HandleFunc("/console/api/httptriggers", s.handleList(httpTrigGVR)) - mux.HandleFunc("/console/api/timetriggers", s.handleList(timeTrigGVR)) + auth := func(h http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + token := strings.TrimSpace(r.Header.Get("X-Auth-Token")) + env := strings.TrimSpace(strings.ToLower(r.Header.Get("X-Auth-Env"))) + if _, ok := deckAPIs[env]; !ok { + env = "test" + } + if token == "" { + writeJSONError(w, http.StatusUnauthorized, "unauthorized") + return + } + if err := s.validateDeckToken(token, env); err != nil { + writeJSONError(w, http.StatusUnauthorized, "unauthorized") + return + } + h(w, r) + } + } + + mux.HandleFunc("/console/api/auth", s.handleAuth) + mux.HandleFunc("/console/api/environments", auth(s.handleList(environmentGVR))) + mux.HandleFunc("/console/api/packages", auth(s.handleList(packageGVR))) + mux.HandleFunc("/console/api/functions", auth(s.handleFunctionsRoot)) + mux.HandleFunc("/console/api/functions/", auth(s.handleFunctionsAction)) + mux.HandleFunc("/console/api/httptriggers", auth(s.handleList(httpTrigGVR))) + mux.HandleFunc("/console/api/timetriggers", auth(s.handleList(timeTrigGVR))) httpServer := &http.Server{ Addr: ":" + port, @@ -714,6 +741,63 @@ func (s *server) getRouterToken() string { return s.cachedJWT } +func (s *server) validateDeckToken(token, env string) error { + cacheKey := env + ":" + token + if v, ok := s.tokenCache.Load(cacheKey); ok { + if time.Now().Before(v.(time.Time)) { + return nil + } + s.tokenCache.Delete(cacheKey) + } + apiBase, ok := deckAPIs[env] + if !ok { + return fmt.Errorf("unknown env: %s", env) + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiBase+"/index.cfm/instances", nil) + if err != nil { + return err + } + req.Header.Set("Authorization", "Bearer "+token) + resp, err := s.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + _, _ = io.ReadAll(resp.Body) + if resp.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("invalid token") + } + s.tokenCache.Store(cacheKey, time.Now().Add(5*time.Minute)) + return nil +} + +func (s *server) handleAuth(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + var body struct { + Token string `json:"token"` + Env string `json:"env"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Token) == "" { + writeJSONError(w, http.StatusBadRequest, "token required") + return + } + env := strings.TrimSpace(strings.ToLower(body.Env)) + if _, ok := deckAPIs[env]; !ok { + env = "test" + } + if err := s.validateDeckToken(body.Token, env); err != nil { + writeJSONError(w, http.StatusUnauthorized, "invalid token") + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "env": env}) +} + func (s *server) handleDeleteFunction(w http.ResponseWriter, r *http.Request, name string) { ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() @@ -817,7 +901,7 @@ func withCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Auth-Token, X-Auth-Env") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return diff --git a/console/ui/index.html b/console/ui/index.html index 86e7a48..d97a62a 100644 --- a/console/ui/index.html +++ b/console/ui/index.html @@ -287,6 +287,31 @@ +