diff --git a/deploy/nginx.conf.template b/deploy/nginx.conf.template index 9a0f5b9..a911dd2 100644 --- a/deploy/nginx.conf.template +++ b/deploy/nginx.conf.template @@ -48,6 +48,11 @@ server { } # Always revalidate HTML/JS/CSS after a deployment. Without this, a browser # can pair a new Kanban page with a cached older script after a rollout. + # The Site's product pages and cart are addresses of the same page (web/site-pages.js). + location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho)/?$ { + expires -1; + try_files /index.html =404; + } location / { expires -1; try_files $uri $uri/ =404; diff --git a/infra/nginx.conf.template b/infra/nginx.conf.template index 25b609c..17e520e 100644 --- a/infra/nginx.conf.template +++ b/infra/nginx.conf.template @@ -19,6 +19,10 @@ server { proxy_set_header X-Forwarded-For $remote_addr; client_max_body_size 2m; } + # The Site's product pages and cart are addresses of the same page (web/site-pages.js). + location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho)/?$ { + try_files /index.html =404; + } location / { try_files $uri $uri/ =404; } } server { diff --git a/tests/browser_test.mjs b/tests/browser_test.mjs index 5f49600..1024ba6 100644 --- a/tests/browser_test.mjs +++ b/tests/browser_test.mjs @@ -63,6 +63,10 @@ try{ await site.call('Page.reload'); await waitFor(()=>site.eval('typeof window.dtfCheckout === "function"'),'reload after security probe'); await site.click('[data-modo="file"]'); + // Each product has its own page; the home's parts are not on it. + assert.deepEqual(await site.eval(`({path:location.pathname,page:document.documentElement.dataset.rota, + cards:getComputedStyle($('cards')).display,cart:getComputedStyle($('carr')).display})`), + {path:'/arquivo-por-metro',page:'produto',cards:'none',cart:'none'}); await site.click('[data-cam="tabela"]'); const root=await site.call('DOM.getDocument'); const input=await site.call('DOM.querySelector',{nodeId:root.root.nodeId,selector:'#inp'}); @@ -77,6 +81,10 @@ try{ await pause(800); await site.call('Page.reload'); await waitFor(()=>site.eval('typeof pedido!=="undefined" && pedido.length===1'),'persistent cart recovery'); + // Reloading a product page reopens that product; the cart is its own page. + assert.equal(await site.eval('modo'),'file'); + await site.click('#cartLink'); + await waitFor(()=>site.eval(`location.pathname==='/carrinho' && getComputedStyle($('carr')).display!=='none' && getComputedStyle($('foco')).display==='none'`),'cart page'); assert.equal(await site.eval('pedido[0].localFiles[0].name'),'local-test.cdr'); assert.equal(await site.eval('pedido[0].total'),21.89); assert.equal(await site.eval('document.getElementById("bPagar").disabled'),false); diff --git a/tests/security_test.py b/tests/security_test.py index 7492c5e..b8a85a4 100644 --- a/tests/security_test.py +++ b/tests/security_test.py @@ -29,6 +29,10 @@ def run(): assert script_src == "script-src 'self'", script_src assert "object-src 'none'" in policy assert 'cdnjs' not in policy, 'pdf.js is vendored; no CDN belongs in the policy' + # Product pages and the cart are addresses of the Site's page, under the same policy. + for page in ('/arquivo-por-metro','/artes-avulsas','/uv-arquivo-por-metro','/uv-artes-avulsas','/carrinho'): + assert raw(page,200)['Content-Security-Policy']==policy,page + raw('/carrinho/outra-coisa',404) raw('/api/health',400,{'Host':'attacker.invalid'}) raw('/api/account/logout',403,{'Content-Type':'application/json','Origin':'https://attacker.invalid'},b'{}') raw('/api/account/logout',403,{'Content-Type':'application/json','Origin':'http://localhost:9999'},b'{}') diff --git a/web/checkout.js b/web/checkout.js index 622d5b6..a0436f4 100644 --- a/web/checkout.js +++ b/web/checkout.js @@ -167,7 +167,7 @@ } else if (quote.status==='paid') { const STAGES={rec:'arte recebida',tra:'arte tratada',fil:'na fila de impressão',imp:'imprimindo',cor:'aguardando correção',fin:'finalizado'}; message('Pedido #'+quote.order.number+' · '+(STAGES[quote.order.state]||quote.order.state)+'.'); - button('Novo pedido',()=>{clearDraft();location.reload();}); + button('Novo pedido',()=>{clearDraft();location.assign('/');}); } else { message('Cotação expirada. Envie o carrinho para uma nova revisão.'); button('Nova cotação',clearDraft); diff --git a/web/index.html b/web/index.html index c39060a..26a0bd7 100644 --- a/web/index.html +++ b/web/index.html @@ -748,7 +748,8 @@ footer a:hover{color:var(--laranja2)} justify-content:space-between;gap:12px;flex-wrap:wrap;font-size:11px;color:#5A6570} @media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto}} - + +
@@ -764,7 +765,7 @@ footer a:hover{color:var(--laranja2)} @@ -785,10 +786,10 @@ footer a:hover{color:var(--laranja2)}Mande a arte, veja a nota e a folha montada antes de pagar. Revisão de 5 pontos e reencaixe por nossa conta.
PNG, JPG ou PDF. Sem cadastro.
PNG, JPG ou PDF, conforme o produto. Sem cadastro.
A folha e o preço aparecem na hora.
Conferimos a metragem e liberamos o pagamento.
Aviso no WhatsApp a cada etapa.
A nota fiscal sai no CNPJ informado. O arquivo fica guardado por 30 dias; depois disso, um novo pedido precisa do arquivo de novo. O histórico do pedido @@ -1205,6 +1206,11 @@ footer a:hover{color:var(--laranja2)}
Escolha um produto, envie a arte e veja o preço na hora.
+ Escolher produto +