gohorsejobs/seeder-api/main.go
2026-01-02 10:29:00 -03:00

283 lines
7.1 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"sync"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/joho/godotenv"
)
type server struct {
pool *pgxpool.Pool
mu sync.Mutex
seeding bool
}
type seedRequest struct {
Type string `json:"type"`
Password string `json:"password"`
}
type healthResponse struct {
Status string `json:"status"`
Database string `json:"database"`
Version string `json:"version"`
Error string `json:"error,omitempty"`
}
type seedResponse struct {
Message string `json:"message"`
Type string `json:"type,omitempty"`
}
func main() {
_ = godotenv.Load()
databaseURL, err := buildDatabaseURL()
if err != nil {
log.Fatalf("❌ Failed to build database URL: %v", err)
}
pool, err := pgxpool.New(context.Background(), databaseURL)
if err != nil {
log.Fatalf("❌ Failed to create database pool: %v", err)
}
defer pool.Close()
logDatabaseConfig(databaseURL)
srv := &server{pool: pool}
mux := http.NewServeMux()
mux.HandleFunc("/health", srv.healthHandler)
mux.HandleFunc("/seed", srv.seedHandler)
mux.HandleFunc("/reset", srv.resetHandler)
handler := withCORS(withLogging(mux))
port := getenv("PORT", "8080")
log.Printf("🌱 GoHorseJobs Seeder API listening on port %s", port)
log.Printf(" Health check: http://localhost:%s/health", port)
if err := http.ListenAndServe(":"+port, handler); err != nil {
log.Fatalf("❌ Server failed: %v", err)
}
}
func (s *server) healthHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, healthResponse{Status: "error", Database: "disconnected", Error: "Method not allowed"})
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
var result int
if err := s.pool.QueryRow(ctx, "SELECT 1").Scan(&result); err != nil {
log.Printf("Health check failed: %v", err)
writeJSON(w, http.StatusInternalServerError, healthResponse{Status: "error", Database: "disconnected", Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, healthResponse{Status: "ok", Database: "connected", Version: "1.0.0"})
}
func (s *server) seedHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "Method not allowed"})
return
}
var req seedRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err.Error() != "EOF" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON body"})
return
}
if !isAuthorized(req.Password) {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "Unauthorized"})
return
}
if !s.tryStartSeeding() {
writeJSON(w, http.StatusConflict, map[string]string{"error": "Seeding already in progress"})
return
}
seedType := strings.TrimSpace(req.Type)
if seedType == "" {
seedType = "full"
}
writeJSON(w, http.StatusOK, seedResponse{Message: "Seeding started", Type: seedType})
go func() {
defer s.finishSeeding()
log.Printf("🚀 Starting manual seed (%s)...", seedType)
if err := runSeed(seedType); err != nil {
log.Printf("❌ Manual seed failed: %v", err)
return
}
log.Printf("✅ Manual seed completed")
}()
}
func (s *server) resetHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "Method not allowed"})
return
}
var req seedRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && err.Error() != "EOF" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON body"})
return
}
if !isAuthorized(req.Password) {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "Unauthorized"})
return
}
if !s.tryStartSeeding() {
writeJSON(w, http.StatusConflict, map[string]string{"error": "Seeding/Reset in progress"})
return
}
writeJSON(w, http.StatusOK, seedResponse{Message: "Reset started"})
go func() {
defer s.finishSeeding()
log.Printf("🚀 Starting manual reset...")
if err := runReset(); err != nil {
log.Printf("❌ Manual reset failed: %v", err)
return
}
log.Printf("✅ Manual reset completed")
}()
}
func (s *server) tryStartSeeding() bool {
s.mu.Lock()
defer s.mu.Unlock()
if s.seeding {
return false
}
s.seeding = true
return true
}
func (s *server) finishSeeding() {
s.mu.Lock()
s.seeding = false
s.mu.Unlock()
}
func runSeed(seedType string) error {
args := []string{"src/index.js"}
switch seedType {
case "lite":
args = append(args, "--lite")
case "no-locations":
args = append(args, "--skip-locations")
case "full":
default:
log.Printf("⚠️ Unknown seed type '%s', using full", seedType)
}
return runNodeCommand(args...)
}
func runReset() error {
return runNodeCommand("src/index.js", "--reset")
}
func runNodeCommand(args ...string) error {
cmd := exec.Command("node", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
return cmd.Run()
}
func buildDatabaseURL() (string, error) {
if databaseURL := strings.TrimSpace(os.Getenv("DATABASE_URL")); databaseURL != "" {
cleaned := strings.ReplaceAll(databaseURL, "?sslmode=require", "")
cleaned = strings.ReplaceAll(cleaned, "&sslmode=require", "")
return cleaned, nil
}
host := getenv("DB_HOST", "localhost")
port := getenv("DB_PORT", "5432")
user := getenv("DB_USER", "postgres")
password := getenv("DB_PASSWORD", "postgres")
name := getenv("DB_NAME", "gohorsejobs")
sslmode := getenv("DB_SSLMODE", "disable")
if sslmode == "" {
sslmode = "disable"
}
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", url.QueryEscape(user), url.QueryEscape(password), host, port, name, sslmode), nil
}
func logDatabaseConfig(databaseURL string) {
masked := databaseURL
if at := strings.Index(masked, "@"); at != -1 {
masked = "postgres://***@" + masked[at+1:]
}
log.Printf("🔌 DB Config: connectionString=%s", masked)
}
func isAuthorized(password string) bool {
expected := strings.TrimSpace(os.Getenv("SEED_PASSWORD"))
if expected == "" {
return true
}
return password == expected
}
func withLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] %s %s", time.Now().Format(time.RFC3339), r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func withCORS(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, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func getenv(key, fallback string) string {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
return value
}