166 lines
4.3 KiB
Go
166 lines
4.3 KiB
Go
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
|
|
}
|