package main import ( "context" "fmt" "log" "log/slog" "net/http" "os" "os/signal" "strconv" "syscall" "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" "e-li.nps/internal/db" elinps "e-li.nps/internal/elinps" ) func main() { // Logger estruturado (stdout). Mantemos log.Printf apenas para logs muito // iniciais/críticos e para compatibilidade; o restante deve usar slog. logger := elinps.LoggerPadrao() slog.SetDefault(logger) // Carrega .env se existir (conveniência para dev local). // Variáveis definidas no SO têm precedência. _ = godotenv.Load() cfg := mustLoadConfig() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() pool, err := db.NewPool(ctx, cfg.DatabaseURL) if err != nil { log.Fatalf("db connect: %v", err) } defer pool.Close() // Garante extensões necessárias. if err := db.EnsurePgcrypto(ctx, pool); err != nil { log.Fatalf("ensure pgcrypto: %v", err) } r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Recoverer) r.Use(elinps.MiddlewareLogRequisicao(logger)) r.Use(middleware.Timeout(15 * time.Second)) r.Use(middleware.Compress(5)) // CORS liberado + preflight. r.Use(elinps.CORSMiddleware()) // Limites básicos. r.Use(elinps.MaxBodyBytesMiddleware(64 * 1024)) // Health. r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) // Home: renderiza README.md // Público (sem senha), para facilitar documentação do serviço. r.Get("/", elinps.NewReadmePage("README.md").ServeHTTP) // Widget estático. fileServer := http.FileServer(http.Dir("web/static")) // Versão do widget para controle de cache. // // Regra do projeto: a versão é gerada a cada inicialização do servidor. // Isso evita que o browser continue usando um gonps.js antigo após uma // atualização e o usuário final veja comportamento quebrado. versaoWidget := strconv.FormatInt(time.Now().UnixNano(), 10) r.Route("/static", func(r chi.Router) { r.Get("/e-li.nps.js", func(w http.ResponseWriter, r *http.Request) { // Estratégia: permitir cache local, mas obrigar revalidação. // Quando a versão mudar, o ETag muda e o browser baixa o JS novo. etag := fmt.Sprintf("\"%s\"", versaoWidget) w.Header().Set("ETag", etag) w.Header().Set("Cache-Control", "no-cache, must-revalidate") // Se o cliente já tem essa versão, evitamos enviar o arquivo novamente. if r.Header.Get("If-None-Match") == etag { w.WriteHeader(http.StatusNotModified) return } http.ServeFile(w, r, "web/static/e-li.nps.js") }) r.Handle("/*", http.StripPrefix("/static/", fileServer)) }) // Conveniência: permitir /teste.html r.Get("/teste.html", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "web/static/teste.html") }) // Rotas NPS. h := elinps.NewHandlers(pool) r.Route("/api/e-li.nps", func(r chi.Router) { r.Post("/pedido", h.PostPedido) r.Patch("/{produto}/{id}", h.PatchResposta) }) r.Route("/e-li.nps", func(r chi.Router) { r.Get("/{produto}/{id}/form", h.GetForm) }) // Painel. // Protegido por SENHA_PAINEL. // Se SENHA_PAINEL estiver vazia, o painel fica desabilitado. painel := elinps.NewPainelHandlers(pool, cfg.SenhaPainel) r.Mount("/painel", painel.Router()) srv := &http.Server{ Addr: cfg.Addr, Handler: r, ReadHeaderTimeout: 5 * time.Second, } go func() { logger.Info("servidor_iniciado", "addr", cfg.Addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen: %v", err) } }() <-ctx.Done() shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { logger.Error("erro_no_shutdown", "err", err) } logger.Info("servidor_finalizado") } type config struct { Addr string DatabaseURL string SenhaPainel string } func mustLoadConfig() config { cfg := config{ Addr: envOr("ADDR", ":8080"), DatabaseURL: os.Getenv("DATABASE_URL"), SenhaPainel: os.Getenv("SENHA_PAINEL"), } if cfg.DatabaseURL == "" { fmt.Fprintln(os.Stderr, "missing DATABASE_URL") os.Exit(2) } return cfg } func envOr(k, def string) string { v := os.Getenv(k) if v == "" { return def } return v }