fix: API JSON error responses + navbar with dropdowns

- Add backend/libs/go-http-errors for consistent JSON errors
- REST API: use writeMethodNotAllowed, writeNotFound, writeInternalError
- middleware, gateway, search: use httperrors.WriteJSON
- SPA: navbar with Explore/Tools/More dropdowns, initNavDropdowns()
- Next.js: Navbar component with dropdowns + mobile menu

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-16 03:09:53 -08:00
parent 53114e75fd
commit a36ab9d47c
16 changed files with 3979 additions and 3191 deletions

View File

@@ -13,6 +13,7 @@ import (
"github.com/explorer/backend/auth"
"github.com/explorer/backend/api/middleware"
httpmiddleware "github.com/explorer/backend/libs/go-http-middleware"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -54,8 +55,12 @@ func (s *Server) Start(port int) error {
// Setup track routes with proper middleware
s.SetupTrackRoutes(mux, authMiddleware)
// Initialize security middleware
securityMiddleware := middleware.NewSecurityMiddleware()
// Security headers (reusable lib; CSP from env or explorer default)
csp := os.Getenv("CSP_HEADER")
if csp == "" {
csp = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data: https:; connect-src 'self' https://explorer.d-bis.org https://rpc-http-pub.d-bis.org wss://rpc-ws-pub.d-bis.org http://192.168.11.221:8545 ws://192.168.11.221:8546;"
}
securityMiddleware := httpmiddleware.NewSecurity(csp)
// Add middleware for all routes (outermost to innermost)
handler := securityMiddleware.AddSecurityHeaders(
@@ -82,9 +87,13 @@ func (s *Server) addMiddleware(next http.Handler) http.Handler {
w.Header().Set("X-Explorer-Version", "1.0.0")
w.Header().Set("X-Powered-By", "SolaceScanScout")
// Add CORS headers for API routes
// Add CORS headers for API routes (optional: set CORS_ALLOWED_ORIGIN to restrict, e.g. https://explorer.d-bis.org)
if strings.HasPrefix(r.URL.Path, "/api/") {
w.Header().Set("Access-Control-Allow-Origin", "*")
origin := os.Getenv("CORS_ALLOWED_ORIGIN")
if origin == "" {
origin = "*"
}
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
@@ -99,10 +108,22 @@ func (s *Server) addMiddleware(next http.Handler) http.Handler {
})
}
// requireDB returns false and writes 503 if db is nil (e.g. in tests without DB)
func (s *Server) requireDB(w http.ResponseWriter) bool {
if s.db == nil {
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "database unavailable")
return false
}
return true
}
// handleListBlocks handles GET /api/v1/blocks
func (s *Server) handleListBlocks(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
writeMethodNotAllowed(w)
return
}
if !s.requireDB(w) {
return
}
@@ -132,7 +153,7 @@ func (s *Server) handleListBlocks(w http.ResponseWriter, r *http.Request) {
rows, err := s.db.Query(ctx, query, s.chainID, pageSize, offset)
if err != nil {
http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
writeInternalError(w, "Database error")
return
}
defer rows.Close()
@@ -191,12 +212,15 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Explorer-Version", "1.0.0")
// Check database connection
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
dbStatus := "ok"
if err := s.db.Ping(ctx); err != nil {
dbStatus = "error: " + err.Error()
if s.db != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := s.db.Ping(ctx); err != nil {
dbStatus = "error: " + err.Error()
}
} else {
dbStatus = "unavailable"
}
health := map[string]interface{}{