primeira versão do e-li-nps construido com IA
This commit is contained in:
commit
06950d6e2c
34 changed files with 2524 additions and 0 deletions
159
cmd/server/main.go
Normal file
159
cmd/server/main.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"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() {
|
||||
// Load .env if present (convenience for local dev). Environment variables
|
||||
// explicitly set in the OS take precedence.
|
||||
_ = 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()
|
||||
|
||||
// Ensures required extensions exist.
|
||||
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(middleware.Timeout(15 * time.Second))
|
||||
r.Use(middleware.Compress(5))
|
||||
|
||||
// CORS wildcard + preflight
|
||||
r.Use(elinps.CORSMiddleware())
|
||||
|
||||
// Basic limits
|
||||
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)
|
||||
|
||||
// Static widget
|
||||
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))
|
||||
})
|
||||
// Convenience: allow /teste.html
|
||||
r.Get("/teste.html", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "web/static/teste.html")
|
||||
})
|
||||
|
||||
// NPS routes
|
||||
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 (dashboard)
|
||||
// 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() {
|
||||
log.Printf("listening on %s", 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 {
|
||||
log.Printf("shutdown: %v", err)
|
||||
}
|
||||
log.Printf("bye")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue