diff --git a/backend/minha-conta/js/app.js b/backend/minha-conta/js/app.js
index 84a4ec0..34dcb3b 100644
--- a/backend/minha-conta/js/app.js
+++ b/backend/minha-conta/js/app.js
@@ -70,9 +70,14 @@ function renderStoreSwitcher() {
}
storeSwitch.classList.remove('hidden');
- storeSelect.innerHTML = ctx.stores
- .map(s => ``)
- .join('');
+ const options = ctx.stores.map((store) => {
+ const option = document.createElement('option');
+ option.value = String(store.id);
+ option.textContent = String(store.nome || 'Loja sem nome');
+ option.selected = store.id === ctx.activeStoreId;
+ return option;
+ });
+ storeSelect.replaceChildren(...options);
storeSelect.addEventListener('change', async () => {
const storeId = Number(storeSelect.value);
diff --git a/backend/minha-conta/js/sections/admin/ModeracaoLojasSection.js b/backend/minha-conta/js/sections/admin/ModeracaoLojasSection.js
index 49c4682..25007f4 100644
--- a/backend/minha-conta/js/sections/admin/ModeracaoLojasSection.js
+++ b/backend/minha-conta/js/sections/admin/ModeracaoLojasSection.js
@@ -53,7 +53,7 @@ async function carregarLojas(container, ctx) {
return;
}
- tbody.innerHTML = lojas.map(l => `
+ tbody.innerHTML = lojas.map((l, index) => `
@@ -66,7 +66,7 @@ async function carregarLojas(container, ctx) {
${l.aberto ? 'Aberto' : 'Fechado'} |
-
+
|
@@ -74,6 +74,16 @@ async function carregarLojas(container, ctx) {
`).join('');
// Eventos
+ tbody.querySelectorAll('.btn-ver-pagina').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const loja = lojas[Number(btn.dataset.index)];
+ if (!loja?.slug) return;
+ const perfilUrl = new URL('/', window.location.origin);
+ perfilUrl.searchParams.set('loja', String(loja.slug));
+ window.open(perfilUrl.href, '_blank', 'noopener,noreferrer');
+ });
+ });
+
tbody.querySelectorAll('.btn-suspender').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
diff --git a/backend/minha-conta/js/sections/common/InicioSection.js b/backend/minha-conta/js/sections/common/InicioSection.js
index 2b57fa3..925ad73 100644
--- a/backend/minha-conta/js/sections/common/InicioSection.js
+++ b/backend/minha-conta/js/sections/common/InicioSection.js
@@ -66,7 +66,7 @@ export function mount(container, ctx) {
${config.emoji}
- Olá, ${user.nome.split(' ')[0]}!
+ Olá, !
${config.titulo}
@@ -173,6 +173,8 @@ export function mount(container, ctx) {
`;
+ container.querySelector('#inicio-primeiro-nome').textContent = String(user.nome || '').split(' ')[0];
+
// Delegação de eventos nos cards de acesso rápido
const handleCardClick = (e) => {
const card = e.target.closest('.inicio-card[data-path]');
diff --git a/backend/minha-conta/js/sections/common/PerfilSection.js b/backend/minha-conta/js/sections/common/PerfilSection.js
index a72c455..8d3e78a 100644
--- a/backend/minha-conta/js/sections/common/PerfilSection.js
+++ b/backend/minha-conta/js/sections/common/PerfilSection.js
@@ -19,10 +19,10 @@ export function mount(container, ctx) {
- ${user.nome.charAt(0).toUpperCase()}
+
- ${user.nome}
- ${{ admin: 'Administrador', comerciante: 'Comerciante', cliente: 'Cliente' }[user.role] || user.role}
+
+
@@ -58,7 +58,7 @@ export function mount(container, ctx) {
E-mail (não editável)
- ${user.email}
+
@@ -97,6 +97,14 @@ export function mount(container, ctx) {
`;
+ const displayName = String(user.nome || '');
+ const roleLabel = { admin: 'Administrador', comerciante: 'Comerciante', cliente: 'Cliente' }[user.role]
+ || String(user.role || '');
+ container.querySelector('#perfil-avatar-preview').textContent = displayName.charAt(0).toUpperCase();
+ container.querySelector('#perfil-avatar-name').textContent = displayName;
+ container.querySelector('#perfil-avatar-role').textContent = roleLabel;
+ container.querySelector('#perfil-email').textContent = String(user.email || '');
+
// Carrega dados atuais
loadPerfil(container, ctx);
diff --git a/backend/minha-conta/js/sections/merchant/ProdutosSection.js b/backend/minha-conta/js/sections/merchant/ProdutosSection.js
index 98e01e8..4b1690a 100644
--- a/backend/minha-conta/js/sections/merchant/ProdutosSection.js
+++ b/backend/minha-conta/js/sections/merchant/ProdutosSection.js
@@ -192,15 +192,15 @@ function mostrarFormulario(container, ctx, produto) {
-
+
-
+
-
+
@@ -218,12 +218,17 @@ function mostrarFormulario(container, ctx, produto) {
`;
+ const form = formEl.querySelector('#form-produto');
+ form.elements.nome.value = produto ? (produto.nome_produto || produto.nome || '') : '';
+ form.elements.descricao.value = produto?.descricao || '';
+ form.elements.preco.value = produto?.preco || '';
+
formEl.querySelector('#btn-cancelar-produto').addEventListener('click', () => {
formEl.classList.add('hidden');
formEl.innerHTML = '';
});
- formEl.querySelector('#form-produto').addEventListener('submit', async (e) => {
+ form.addEventListener('submit', async (e) => {
e.preventDefault();
const f = e.target;
const body = {
diff --git a/backend/src/__tests__/accountPanelXss.test.js b/backend/src/__tests__/accountPanelXss.test.js
new file mode 100644
index 0000000..ebdc0b5
--- /dev/null
+++ b/backend/src/__tests__/accountPanelXss.test.js
@@ -0,0 +1,42 @@
+const fs = require('fs');
+const path = require('path');
+
+const projectRoot = path.join(__dirname, '..', '..', '..');
+
+function readPanelFile(...segments) {
+ return fs.readFileSync(path.join(projectRoot, 'backend', 'minha-conta', 'js', ...segments), 'utf8');
+}
+
+describe('Sinks XSS do painel Minha Conta', () => {
+ it('preenche dados editaveis de produto pelas propriedades value do DOM', () => {
+ const source = readPanelFile('sections', 'merchant', 'ProdutosSection.js');
+
+ expect(source).not.toContain('value="${produto');
+ expect(source).not.toContain('${produto?.descricao');
+ expect(source).toContain('form.elements.nome.value =');
+ expect(source).toContain('form.elements.descricao.value =');
+ });
+
+ it('renderiza nomes e dados da conta com textContent', () => {
+ const app = readPanelFile('app.js');
+ const perfil = readPanelFile('sections', 'common', 'PerfilSection.js');
+ const inicio = readPanelFile('sections', 'common', 'InicioSection.js');
+
+ expect(app).toContain('option.textContent =');
+ expect(app).not.toContain('>${s.nome}');
+ expect(perfil).not.toContain('${user.nome}');
+ expect(perfil).not.toContain('${user.email}');
+ expect(perfil).toContain("querySelector('#perfil-avatar-name').textContent");
+ expect(perfil).toContain("querySelector('#perfil-email').textContent");
+ expect(inicio).not.toContain("${user.nome.split(' ')[0]}");
+ expect(inicio).toContain("querySelector('#inicio-primeiro-nome').textContent");
+ });
+
+ it('nao usa handler inline para abrir a pagina moderada', () => {
+ const source = readPanelFile('sections', 'admin', 'ModeracaoLojasSection.js');
+
+ expect(source).not.toMatch(/\sonclick\s*=/i);
+ expect(source).toContain("searchParams.set('loja', String(loja.slug))");
+ expect(source).toContain("'noopener,noreferrer'");
+ });
+});
diff --git a/backend/src/__tests__/frontendSecurity.test.js b/backend/src/__tests__/frontendSecurity.test.js
new file mode 100644
index 0000000..9fd2d15
--- /dev/null
+++ b/backend/src/__tests__/frontendSecurity.test.js
@@ -0,0 +1,149 @@
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+const request = require('supertest');
+const app = require('../server');
+
+const projectRoot = path.join(__dirname, '..', '..', '..');
+
+function filesBelow(relativeRoot, extensions) {
+ const absoluteRoot = path.join(projectRoot, relativeRoot);
+ if (!fs.existsSync(absoluteRoot)) return [];
+
+ return fs.readdirSync(absoluteRoot, { withFileTypes: true }).flatMap(entry => {
+ const relativePath = path.join(relativeRoot, entry.name);
+ return entry.isDirectory()
+ ? filesBelow(relativePath, extensions)
+ : extensions.includes(path.extname(entry.name)) ? [relativePath] : [];
+ });
+}
+
+function read(relativePath) {
+ return fs.readFileSync(path.join(projectRoot, relativePath), 'utf8');
+}
+
+function loadFrontendModule(relativePath, exports, context = {}) {
+ const module = { exports: {} };
+ const source = read(relativePath)
+ .replace(/^import .*;\r?\n/gm, '')
+ .replace(/export function /g, 'function ')
+ .concat(`\nmodule.exports = { ${exports.join(', ')} };`);
+
+ vm.runInNewContext(source, {
+ module,
+ URL,
+ window: { location: { origin: 'https://comerciobes.com.br' } },
+ ...context,
+ }, { filename: relativePath });
+
+ return module.exports;
+}
+
+describe('XSS e Content Security Policy do frontend', () => {
+ it('nao usa handlers nem scripts executaveis inline', () => {
+ const sourceFiles = [
+ 'index.html',
+ ...filesBelow('html', ['.html']),
+ ...filesBelow('js', ['.js']),
+ ...filesBelow(path.join('backend', 'minha-conta'), ['.html', '.js']),
+ ];
+
+ sourceFiles.forEach(relativePath => {
+ expect(read(relativePath)).not.toMatch(/\bon[a-z]+\s*=\s*["']/i);
+ });
+
+ const htmlFiles = sourceFiles.filter(relativePath => relativePath.endsWith('.html'));
+ htmlFiles.forEach(relativePath => {
+ const scriptTags = read(relativePath).match(/
+
|