migrado de js para go
This commit is contained in:
parent
663a8d5bf2
commit
6f78511946
7 changed files with 964 additions and 83 deletions
|
|
@ -92,6 +92,38 @@ func main() {
|
|||
|
||||
http.ServeFile(w, r, "web/static/e-li.nps.js")
|
||||
})
|
||||
|
||||
// WASM do widget.
|
||||
// Regra: cache controlado por ETag e revalidação obrigatória.
|
||||
// Importante: mantemos o MESMO ETag da inicialização (versaoWidget)
|
||||
// para JS e WASM, garantindo que ambos "andem juntos".
|
||||
r.Get("/e-li.nps.wasm", func(w http.ResponseWriter, r *http.Request) {
|
||||
etag := fmt.Sprintf("\"%s\"", versaoWidget)
|
||||
w.Header().Set("ETag", etag)
|
||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||
w.Header().Set("Content-Type", "application/wasm")
|
||||
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, "web/static/e-li.nps.wasm")
|
||||
})
|
||||
|
||||
// Runtime JS do Go para WASM.
|
||||
// Também fica sob ETag + revalidação.
|
||||
r.Get("/wasm_exec.js", func(w http.ResponseWriter, r *http.Request) {
|
||||
etag := fmt.Sprintf("\"%s\"", versaoWidget)
|
||||
w.Header().Set("ETag", etag)
|
||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, "web/static/wasm_exec.js")
|
||||
})
|
||||
r.Handle("/*", http.StripPrefix("/static/", fileServer))
|
||||
})
|
||||
// Conveniência: permitir /teste.html
|
||||
|
|
|
|||
249
cmd/widgetwasm/main.go
Normal file
249
cmd/widgetwasm/main.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
//go:build js && wasm
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
// IMPORTANTE (.agent): o backend Go continua sendo a autoridade das regras.
|
||||
// Este WASM existe para concentrar regras de negócio do widget (cliente) em Go,
|
||||
// mantendo o arquivo JS pequeno e fácil de auditar.
|
||||
|
||||
var dataISORe = regexp.MustCompile(`^([0-9]{4})-([0-9]{2})-([0-9]{2})$`)
|
||||
|
||||
type cfgWidget struct {
|
||||
ProdutoNome string
|
||||
InquilinoCodigo string
|
||||
InquilinoNome string
|
||||
UsuarioCodigo string
|
||||
UsuarioNome string
|
||||
UsuarioTelefone string
|
||||
UsuarioEmail string
|
||||
CooldownHours float64
|
||||
DataMinimaAbertura string
|
||||
}
|
||||
|
||||
func main() {
|
||||
js.Global().Set("__eli_nps_wasm_preflight", js.FuncOf(preflight))
|
||||
js.Global().Set("__eli_nps_wasm_decidir", js.FuncOf(decidir))
|
||||
js.Global().Set("__eli_nps_wasm_cooldown_ativo", js.FuncOf(cooldownAtivo))
|
||||
js.Global().Set("__eli_nps_wasm_set_cooldown", js.FuncOf(setCooldown))
|
||||
js.Global().Set("__eli_nps_wasm_ready", true)
|
||||
|
||||
// Mantém o módulo vivo.
|
||||
select {}
|
||||
}
|
||||
|
||||
func cooldownAtivo(this js.Value, args []js.Value) any {
|
||||
if len(args) < 1 {
|
||||
return false
|
||||
}
|
||||
chave := strings.TrimSpace(args[0].String())
|
||||
if chave == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Best-effort: se der erro, tratamos como não ativo.
|
||||
storage := js.Global().Get("localStorage")
|
||||
if storage.IsUndefined() || storage.IsNull() {
|
||||
return false
|
||||
}
|
||||
|
||||
v := storage.Call("getItem", chave)
|
||||
if v.IsNull() || v.IsUndefined() {
|
||||
return false
|
||||
}
|
||||
|
||||
parsed := js.Global().Get("JSON").Call("parse", v)
|
||||
until := parsed.Get("until")
|
||||
if until.IsUndefined() || until.IsNull() {
|
||||
return false
|
||||
}
|
||||
|
||||
agora := js.Global().Get("Date").Call("now").Int()
|
||||
return int64(agora) < int64(until.Int())
|
||||
}
|
||||
|
||||
func setCooldown(this js.Value, args []js.Value) any {
|
||||
if len(args) < 2 {
|
||||
return nil
|
||||
}
|
||||
chave := strings.TrimSpace(args[0].String())
|
||||
if chave == "" {
|
||||
return nil
|
||||
}
|
||||
ateMs := int64(args[1].Int())
|
||||
if ateMs <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
storage := js.Global().Get("localStorage")
|
||||
if storage.IsUndefined() || storage.IsNull() {
|
||||
return nil
|
||||
}
|
||||
|
||||
obj := js.Global().Get("Object").New()
|
||||
obj.Set("until", ateMs)
|
||||
json := js.Global().Get("JSON").Call("stringify", obj)
|
||||
storage.Call("setItem", chave, json)
|
||||
return nil
|
||||
}
|
||||
|
||||
func preflight(this js.Value, args []js.Value) any {
|
||||
if len(args) < 1 {
|
||||
return map[string]any{"ok": false, "motivo": "sem_cfg"}
|
||||
}
|
||||
cfg := lerCfg(args[0])
|
||||
|
||||
// Bloqueio por data mínima.
|
||||
if antesDaDataMinima(cfg.DataMinimaAbertura) {
|
||||
return map[string]any{"ok": false, "motivo": "antes_da_data_minima"}
|
||||
}
|
||||
|
||||
// controle de exibição: produto + inquilino_codigo + usuario_codigo
|
||||
if cfg.ProdutoNome == "" || cfg.InquilinoCodigo == "" || cfg.UsuarioCodigo == "" {
|
||||
return map[string]any{"ok": false, "motivo": "contexto_incompleto"}
|
||||
}
|
||||
|
||||
chaveCooldown := chaveCooldown(cfg.ProdutoNome, cfg.InquilinoCodigo, cfg.UsuarioCodigo)
|
||||
untilMs := calcularCooldownAteMs(cfg.CooldownHours)
|
||||
|
||||
// Enviamos exatamente o produto_nome informado.
|
||||
payload := map[string]any{
|
||||
"produto_nome": cfg.ProdutoNome,
|
||||
"inquilino_codigo": cfg.InquilinoCodigo,
|
||||
"inquilino_nome": cfg.InquilinoNome,
|
||||
"usuario_codigo": cfg.UsuarioCodigo,
|
||||
"usuario_nome": cfg.UsuarioNome,
|
||||
"usuario_telefone": cfg.UsuarioTelefone,
|
||||
"usuario_email": normalizarEmail(cfg.UsuarioEmail),
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"ok": true,
|
||||
"chave_cooldown": chaveCooldown,
|
||||
"cooldown_ate_ms": untilMs,
|
||||
"payload": payload,
|
||||
}
|
||||
}
|
||||
|
||||
func decidir(this js.Value, args []js.Value) any {
|
||||
// Entrada:
|
||||
// - args[0] = cfg (para cooldownHours)
|
||||
// - args[1] = resposta JSON do servidor (obj)
|
||||
if len(args) < 2 {
|
||||
return map[string]any{"abrir": false, "motivo": "sem_dados"}
|
||||
}
|
||||
cfg := lerCfg(args[0])
|
||||
data := args[1]
|
||||
|
||||
untilMs := calcularCooldownAteMs(cfg.CooldownHours)
|
||||
|
||||
// Fail-closed.
|
||||
if data.IsUndefined() || data.IsNull() {
|
||||
return map[string]any{"abrir": false, "motivo": "resposta_invalida", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs}
|
||||
}
|
||||
|
||||
// Esperado: {pode_abrir:bool, id:string, produto:string}
|
||||
podeAbrir := data.Get("pode_abrir")
|
||||
if !podeAbrir.Truthy() {
|
||||
return map[string]any{"abrir": false, "motivo": "nao_pode_abrir", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs}
|
||||
}
|
||||
id := strings.TrimSpace(data.Get("id").String())
|
||||
produtoRota := strings.TrimSpace(data.Get("produto").String())
|
||||
if id == "" || produtoRota == "" {
|
||||
// Não dá para montar URL segura.
|
||||
return map[string]any{"abrir": false, "motivo": "sem_produto_ou_id", "aplicar_cooldown": true, "cooldown_ate_ms": untilMs}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"abrir": true,
|
||||
"id": id,
|
||||
"produto_rota": produtoRota,
|
||||
"aplicar_cooldown": true,
|
||||
"cooldown_ate_ms": untilMs,
|
||||
}
|
||||
}
|
||||
|
||||
func lerCfg(v js.Value) cfgWidget {
|
||||
// Leitura defensiva: tudo é best-effort.
|
||||
getStr := func(k string) string {
|
||||
if v.Type() != js.TypeObject {
|
||||
return ""
|
||||
}
|
||||
vv := v.Get(k)
|
||||
if vv.IsUndefined() || vv.IsNull() {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(vv.String())
|
||||
}
|
||||
getNum := func(k string) float64 {
|
||||
if v.Type() != js.TypeObject {
|
||||
return 0
|
||||
}
|
||||
vv := v.Get(k)
|
||||
if vv.IsUndefined() || vv.IsNull() {
|
||||
return 0
|
||||
}
|
||||
return vv.Float()
|
||||
}
|
||||
|
||||
return cfgWidget{
|
||||
ProdutoNome: getStr("produto_nome"),
|
||||
InquilinoCodigo: getStr("inquilino_codigo"),
|
||||
InquilinoNome: getStr("inquilino_nome"),
|
||||
UsuarioCodigo: getStr("usuario_codigo"),
|
||||
UsuarioNome: getStr("usuario_nome"),
|
||||
UsuarioTelefone: getStr("usuario_telefone"),
|
||||
UsuarioEmail: getStr("usuario_email"),
|
||||
CooldownHours: getNum("cooldownHours"),
|
||||
DataMinimaAbertura: getStr("data_minima_abertura"),
|
||||
}
|
||||
}
|
||||
|
||||
func normalizarEmail(email string) string {
|
||||
return strings.ToLower(strings.TrimSpace(email))
|
||||
}
|
||||
|
||||
func chaveCooldown(produto, inquilino, usuarioCodigo string) string {
|
||||
// Prefixo de storage atualizado para o novo nome do projeto.
|
||||
return "eli-nps:cooldown:" + produto + ":" + inquilino + ":" + usuarioCodigo
|
||||
}
|
||||
|
||||
func calcularCooldownAteMs(hours float64) int64 {
|
||||
if hours <= 0 {
|
||||
hours = 24
|
||||
}
|
||||
agora := js.Global().Get("Date").Call("now").Int()
|
||||
return int64(agora) + int64(hours*3600*1000)
|
||||
}
|
||||
|
||||
func antesDaDataMinima(s string) bool {
|
||||
// Aceita somente ISO (data) YYYY-MM-DD.
|
||||
v := strings.TrimSpace(s)
|
||||
if v == "" {
|
||||
return false
|
||||
}
|
||||
m := dataISORe.FindStringSubmatch(v)
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
// new Date(ano, mes-1, dia, 0, 0, 0, 0)
|
||||
ano := m[1]
|
||||
mes := m[2]
|
||||
dia := m[3]
|
||||
|
||||
// Converte para números via JS (simplifica validação e compatibilidade de timezone).
|
||||
nAno := js.Global().Get("Number").Invoke(ano).Int()
|
||||
nMes := js.Global().Get("Number").Invoke(mes).Int()
|
||||
nDia := js.Global().Get("Number").Invoke(dia).Int()
|
||||
if nAno <= 0 || nMes < 1 || nMes > 12 || nDia < 1 || nDia > 31 {
|
||||
return false
|
||||
}
|
||||
dataMin := js.Global().Get("Date").New(nAno, nMes-1, nDia, 0, 0, 0, 0)
|
||||
agora := js.Global().Get("Date").New()
|
||||
return agora.Call("getTime").Int() < dataMin.Call("getTime").Int()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue