Files
sar/apps/web/public/sw.js
julian 518769218f fix(api,web): corrige autorização da área do rep e falhas silenciosas no front
Auditoria de segurança + robustez da área do representante antes de produção.

API (autorização e injeção):
- IDOR em clients: endpoints de detalhe (findOne, notas, ctr, top-produtos,
  histórico, contatos) validam carteira do rep via assertClienteDaCarteira;
  cliente alheio retorna 404. orders.create idem antes de aceitar idCliente.
- Role guard em dashboards supervisor/manager e ger/chamados (rep -> 403).
- Datas em $queryRawUnsafe agora exigem YYYY-MM-DD (DateOnlySchema + revalidação);
  dtAniversario de contato validado e escapado.
- id_empresa de CTR/NF usa matrizEmpresa() em vez de 1/9001 hardcoded.
- Alçada de desconto: transmit valida desconto efetivo por item (preço vs tabela
  pauta>promo>base + promoção ativa), não só o desconto global.
- Idempotency-Key escopado a empresa+vendedor; unsubscribe com DTO e dono.

Web (erros nunca silenciosos):
- Remove dupla serialização (apiFetch já faz JSON.stringify) em funil, sac e
  politicas — corrige criar oportunidade, abrir chamado e editar políticas.
- Handler global de erro no QueryClient (query e mutation viram toast).
- Error boundary por rota (payload divergente não derruba o app em branco).
- Alert usa message= em vez de title= (AntD 6); sw.js usa globalThis.

Qualidade:
- Corrige ping.contract.spec (idEmpresa) e zera erros de lint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:40:19 +00:00

124 lines
3.7 KiB
JavaScript

// Service Worker SAR
// C4/NFR-2: cache de API para uso offline (network-first, fallback to cache)
// C6: Web Push
// App shell: stale-while-revalidate para assets estáticos
const API_CACHE = 'sar-api-v2';
const SHELL_CACHE = 'sar-shell-v2';
// Paths de API que valem ser cacheados para offline
// Auth e mutations (POST/PATCH) nunca são interceptados
const CACHEABLE_API = [
'/api/v1/clients',
'/api/v1/catalog',
'/api/v1/orders',
'/api/v1/dashboard',
'/api/v1/auth/me',
];
// ── Fetch ──────────────────────────────────────────────────────────────────────
globalThis.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;
const url = new URL(request.url);
if (url.pathname.startsWith('/api/v1/')) {
const cacheable = CACHEABLE_API.some((p) => url.pathname.startsWith(p));
if (cacheable) {
event.respondWith(networkFirst(request, API_CACHE));
}
return;
}
if (request.mode === 'navigate') {
// App shell HTML — network first, cache fallback
event.respondWith(shellNavigate(request));
return;
}
// Assets estáticos (JS/CSS/fontes/imagens) — stale-while-revalidate
if (/\.(js|css|woff2?|png|svg|ico)$/.test(url.pathname)) {
event.respondWith(staleWhileRevalidate(request, SHELL_CACHE));
}
});
// Network-first: tenta rede, cai no cache se offline
async function networkFirst(request, cacheName) {
const cache = await caches.open(cacheName);
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch {
const cached = await cache.match(request);
if (cached) return cached;
return offlineResponse();
}
}
// Navigate: network first; fallback para a raiz cacheada
async function shellNavigate(request) {
const cache = await caches.open(SHELL_CACHE);
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
// Sempre armazena a raiz como fallback universal
cache.put(new Request('/'), response.clone());
}
return response;
} catch {
const cached = (await cache.match(request)) ?? (await cache.match('/'));
if (cached) return cached;
return offlineResponse();
}
}
// Stale-while-revalidate: responde do cache, atualiza em background
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const networkFetch = fetch(request).then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
});
return cached ?? networkFetch;
}
function offlineResponse() {
return new Response(
JSON.stringify({
type: 'sar:offline',
title: 'Sem conexão',
status: 503,
}),
{ status: 503, headers: { 'Content-Type': 'application/json' } },
);
}
// ── Push (C6) ─────────────────────────────────────────────────────────────────
globalThis.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
globalThis.registration.showNotification(data.title ?? 'SAR', {
body: data.body ?? '',
icon: '/sar-icon.png',
badge: '/sar-icon.png',
data: data.url ? { url: data.url } : undefined,
}),
);
});
globalThis.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data?.url;
if (url) {
event.waitUntil(clients.openWindow(url));
}
});