package api import ( "context" "encoding/json" "fmt" "net/http" "regexp" "strconv" "strings" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/kjannette/trahn-backend/internal/repository" ) const maxQueryLimit = 1000 var dateRegexp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) type Server struct { pool *pgxpool.Pool priceRepo *repository.PriceRepo tradeRepo *repository.TradeRepo srRepo *repository.SRRepo gridRepo *repository.GridStateRepo httpServer *http.Server apiKey string } func NewServer(pool *pgxpool.Pool, port int, apiKey, corsOrigin string) *Server { s := &Server{ pool: pool, priceRepo: repository.NewPriceRepo(pool), tradeRepo: repository.NewTradeRepo(pool), srRepo: repository.NewSRRepo(pool), gridRepo: repository.NewGridStateRepo(pool), apiKey: apiKey, } mux := http.NewServeMux() // Price routes mux.HandleFunc("GET /v1/prices/today", s.handlePricesToday) mux.HandleFunc("GET /v1/prices/day/{date}", s.handlePricesByDay) mux.HandleFunc("GET /v1/prices/days", s.handleAvailableDays) mux.HandleFunc("GET /v1/prices/latest", s.handleLatestPrice) // Trade routes mux.HandleFunc("GET /v1/trades/today", s.handleTradesToday) mux.HandleFunc("GET /v1/trades/day/{date}", s.handleTradesByDay) mux.HandleFunc("GET /v1/trades/all", s.handleAllTrades) mux.HandleFunc("GET /v1/trades/stats", s.handleTradeStats) // Grid routes mux.HandleFunc("GET /v1/grid/current", s.handleGridCurrent) // S/R routes mux.HandleFunc("GET /v1/support-resistance/latest", s.handleSRLatest) mux.HandleFunc("GET /v1/support-resistance/history", s.handleSRHistory) // Health check (no auth required) mux.HandleFunc("GET /health", s.handleHealth) handler := s.authMiddleware(corsMiddleware(mux, corsOrigin)) s.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: handler, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } return s } func (s *Server) Start() error { fmt.Printf("[API] REST API server started on http://localhost%s\n", s.httpServer.Addr) fmt.Printf("[API] Health check: http://localhost%s/health\n", s.httpServer.Addr) if s.apiKey != "" { fmt.Println("[API] Authentication: enabled (Bearer token)") } else { fmt.Println("[API] Authentication: disabled (no API_KEY configured)") } return s.httpServer.ListenAndServe() } func (s *Server) Shutdown(ctx context.Context) error { return s.httpServer.Shutdown(ctx) } // --- middleware --- func (s *Server) authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s.apiKey == "" || r.URL.Path == "/health" { next.ServeHTTP(w, r) return } auth := r.Header.Get("Authorization") if auth == "" { writeError(w, http.StatusUnauthorized, "missing Authorization header") return } token := strings.TrimPrefix(auth, "Bearer ") if token == auth || token != s.apiKey { writeError(w, http.StatusUnauthorized, "invalid API key") return } next.ServeHTTP(w, r) }) } func corsMiddleware(next http.Handler, allowOrigin string) http.Handler { if allowOrigin == "" { allowOrigin = "*" } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", allowOrigin) w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } // --- validation helpers --- func validateDate(date string) bool { if !dateRegexp.MatchString(date) { return false } _, err := time.Parse("2006-01-02", date) return err == nil } func parseLimit(r *http.Request, defaultLimit int) int { v := r.URL.Query().Get("limit") if v == "" { return defaultLimit } n, err := strconv.Atoi(v) if err != nil || n <= 0 { return defaultLimit } if n > maxQueryLimit { return maxQueryLimit } return n } // --- response helpers --- func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) }