feat: add version endpoint and expand storage/network API with granular handlers

This commit is contained in:
anonpenguin 2025-08-16 16:18:47 +03:00
parent 5eca56cd1e
commit 5b0a6864f9
4 changed files with 225 additions and 4 deletions

View File

@ -145,7 +145,7 @@ func extractAPIKey(r *http.Request) string {
// isPublicPath returns true for routes that should be accessible without API key auth // isPublicPath returns true for routes that should be accessible without API key auth
func isPublicPath(p string) bool { func isPublicPath(p string) bool {
switch p { 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 return true
default: default:
return false 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 // requiresNamespaceOwnership returns true if the path should be guarded by
// namespace ownership checks. // namespace ownership checks.
func requiresNamespaceOwnership(p string) bool { func requiresNamespaceOwnership(p string) bool {
if p == "/storage" || p == "/v1/storage" { if p == "/storage" || p == "/v1/storage" || strings.HasPrefix(p, "/v1/storage/") {
return true return true
} }
if p == "/v1/apps" || strings.HasPrefix(p, "/v1/apps/") { if p == "/v1/apps" || strings.HasPrefix(p, "/v1/apps/") {

View File

@ -10,10 +10,12 @@ func (g *Gateway) Routes() http.Handler {
mux.HandleFunc("/health", g.healthHandler) mux.HandleFunc("/health", g.healthHandler)
mux.HandleFunc("/status", g.statusHandler) mux.HandleFunc("/status", g.statusHandler)
mux.HandleFunc("/v1/health", g.healthHandler) mux.HandleFunc("/v1/health", g.healthHandler)
mux.HandleFunc("/v1/version", g.versionHandler)
mux.HandleFunc("/v1/status", g.statusHandler) mux.HandleFunc("/v1/status", g.statusHandler)
// auth endpoints // auth endpoints
mux.HandleFunc("/v1/auth/jwks", g.jwksHandler) 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/challenge", g.challengeHandler)
mux.HandleFunc("/v1/auth/verify", g.verifyHandler) mux.HandleFunc("/v1/auth/verify", g.verifyHandler)
mux.HandleFunc("/v1/auth/register", g.registerHandler) 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)
mux.HandleFunc("/v1/apps/", g.appsHandler) mux.HandleFunc("/v1/apps/", g.appsHandler)
// storage and network // storage
mux.HandleFunc("/v1/storage", g.storageHandler) 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/status", g.networkStatusHandler)
mux.HandleFunc("/v1/network/peers", g.networkPeersHandler) 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) return g.withMiddleware(mux)
} }

View File

@ -10,6 +10,13 @@ import (
"go.uber.org/zap" "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 // healthResponse is the JSON structure used by healthHandler
type healthResponse struct { type healthResponse struct {
Status string `json:"status"` Status string `json:"status"`
@ -67,3 +74,14 @@ func (g *Gateway) statusHandler(w http.ResponseWriter, r *http.Request) {
"network": status, "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(),
})
}

View File

@ -1,8 +1,12 @@
package gateway package gateway
import ( import (
"encoding/json"
"io" "io"
"net/http" "net/http"
"strconv"
"git.debros.io/DeBros/network/pkg/storage"
) )
func (g *Gateway) storageHandler(w http.ResponseWriter, r *http.Request) { 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) 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
}