mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-10-06 08:19:07 +00:00
feat: add version endpoint and expand storage/network API with granular handlers
This commit is contained in:
parent
5eca56cd1e
commit
5b0a6864f9
@ -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/") {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user