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) }