Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
224
backend/api/rest/server.go
Normal file
224
backend/api/rest/server.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/explorer/backend/auth"
|
||||
"github.com/explorer/backend/api/middleware"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Server represents the REST API server
|
||||
type Server struct {
|
||||
db *pgxpool.Pool
|
||||
chainID int
|
||||
walletAuth *auth.WalletAuth
|
||||
jwtSecret []byte
|
||||
}
|
||||
|
||||
// NewServer creates a new REST API server
|
||||
func NewServer(db *pgxpool.Pool, chainID int) *Server {
|
||||
// Get JWT secret from environment or use default
|
||||
jwtSecret := []byte(os.Getenv("JWT_SECRET"))
|
||||
if len(jwtSecret) == 0 {
|
||||
jwtSecret = []byte("change-me-in-production-use-strong-random-secret")
|
||||
log.Println("WARNING: Using default JWT secret. Set JWT_SECRET environment variable in production!")
|
||||
}
|
||||
|
||||
walletAuth := auth.NewWalletAuth(db, jwtSecret)
|
||||
|
||||
return &Server{
|
||||
db: db,
|
||||
chainID: chainID,
|
||||
walletAuth: walletAuth,
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (s *Server) Start(port int) error {
|
||||
mux := http.NewServeMux()
|
||||
s.SetupRoutes(mux)
|
||||
|
||||
// Initialize auth middleware
|
||||
authMiddleware := middleware.NewAuthMiddleware(s.walletAuth)
|
||||
|
||||
// Setup track routes with proper middleware
|
||||
s.SetupTrackRoutes(mux, authMiddleware)
|
||||
|
||||
// Initialize security middleware
|
||||
securityMiddleware := middleware.NewSecurityMiddleware()
|
||||
|
||||
// Add middleware for all routes (outermost to innermost)
|
||||
handler := securityMiddleware.AddSecurityHeaders(
|
||||
authMiddleware.OptionalAuth( // Optional auth for Track 1, required for others
|
||||
s.addMiddleware(
|
||||
s.loggingMiddleware(
|
||||
s.compressionMiddleware(mux),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
log.Printf("Starting SolaceScanScout REST API server on %s", addr)
|
||||
log.Printf("Tiered architecture enabled: Track 1 (public), Track 2-4 (authenticated)")
|
||||
return http.ListenAndServe(addr, handler)
|
||||
}
|
||||
|
||||
// addMiddleware adds common middleware to all routes
|
||||
func (s *Server) addMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Add branding headers
|
||||
w.Header().Set("X-Explorer-Name", "SolaceScanScout")
|
||||
w.Header().Set("X-Explorer-Version", "1.0.0")
|
||||
w.Header().Set("X-Powered-By", "SolaceScanScout")
|
||||
|
||||
// Add CORS headers for API routes
|
||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
|
||||
|
||||
// Handle preflight
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate pagination
|
||||
page, pageSize, err := validatePagination(
|
||||
r.URL.Query().Get("page"),
|
||||
r.URL.Query().Get("page_size"),
|
||||
)
|
||||
if err != nil {
|
||||
writeValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
query := `
|
||||
SELECT chain_id, number, hash, timestamp, timestamp_iso, miner, transaction_count, gas_used, gas_limit
|
||||
FROM blocks
|
||||
WHERE chain_id = $1
|
||||
ORDER BY number DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
// Add query timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
blocks := []map[string]interface{}{}
|
||||
for rows.Next() {
|
||||
var chainID, number, transactionCount int
|
||||
var hash, miner string
|
||||
var timestamp time.Time
|
||||
var timestampISO sql.NullString
|
||||
var gasUsed, gasLimit int64
|
||||
|
||||
if err := rows.Scan(&chainID, &number, &hash, ×tamp, ×tampISO, &miner, &transactionCount, &gasUsed, &gasLimit); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
block := map[string]interface{}{
|
||||
"chain_id": chainID,
|
||||
"number": number,
|
||||
"hash": hash,
|
||||
"timestamp": timestamp,
|
||||
"miner": miner,
|
||||
"transaction_count": transactionCount,
|
||||
"gas_used": gasUsed,
|
||||
"gas_limit": gasLimit,
|
||||
}
|
||||
|
||||
if timestampISO.Valid {
|
||||
block["timestamp_iso"] = timestampISO.String
|
||||
}
|
||||
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"data": blocks,
|
||||
"meta": map[string]interface{}{
|
||||
"pagination": map[string]interface{}{
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// handleGetBlock, handleListTransactions, handleGetTransaction, handleGetAddress
|
||||
// are implemented in blocks.go, transactions.go, and addresses.go respectively
|
||||
|
||||
// handleHealth handles GET /health
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("X-Explorer-Name", "SolaceScanScout")
|
||||
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()
|
||||
}
|
||||
|
||||
health := map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
"services": map[string]string{
|
||||
"database": dbStatus,
|
||||
"api": "ok",
|
||||
},
|
||||
"chain_id": s.chainID,
|
||||
"explorer": map[string]string{
|
||||
"name": "SolaceScanScout",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
statusCode := http.StatusOK
|
||||
if dbStatus != "ok" {
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
health["status"] = "degraded"
|
||||
}
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(health)
|
||||
}
|
||||
Reference in New Issue
Block a user