package main import ( "context" "database/sql" "encoding/json" "errors" "fmt" "net/http" "os" "os/signal" "syscall" "time" "forgejo.digital-droplets.de/philschlo/proxui/platform/config" "forgejo.digital-droplets.de/philschlo/proxui/platform/logging" _ "github.com/jackc/pgx/v5/stdlib" "proxui/backend/internal/auth" "proxui/backend/internal/profile" ) func main() { cfg, err := config.Load() if err != nil { logger := logging.New("backend", "unknown", 0) logger.Error("failed to load config", "error", err) os.Exit(1) } logger := logging.New("backend", cfg.AppEnv, cfg.LogLevel) jwtValidator, err := auth.NewValidator( cfg.SupabaseIssuer, cfg.SupabaseJWKSURL, auth.WithHMACSecret(cfg.SupabaseJWTSecret), ) if err != nil { logger.Error("failed to initialize jwt validator", "error", err) os.Exit(1) } authMiddleware := auth.NewMiddleware(jwtValidator) db, err := openDatabase(cfg.DatabaseURL) if err != nil { logger.Error("failed to connect database", "error", err) os.Exit(1) } defer db.Close() profileMiddleware := profile.NewMiddleware(profile.NewRepository(db), logger) mux := http.NewServeMux() mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"ok","service":"backend"}`)) }) meHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { principal, _ := auth.PrincipalFromRequest(r) w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(principal) }) mux.Handle("GET /me", authMiddleware.RequireAuth(profileMiddleware.EnsureProfile(meHandler))) server := &http.Server{ Addr: cfg.BackendAddr, Handler: withCORS(cfg.AppSiteURL, mux), ReadHeaderTimeout: 5 * time.Second, } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go func() { logger.Info("backend listening", "addr", cfg.BackendAddr) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error("backend failed", "error", err) os.Exit(1) } }() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { logger.Error("backend shutdown failed", "error", err) os.Exit(1) } } func openDatabase(databaseURL string) (*sql.DB, error) { if databaseURL == "" { return nil, fmt.Errorf("DATABASE_URL is required") } db, err := sql.Open("pgx", databaseURL) if err != nil { return nil, err } if err := db.Ping(); err != nil { _ = db.Close() return nil, err } return db, nil } func withCORS(allowedOrigin string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" && origin == allowedOrigin { w.Header().Set("Access-Control-Allow-Origin", allowedOrigin) w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") w.Header().Set("Vary", "Origin") } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) }