16 KiB
16 KiB
TechXCar — Análise do Repositório
Data: 02/07/2026
Branch: feat/plan2-auth-multitenancy
📋 Visão Geral
TechXCar é um SaaS multi-tenant de gestão de oficina automóvel, construído com Go (Fiber) no backend e React 19 + TypeScript + Vite no frontend. Cada oficina (tenant) tem um schema PostgreSQL isolado (tenant_<uuid>).
🏗 Stack Tecnológica
| Camada | Tecnologia |
|---|---|
| Frontend | React 19, TypeScript 6, Vite 8, Tailwind CSS 4, React Router 7, TanStack Query 5, Zustand 5, React Hook Form + Zod |
| Backend | Go 1.25, Fiber v2, pgx v5, golang-jwt, golang-migrate, bcrypt, go-redis |
| Base de Dados | PostgreSQL 16 (multi-tenancy via schema-per-tenant) |
| Cache | Redis 7 |
| go-pdf/fpdf (server-side, nativo — sem Chrome headless) | |
| Infra | Docker Compose, Nginx, Coolify |
📁 Estrutura do Projeto
techxcar/
├── frontend/ # React 19 + Vite
│ ├── src/
│ │ ├── components/
│ │ │ ├── ui/ # shadcn-style (button, input, label, badge, dialog)
│ │ │ └── layout/ # AppLayout, AdminLayout
│ │ ├── hooks/ # useAuth, useClients
│ │ ├── lib/ # api.ts, queryClient.ts, types.ts, utils.ts
│ │ ├── pages/
│ │ │ ├── auth/ # LoginPage
│ │ │ ├── admin/ # DashboardPage, TenantsPage
│ │ │ ├── app/ # 10 páginas de gestão da oficina
│ │ │ └── public/ # InviteRedeemPage
│ │ ├── store/ # authStore (Zustand + persist)
│ │ └── test/ # setup Vitest
│ ├── public/ # favicon, icons
│ ├── Dockerfile # Nginx a servir build Vite
│ └── nginx.conf
├── backend/ # Go + Fiber
│ ├── cmd/server/main.go # Entrypoint
│ ├── internal/
│ │ ├── auth/ # (10 ficheiros) JWT, bcrypt, middleware, rate-limit, routes
│ │ ├── tenant/ # (6 ficheiros) multi-tenant, invites, provisioning, access
│ │ ├── client/ # (4 ficheiros) clientes + veículos CRUD
│ │ ├── catalog/ # (4 ficheiros) catálogo de itens CRUD
│ │ ├── workorder/ # (4 ficheiros) OT CRUD + state machine + items + staff hours
│ │ ├── staff/ # (4 ficheiros) técnicos CRUD
│ │ ├── expense/ # (4 ficheiros) despesas CRUD
│ │ ├── invoice/ # (4 ficheiros) faturação + geração PDF
│ │ ├── settings/ # (4 ficheiros) definições da oficina
│ │ ├── server/ # (2 ficheiros) Fiber app, health routes, error handler
│ │ └── config/ # (2 ficheiros) env config (DATABASE_URL, JWT_SECRET, etc.)
│ ├── pkg/
│ │ ├── database/ # pgxpool wrapper, migrations (public + tenant)
│ │ ├── redis/ # go-redis wrapper
│ │ └── pdf/ # go-pdf/fpdf generation
│ ├── migrations/
│ │ ├── public/ # 2 migrações (tenants, invites, super_admins, platform_settings)
│ │ └── tenant/ # 2 migrações (11 tabelas por tenant)
│ ├── Dockerfile # Multi-stage Go build
│ ├── go.mod / go.sum
│ └── Makefile (via CLAUDE.md)
├── docker-compose.yml # Dev: postgres + redis + backend + frontend
├── docker-compose.prod.yml # Produção (Coolify)
├── .env.example # Template de env vars
└── docs/
└── superpowers/
├── specs/ # Design specs (arquitetura, multi-tenancy, impersonação)
└── plans/ # Planos de implementação (6 planos)
🔐 Autenticação & Autorização
Mecanismo
- JWT com access token (15 min) + refresh token (30 dias, httpOnly cookie)
- Refresh rotativo: armazenado em Redis (
refresh:<userID>) — cada refresh gera novo token - Rate limiting: 10 tentativas/min/IP nas rotas de auth (via Redis)
Roles
| Role | Acesso |
|---|---|
super_admin |
Painel /admin, gestão de tenants, impersonação |
tenant_admin |
Acesso total à oficina |
manager |
Acesso operacional (sem settings financeiras) |
technician |
Acesso às OTs atribuídas |
Fluxo de Login
POST /api/v1/auth/login { email, password, tenant_slug? }
- Se
tenant_slugvazio → autentica comosuper_admin(tabelapublic.super_admins) - Se
tenant_slugpreenchido → procura tenant → procura user no schema do tenant - Devolve
access_token(body) +refresh_token(httpOnly cookie)
Middleware Stack
RequireAuth(secret) → valida JWT, extrai claims
RequireRole("tenant_admin", "manager") → verifica role
TenantMiddleware(db) → adquire conexão, SET search_path = tenant_<uuid>, public
🏢 Multi-tenancy (Schema-per-tenant)
Estrutura
- Schema
public:tenants,invites,super_admins,platform_settings - Schema
tenant_<uuid>(por oficina): todas as tabelas de negócio
Provisionamento
- Super-admin cria tenant (
INSERT INTO public.tenants) ProvisionTenantSchema()cria schema e corre migrations de tenant- Cria user admin no schema do tenant
Isolamento
TenantMiddlewareadquire conexão pgxpool dedicada por request- Define
SET search_path = "tenant_<uuid>", public - Bug corrigido: schema name usa underscores em vez de hífens
validTenantIDregex:^[a-zA-Z0-9_-]{1,63}$
Impersonação (Super-admin → Tenant)
POST /api/v1/admin/tenants/:id/access→ gera token JWT comrole="tenant_admin"+tenantID- Frontend guarda sessão anterior em
previousSession(não persistido) - Navega para
/appcom banner amarelo: "TechXCar Admin — a gerir: " - "Voltar" restaura sessão anterior (
restoreSession())
🧩 Módulos de Negócio
1. Auth (internal/auth/)
| Ficheiro | Descrição |
|---|---|
handler.go |
Login, refresh, logout handlers |
jwt.go |
Geração/validação de tokens (access + refresh) |
bcrypt.go |
Hash + verify com cost 12 |
middleware.go |
RequireAuth, RequireRole, TenantMiddleware |
ratelimit.go |
Rate limiter com Redis storage |
routes.go |
Registo de rotas /api/v1/auth/* |
2. Tenant / Invites (internal/tenant/)
| Ficheiro | Descrição |
|---|---|
handler.go |
CRUD tenants, invites, redeem, access |
repository.go |
Queries: super_admins, tenants, users, invites |
routes.go |
Registo de rotas + loginAdapter |
Endpoints:
GET /api/v1/admin/tenants— listar (super_admin)POST /api/v1/admin/tenants— criar + provisionar schemaPOST /api/v1/admin/tenants/:id/invite— gerar convite para tenantPOST /api/v1/admin/tenants/:id/access— impersonar tenantPOST /api/v1/admin/invites— gerar convite globalGET /api/v1/invites/:token— consultar convite (público)POST /api/v1/invites/:token/redeem— resgatar convite + criar conta
3. Work Orders (internal/workorder/)
Máquina de estados:
open → in_progress → completed → invoiced
↓
cancelled (de qualquer estado excepto invoiced)
Endpoints:
GET /api/v1/work-orders— listar (com filtro?status=)POST /api/v1/work-orders— criarGET /api/v1/work-orders/:id— detalhe (com items + staff_hours)PUT /api/v1/work-orders/:id— actualizarPOST /api/v1/work-orders/:id/transition— transição de estadoPOST /api/v1/work-orders/:id/items— adicionar itemDELETE /api/v1/work-orders/:id/items/:itemId— remover itemPOST /api/v1/work-orders/:id/staff-hours— adicionar horas técnicoDELETE /api/v1/work-orders/:id/staff-hours/:shId— remover horas
Destaques:
wo_status_logregista cada transição (from, to, changed_by, timestamp)- Totais calculados via
GENERATED ALWAYS ASno PostgreSQL
4. Clientes & Veículos (internal/client/)
- CRUD clientes + veículos
- Veículos aninhados a clientes (opcional)
- Veículo pode ser criado sem cliente
5. Catálogo (internal/catalog/)
- Items com código, nome, categoria, unidade (
un,hora,litro,kg) - Preço base + activo/inactivo
- Preço copiado para OT no momento de adição (editável)
6. Staff (internal/staff/)
- Interno (com conta no sistema) / Externo (registo simplificado)
- Custo/hora para cálculo de mão de obra
7. Despesas (internal/expense/)
- Tipos:
fuel,parts,tools,other - Filtro por tipo, ordenado por data
8. Definições (internal/settings/)
| Chave | Descrição |
|---|---|
company_name |
Nome da oficina |
company_nif |
NIF |
company_address |
Morada |
company_iban |
IBAN |
company_phone |
Telefone |
company_email |
Apenas tenant_admin pode escrever; manager pode ler.
9. Faturação (internal/invoice/)
- Criação de orçamentos (
quote) e faturas (invoice) - Geração PDF com go-pdf/fpdf (dados da oficina + cliente + veículo + items + staff hours)
- Ao faturar, OT transita automaticamente para
invoiced - PDFs servidos via endpoint autenticado
10. Health (internal/server/health.go)
GET /api/v1/health → { "status": "ok", "version": "0.1.0" }
🖥 Frontend
Páginas Implementadas
| Rota | Página | Função |
|---|---|---|
/login |
LoginPage | Autenticação (com/sem slug) |
/invite/:token |
InviteRedeemPage | Resgatar convite (criar oficina) |
/admin |
AdminDashboardPage | Placeholder |
/admin/tenants |
TenantsPage | Listar/criar tenants, gerar convites, Gerir (impersonar) |
/app |
DashboardPage | Placeholder |
/app/clients |
ClientsPage | CRUD clientes + veículos inline |
/app/catalog |
CatalogPage | CRUD catálogo com categorias |
/app/work-orders |
WorkOrdersPage | Listar OTs com filtro de estado |
/app/work-orders/:id |
WorkOrderDetailPage | Detalhe OT: items, staff_hours, transições |
/app/staff |
StaffPage | CRUD técnicos |
/app/expenses |
ExpensesPage | CRUD despesas com filtro |
/app/invoices |
InvoicesPage | Listar faturas, gerar novo documento, download PDF |
/app/settings |
SettingsPage | Definições da oficina (formulário) |
UI/UX
- Tema escuro:
bg-slate-950, texto branco, sidebar slate-900 - Componentes shadcn-style: Button, Input, Label, Badge, Dialog
- Formulários: React Hook Form + Zod (validação client-side)
- Cache: TanStack Query (staleTime 5 min, retry apenas em erros 500+)
- Auto-refresh: apiFetch tenta refresh em 401, limpa sessão se falhar
Store (Zustand)
authStore (persistida em localStorage):
- user, accessToken, isAuthenticated
- previousSession (NÃO persistido — apenas para impersonação)
- setAuth, clearAuth, updateToken
- impersonateTenant, restoreSession
Layouts
AppLayout (área da oficina):
- Sidebar com navegação: Dashboard, OT, Clientes, Catálogo, Técnicos, Despesas, Faturação, Definições
- Banner de impersonação (amarelo) quando super-admin gere tenant
- Botão "Terminar sessão"
AdminLayout (área super-admin):
- Sidebar simplificada: Dashboard, Oficinas
- Botão "Terminar sessão"
Protecção: RequireAuth wrapper verifica roles + redirect para /login se não autenticado
🗄 Base de Dados
Migrations Public (2)
| Migração | Descrição |
|---|---|
000001 |
uuid-ossp, tenants, invites, super_admins, platform_settings + índices |
000002 |
invites.tenant_id → nullable + índice condicional |
Migrations Tenant (2)
| Migração | Descrição |
|---|---|
000001 |
11 tabelas: users, clients, vehicles, staff, catalog_items, work_orders, wo_items, wo_staff_hours, wo_status_log, invoices, expenses, tenant_settings |
000002 |
Adiciona fuel_type a vehicles |
Destaques do Schema
- Colunas geradas:
wo_items.total = qty * unit_price * (1 - discount_pct / 100),wo_staff_hours.total = hours * cost_per_hour - Serial unique:
work_orders.numbereinvoices.number(únicos por schema tenant) - ON DELETE: SET NULL para FKs opcionais, CASCADE para dependentes, RESTRICT para invoices
- Índices: status, client_id, plate, date
🧪 Testes
Backend (Go + testify)
| Pacote | Testes |
|---|---|
auth |
JWT gen/validation (access + refresh), bcrypt (hash + verify), middleware (auth, role), login handler (stub repo) |
tenant |
CRUD super_admin, CRUD tenant, invites (create + use), access handler (success, not found, inactive) |
workorder |
CRUD OT, transições de estado, validação de transições (11 casos) |
config |
Parse de env vars |
database |
Pool creation |
server |
Health endpoint |
redis |
Conexão |
Integração: testes com TEST_DATABASE_URL / TEST_REDIS_URL usam t.Skip() quando não definidas.
Frontend (Vitest + Testing Library)
authStore.test.tsuseClients.test.ts- Setup:
src/test/setup.ts
🐳 Docker
docker-compose.yml (Dev)
services:
postgres: # 16-alpine, healthcheck, porta 5432
redis: # 7-alpine, appendonly, healthcheck, porta 6379
backend: # Go build, porta 8080, depende de postgres+redis
frontend: # Nginx build Vite, porta 3000, depende de backend
volumes: postgres_data, redis_data, pdf_storage
docker-compose.prod.yml
Produção para Coolify com HTTPS via Let's Encrypt.
Dockerfiles
- Backend: Multi-stage (builder Go → distroless/debug)
- Frontend: Build Vite → Nginx (porta 8080 no container)
🔍 Observações Técnicas
Pontos Fortes
- Arquitetura multi-tenant sólida: schema isolation com validação de inputs e search_path dinâmico
- Máquina de estados robusta: transições validadas, log de todas as mudanças
- Auto-refresh de token: renovação silenciosa sem perder sessão
- Impersonação bem desenhada: session restore puramente client-side, sem persistência
- PDF nativo: go-pdf/fpdf em vez de chromedp (menos dependências, mais rápido)
- Colunas GENERATED: totais computados pelo PostgreSQL (consistência garantida)
- Cobertura de testes: handler tests com stub repo, integração opcional
Problemas / Risco
- Acoplamento entre packages:
internal/invoice/handler.goimportainternal/workorderparaGetWorkOrderDetaileTransitionStatus— risco de dependência circular ao crescer - PDF path hardcoded:
storageRoot = "/app/storage"— spec diz configurável (S3-compatible) - Sem paginação: listagens (
GET /clients, etc.) sem limit/offset — problemático com muitos registos - Go 1.25.0: versão futura (não lançada oficialmente) — pode não compilar em todos os ambientes
- Vite 8: bleeding edge — possível instabilidade ou breaking changes
- Dashboard placeholders: admin e app Dashboard sem métricas reais
- fuel_type hardcoded: frontend tem lista fixa em vez de vir do backend/catálogo
- Sem notificações: módulo
notification/(Telegram + SMTP) não implementado - Sem relatórios: módulo
report/não implementado - i18n: apenas estrutura preparada, só Português de Portugal
✅ Estado Actual
Implementado
- Autenticação (login, refresh, logout, rate-limit)
- Multi-tenancy (schema isolation + provisioning)
- Convites (redeem flow completo com criação de tenant + schema + admin)
- Impersonação super-admin (acesso a tenants com session restore)
- Clientes + Veículos (CRUD, inline por cliente)
- Catálogo de itens (CRUD com categorias e unidades)
- Ordens de Trabalho (CRUD + state machine + items + staff hours + status log)
- Técnicos (CRUD, internos/externos, custo/hora)
- Despesas (CRUD por tipo)
- Definições da oficina (6 chaves)
- Faturação/Orçamentos (geração PDF com go-pdf/fpdf)
- Frontend completo (11 páginas, tema escuro, navegação sidebar)
Não Implementado
- Notificações (Telegram + SMTP)
- Relatórios com métricas reais
- i18n completo (apenas PT)
- Paginação em listagens
- Upload de logo para PDF
- Armazenamento S3 para PDFs