refatoração de segurança e logs

This commit is contained in:
Luiz Silva 2026-01-01 19:13:24 -03:00
parent 6873b87a85
commit 663a8d5bf2
12 changed files with 362 additions and 37 deletions

View file

@ -3,6 +3,7 @@ package elinps
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"strings"
@ -38,21 +39,22 @@ func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
return
}
// Ensure per-product table exists (also normalizes produto).
// Garante a tabela do produto (e normaliza o identificador técnico).
table, err := h.store.EnsureTableForProduto(ctx, in.ProdutoNome)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "produto_invalido"})
return
}
// Keep normalized form for the widget to build URLs safely.
// Mantemos a forma normalizada para o widget montar URLs com segurança.
// table = "nps_" + produto_normalizado
produtoNormalizado := strings.TrimPrefix(table, "nps_")
// Rules
// Regras.
respRecente, err := h.store.HasRespostaValidaRecente(ctx, table, in.InquilinoCodigo, in.UsuarioCodigo)
if err != nil {
// Fail-closed
slog.Error("erro ao checar resposta recente", "err", err)
// Fail-closed.
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
return
}
@ -63,6 +65,7 @@ func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
pedidoAberto, err := h.store.HasPedidoEmAbertoRecente(ctx, table, in.InquilinoCodigo, in.UsuarioCodigo)
if err != nil {
slog.Error("erro ao checar pedido em aberto", "err", err)
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
return
}
@ -73,6 +76,7 @@ func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
id, err := h.store.CreatePedido(ctx, table, in, r)
if err != nil {
slog.Error("erro ao criar pedido", "err", err)
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
return
}
@ -85,7 +89,7 @@ func (h *Handlers) PatchResposta(w http.ResponseWriter, r *http.Request) {
produtoParam := chi.URLParam(r, "produto")
id := chi.URLParam(r, "id")
// produtoParam already in path; sanitize again.
// produtoParam já está no path; sanitizamos novamente por segurança.
prod, err := db.NormalizeProduto(produtoParam)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "produto_invalido"})
@ -93,6 +97,7 @@ func (h *Handlers) PatchResposta(w http.ResponseWriter, r *http.Request) {
}
table := db.TableNameForProduto(prod)
if err := db.EnsureNPSTable(ctx, h.store.pool, table); err != nil {
slog.Error("erro ao garantir tabela", "err", err)
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "db"})
return
}
@ -113,14 +118,16 @@ func (h *Handlers) PatchResposta(w http.ResponseWriter, r *http.Request) {
}
if err := h.store.PatchRegistro(ctx, table, id, in); err != nil {
slog.Error("erro ao atualizar registro", "err", err)
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "db"})
return
}
// If called via HTMX, respond with refreshed HTML fragment.
// Se chamado via HTMX, respondemos com fragmento HTML atualizado.
if r.Header.Get("HX-Request") == "true" {
reg, err := h.store.GetRegistro(ctx, table, id)
if err != nil {
slog.Error("erro ao buscar registro", "err", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("db"))
return
@ -147,6 +154,7 @@ func (h *Handlers) GetForm(w http.ResponseWriter, r *http.Request) {
}
table := db.TableNameForProduto(prod)
if err := db.EnsureNPSTable(ctx, h.store.pool, table); err != nil {
slog.Error("erro ao garantir tabela", "err", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("db"))
return
@ -159,6 +167,7 @@ func (h *Handlers) GetForm(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("nao encontrado"))
return
}
slog.Error("erro ao buscar registro", "err", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("db"))
return
@ -170,8 +179,8 @@ func (h *Handlers) GetForm(w http.ResponseWriter, r *http.Request) {
Reg: reg,
}
// Always return a standalone HTML page so the widget can use iframe.
// But the inner container is also HTMX-friendly (it swaps itself).
// Sempre retornamos uma página HTML completa para o widget usar iframe.
// Porém o container interno também é compatível com HTMX (swap de si mesmo).
w.Header().Set("Content-Type", "text/html; charset=utf-8")
h.tpl.Render(w, "form_page.html", data)
}