package server import ( "encoding/json" "fmt" "net/http" "git.rouggy.com/rouggy/stockradar/internal/db" "git.rouggy.com/rouggy/stockradar/internal/edgar" "git.rouggy.com/rouggy/stockradar/internal/etoro" "git.rouggy.com/rouggy/stockradar/internal/finnhub" "git.rouggy.com/rouggy/stockradar/internal/scanner" "git.rouggy.com/rouggy/stockradar/internal/settings" "github.com/gorilla/mux" ) type Server struct { db *db.DB port string router *mux.Router settings *settings.Settings poller *finnhub.Poller scanner *scanner.Scanner discovery *scanner.DiscoveryScanner edgarPoller *edgar.Poller etoroPoller *etoro.Poller } func New(database *db.DB, port string) (*Server, error) { svc, err := settings.New(database) if err != nil { return nil, err } s := &Server{ db: database, port: port, router: mux.NewRouter(), settings: svc, } s.poller = finnhub.NewPoller(database, func() (string, error) { return svc.Get("finnhub_api_key") }) s.poller.Start() s.scanner = scanner.New(database) s.scanner.Start() s.edgarPoller = edgar.NewPoller(database) s.edgarPoller.Start() s.scanner.SetEdgar(s.edgarPoller) s.scanner.SetEarnings(s.poller) s.discovery = scanner.NewDiscovery(database) s.etoroPoller = etoro.NewPoller(database, func() (string, string, error) { apiKey, err := svc.Get("etoro_api_key") if err != nil { return "", "", err } userKey, err := svc.Get("etoro_user_key") if err != nil { return "", "", err } return apiKey, userKey, nil }) s.etoroPoller.Start() s.setupRoutes() return s, nil } func (s *Server) setupRoutes() { s.router.Use(corsMiddleware) api := s.router.PathPrefix("/api").Subrouter() api.HandleFunc("/health", s.handleHealth).Methods("GET", "OPTIONS") // Settings api.HandleFunc("/settings", s.handleGetSettings).Methods("GET", "OPTIONS") api.HandleFunc("/settings", s.handleSaveSettings).Methods("POST", "OPTIONS") api.HandleFunc("/settings/test/{provider}", s.handleTestKey).Methods("GET", "OPTIONS") // Watchlist api.HandleFunc("/watchlist", s.handleGetWatchlist).Methods("GET", "OPTIONS") api.HandleFunc("/watchlist", s.handleAddWatchlist).Methods("POST", "OPTIONS") api.HandleFunc("/watchlist/{ticker}", s.handleRemoveWatchlist).Methods("DELETE", "OPTIONS") // News api.HandleFunc("/news", s.handleGetNews).Methods("GET", "OPTIONS") api.HandleFunc("/news/sync", s.handleNewsSync).Methods("POST", "OPTIONS") // Scanner / Signals api.HandleFunc("/signals", s.handleGetSignals).Methods("GET", "OPTIONS") api.HandleFunc("/signals/scan", s.handleTriggerScan).Methods("POST", "OPTIONS") api.HandleFunc("/prices", s.handleGetPrices).Methods("GET", "OPTIONS") // Insider trades (SEC EDGAR) api.HandleFunc("/insider-trades", s.handleGetInsiderTrades).Methods("GET", "OPTIONS") api.HandleFunc("/insider-trades/sync", s.handleSyncInsider).Methods("POST", "OPTIONS") // eToro universe api.HandleFunc("/etoro/sync", s.handleSyncEtoro).Methods("POST", "OPTIONS") api.HandleFunc("/etoro/status", s.handleEtoroStatus).Methods("GET", "OPTIONS") // Discovery api.HandleFunc("/discover", s.handleGetDiscovery).Methods("GET", "OPTIONS") api.HandleFunc("/discover/run", s.handleRunDiscovery).Methods("POST", "OPTIONS") api.HandleFunc("/discover/status", s.handleDiscoveryStatus).Methods("GET", "OPTIONS") api.HandleFunc("/discover/analyze", s.handleAnalyzeDeep).Methods("POST", "OPTIONS") api.HandleFunc("/discover/analyze/status", s.handleAnalyzeStatus).Methods("GET", "OPTIONS") s.router.PathPrefix("/").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "StockRadar API running") }) } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"ok","port":"%s"}`, s.port) } func (s *Server) handleGetSettings(w http.ResponseWriter, r *http.Request) { all, err := s.settings.GetAll() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(all) } func (s *Server) handleSaveSettings(w http.ResponseWriter, r *http.Request) { var body map[string]interface{} if err := json.NewDecoder(r.Body).Decode(&body); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Clés API → chiffrées, reste → plain text encryptedKeys := map[string]bool{ "etoro_api_key": true, "etoro_user_key": true, "finnhub_api_key": true, "alphavantage_key": true, } for key, val := range body { value, ok := val.(string) if !ok { continue } encrypted := encryptedKeys[key] if err := s.settings.Set(key, value, encrypted); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"saved"}`) } func (s *Server) handleTestKey(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) provider := vars["provider"] keyMap := map[string]string{ "etoro": "etoro_api_key", "finnhub": "finnhub_api_key", "alphavantage": "alphavantage_key", } keyName, ok := keyMap[provider] if !ok { http.Error(w, "unknown provider", http.StatusBadRequest) return } if !s.settings.HasKey(keyName) { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"error","message":"API key not configured"}`) return } if provider == "finnhub" { apiKey, err := s.settings.Get(keyName) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if err := finnhub.New(apiKey).Ping(); err != nil { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"error","message":%q}`, err.Error()) return } } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"status":"ok","provider":"%s"}`, provider) } func (s *Server) Start() error { return http.ListenAndServe(":"+s.port, s.router) }