diff --git a/pkg/gateway/middleware.go b/pkg/gateway/middleware.go index b6369a9..e9ac542 100644 --- a/pkg/gateway/middleware.go +++ b/pkg/gateway/middleware.go @@ -145,7 +145,7 @@ func extractAPIKey(r *http.Request) string { // isPublicPath returns true for routes that should be accessible without API key auth func isPublicPath(p string) bool { switch p { - case "/health", "/v1/health", "/status", "/v1/status", "/v1/auth/jwks", "/v1/auth/challenge", "/v1/auth/verify", "/v1/auth/register", "/v1/auth/refresh", "/v1/auth/logout": + case "/health", "/v1/health", "/status", "/v1/status", "/v1/auth/jwks", "/.well-known/jwks.json", "/v1/version", "/v1/auth/challenge", "/v1/auth/verify", "/v1/auth/register", "/v1/auth/refresh", "/v1/auth/logout": return true default: return false @@ -241,7 +241,7 @@ func (g *Gateway) authorizationMiddleware(next http.Handler) http.Handler { // requiresNamespaceOwnership returns true if the path should be guarded by // namespace ownership checks. func requiresNamespaceOwnership(p string) bool { - if p == "/storage" || p == "/v1/storage" { + if p == "/storage" || p == "/v1/storage" || strings.HasPrefix(p, "/v1/storage/") { return true } if p == "/v1/apps" || strings.HasPrefix(p, "/v1/apps/") { diff --git a/pkg/gateway/routes.go b/pkg/gateway/routes.go index 833f8ce..abea1df 100644 --- a/pkg/gateway/routes.go +++ b/pkg/gateway/routes.go @@ -10,10 +10,12 @@ func (g *Gateway) Routes() http.Handler { mux.HandleFunc("/health", g.healthHandler) mux.HandleFunc("/status", g.statusHandler) mux.HandleFunc("/v1/health", g.healthHandler) + mux.HandleFunc("/v1/version", g.versionHandler) mux.HandleFunc("/v1/status", g.statusHandler) // auth endpoints mux.HandleFunc("/v1/auth/jwks", g.jwksHandler) + mux.HandleFunc("/.well-known/jwks.json", g.jwksHandler) mux.HandleFunc("/v1/auth/challenge", g.challengeHandler) mux.HandleFunc("/v1/auth/verify", g.verifyHandler) mux.HandleFunc("/v1/auth/register", g.registerHandler) @@ -25,10 +27,19 @@ func (g *Gateway) Routes() http.Handler { mux.HandleFunc("/v1/apps", g.appsHandler) mux.HandleFunc("/v1/apps/", g.appsHandler) - // storage and network - mux.HandleFunc("/v1/storage", g.storageHandler) + // storage + mux.HandleFunc("/v1/storage", g.storageHandler) // legacy/basic + mux.HandleFunc("/v1/storage/get", g.storageGetHandler) + mux.HandleFunc("/v1/storage/put", g.storagePutHandler) + mux.HandleFunc("/v1/storage/delete", g.storageDeleteHandler) + mux.HandleFunc("/v1/storage/list", g.storageListHandler) + mux.HandleFunc("/v1/storage/exists", g.storageExistsHandler) + + // network mux.HandleFunc("/v1/network/status", g.networkStatusHandler) mux.HandleFunc("/v1/network/peers", g.networkPeersHandler) + mux.HandleFunc("/v1/network/connect", g.networkConnectHandler) + mux.HandleFunc("/v1/network/disconnect", g.networkDisconnectHandler) return g.withMiddleware(mux) } diff --git a/pkg/gateway/status_handlers.go b/pkg/gateway/status_handlers.go index baf5b83..c09714d 100644 --- a/pkg/gateway/status_handlers.go +++ b/pkg/gateway/status_handlers.go @@ -10,6 +10,13 @@ import ( "go.uber.org/zap" ) +// Build info (set via -ldflags at build time; defaults for dev) +var ( + BuildVersion = "dev" + BuildCommit = "" + BuildTime = "" +) + // healthResponse is the JSON structure used by healthHandler type healthResponse struct { Status string `json:"status"` @@ -67,3 +74,14 @@ func (g *Gateway) statusHandler(w http.ResponseWriter, r *http.Request) { "network": status, }) } + +// versionHandler returns gateway build/runtime information +func (g *Gateway) versionHandler(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]any{ + "version": BuildVersion, + "commit": BuildCommit, + "build_time": BuildTime, + "started_at": g.startedAt, + "uptime": time.Since(g.startedAt).String(), + }) +} diff --git a/pkg/gateway/storage_handlers.go b/pkg/gateway/storage_handlers.go index 2ef6836..62d67e6 100644 --- a/pkg/gateway/storage_handlers.go +++ b/pkg/gateway/storage_handlers.go @@ -1,8 +1,12 @@ package gateway import ( + "encoding/json" "io" "net/http" + "strconv" + + "git.debros.io/DeBros/network/pkg/storage" ) func (g *Gateway) storageHandler(w http.ResponseWriter, r *http.Request) { @@ -85,3 +89,191 @@ func (g *Gateway) networkPeersHandler(w http.ResponseWriter, r *http.Request) { } writeJSON(w, http.StatusOK, peers) } + +func (g *Gateway) storageGetHandler(w http.ResponseWriter, r *http.Request) { + if g.client == nil { + writeError(w, http.StatusServiceUnavailable, "client not initialized") + return + } + key := r.URL.Query().Get("key") + if key == "" { + writeError(w, http.StatusBadRequest, "missing 'key'") + return + } + if !g.validateNamespaceParam(r) { + writeError(w, http.StatusForbidden, "namespace mismatch") + return + } + val, err := g.client.Storage().Get(r.Context(), key) + if err != nil { + writeError(w, http.StatusNotFound, err.Error()) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(val) +} + +func (g *Gateway) storagePutHandler(w http.ResponseWriter, r *http.Request) { + if g.client == nil { + writeError(w, http.StatusServiceUnavailable, "client not initialized") + return + } + key := r.URL.Query().Get("key") + if key == "" { + writeError(w, http.StatusBadRequest, "missing 'key'") + return + } + if !g.validateNamespaceParam(r) { + writeError(w, http.StatusForbidden, "namespace mismatch") + return + } + defer r.Body.Close() + b, err := io.ReadAll(r.Body) + if err != nil { + writeError(w, http.StatusBadRequest, "failed to read body") + return + } + if err := g.client.Storage().Put(r.Context(), key, b); err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusCreated, map[string]any{"status": "ok", "key": key, "size": len(b)}) +} + +func (g *Gateway) storageDeleteHandler(w http.ResponseWriter, r *http.Request) { + if g.client == nil { + writeError(w, http.StatusServiceUnavailable, "client not initialized") + return + } + if !g.validateNamespaceParam(r) { + writeError(w, http.StatusForbidden, "namespace mismatch") + return + } + key := r.URL.Query().Get("key") + if key == "" { + var body struct { + Key string `json:"key"` + Namespace string `json:"namespace"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err == nil { + key = body.Key + } + } + if key == "" { + writeError(w, http.StatusBadRequest, "missing 'key'") + return + } + if err := g.client.Storage().Delete(r.Context(), key); err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "key": key}) +} + +func (g *Gateway) storageListHandler(w http.ResponseWriter, r *http.Request) { + if g.client == nil { + writeError(w, http.StatusServiceUnavailable, "client not initialized") + return + } + if !g.validateNamespaceParam(r) { + writeError(w, http.StatusForbidden, "namespace mismatch") + return + } + prefix := r.URL.Query().Get("prefix") + limitStr := r.URL.Query().Get("limit") + limit := 100 + if limitStr != "" { + if n, err := strconv.Atoi(limitStr); err == nil && n > 0 { + limit = n + } + } + keys, err := g.client.Storage().List(r.Context(), prefix, limit) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"keys": keys}) +} + +func (g *Gateway) storageExistsHandler(w http.ResponseWriter, r *http.Request) { + if g.client == nil { + writeError(w, http.StatusServiceUnavailable, "client not initialized") + return + } + if !g.validateNamespaceParam(r) { + writeError(w, http.StatusForbidden, "namespace mismatch") + return + } + key := r.URL.Query().Get("key") + if key == "" { + writeError(w, http.StatusBadRequest, "missing 'key'") + return + } + exists, err := g.client.Storage().Exists(r.Context(), key) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"exists": exists}) +} + +func (g *Gateway) networkConnectHandler(w http.ResponseWriter, r *http.Request) { + if g.client == nil { + writeError(w, http.StatusServiceUnavailable, "client not initialized") + return + } + if r.Method != http.MethodPost { + writeError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + var body struct { + Multiaddr string `json:"multiaddr"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Multiaddr == "" { + writeError(w, http.StatusBadRequest, "invalid body: expected {multiaddr}") + return + } + if err := g.client.Network().ConnectToPeer(r.Context(), body.Multiaddr); err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"status": "ok"}) +} + +func (g *Gateway) networkDisconnectHandler(w http.ResponseWriter, r *http.Request) { + if g.client == nil { + writeError(w, http.StatusServiceUnavailable, "client not initialized") + return + } + if r.Method != http.MethodPost { + writeError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + var body struct { + PeerID string `json:"peer_id"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.PeerID == "" { + writeError(w, http.StatusBadRequest, "invalid body: expected {peer_id}") + return + } + if err := g.client.Network().DisconnectFromPeer(r.Context(), body.PeerID); err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, http.StatusOK, map[string]any{"status": "ok"}) +} + +func (g *Gateway) validateNamespaceParam(r *http.Request) bool { + qns := r.URL.Query().Get("namespace") + if qns == "" { + return true + } + if v := r.Context().Value(storage.CtxKeyNamespaceOverride); v != nil { + if s, ok := v.(string); ok && s != "" { + return s == qns + } + } + // If no namespace in context, disallow explicit namespace param + return false +}