package elinps import ( "crypto/subtle" "fmt" "html/template" "net/http" "net/url" "strings" "time" "e-li.nps/internal/db" "github.com/jackc/pgx/v5" ) // Proteção simples do painel administrativo. // // Objetivo: bloquear acesso ao painel com uma senha definida no .env. // Implementação: cookie assinado de forma simples (token aleatório por boot). // // Observação: é propositalmente simples (sem banco) para manter o projeto leve. // Se precisar evoluir depois, podemos trocar por JWT ou sessão persistida. type AuthPainel struct { Senha string Token string } func (a AuthPainel) handlerLoginPost(w http.ResponseWriter, r *http.Request) { if !a.habilitado() { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("painel desabilitado")) return } if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusBadRequest) return } senha := r.FormValue("senha") if subtle.ConstantTimeCompare([]byte(senha), []byte(a.Senha)) != 1 { w.WriteHeader(http.StatusUnauthorized) w.Write([]byte("senha invalida")) return } http.SetCookie(w, &http.Cookie{ Name: a.cookieName(), Value: a.Token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, // Secure deve ser true em produção com HTTPS. Secure: false, // Expira em 24h (relogin simples). Expires: time.Now().Add(24 * time.Hour), }) http.Redirect(w, r, "/painel", http.StatusFound) } // NPSMensal representa o cálculo do NPS agregado por mês. type NPSMensal struct { Mes string Detratores int Neutros int Promotores int Total int NPS int } // RespostaPainel representa uma resposta para listagem no painel. type RespostaPainel struct { ID string RespondidoEm *time.Time PedidoCriadoEm time.Time UsuarioCodigo *string UsuarioNome string UsuarioEmail *string Nota *int Justificativa *string } type PainelDados struct { Produto string Produtos []string Meses []NPSMensal Respostas []RespostaPainel Pagina int SomenteBaixas bool MsgErro string } func (a AuthPainel) handlerPainel(w http.ResponseWriter, r *http.Request, store *Store) { ctx := r.Context() // Query params produto := r.URL.Query().Get("produto") pagina := 1 if p := r.URL.Query().Get("pagina"); p != "" { // best-effort parse _, _ = fmt.Sscanf(p, "%d", &pagina) if pagina <= 0 { pagina = 1 } } somenteBaixas := r.URL.Query().Get("baixas") == "1" produtos, err := store.ListarProdutos(ctx) if err != nil { a.renderPainelHTML(w, PainelDados{MsgErro: "erro ao listar produtos"}) return } if produto == "" && len(produtos) > 0 { produto = produtos[0] } dados := PainelDados{Produto: produto, Produtos: produtos, Pagina: pagina, SomenteBaixas: somenteBaixas} if produto == "" { a.renderPainelHTML(w, dados) return } // tabela segura prodNorm, err := db.NormalizeProduto(produto) if err != nil { dados.MsgErro = "produto inválido" a.renderPainelHTML(w, dados) return } tabela := db.TableNameForProduto(prodNorm) if err := db.EnsureNPSTable(ctx, store.poolRef(), tabela); err != nil { dados.MsgErro = "erro ao garantir tabela" a.renderPainelHTML(w, dados) return } meses, err := store.NPSMesAMes(ctx, tabela, 12) if err != nil { dados.MsgErro = "erro ao calcular NPS" a.renderPainelHTML(w, dados) return } dados.Meses = meses respostas, err := store.ListarRespostas(ctx, tabela, ListarRespostasFiltro{SomenteNotasBaixas: somenteBaixas, Pagina: pagina, PorPagina: 50}) if err != nil { // Se a tabela ainda não tem coluna ip_real/ etc, EnsureNPSTable deveria ajustar. if err == pgx.ErrNoRows { respostas = []RespostaPainel{} } else { dados.MsgErro = "erro ao listar respostas" } } dados.Respostas = respostas a.renderPainelHTML(w, dados) } func (a AuthPainel) renderPainelHTML(w http.ResponseWriter, d PainelDados) { w.Header().Set("Content-Type", "text/html; charset=utf-8") // HTML propositalmente simples (sem template engine) para manter isolado. // Se quiser evoluir, dá pra migrar para templates. var b strings.Builder b.WriteString("
") b.WriteString("" + template.HTMLEscapeString(d.MsgErro) + "
") } b.WriteString("| Mês | Detratores | Neutros | Promotores | Total | NPS |
|---|---|---|---|---|---|
| %s | %d | %d | %d | %d | %d |
| Data | Nota | Usuário | Comentário |
|---|---|---|---|
| " + template.HTMLEscapeString(data) + " | " + template.HTMLEscapeString(nota) + " | " + usuario + " | " + coment + " |
Acesso protegido por senha (SENHA_PAINEL).