Sistema full-stack para gestão de oficina — API serverless + interface web.
Full-stack mechanic-shop management system — serverless API + web UI.
Sistema completo para gestão de oficina mecânica: cadastro de clientes, controle de veículos, abertura e acompanhamento de ordens de serviço, autenticação, upload de foto de veículo e geração de PDF da OS.
Complete mechanic-shop management system: customer registration, vehicle tracking, work-order lifecycle, authentication, vehicle photo upload, and work-order PDF generation.
- 🔐 Autenticação JWT — cadastro, login e sessão do usuário
- 👤 Clientes — cadastro, listagem e busca por usuário
- 🚗 Veículos — vinculados a clientes, com tipo, cor e foto
- 📋 Ordens de serviço — abertura, atualização de status e histórico
- 🧾 PDF da OS — geração de documento para ordem de serviço
- 📊 Dashboard — métricas e visão geral da operação
- 🏢 Multi-tenant — múltiplas oficinas por conta, com isolamento dos dados
- 👥 Equipe e RBAC — convites e papéis de proprietário, gerente e mecânico
- 🧪 Testes unitários — Vitest + Testing Library + cobertura
Backend / API
- Vercel Serverless Functions em Node.js + TypeScript
- Prisma ORM + PostgreSQL (Neon ou outro Postgres compatível)
- JWT + bcryptjs para autenticação
- Vercel Blob para upload de fotos
- Zod para validação de entrada
Frontend
- React 18 + TypeScript 5.5
- Vite 5 + Tailwind CSS 3
- Componentes UI com Radix + utilitários próprios
- Lucide React para ícones
Qualidade
- ESLint
- Vitest + @testing-library/react
- GitHub Actions + Codecov
Pré-requisitos:
- Node.js 20+
- Um banco PostgreSQL (Neon, Supabase, Docker local etc.)
- Vercel CLI se quiser rodar as functions localmente
git clone https://github.com/guuszz/OFICINA.git
cd OFICINA
npm install
cp .env.example .envPreencha o .env com suas credenciais e rode:
npm run db:generate
npm run db:migrate
npm run db:seed # opcional: cria/restaura o cenário demonstrativo
# recomendado: sobe frontend + API serverless pelo Vercel Dev
npm run dev:fullAcesse a URL exibida pelo Vercel CLI, normalmente http://localhost:3000.
Se quiser rodar somente a interface Vite, sem API serverless:
npm run devVeja o arquivo .env.example.
| Variável | Obrigatória | Descrição |
|---|---|---|
DATABASE_URL |
✅ | String de conexão PostgreSQL usada pelo Prisma |
JWT_SECRET |
✅ | Segredo longo para assinar tokens JWT |
ALLOWED_ORIGINS |
Origens CORS adicionais, separadas por vírgula | |
BLOB_READ_WRITE_TOKEN |
Token do Vercel Blob, necessário para upload/remover fotos | |
LOG_LEVEL |
Use silent apenas para desativar logs estruturados em testes |
|
UPSTASH_REDIS_REST_URL |
✅ produção | URL HTTPS do Redis usado pelo rate limiting |
UPSTASH_REDIS_REST_TOKEN |
✅ produção | Token do Redis REST; nunca exponha no frontend |
RATE_LIMIT_SALT |
✅ produção | Valor aleatório usado para anonimizar o identificador do cliente |
RATE_LIMIT_ALLOW_MEMORY |
❌ produção | Fallback efêmero destinado apenas a testes controlados |
BACKUP_ENCRYPTION_KEY |
✅ backup | Chave Base64 de 32 bytes para AES-256-GCM |
BACKUP_DIR |
Diretório local dos backups; padrão backups |
|
BACKUP_RETENTION_DAYS |
Retenção local; padrão 30 dias | |
RESTORE_DATABASE_URL |
✅ restauração | Banco isolado que receberá a restauração |
RESTORE_CONFIRM |
✅ restauração | Confirmação exata do nome do banco de destino |
SEED_DEMO_EMAIL |
Email opcional do usuário criado pelo seed local | |
SEED_DEMO_PASSWORD |
Senha opcional do usuário criado pelo seed local |
npm run db:seedO seed restaura um usuário demonstrativo com 2 clientes, 3 veículos e 4 ordens. Ele pode ser executado repetidamente sem duplicar registros. Como o usuário demo é descartável, seus dados anteriores são substituídos a cada execução.
Credenciais locais padrão:
- email:
demo@oficina.local - senha:
OficinaDemo@2026
Por segurança, a execução é bloqueada quando NODE_ENV=production. Um seed intencional nesse
ambiente exige ALLOW_PRODUCTION_SEED=1.
O contrato completo da API está em docs/openapi.yaml, no padrão
OpenAPI 3.1. Ele pode ser importado no Swagger Editor, Insomnia ou Postman.
| Método | Rota | Descrição |
|---|---|---|
| POST | /api/auth/register |
Cria conta e inicia sessão segura |
| GET | /api/health |
Verifica API e conexão PostgreSQL |
| POST | /api/auth/login |
Autentica usuário e inicia sessão segura |
| GET | /api/auth/me |
Retorna usuário autenticado |
| POST | /api/auth/logout |
Encerra a sessão e remove os cookies |
| GET | /api/clientes |
Lista clientes do usuário |
| POST | /api/clientes |
Cadastra cliente |
| GET | /api/veiculos |
Lista veículos do usuário |
| POST | /api/veiculos |
Cadastra veículo |
| POST | /api/veiculos/:id/foto |
Envia foto do veículo |
| DELETE | /api/veiculos/:id/foto |
Remove foto do veículo |
| GET | /api/ordens |
Lista ordens de serviço |
| POST | /api/ordens |
Cria ordem de serviço |
| PUT | /api/ordens/:id/status |
Atualiza status da ordem |
As três rotas de listagem aceitam page e limit (máximo 100). Quando algum deles é enviado,
a resposta contém data e metadados em pagination; sem paginação, permanece como array para
compatibilidade com o frontend atual.
| Rota | Filtros disponíveis |
|---|---|
/api/clientes |
q (nome, email ou telefone) |
/api/veiculos |
q, tipo, clienteId |
/api/ordens |
q, status, veiculoId, clienteId |
Erros seguem o formato { "message": "...", "error": { "code": "...", "message": "...", "details": {} } }.
O campo superior message mantém compatibilidade com clientes antigos.
Cada requisição recebe um X-Request-Id. Se o cliente enviar um identificador válido, ele é
preservado; caso contrário, a API cria um UUID. O mesmo valor aparece no corpo das respostas de
erro e no objeto ApiError do frontend.
Os logs são emitidos em JSON, prontos para busca no Vercel ou ingestão por ferramentas externas:
{
"timestamp": "2026-07-26T15:00:00.000Z",
"level": "info",
"service": "oficina-api",
"event": "request.completed",
"requestId": "3d1d...",
"route": "/api/ordens",
"method": "GET",
"statusCode": 200,
"durationMs": 18.42
}Campos sensíveis — senha, token, cookie, segredo, API key e autorização — são substituídos por
[REDACTED], inclusive em objetos aninhados. O health check não expõe credenciais nem detalhes
do banco:
curl http://localhost:3000/api/healthO JWT de sessão é armazenado em cookie HttpOnly, SameSite=Lax e, em produção, Secure
com prefixo __Host-. O token não é devolvido no JSON nem salvo em localStorage.
Operações autenticadas com efeito colateral (POST, PUT e DELETE) também exigem
X-CSRF-Token, validado contra o cookie CSRF e contra o hash incorporado ao JWT.
Cada conta recebe uma oficina própria e pode também participar de outras organizações por convite.
O seletor da barra lateral troca a oficina ativa e renova a sessão; clientes, veículos e ordens
são sempre filtrados no backend pelo organizationId, evitando vazamento entre oficinas.
| Papel | Operação | Clientes/veículos | Ordens | Equipe |
|---|---|---|---|---|
Proprietário (OWNER) |
leitura | criar e consultar | criar e atualizar | convidar, alterar papéis e remover |
Gerente (MANAGER) |
leitura | criar e consultar | criar e atualizar | consultar e convidar mecânicos |
Mecânico (MECHANIC) |
leitura | somente consulta | criar e atualizar | sem acesso |
Convites são vinculados ao email, expiram em sete dias e têm somente o hash SHA-256 persistido. A oficina nunca pode ficar sem ao menos um proprietário. Contas e clientes antigos são migrados progressivamente na primeira autenticação, sem exigir perda ou recadastro de dados.
A aplicação inclui Web App Manifest, ícones 192x192, 512x512, variante maskable,
ícone Apple e service worker. Em navegadores compatíveis, a opção Instalar aplicativo
aparece na barra lateral e a OFICINA pode abrir em uma janela independente.
O shell da interface funciona offline, mas dados de clientes, veículos, ordens e autenticação
continuam dependendo do servidor. Por segurança, o service worker nunca armazena /api/*:
nenhuma resposta autenticada ou dado operacional é persistido no cache offline. Navegações usam
estratégia network-first e assets versionados usam cache-first. Quando uma nova versão fica pronta,
a interface oferece atualização explícita antes de recarregar.
O build valida automaticamente manifest, dimensões dos ícones, referências HTML e exclusão da API:
npm run build
npm run check:pwaO projeto possui backup PostgreSQL em formato customizado, criptografia AES-256-GCM, checksum SHA-256, retenção e restauração protegida. Um restore drill automatizado prova que o arquivo consegue reconstruir usuários, organizações, membros, convites, clientes, veículos e ordens:
npm run db:backup
npm run test:backupO procedimento completo, RPO/RTO, automação diária, recuperação e tratamento separado das fotos
do Vercel Blob estão em docs/BACKUP_RECOVERY.md.
Login e cadastro usam janela deslizante distribuída com Redis REST:
| Operação | Política |
|---|---|
| Login | 5 tentativas a cada 15 minutos por cliente |
| Cadastro | 3 tentativas a cada hora por cliente |
Ao exceder o limite, a API retorna 429 RATE_LIMITED e os headers RateLimit-Limit,
RateLimit-Remaining, RateLimit-Reset e Retry-After. O endereço do cliente nunca é salvo
diretamente: ele é transformado em SHA-256 com RATE_LIMIT_SALT.
Em desenvolvimento e testes existe um contador em memória. Em produção, a ausência das
credenciais Upstash retorna 503 em vez de oferecer uma proteção falsa que seria reiniciada a
cada função serverless. Falhas transitórias do provedor são registradas e seguem estratégia
fail-open para preservar disponibilidade.
A implementação utiliza @upstash/ratelimit
e o cliente REST, adequado a ambientes serverless sem conexões TCP persistentes.
# 1) criar conta e salvar os cookies
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{"name":"Gustavo","email":"gustavo@email.com","password":"senha-segura-123"}'
# 2) leia oficina_csrf em cookies.txt e envie junto à sessão
curl -X POST http://localhost:3000/api/clientes \
-H "Content-Type: application/json" \
-H "X-CSRF-Token: SEU_TOKEN_CSRF" \
-b cookies.txt \
-d '{"nome":"João Silva","telefone":"11987654321","email":"joao@email.com"}'api/ # roteador serverless único da Vercel
server/ # handlers, domínio e infraestrutura da API
├── _lib/ # auth, prisma e helpers compartilhados
├── auth/ # register, login, me
├── clientes/ # endpoints de clientes
├── veiculos/ # endpoints de veículos + foto
└── ordens/ # endpoints de ordens de serviço
prisma/
└── schema.prisma # modelos User, Cliente, Veiculo e Ordem
src/ # Frontend React
├── components/
├── contexts/
├── lib/
├── App.tsx
└── main.tsxnpm test # roda todos os testes
npm run test:watch # modo watch durante dev
npm run test:coverage # gera relatório de cobertura + HTML report
npm run test:integration # sobe PostgreSQL temporário e testa a API
npm run test:e2e # testa o fluxo completo no ChromiumStack de testes:
- Vitest — runner rápido, compatível com Jest API
- @testing-library/react — testa componentes pela perspectiva do usuário
- jsdom — DOM virtual para hooks e components
- v8 coverage — relatórios em
coverage/index.html
55 testes cobrindo: autenticação, senha, status, observabilidade, rate limiting, cn, hooks
(useNumberTicker, useTheme) e componentes (StatusBadge, CarSilhouette, PageHeader).
Os testes de integração utilizam um PostgreSQL 16 descartável:
# requer Docker Desktop funcionando
npm run test:integrationO comando:
- inicia
docker-compose.test.yml; - aplica o schema Prisma no banco temporário;
- testa autenticação, isolamento entre usuários e o fluxo cliente → veículo → ordem → status;
- valida seed idempotente, filtros, paginação e erros padronizados;
- remove o container e o volume ao final.
No GitHub Actions o mesmo conjunto roda contra um serviço PostgreSQL isolado, sem depender do banco de produção.
Na primeira execução, instale o Chromium controlado pelo Playwright:
npx playwright install chromium
npm run test:e2eO Playwright:
- sobe o PostgreSQL 16 temporário e restaura o seed;
- gera o build de produção e inicia um servidor full-stack isolado;
- valida login inválido, bloqueio de força bruta e o fluxo login → cliente → veículo → ordem → status → download do PDF;
- valida health check e correlação por
X-Request-Id; - confirma que o chunk pesado do PDF só é baixado quando solicitado;
- salva trace, screenshot e vídeo quando ocorre uma falha;
- remove o banco temporário ao terminar.
O build também aplica orçamento de tamanho: até 350 kB para o JavaScript principal e 1.550 kB para o chunk isolado do PDF. Assim, o limite maior do PDF não mascara crescimento acidental do bundle carregado na abertura da aplicação.
Para depuração visual:
npm run test:e2e:ui- Persistência real (PostgreSQL via Neon + Prisma)
- Autenticação JWT
- Geração de PDF da OS
- Testes unitários (Vitest)
- Documentação OpenAPI
- Testes E2E (Playwright) do fluxo principal e PDF
- Testes de integração da API com PostgreSQL efêmero
- Seed demonstrativo reproduzível
- Paginação, filtros e erros padronizados na API
- Carregamento sob demanda do gerador de PDF
- Rate limiting distribuído em login e cadastro
- Migração do JWT para cookie HttpOnly com proteção CSRF
- PWA instalável com shell offline e atualização controlada
- Backup PostgreSQL criptografado e restore drill automatizado
- Multi-tenant com organizações, convites, isolamento e papéis por funcionário
Os achados, correções e próximas fases estão registrados em
docs/AUDITORIA_TECNICA.md.