Fase 1 — Auditoria. Nenhum arquivo de código foi alterado para produzir este relatório. Data: 2026-10-06 · Commit auditado:
c4a02b4(branchmaster)
O repositório não é um app legado em JavaScript à espera de migração. Na verdade, ele contém duas coisas:
- O legado (
legado/): um protótipo navegável em HTML + JavaScript puro (legado/index.html, ~1.200 linhas, tudo com dados fictícios), 11 screenshots do produto, uma landing page de apresentação (index_final.html) e um scaffold React vazio (boilerplate do Vite). Ele define o comportamento e a identidade visual de referência (ENGENOVA, laranja, fluxo de entrega em 9 telas). - A reescrita em andamento (
apps/,packages/,database/): um monorepo pnpm + Turborepo que já usa TypeScript strict, React 19 + Vite, Express 5, Zod, Prisma 7 e PostgreSQL. Ou seja, a stack-alvo da Seção 5 do pedido já foi adotada.
Estado real da reescrita:
| Área | Situação |
|---|---|
| API — autenticação (JWT + refresh rotativo + RBAC) | Implementada, com falhas pontuais (ver A-04, A-05) |
| API — empresas, unidades, setores, cargos, colaboradores, EPIs, CAs | Implementadas (CRUD sem exclusão) |
| API — estoque, entregas, devoluções, assinatura, QR Code, auditoria, relatórios | Não existem |
| Banco | Schema Prisma completo, sem nenhuma migration |
| Web | 9 telas do fluxo de entrega, 100% com dados fictícios: nenhuma chamada à API, sem login funcional e sem guarda de rotas |
| Testes | 16 testes unitários na API (todos passam); zero no web |
| Build / Lint | Passam |
Os 4 problemas críticos:
- C-01: o fluxo de entrega exibe "Entrega registrada com sucesso", mas não grava nada.
- C-02: não há migrations versionadas.
- C-03: o modelo de estoque permite saldo negativo e não tem controle de concorrência.
- C-04: a entrega não registra o CA nem uma assinatura com valor de evidência, o que é exigido pela NR-6.
Achado de alto impacto em produto: a reescrita trocou a identidade ENGENOVA (laranja #EE5A24, logo, Sora) por uma marca genérica "EPI Manager" em azul (A-01).
Consequência para o plano: a "migração JS → TS" (Fase 3) se resume a portar o comportamento do protótipo e endurecer os contratos de tipo (DTOs compartilhados, eliminar any implícito de req.body). O grosso do trabalho está em domínio (entrega/estoque/assinatura), integração web↔API, segurança e testes.
Arquivos lidos integralmente:
- Todos os 183 arquivos versionados: configs raiz,
apps/api/src/**,apps/web/src/**,packages/**,database/prisma/**, CI, husky elegado/index.html. - Os scripts
apply_ui_v2.*, por amostragem. - As 11 screenshots em
legado/*.png.
Comandos executados:
| Comando | Resultado |
|---|---|
pnpm lint |
✅ 2/2 pacotes OK (sem warnings) |
pnpm turbo run build test --force |
✅ build API (tsc -b) e web (tsc -b && vite build) OK. ✅ 16/16 testes (4 arquivos). |
Import do pacote @epi-manager/database em runtime (Node 24) |
✅ funciona, mas só porque o Node 24 remove tipos de .ts nativamente (ver M-06) |
Busca de segredos no histórico git (git log -p) |
✅ nenhum segredo real commitado. .env e apps/api/.env estão no .gitignore |
Checagem do apps/api/.env local |
JWT_SECRET do .env.example (aceito pela validação de ≥16 caracteres) |
Busca por any, dangerouslySetInnerHTML, localStorage |
✅ nenhuma ocorrência no código novo |
| Item | Legado (legado/index.html) |
Reescrita (apps/*) |
|---|---|---|
| Framework | Nenhum (DOM manual + innerHTML) |
React 19.2 (web) · Express 5.2 (API) |
| Linguagem | JavaScript (inline no HTML) | TypeScript 5.7, strict + noUncheckedIndexedAccess |
| Bundler | Nenhum | Vite 8 (rolldown) · tsc -b na API |
| Monorepo | — | pnpm 9.15 workspaces + Turborepo 2 |
| UI | CSS próprio (variáveis, Sora/Inter/JetBrains Mono) | TailwindCSS 4 + componentes próprios estilo shadcn (cva + tailwind-merge), lucide-react |
| Estado | Objeto global state mutável |
Zustand 5 (fluxo de entrega + sessão, esta não usada) |
| Server state | — | TanStack Query 5 (provider instalado, nenhuma query) |
| Roteamento | Troca de classe .active em <section> |
React Router 7 (createBrowserRouter) |
| HTTP | — | axios (instância criada, nunca importada) |
| Formulários | — | react-hook-form + @hookform/resolvers (instalados, não usados) |
| Validação | — | Zod 4 (API: body, query e env). Web: não usado |
| Banco / ORM | — | PostgreSQL 16 (docker-compose) · Prisma 7.9 com @prisma/adapter-pg |
| Autenticação | doLogin(){ goTo('home') } (falsa) |
JWT de acesso (15 min, Bearer) + refresh token opaco (7 dias, cookie httpOnly, hash SHA-256 no banco, rotação) · bcrypt |
| Autorização | — | Middleware authorize(...roles) por rota (7 perfis) |
| Logs | — | winston (JSON em produção) |
| Segurança HTTP | — | helmet, cors (origem única), express-rate-limit (global 300/15 min + login 10/15 min) |
| Docs de API | — | swagger-jsdoc + swagger-ui em /docs |
| QR Code | Botão "Simular leitura" | html5-qrcode (câmera real, mas qualquer QR lido → funcionário fictício) |
| Assinatura | <canvas> manual |
react-signature-canvas → data URL em memória |
| Testes | — | Vitest 4 (somente API, unitários com mocks) |
| Lint | oxlint (scaffold) | ESLint 9 flat config + typescript-eslint + react-hooks + react-refresh |
| Formatter | — | Prettier 3 (sem verificação no CI) |
| Commits | — | commitlint (conventional) + husky |
| CI | — | GitHub Actions: install → lint → build (sem testes) |
.
├── apps/
│ ├── api/ API REST (Express 5). Módulos em camadas: domain / application / http / infra
│ │ └── src/
│ │ ├── modules/
│ │ │ ├── auth/ login, refresh, logout, me
│ │ │ ├── organization/ companies, business-units, departments, job-roles
│ │ │ ├── employee/ colaboradores (CRUD + bloquear/desbloquear/desligar)
│ │ │ ├── epi/ itens de EPI + histórico de CA
│ │ │ └── health/ GET /health
│ │ └── shared/ env (zod), logger, middlewares (auth, rbac, validação, erro, rate limit),
│ │ segurança (bcrypt, JWT), paginação, swagger
│ ├── web/ SPA React. Hoje só o fluxo de entrega (mock) + dashboard/histórico/menu (mock)
│ │ └── src/
│ │ ├── app/ App, router, query-client, error-page, layout (sidebar, bottom-nav, focus-header)
│ │ ├── features/ auth (login visual), dashboard, delivery (9 telas + store zustand + mocks), menu
│ │ ├── shared/ components/ui (button, input, card, badge, checkbox, radio, textarea), lib (cn, http-client)
│ │ ├── stores/ session-store (não usado)
│ │ └── styles/ globals.css (tokens Tailwind; paleta azul)
│ └── mobile-pwa/ Apenas package.json (vazio)
├── packages/
│ ├── config/ ESLint base/react + tsconfig.base (usado)
│ ├── database/ Singleton PrismaClient com adapter pg (usado; exporta o TS fonte)
│ ├── types/ Só o tipo UserRole (usado pela API)
│ ├── ui/ hooks/ utils/ Apenas package.json (vazios)
├── database/
│ └── prisma/ schema.prisma (completo), seeds/seed.ts, migrations/ (VAZIA)
├── docker/ docker-compose: só PostgreSQL
├── legado/ Protótipo HTML/JS, 11 screenshots, landing de apresentação, scripts de restyle, scaffold React vazio
├── docs/ scripts/ Diretórios vazios (não versionados)
├── .github/workflows/ci.yml lint + build
└── prisma.config.ts Aponta o Prisma CLI para database/prisma
Avaliação: a separação domain / application / http / infra na API é coerente e pragmática: interfaces de repositório são usadas de verdade nos testes. Não é preciso reorganizar a API, apenas completá-la. No web, a organização por features/ é adequada, mas falta a camada de dados (api/, hooks de query) e de sessão.
Legenda: ✅ implementado · 🟡 só visual / mock · ❌ ausente
| Funcionalidade (fonte: protótipo + screenshots) | Legado | Web novo | API nova | Observação |
|---|---|---|---|---|
| Login usuário/senha | 🟡 | 🟡 sem submit | ✅ | Legado usa "usuário"; API usa e-mail |
| "Lembrar de mim" / "Esqueci minha senha" | 🟡 | 🟡 | ❌ | Link href="#" |
| Logout | 🟡 | 🟡 botão morto | ✅ | |
| Dashboard: saudação, almoxarifado, resumo do dia, últimas entregas | 🟡 | 🟡 hardcoded "Marcio" | ❌ | Não existe vínculo usuário↔almoxarifado nem User.name |
| Notificações (sino) | 🟡 toast | 🟡 botão morto | ❌ | |
| Escanear QR do crachá | 🟡 simulado | 🟡 câmera real, resultado fictício | ❌ | Ver A-03 |
| Buscar funcionário por nome/matrícula (lista) | 🟡 | ❌ perdida | 🟡 lista sem busca textual | Card "Buscar Funcionário" leva ao scanner |
| Busca manual por matrícula no scanner | — | 🟡 ignora o valor digitado | ❌ | |
| Funcionário encontrado (foto, status, matrícula, função, empresa, CC) | 🟡 | 🟡 | ✅ dados existem | Bloqueia continuar se não "Ativo" (bom) |
| Seleção múltipla de EPIs com busca | 🟡 | 🟡 | ✅ catálogo | |
| EPI "Outro (especificar)" | 🟡 | ❌ perdida | ❌ | |
| Quantidade por EPI (passo a passo, 1–50) + observação (200 caracteres) | 🟡 | 🟡 tudo em uma tela, sem limite superior | ❌ | |
| Tamanho / lote | ❌ | ❌ | 🟡 StockItem.batchNumber |
Pedido na Seção 11 |
| Validação de estoque | ❌ | ❌ | ❌ | |
| Motivo da entrega (8 opções) | 🟡 | 🟡 | ❌ sem campo no schema | "Outro" sem texto livre |
| Assinatura digital | 🟡 | 🟡 data URL em memória | ❌ | |
| Confirmação/sucesso com resumo | 🟡 | 🟡 afirma sucesso sem gravar | ❌ | C-01 |
| Comprovante | 🟡 | 🟡 window.print() da tela escura |
❌ receiptPdfUrl sem gerador |
|
| Histórico com busca e filtros (Hoje/Ontem/Semana/Mês) | 🟡 | 🟡 filtros sem efeito; busca só por nome | ❌ | Screenshot mostra detalhe (chevron) |
| Menu: Funcionários, EPIs, Relatórios, Estatísticas, Almoxarifados, Configurações | 🟡 | 🟡 "em breve" | 🟡 parcial (colab./EPIs) | |
| Cadastros organizacionais | ❌ | ❌ | ✅ | Sem telas |
| Estoque / movimentações | ❌ | ❌ | ❌ | Só schema |
| Devolução | ❌ | ❌ | ❌ | Só schema (EpiReturn) |
| Auditoria | ❌ | ❌ | ❌ | AuditLog nunca escrito |
| EPIs obrigatórios por cargo | ❌ | ❌ | ❌ | Só schema (JobRoleRequiredEpi), útil para sugerir EPIs |
| Treinamentos | ❌ | ❌ | ❌ | Só schema (Training) |
| ID | Arquivo(s) | Problema | Impacto | Solução recomendada |
|---|---|---|---|---|
| C-01 | apps/web/src/features/delivery/signature-page.tsx, success-page.tsx; ausência de apps/api/src/modules/delivery |
O fluxo termina em "Entrega registrada com sucesso!" com "Responsável: Marcio (Almoxarifado)" hardcoded, mas nenhuma entrega é enviada ou gravada. Não existe módulo de entrega na API. | Se usado como está, o sistema mente sobre a operação mais importante; não há rastreabilidade legal da entrega. | Criar o caso de uso RegisterDelivery no backend (transação única: entrega + itens + baixa de estoque + movimentações + assinatura + auditoria). A tela de sucesso só é exibida após 201 da API, com dados vindos da resposta. |
| C-02 | database/prisma/migrations/ (vazio) |
Nenhuma migration versionada. O banco só pode ser criado por db push ou manualmente. |
Impossível evoluir o schema com segurança, reproduzir ambientes ou implantar em produção sem alteração manual. | Gerar a migration inicial (prisma migrate dev --name init) depois de corrigir o schema (A-09), versionar e usar prisma migrate deploy no deploy. Restrições que o Prisma não expressa (ex.: CHECK (quantity >= 0)) entram em SQL na própria migration. |
| C-03 | database/prisma/schema.prisma (StockItem, StockMovement) |
O modelo de estoque é um número sobrescrito (StockItem.quantity), sem CHECK >= 0, sem saldo anterior/posterior na movimentação, sem vínculo movimentação↔item de entrega, e com tipos incompletos (ENTRADA, SAIDA, TRANSFERENCIA, AJUSTE, INVENTARIO; faltam ENTREGA, DEVOLUCAO, PERDA, AVARIA). Não há módulo de estoque. |
O cenário da Seção 14 (dois almoxarifes, estoque = 1) resultaria em estoque negativo, e o histórico não explica como se chegou ao saldo. | Manter o saldo materializado (necessário para leitura rápida), mas só alterá-lo junto com a movimentação, na mesma transação, via UPDATE stock_items SET quantity = quantity - $n WHERE id = $id AND quantity >= $n (0 linhas afetadas → 409 ESTOQUE_INSUFICIENTE). Adicionar CHECK (quantity >= 0) como rede de segurança, balanceBefore/balanceAfter e deliveryItemId? em StockMovement, e os tipos faltantes. Teste de concorrência real contra PostgreSQL. |
| C-04 | schema.prisma (Delivery, DeliveryItem) |
A entrega não guarda o número do CA vigente no momento da entrega (apenas epiItemId; o CA pode mudar ou vencer depois), e a assinatura é só signatureUrl String?, sem data/hora da assinatura, hash do conteúdo assinado, dispositivo ou vínculo com os itens. |
A ficha de EPI da NR-6 exige CA, data, quantidade e assinatura do trabalhador. Sem esses dados congelados, o registro perde valor de prova. | Snapshot em DeliveryItem (caNumber, epiName). Entidade DeliverySignature (1:1 com Delivery): imageKey, contentHash (SHA-256 do payload canônico da entrega + imagem), signedAt, ipAddress, userAgent. Entrega imutável após assinada (correções via devolução ou estorno, nunca UPDATE). |
| ID | Arquivo(s) | Problema | Impacto | Solução recomendada |
|---|---|---|---|---|
| A-01 | apps/web/src/styles/globals.css, sidebar.tsx, login-page.tsx, index.html |
Identidade ENGENOVA substituída por "EPI Manager" com paleta azul e ícone genérico. O legado usa laranja #EE5A24/#FF8A3D, tinta #1B1B1F, fonte Sora em títulos, logo da construtora e textos com acentuação. |
Viola "preservar a identidade da ENGENOVA". O produto deixa de ser reconhecível. | Recriar os tokens a partir de legado/index.html (:root) e legado/ui_v2_styles.css. Extrair o logo oficial (PNG base64 dentro de legado/index_final.html) para apps/web/src/assets. Nome do produto: "ENGENOVA · Controle de Entrega de EPIs". |
| A-02 | apps/web/src/app/router.tsx, login-page.tsx, shared/lib/http-client.ts, stores/session-store.ts |
O web não tem autenticação: o formulário de login não submete, não há guarda de rota (todas as rotas, inclusive /entregas/nova/*, ficam acessíveis), não há interceptor de token/refresh, o httpClient e o useSessionStore nunca são importados e "Sair" não faz nada. |
O frontend é inutilizável contra a API real (todas as rotas da API exigem Bearer). | Camada auth: access token só em memória; refresh via cookie httpOnly já existente; bootstrap com POST /auth/refresh ao abrir o app; interceptor 401 → refresh único (com lock) → repetir a requisição; RequireAuth / RequireRole no router; logout chama /auth/logout e limpa o cache do React Query. |
| A-03 | apps/web/src/features/delivery/scanner-page.tsx; schema sem identificador de crachá |
Qualquer QR lido vai para o funcionário fictício. Não há definição do conteúdo do QR nem endpoint que o resolva. | Risco de, na implementação real, colocar ID interno, CPF ou matrícula no QR, que são dados previsíveis ou pessoais e permitem enumeração. | Coluna Employee.badgeCode (aleatório, ≥128 bits, único, revogável/reemitível). O QR contém só esse código. GET /employees/by-badge/:code exige perfil de almoxarifado, retorna o mínimo necessário (sem CPF) e grava auditoria. O backend valida status (ATIVO) antes de permitir a entrega. |
| A-04 | apps/api/src/modules/auth/application/refresh-token.use-case.ts, schema.prisma (RefreshToken) |
A rotação faz findValidByHash → revoke → create sem atomicidade: duas requisições concorrentes com o mesmo token ambas recebem um novo par. Também não há detecção de reuso (token já revogado reapresentado), e tokenHash não é @unique nem indexado (findFirst faz varredura). |
Um refresh token roubado continua utilizável em paralelo ao legítimo, sem detecção. A performance degrada com o volume de tokens. | tokenHash @unique; revogar com updateMany({ where: { id, revokedAt: null } }) e exigir count === 1; se o token apresentado estiver revogado, revogar toda a família (revokeAllForUser, hoje código morto) e registrar auditoria. |
| A-05 | apps/api/src/shared/middlewares/authenticate.ts, authorize.ts, token-service.ts |
O access token não é revalidado: usuário desativado ou com perfil alterado mantém acesso por até 15 min. role vem do JWT como string e é convertido com as UserRole; jwt.verify não fixa algorithms, issuer nem audience; o payload é convertido com as AccessTokenPayload sem validação. |
Revogação de acesso atrasada; conversões de tipo escondem payload malformado. | Validar o payload com Zod; fixar algorithms: ["HS256"], issuer e audience; incluir tokenVersion no usuário (incrementado ao desativar ou trocar perfil) e checá-lo no authenticate com cache curto, ou aceitar 15 min explicitamente documentados no SECURITY.md. |
| A-06 | apps/api/src/modules/employee/http/employee.routes.ts (GETs), company.routes.ts etc. |
GET /employees e /employees/:id estão abertos a qualquer usuário autenticado (inclusive CONSULTA) e retornam o registro completo com CPF, e-mail e telefone. Também não há escopo por unidade: um almoxarife da unidade A lista colaboradores da unidade B. |
Exposição de dados pessoais (LGPD) e IDOR horizontal entre unidades. | Projeções de resposta por caso de uso (ex.: EmployeeSummaryDto sem CPF para o fluxo de entrega; CPF mascarado salvo para ADMIN/RH). authorize nos GETs. Escopo por unidade derivado do usuário (User.businessUnitIds ou do almoxarifado) aplicado no repositório. |
| A-07 | Toda a API; schema.prisma (AuditLog) |
O modelo AuditLog existe, mas nenhum código o escreve (0 ocorrências). Login, criação/edição de cadastros e mudanças de status não são registrados. |
Não atende a Seção 27; é impossível investigar quem fez o quê. | Serviço audit.record({ actorId, action, entity, entityId, before, after, ip, userAgent }) chamado dentro da mesma transação das operações críticas (entrega, estoque, status, perfis) e nos eventos de login (sucesso/falha) e refresh com reuso. |
| A-08 | apps/api/src/shared/middlewares/error-handler.ts e todos os *.service.ts |
Erros do Prisma não são mapeados: P2002 (unique), P2025 (não encontrado) e P2003 (FK) viram 500 "Erro interno". As checagens de unicidade são "consulta, depois insere" (ex.: CPF, CNPJ, matrícula), então requisições concorrentes passam pela checagem e explodem no banco com 500. Erros não têm code estável para o frontend traduzir. |
Mensagens inúteis ao usuário (viola a Seção 18) e corrida em cadastros. | Formato de erro estruturado { code: "ESTOQUE_INSUFICIENTE", message, details? }; mapear erros do Prisma no handler (P2002 → 409 CONFLITO, P2025 → 404, P2003 → 409/422). Manter a checagem prévia só para mensagem amigável e confiar na constraint do banco. |
| A-09 | database/prisma/schema.prisma |
Lacunas de domínio para o que o produto já mostra ou o pedido exige: Delivery sem status, reason, notes; DeliveryItem sem size, notes, snapshot de CA; nenhum conceito de tamanho/grade no estoque; EpiItem.category é texto livre (sem entidade Categoria); User sem name (a UI precisa de "Olá, Márcio"); sem vínculo usuário↔almoxarifado; EpiReturn.restockedToStockId sem FK; Employee.companyId sem relação/FK (e redundante com businessUnit.companyId). |
Impossível implementar as telas existentes sem retrabalho; dados inconsistentes possíveis. | Ajustar o schema antes da migration inicial (não há dados de produção, então é o momento mais barato). Ver §11 para a lista completa. |
| A-10 | apps/api/src/app.ts, shared/middlewares/rate-limiters.ts |
Rate limit em memória, só por IP, sem app.set("trust proxy", …). Atrás de proxy/load balancer, todos os clientes compartilham o IP do proxy (um usuário bloqueia o login de todos) ou o cabeçalho é ignorado. Não há limite por conta. |
Proteção contra força bruta ineficaz ou causando negação de serviço em produção. | Configurar trust proxy conforme o deploy; limitador de login por IP e por e-mail; bloqueio progressivo por conta registrado em auditoria. Store compartilhada (Postgres/Redis) só se houver mais de uma instância. |
| A-11 | schema.prisma (UserRole), packages/types |
O schema define 7 perfis (ADMINISTRADOR, SUPERVISOR, ALMOXARIFE, TECNICO_SEGURANCA, RH, GESTOR, CONSULTA); o pedido fala em COLABORADOR, ALMOXARIFADO, ADMIN. Não existe perfil de colaborador, e não está definido se o colaborador faz login ou assina no aparelho do almoxarife (como no legado). |
Matriz de permissões indefinida; risco de overengineering (7 perfis sem usuários reais) ou de lacuna (colaborador sem fluxo). | Decisão pendente (§14). |
| A-12 | apps/web/src/features/delivery/*, history-page.tsx, dashboard-page.tsx |
Comportamentos do legado perdidos ou quebrados: busca de funcionário por nome/matrícula; EPI "Outro (especificar)"; limite de quantidade (50); filtros de período (os chips não filtram); busca do histórico por matrícula (o placeholder promete, o código só filtra por nome); detalhe da entrega no histórico; texto livre para motivo "Outro". | Regressão funcional em relação à referência. | Reimplementar sobre a API real (§4). |
| ID | Arquivo(s) | Problema | Impacto | Solução recomendada |
|---|---|---|---|---|
| M-01 | apps/web/src/app/router.tsx, vite.config.ts |
Todas as rotas são importadas estaticamente. O bundle é um chunk de 769 KB (232 KB gzip), incluindo html5-qrcode e react-signature-canvas já na tela de login. |
Carga inicial lenta em celular na obra (rede ruim). | lazy por rota (React Router lazy), import dinâmico do scanner e da assinatura. Meta: < 200 KB gzip na rota inicial. Medir antes/depois. |
| M-02 | apps/web/src/features/delivery/store.ts, scanner-page.tsx |
O estado do fluxo só é limpo em "Nova entrega" na tela de sucesso. Abandonar um fluxo e iniciar outro pela navegação mantém os EPIs, quantidades e motivo anteriores para o próximo funcionário. Voltar da tela de sucesso permite "confirmar" de novo. | Entrega errada para o colaborador seguinte; dupla submissão quando houver API. | reset() ao entrar em /entregas/nova; chave de idempotência por entrega (Idempotency-Key) gerada no início do fluxo e validada no backend; replace na navegação pós-sucesso. |
| M-03 | Todos os *.controller.ts da API |
req.body (tipo any do Express) é repassado direto aos serviços; req.params.id as string sem validar UUID; req.body.terminationDate usado sem tipo. A validação existe (middleware), mas o tipo não flui: o compilador não protege o contrato. |
Erros de contrato só aparecem em runtime; refatorações inseguras. | Helper validated<T>(schema) que valida body/params/query e entrega o tipo inferido ao handler; schema idParam = z.object({ id: z.uuid() }). |
| M-04 | apps/api/src/modules/*/domain/*.ts, apps/web/src/features/delivery/types.ts, packages/types |
Tipos de domínio copiados à mão do Prisma (risco de divergência). Enums repetidos em 4 lugares, com grafias diferentes: web "Ativo" | "Bloqueado" × API "ATIVO" | "BLOQUEADO". packages/types só tem UserRole. |
O web não consegue consumir a API sem conversões ad hoc; drift silencioso. | packages/contracts: schemas Zod de request/response compartilhados (a fonte única) → tipos via z.infer. A API valida com eles; o web os usa para tipar respostas e formulários. Rótulos de exibição (ATIVO → "Ativo") só na camada de UI. |
| M-05 | apps/api/src/shared/swagger.ts, app.ts |
/docs é exposto em produção, e o glob ./src/modules/**/*.routes.ts depende do diretório corrente e de arquivos .ts, que não existem no dist. |
Superfície desnecessária em produção; documentação vazia no build. | Servir /docs só fora de produção (ou atrás de auth ADMIN). Gerar o OpenAPI a partir dos schemas Zod (ex.: zod-openapi), eliminando JSDoc duplicado. |
| M-06 | packages/database/package.json (main: ./src/client.ts) |
A API compilada importa código TypeScript fonte do pacote de banco. Funciona no Node 24 (remoção de tipos nativa) mas engines declara >=20 e o CI usa Node 20. |
node dist/main.js pode falhar no Node 20/22 de produção. |
Compilar packages/database (ou fixar engines.node >= 24 e documentar), e adicionar ao CI um passo de smoke test do dist. |
| M-07 | .github/workflows/ci.yml |
O CI não roda testes, typecheck dos pacotes, prisma generate, prisma validate nem prettier --check. Numa instalação limpa, provavelmente falta o client gerado (não há postinstall); não verificado, porque o CI não foi executado nesta auditoria. |
Regressões e build quebrado chegam ao main. |
Passos: prisma generate → lint → typecheck → test (com Postgres de serviço para os testes de integração) → build. |
| M-08 | schema.prisma (Employee, Department), department.service.ts, employee.service.ts |
Employee.cpf @unique global conflita com o soft delete (deletedAt): não dá para recadastrar. A hierarquia de setores só impede ser pai de si mesmo (ciclos A→B→A passam). O supervisor pode ser de outra empresa ou o próprio colaborador. |
Inconsistências de dados. | Índice único parcial WHERE deleted_at IS NULL (SQL na migration); validação de ciclo ao trocar parentId; supervisor da mesma empresa e ≠ self. |
| M-09 | apps/api/src/modules/auth/application/login.use-case.ts, auth.schemas.ts |
Quando o e-mail não existe, o bcrypt não roda, e a diferença de tempo revela quais e-mails existem. O e-mail não é normalizado (maiúsculas/minúsculas). | Enumeração de usuários; contas "duplicadas" por caixa. | Comparar com um hash fictício quando o usuário não existir; email.trim().toLowerCase() no schema e na gravação (ou citext). |
| M-10 | shared/components/ui/checkbox.tsx, radio.tsx, select-epis-page.tsx, history-page.tsx, scanner-page.tsx, várias telas |
Acessibilidade: checkbox e radio usam sr-only sem indicador de foco visível; campos de busca sem <label> (só placeholder); texto neutral-400 (#94a3b8) sobre branco ≈ 2,6:1, abaixo de 4,5:1 (CA, horários, versão); controles mortos (lanterna, filtro, sino, "Esqueci minha senha", "Sair"); quantidade sem aria-live. |
Uso por teclado e leitura em ambiente externo (sol forte na obra) prejudicados. | peer-focus-visible:ring, labels visualmente ocultos, tokens de contraste AA, remover ou implementar controles mortos, aria-live="polite" no stepper. Adicionar eslint-plugin-jsx-a11y. |
| M-11 | apps/web (sem testes), apps/api (só unitários) |
Web: zero testes e nenhum setup (Vitest/RTL). API: 16 testes unitários com mocks; não há testes de refresh/logout, middlewares de autenticação/autorização, rotas HTTP, nem integração com banco. | Regras críticas (autorização, estoque, concorrência) ficariam sem rede de segurança. | Ver §13. |
| M-12 | apps/web/package.json, apps/api/package.json |
Dependências instaladas sem uso: web framer-motion, react-hook-form, @hookform/resolvers, zod, @epi-manager/types; API multer (+ @types/multer). |
Ruído e superfície de supply chain. | Ver §12 (algumas serão usadas, outras removidas). |
| M-13 | apps/web/src/styles/globals.css |
html, body { overflow-x: hidden } esconde estouros de layout em vez de corrigi-los. |
Bugs de responsividade passam despercebidos (Seção 20). | Remover e corrigir as causas; testar em 360/390/768/1024/1366/1920 px. |
| M-14 | database/prisma/seeds/seed.ts |
Cria ADMIN com senha conhecida (Trocar@123) sem trava de ambiente nem troca obrigatória no primeiro acesso. |
Se rodar em produção, cria uma credencial administrativa pública. | Bloquear o seed quando NODE_ENV=production; senha do admin inicial via variável de ambiente; flag mustChangePassword. |
| M-15 | apps/api/src/modules/auth/http/auth.controller.ts |
Cookie de refresh SameSite=Lax, path=/auth. Se web e API ficarem em sites diferentes em produção, o cookie não é enviado e o refresh quebra. |
Sessão cai a cada 15 min. | Decisão de deploy (§14): mesmo site (ex.: app. e api. do mesmo domínio, ou proxy /api no mesmo host, que é o recomendado) mantém Lax; caso contrário, SameSite=None; Secure + token CSRF. |
| ID | Arquivo(s) | Problema | Solução |
|---|---|---|---|
| B-01 | packages/ui, packages/hooks, packages/utils, apps/mobile-pwa; docs/, scripts/, database/diagrams |
Pacotes com apenas package.json; diretórios vazios. |
Remover até haver conteúdo real (evita a impressão de estrutura que não existe). |
| B-02 | legado/App/**, legado/apply_ui_v2.py + .ps1, legado/ui_v2_styles.css, legado/index_final.html (3,6 MB) |
Scaffold React com boilerplate do Vite; legado/App/package.json com MUI/chart.js nunca usados; dois scripts que fazem a mesma coisa; landing de apresentação de 3,6 MB. |
Manter index.html + screenshots como referência funcional; extrair o logo; remover o restante (fica no histórico git). Confirmar antes (§14). |
| B-03 | Toda a UI nova | Textos sem acentuação ("Funcionario", "Historico", "Ola", "Matricula"); o legado tem acentos corretos. | Corrigir as strings (arquivos já são UTF-8). |
| B-04 | dashboard-page.tsx, success-page.tsx, login-page.tsx, menu-page.tsx |
"Marcio", "Almoxarifado Central" e "Versão 1.0.0" hardcoded (versão repetida 3×). | Dados da sessão; versão via import.meta.env / package.json. |
| B-05 | .husky/pre-commit |
Roda o lint do monorepo inteiro a cada commit. | lint-staged. |
| B-06 | raiz | Aviso do turbo: versão global (2.10.6) ≠ local (^2.3.3). | Usar pnpm turbo/pnpm exec; alinhar a versão. |
| B-07 | shared/security/password-hasher.ts |
bcrypt custo 10. | Subir para 12 (medir a latência do login). |
| B-08 | shared/middlewares/not-found.ts |
Ecoa método e URL na resposta. | Mensagem fixa + code: "ROTA_NAO_ENCONTRADA". |
| B-09 | apps/web/index.html |
Fontes do Google carregadas externamente. | Self-host (@fontsource): privacidade, offline/PWA, sem bloqueio de render. |
| B-10 | README.md |
Diz que "os apps ainda não foram implementados", cita docs/roadmap.md (inexistente) e shadcn/ui (não usado). |
Reescrever na Fase 10 (+ ARCHITECTURE.md e SECURITY.md). |
| B-11 | card.tsx (CardHeader, CardContent), ui/index.ts, refresh-token-repository.revokeAllForUser, http-client.ts, session-store.ts |
Código não referenciado. | Usar (A-02, A-04) ou remover. |
| B-12 | auth.schemas.ts |
password.min(8) no login revela a política e rejeita senhas antigas antes da checagem. |
Política só na criação/troca de senha; no login, min(1). |
- Fluxo de entrega acoplado a mocks e à navegação. Cada tela lê um
zustandglobal e faz<Navigate>defensivo. Não existe um "rascunho de entrega" com regras (ex.: colaborador ativo, quantidade 1..50, EPI com CA válido). A regra "só ativo pode receber" está no componente (employee-found-page.tsx:61). Proposta:features/deliveries/modelcom funções puras de validação do rascunho (testáveis) e a mesma regra validada no backend. - Composição de dependências espalhada. Cada
*.routes.tsinstancia seus repositórios e serviços (PrismaEpiItemRepositoryé criado 2×,PrismaBusinessUnitRepository2×). Funciona, mas dificulta transações entre módulos. Entrega e estoque precisarão compartilhar a mesma transação (prisma.$transaction(tx => …)). Proposta: repositórios aceitarem umtxopcional, ou o caso de uso de entrega usar o Prisma transacional diretamente. Não introduzir container de DI. - Classes
Servicemonolíticas por entidade (EmployeeServicecom create/update/block/unblock/terminate/list). Aceitável hoje. Para entrega e estoque, preferir casos de uso nomeados (RegisterDelivery,ReturnEpi,AdjustStock), como já é feito emauth/. - Datas e números como
Date/numbercrus no JSON.unitValueDecimal →Number(perde precisão em valores grandes); datas sem padrão de timezone. Definir no contrato: ISO-8601 UTC e dinheiro em centavos (inteiro) ou string decimal. - Paginação offset (
skip/take) +countem todas as listas. Suficiente para o porte esperado; registrar o limite (histórico de entregas deve ter filtro de data obrigatório e índice(delivered_at)). - Status de colaborador sem máquina de estados.
block/unblock/terminateaceitam qualquer origem (é possível desbloquear um DESLIGADO). Regras de transição explícitas no domínio. Delivery.latitude/longitude/deviceInfoexistem no schema sem requisito correspondente. Coletar geolocalização é dado pessoal (LGPD): manter apenas se houver base legal; senão, remover.
| Tipo | Onde | Duplicação | Ação |
|---|---|---|---|
| Componentes | 6 telas de features/delivery/* |
Rodapé fixo com CTA (sticky bottom-0 border-t … max-w-xl + <Button fullWidth size="lg">) copiado 6×; wrapper de página flex min-h-screen flex-col bg-neutral-50 copiado 6× |
FlowLayout com title, children, footer |
| Componentes | select-epis-page.tsx, history-page.tsx, scanner-page.tsx |
Campo de busca com ícone repetido; history-page reimplementa o <Input> com <input> cru e as mesmas classes |
SearchInput |
| Componentes | dashboard-page.tsx |
Cabeçalho e resumo renderizados duas vezes (mobile e desktop) com markup quase igual | Um bloco responsivo |
| Navegação | nav-items.ts, menu-page.tsx (ACTIVE_ITEMS), bottom-nav.tsx (MOBILE_ITEMS) |
Lista de destinos repetida | Fonte única de navegação, filtrada por perfil |
| Ações | sidebar.tsx, menu-page.tsx |
Botão "Sair" duplicado (e ambos mortos) | useLogout() + um componente |
| Constantes de domínio | mock-data.ts (DELIVERY_REASONS), schema (inexistente), API (inexistente) |
Motivos só existem no frontend | Enum no schema/contrato; rótulos na UI |
| Tipos/enums | Prisma, */domain/*.ts, *.schemas.ts (Zod), web/.../types.ts, packages/types |
EmployeeStatus, CaSituation, EpiControlType, UserRole definidos 3–4× |
packages/contracts (M-04) |
| Controllers HTTP | 7 controllers da API | Mesmo formato create/update/getById/list com paginationQuerySchema.parse(req.query) |
Helper pequeno parsePage(req); não criar controller genérico |
| Repositórios | 6 repositórios Prisma | where → Promise.all([findMany, count]) |
Helper paginate(model, args) |
| Serviços | 6 serviços | getOrThrow e list (cálculo de skip) idênticos |
Aceitável. Extrair só toPage(page, pageSize) |
| Lógica | avatar.tsx initials() e legado initials() |
Mesma função | — (o legado sai) |
| Legado | apply_ui_v2.py × apply_ui_v2.ps1 × ui_v2_styles.css |
Mesmo restyle em três arquivos | Remover (B-02) |
| Tema | Situação | Severidade | Referência |
|---|---|---|---|
| XSS | Código novo: React escapa, sem dangerouslySetInnerHTML. Legado: innerHTML com template strings (${e.nome}, onclick='…("${e.mat}")'), seguro só porque os dados são fictícios; não portar esse padrão. Futuro: a imagem da assinatura deve ser servida com Content-Type: image/png fixo e nunca como SVG enviado pelo cliente. |
Baixo hoje | — |
| Injection (SQL) | Só Prisma parametrizado; nenhum $queryRaw. O UPDATE condicional de estoque (C-03) deve usar $executeRaw com template tag (parametrizado), nunca $executeRawUnsafe. |
OK | C-03 |
| IDOR | Sem escopo por unidade; qualquer autenticado lê qualquer colaborador/empresa por ID. | Alto | A-06 |
| Exposição de dados | CPF, e-mail e telefone em listas para todos os perfis. GET /auth/me devolve só {id, role} do token. |
Alto | A-06 |
| Autenticação fraca | Bom desenho (bcrypt, refresh opaco com hash, cookie httpOnly/secure em produção, rotação). Falhas: corrida e reuso do refresh (A-04), revogação atrasada (A-05), timing (M-09), sem fluxo de troca/reset de senha nem gestão de usuários. | Alto | A-04, A-05, M-09 |
| Autorização | authorize() presente em todas as escritas; leituras sem restrição de perfil. O frontend não esconde nem protege nada (sem login). |
Alto | A-02, A-06 |
| Secrets no frontend | Nenhum. VITE_API_URL é a única variável (pública por natureza). |
OK | — |
| Secrets no repositório | Nenhum segredo real no histórico. Credenciais do docker-compose são de desenvolvimento (aceitável, documentar). .env local usa o placeholder do exemplo. |
Baixo | — |
| Armazenamento inseguro | Nada em localStorage. O desenho previsto (access token em memória + refresh httpOnly) é o correto; manter. A assinatura (data URL) fica só em memória: não persistir em storage do navegador. |
OK | A-02 |
| Manipulação de IDs | UUIDs nos bodies validados por Zod; :id nas rotas não validados (vão direto ao Prisma; inofensivo hoje porque as colunas são text, mas retornam 404/500 inconsistentes). QR deve usar código opaco. |
Médio | M-03, A-03 |
| Endpoints sem proteção | Públicos: /health, /auth/login, /auth/refresh, /auth/logout, /docs. /docs não deveria ser público em produção. |
Médio | M-05 |
| Validação server-side | Presente em todos os bodies (Zod) e nas queries de listagem. Faltam: params, regras de domínio (quantidade máxima, transições de status, ciclo de setor), normalização de e-mail. | Médio | M-03, M-08, M-09 |
| Rate limiting | Existe, mas é ineficaz atrás de proxy e só por IP. | Alto | A-10 |
| Vazamento via erros | O handler devolve 500 genérico e loga o stack (correto). notFound ecoa a URL. ZodError devolve fieldErrors (aceitável). Erros do Prisma não mapeados viram 500 em vez de mensagens úteis. |
Médio | A-08, B-08 |
| CORS | Origem única via env, credentials: true. OK. Validar que CORS_ORIGIN não seja * em produção (o schema do env deveria exigir URL). |
Baixo | — |
| Headers | helmet() padrão. Faltam CSP específica para a SPA (servida pelo host do front) e HSTS no proxy. |
Baixo | — |
| CSRF | Cookie de refresh SameSite=Lax + path=/auth + POST-only: protegido no cenário mesmo-site. |
Depende do deploy | M-15 |
| Uploads | multer instalado, nenhum upload ainda. Assinatura e fotos precisarão de limite de tamanho, validação de MIME por magic bytes e armazenamento fora do webroot com nome gerado pelo servidor. |
Futuro | — |
| Body limit | express.json({ limit: "2mb" }) global. A assinatura em base64 cabe; reduzir o global para ~100 KB e liberar 2 MB só na rota de entrega. |
Baixo | — |
| Logging | winston sem redação de campos sensíveis; hoje não loga bodies (bom). Ao adicionar log de requisição, redigir password, authorization, cookies, CPF. |
Baixo | — |
| LGPD | Foto, CPF, geolocalização e assinatura são dados pessoais. Faltam política de retenção e minimização. | Médio | §6.7 |
| Tema | Achado | Ação |
|---|---|---|
| Bundle | Chunk único de 769 KB (232 KB gzip); html5-qrcode (~350 KB min) e o canvas de assinatura carregados no login |
Lazy routes + import dinâmico (M-01). Medir com vite build antes/depois |
| Imports desnecessários | Nenhum import morto (tree-shaking remove o que não é importado); as dependências não usadas só pesam no install | §12 |
| Renders | Seletores do zustand granulares (bom). Object.values(selected) a cada render é trivial. Não há problema de render mensurável; não adicionar memo preventivo |
— |
| Requests duplicados | Não há requests. Ao integrar: React Query com staleTime já configurado (30 s); catálogo de EPIs com staleTime maior; busca de colaborador com debounce de 250–300 ms |
Fase 6 |
| Listas | Catálogo e histórico renderizam tudo. Com paginação no servidor (já existe na API) não é necessário virtualizar | Paginação/infinite query no histórico |
| Loading | Nenhum estado de loading/erro/vazio além do "Nenhum … encontrado" | Seção 22: idle/loading/success/error/empty em toda operação |
| Imagens | Novo: só ícones SVG (lucide). Legado: fotos do Unsplash (externas). Fotos de colaborador futuras: miniatura redimensionada no servidor, loading="lazy", width/height fixos |
— |
| Fontes | Google Fonts bloqueante | Self-host (B-09) |
| API | validateReferences faz 4–5 consultas sequenciais (paralelizáveis); faltam índices para as buscas futuras: employees(name) (trigram ou lower(name)), employees(registration), deliveries(delivered_at), deliveries(employee_id, delivered_at), stock_movements(created_at) |
Fase 5 |
| Cálculos repetidos | Nenhum relevante | — |
- Mobile: as telas do fluxo usam
max-w-xlcentralizado e rodapé fixo; o padrão é bom para 360–430 px. Não testado em dispositivo nesta auditoria. Ooverflow-x: hiddenglobal (M-13) pode estar escondendo estouros. - Desktop: sidebar a partir de
lg. O fluxo de entrega continua com layout de celular centralizado no desktop. Para o almoxarifado (desktop-first) faltará uma versão densa: busca + seleção + quantidades na mesma tela. - Alvos de toque: botões de 44–48 px (bom). Os ícones do cabeçalho têm 36 px (
h-9 w-9), abaixo de 44 px. - Acessibilidade: ver M-10. Positivos:
lang="pt-BR",aria-labelnos ícones,<label htmlFor>no login,<h1>por tela.
Company 1─N BusinessUnit 1─N Department (auto-relação parent/children)
1─N Warehouse 1─N StockItem N─1 EpiItem
1─N Employee N─1 Department, N─1 JobRole, N─1 Employee (supervisor)
User 0..1─1 Employee · User 1─N RefreshToken · User 1─N AuditLog · User 1─N StockMovement · User 1─N Delivery (deliveredBy)
JobRole N─N EpiItem (JobRoleRequiredEpi, com quantity)
EpiItem 1─N EpiCA
StockItem 1─N StockMovement
Delivery 1─N DeliveryItem N─1 EpiItem, N─0..1 StockItem · DeliveryItem 1─0..1 EpiReturn
Employee 1─N Training · Employee 1─N Delivery
Pontos positivos: UUIDs, snake_case via @map, createdAt/updatedAt, onDelete: Restrict nas relações críticas, uniques compostos ([businessUnitId, registration], [companyId, code]).
| # | Problema | Proposta |
|---|---|---|
| 1 | Employee.companyId sem relação/FK, redundante com businessUnit.companyId |
Remover (derivar pela unidade) ou criar a FK + checagem de consistência |
| 2 | Employee.cpf @unique global × soft delete |
Único parcial WHERE deleted_at IS NULL |
| 3 | RefreshToken.tokenHash sem índice/unique |
@unique |
| 4 | StockItem.quantity sem CHECK >= 0; sem tamanho |
CHECK; coluna size (nullable) e unique (epiItemId, warehouseId, size, batchNumber) |
| 5 | StockMovementType incompleto; movimentação sem balanceBefore/After, sem deliveryItemId/returnId |
Completar (C-03) |
| 6 | Delivery sem status, reason, notes, warehouseId, idempotencyKey |
Adicionar; reason como enum (DESGASTE, DANO, ROTINA, ATIVIDADE_ESPECIAL, PERDA, EXTRAVIO, NOVO_COLABORADOR, OUTRO) + reasonDetail |
| 7 | DeliveryItem sem size, notes, caNumber (snapshot), epiName (snapshot) |
Adicionar (C-04) |
| 8 | Assinatura como signatureUrl solto |
Entidade DeliverySignature (C-04) |
| 9 | EpiItem.category texto livre |
Entidade EpiCategory (o domínio pede "categoria") |
| 10 | EpiCA.situation gravada (VENCIDO fica desatualizado) |
Derivar de expiresAt; gravar só CANCELADO manual |
| 11 | User sem name, sem vínculo com almoxarifado/unidade, sem tokenVersion, mustChangePassword, failedLoginCount/lockedUntil |
Adicionar conforme as decisões de §14 |
| 12 | Employee sem badgeCode (QR) |
Adicionar (A-03) |
| 13 | EpiReturn.restockedToStockId sem FK |
Relação com StockItem |
| 14 | Department sem proteção a ciclos |
Validação no serviço |
| 15 | Delivery.latitude/longitude |
Remover salvo requisito com base legal |
Não há dados em produção (nenhuma migration foi aplicada em lugar algum rastreável). Corrigir o schema antes de gerar a migration inicial é a opção de menor custo e risco. Se existir um banco local com dados de teste, ele pode ser recriado pelo seed.
| Dependência | Decisão | Justificativa |
|---|---|---|
| react, react-dom 19 | MANTER | Base |
| react-router-dom 7 | MANTER | Já usado. Adotar lazy por rota |
| @tanstack/react-query 5 | MANTER | Server state (cache, dedupe, retry) quando integrar a API |
| zustand 5 | MANTER | Só para o rascunho do fluxo de entrega (UI state entre telas). Não usar para server state |
| axios | SUBSTITUIR por fetch tipado (~40 linhas) |
Usado em 0 lugares. Interceptor de refresh é simples com fetch. Remove ~13 KB gzip e uma dependência. Alternativa aceitável: manter axios se preferirem interceptors prontos. |
| react-hook-form + @hookform/resolvers | MANTER (passarão a ser usados) | Formulários de login e cadastros (EPI, colaborador, estoque) |
| zod (web) | MANTER | Virá de packages/contracts; validar formulários com os mesmos schemas da API |
| framer-motion | REMOVER | Não usado; a Seção 19 pede evitar animações inúteis |
| html5-qrcode | MANTER (com lazy) | Funciona e já está integrado. Biblioteca sem manutenção ativa: reavaliar (@zxing/browser ou BarcodeDetector nativo) se der problema em iOS |
| react-signature-canvas | MANTER (com lazy) | Pequena e adequada |
| class-variance-authority, clsx, tailwind-merge | MANTER | Base do design system |
| lucide-react | MANTER | Ícones tree-shaken |
| @epi-manager/types | SUBSTITUIR | Por @epi-manager/contracts |
| tailwindcss 4, @tailwindcss/vite | MANTER | |
| novas: vitest, @testing-library/react, @testing-library/user-event, jsdom, msw | ADICIONAR (dev) | Testes de componentes/fluxo com API simulada (msw evita mocks manuais de fetch) |
| nova: eslint-plugin-jsx-a11y | ADICIONAR (dev) | Acessibilidade (M-10) |
| nova: @fontsource/inter, @fontsource/sora | ADICIONAR | Self-host das fontes da marca (B-09) |
| Dependência | Decisão | Justificativa |
|---|---|---|
| express 5, helmet, cors, cookie-parser, express-rate-limit | MANTER | Usados e adequados |
| zod 4 | MANTER | Validação + contratos |
| bcrypt | MANTER | Funciona. (argon2 seria opcional, sem necessidade) |
| jsonwebtoken | MANTER | Fixar algoritmo (A-05) |
| winston | MANTER | Já integrado. (pino seria mais rápido, mas sem ganho que justifique a troca) |
| swagger-jsdoc + swagger-ui-express | SUBSTITUIR swagger-jsdoc por geração a partir do Zod (zod-openapi) e MANTER swagger-ui |
Elimina JSDoc duplicado e documentação que diverge do código (M-05) |
| multer | REMOVER agora; readicionar quando houver upload de foto | Não usado |
| dotenv | MANTER | |
| novas: supertest | ADICIONAR (dev) | Testes HTTP de rotas, autenticação e autorização |
| nova: testcontainers (ou Postgres de serviço no CI) | ADICIONAR (dev) | Testes de integração de estoque/concorrência contra Postgres real |
| Item | Decisão | Justificativa |
|---|---|---|
| prisma / @prisma/client 7.9 + @prisma/adapter-pg + pg | MANTER | Já é a stack-alvo |
| bcrypt, @types/bcrypt, @prisma/client, @epi-manager/database na raiz | MOVER para packages/database (junto com o seed) |
A raiz não deveria ter dependências de runtime |
| turbo | ATUALIZAR para alinhar com a versão em uso (2.10.x) | B-06 |
| husky, commitlint, prettier | MANTER; ADICIONAR lint-staged |
B-05 |
legado/App/** (MUI, emotion, chart.js, react-hook-form, oxlint, vite) |
REMOVER | Scaffold não usado (B-02) |
Situação: 4 arquivos e 16 testes, todos unitários de serviço com repositórios simulados (login, company, employee, epi-ca). Eles têm valor real (cobrem regras de unicidade, referência cruzada e credenciais) e devem ser mantidos.
Lacunas, em ordem de prioridade (Seção 32):
- Estoque e entrega: integração contra Postgres real, incluindo o teste de concorrência (N entregas paralelas com saldo 1 → exatamente 1 sucesso, saldo final 0, N−1 recebem
409 ESTOQUE_INSUFICIENTE) e idempotência (mesma chave → uma entrega). - Autenticação: refresh com rotação, reuso de token revogado, logout, usuário inativo.
- Autorização: matriz perfil × rota via supertest (cada rota de escrita retorna 403 para os perfis não permitidos), escopo por unidade (IDOR).
- Entrega (domínio): colaborador não ativo, CA vencido, quantidade fora de 1..50, snapshot de CA, assinatura obrigatória.
- Assinatura: hash calculado e verificável; entrega imutável após assinada.
- Web: fluxo de entrega ponta a ponta com msw (incluindo erro de estoque insuficiente exibindo mensagem específica e o botão desabilitado durante o envio); guarda de rotas; interceptor de refresh.
| # | Decisão |
|---|---|
| 1 | Colaborador não faz login nesta fase: assina no aparelho do almoxarife, como no legado. Login próprio (matrícula + PIN) fica para uma fase posterior. |
| 2 | Perfis: ADMIN e ALMOXARIFADO. COLABORADOR entra junto com o login do colaborador. Os outros 5 perfis do schema saem. |
| 3 | App web único e responsivo, com layout e rotas por perfil; PWA depois. apps/mobile-pwa será removido. |
| 4 | legado/ será enxugado: ficam index.html + screenshots; o logo é extraído; scaffold, scripts e landing saem (permanecem no histórico git). |
Os itens 1–4 abaixo foram decididos (ver 14.1); os itens 5–7 continuam em aberto.
-
O colaborador faz login? No legado e nas telas atuais, quem opera é o almoxarife; o colaborador só assina no aparelho do almoxarife. A Seção 10 descreve um fluxo próprio do colaborador. Opções:
- (a) manter como no legado (sem login do colaborador;
COLABORADORé uma pessoa, não um usuário); - (b) colaborador com login próprio (matrícula + PIN) para consultar suas entregas e assinar;
- (c) (a) agora e (b) depois.
Recomendação: (c).
- (a) manter como no legado (sem login do colaborador;
-
Perfis: reduzir os 7 perfis do schema para os 3 do pedido (
ADMIN,ALMOXARIFADO,COLABORADOR), ou manter alguns (ex.:TECNICO_SEGURANCA,CONSULTA)? Recomendação:ADMIN,ALMOXARIFADOe, se (b)/(c),COLABORADOR. Adicionar outros só quando houver um usuário real com necessidade distinta. -
Um app ou dois?
apps/mobile-pwaestá vazio. Recomendação: um único app web responsivo (rotas e layout por perfil) e, depois, torná-lo PWA. Removerapps/mobile-pwa. -
Pasta
legado/: manter sóindex.html+ screenshots como referência e remover scaffold, scripts e a landing de 3,6 MB (após extrair o logo)? Tudo permanece no histórico git. -
Tamanho e lote: os EPIs têm grade de tamanho (bota 38–44, luva P/M/G) no estoque? Há mais de um almoxarifado por unidade em uso real?
-
Deploy: web e API ficarão no mesmo domínio (ex.: proxy
/api)? Isso define a política do cookie (M-15) e otrust proxy(A-10). -
Login por e-mail ou por usuário? O legado mostra "Usuário" (
marcio.almox); a API usa e-mail.
Com as respostas de §14, a Fase 2 (Planejamento) produzirá o plano de refatoração com a arquitetura proposta e a ordem de execução. A ordem prevista, ajustável pelas decisões acima:
- Schema + migration inicial (C-02, C-03, C-04, A-09, §11).
- Contratos compartilhados (M-04) e endurecimento de tipos/erros na API (M-03, A-08).
- Segurança de auth (A-04, A-05, A-10, M-09) + auditoria (A-07).
- Módulos de estoque e entrega com transação e controle de concorrência + testes de integração.
- Web: auth/guardas (A-02) → identidade ENGENOVA (A-01) → integração do fluxo com a API, estados de UI e recuperação dos comportamentos perdidos (A-12, M-02) → lazy loading (M-01) → acessibilidade (M-10).
- CI completo, limpeza (§12, B-01, B-02) e documentação (README, ARCHITECTURE.md, SECURITY.md).
Cada etapa termina com lint + typecheck + test + build verdes e uma entrada de changelog no formato ATUAL → problema / NOVO → solução / IMPACTO → compatibilidade.
Legenda: ✅ resolvido · 🟡 parcial · ⏳ pendente (com motivo)
| ID | Situação | Como foi resolvido / observação |
|---|---|---|
| C-01 | ✅ | POST /api/deliveries grava tudo numa transação; a tela de sucesso só aparece com os dados devolvidos pela API (testado no Edge de ponta a ponta). |
| C-02 | ✅ | Migration inicial versionada (database/prisma/migrations/20261006000000_init), aplicada com migrate deploy; CI confere ausência de drift. |
| C-03 | ✅ | Baixa condicional atômica + CHECK (quantity >= 0) + razão de movimentações com saldo antes/depois. Teste: 10 entregas simultâneas por 1 unidade → 1 sucesso, saldo 0. |
| C-04 | ✅ | DeliveryItem congela nome e CA; DeliverySignature com PNG validado, SHA-256, data/hora, IP/UA; entrega exige CA vigente. |
| A-01 | ✅ | Paleta, tipografia (Sora/Inter self-hosted), logo e textos do legado, com ajustes de contraste AA. |
| A-02 | ✅ | Sessão em memória + refresh por cookie, guardas de rota, logout real, troca de senha obrigatória. |
| A-03 | ✅ | badgeCode opaco e reemitível; GET /employees/by-badge/:code com escopo; QR gerado na ficha do colaborador. |
| A-04 | ✅ | tokenHash único, rotação atômica, detecção de reuso com revogação total e auditoria. |
| A-05 | ✅ | Claims validadas, alg/iss/aud fixos, tokenVersion e active conferidos a cada requisição. |
| A-06 | ✅ | GETs com perfil e escopo por unidade; CPF mascarado e crachá omitido para o almoxarifado. |
| A-07 | ✅ | recordAudit na mesma transação para login, sessão, cadastros, status, permissões, estoque, entregas, devoluções. |
| A-08 | ✅ | Erro estruturado {error:{code,message,details}}; Prisma P2002/P2025/P2003 mapeados; web traduz por código. |
| A-09 | ✅ | Schema revisado (ver docs/refactor-plan.md, Etapa 1). |
| A-10 | ✅ | TRUST_PROXY, bloqueio por conta no banco, limitadores separados para login e refresh (o e2e revelou que o refresh consumia o limite do login; corrigido). |
| A-11 | ✅ | Perfis ADMIN e ALMOXARIFADO (decisão 14.1). |
| A-12 | 🟡 | Recuperados: busca por nome/matrícula, limite 50, filtros de período, busca por matrícula, detalhe, motivo "Outro" com descrição. Mudança justificada: o item "Outro (especificar)" do catálogo foi removido — EPI sem cadastro não tem CA nem estoque e não pode ser entregue conforme NR-6; o caminho é cadastrar o EPI. Quantidades passaram a ser informadas numa única tela (menos toques que o passo a passo do legado). |
| M-01 | ✅ | Lazy por rota; carga inicial ≈ 157 KB gzip (antes 233 KB num chunk único); leitor de QR (107 KB gzip) só na tela do scanner. |
| M-02 | ✅ | Rascunho reiniciado em todo ponto de entrada; idempotencyKey; navegação pós-sucesso com replace. |
| M-03 | ✅ | Helper handler() valida params/query/body e entrega tipos inferidos; IDs validados como UUID. |
| M-04 | ✅ | packages/contracts é a fonte única de enums, schemas e DTOs. |
| M-05 | 🟡 | /docs (swagger-jsdoc) removido por estar vazio no build e exposto em produção. A referência de rotas está no README; gerar OpenAPI a partir dos schemas Zod (z.toJSONSchema) fica como próximo passo. |
| M-06 | ✅ | contracts e database compilados para dist. |
| M-07 | ✅ | CI com Postgres de serviço: generate/validate, format, lint, typecheck, testes, integração, drift e build. |
| M-08 | ✅ | deletedAt removido (desligado = status), ciclo de setores e supervisor validados. |
| M-09 | ✅ | Hash fictício real para e-mail inexistente; e-mail normalizado. |
| M-10 | ✅ | Foco visível, rótulos (o e2e encontrou e corrigiu o campo de senha sem rótulo), contraste AA, controles mortos removidos ou implementados (lanterna real), jsx-a11y. |
| M-11 | ✅ | 103 testes: 34 unitários API + 28 integração (Postgres real) + 9 contratos + 20 web; e2e no Edge (fora do repositório) validou o fluxo completo. |
| M-12 | ✅ | Removidos: axios, framer-motion, multer, swagger-*; adicionados só com uso real (§12). |
| M-13 | ✅ | overflow-x: hidden removido; 0 overflow medido em 7 telas × 6 larguras. |
| M-14 | ✅ | Seed bloqueado em produção, senhas por variável, ADMIN com troca obrigatória. |
| M-15 | ✅ | Mesmo site via proxy /api; cookie SameSite=Strict. |
| B-01, B-02 | ✅ | Pacotes vazios, mobile-pwa, scaffold e scripts do legado removidos (decisões 3 e 4). |
| B-03, B-04 | ✅ | Acentuação na UI e no seed; nome/almoxarifado/versão vêm da sessão e do package.json. |
| B-05 | ⏳ | Pre-commit ainda roda o lint completo; lint-staged é melhoria opcional. |
| B-06 | ✅ | Comandos via pnpm/turbo local. |
| B-07, B-08, B-09, B-10, B-11, B-12 | ✅ | bcrypt 12; 404 sem eco de URL; fontes self-hosted; README reescrito; código morto removido; login sem política de senha. |
- Mensagens de erro geradas pela API ainda estão sem acentuação (o web substitui as mais comuns por textos acentuados por código).
- Recuperação de senha por e-mail, cancelamento de entrega (status
CANCELADAexiste no modelo, sem fluxo), relatórios/estatísticas e telas de cadastro organizacional (via API/seed por enquanto). - Login do colaborador (decisão 14.1, fase futura) e PWA/offline.