Files
Luciano Milani 5de37bb512 Inicial
2026-07-02 12:47:55 +01:00

430 lines
16 KiB
Markdown

# 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 |
| **PDF** | 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? }
```
1. Se `tenant_slug` vazio → autentica como `super_admin` (tabela `public.super_admins`)
2. Se `tenant_slug` preenchido → procura tenant → procura user no schema do tenant
3. 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
1. Super-admin cria tenant (`INSERT INTO public.tenants`)
2. `ProvisionTenantSchema()` cria schema e corre migrations de tenant
3. Cria user admin no schema do tenant
### Isolamento
- `TenantMiddleware` adquire conexão pgxpool dedicada por request
- Define `SET search_path = "tenant_<uuid>", public`
- **Bug corrigido**: schema name usa underscores em vez de hífens
- `validTenantID` regex: `^[a-zA-Z0-9_-]{1,63}$`
### Impersonação (Super-admin → Tenant)
1. `POST /api/v1/admin/tenants/:id/access` → gera token JWT com `role="tenant_admin"` + `tenantID`
2. Frontend guarda sessão anterior em `previousSession` (não persistido)
3. Navega para `/app` com banner amarelo: "TechXCar Admin — a gerir: <tenant>"
4. "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 schema
- `POST /api/v1/admin/tenants/:id/invite` — gerar convite para tenant
- `POST /api/v1/admin/tenants/:id/access` — impersonar tenant
- `POST /api/v1/admin/invites` — gerar convite global
- `GET /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` — criar
- `GET /api/v1/work-orders/:id` — detalhe (com items + staff_hours)
- `PUT /api/v1/work-orders/:id` — actualizar
- `POST /api/v1/work-orders/:id/transition` — transição de estado
- `POST /api/v1/work-orders/:id/items` — adicionar item
- `DELETE /api/v1/work-orders/:id/items/:itemId` — remover item
- `POST /api/v1/work-orders/:id/staff-hours` — adicionar horas técnico
- `DELETE /api/v1/work-orders/:id/staff-hours/:shId` — remover horas
**Destaques:**
- `wo_status_log` regista cada transição (from, to, changed_by, timestamp)
- Totais calculados via `GENERATED ALWAYS AS` no 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` | 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.number` e `invoices.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.ts`
- `useClients.test.ts`
- Setup: `src/test/setup.ts`
---
## 🐳 Docker
### docker-compose.yml (Dev)
```yaml
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
1. **Acoplamento entre packages**: `internal/invoice/handler.go` importa `internal/workorder` para `GetWorkOrderDetail` e `TransitionStatus` — risco de dependência circular ao crescer
2. **PDF path hardcoded**: `storageRoot = "/app/storage"` — spec diz configurável (S3-compatible)
3. **Sem paginação**: listagens (`GET /clients`, etc.) sem limit/offset — problemático com muitos registos
4. **Go 1.25.0**: versão futura (não lançada oficialmente) — pode não compilar em todos os ambientes
5. **Vite 8**: bleeding edge — possível instabilidade ou breaking changes
6. **Dashboard placeholders**: admin e app Dashboard sem métricas reais
7. **fuel_type hardcoded**: frontend tem lista fixa em vez de vir do backend/catálogo
8. **Sem notificações**: módulo `notification/` (Telegram + SMTP) não implementado
9. **Sem relatórios**: módulo `report/` não implementado
10. **i18n**: apenas estrutura preparada, só Português de Portugal
---
## ✅ Estado Actual
### Implementado
- [x] Autenticação (login, refresh, logout, rate-limit)
- [x] Multi-tenancy (schema isolation + provisioning)
- [x] Convites (redeem flow completo com criação de tenant + schema + admin)
- [x] Impersonação super-admin (acesso a tenants com session restore)
- [x] Clientes + Veículos (CRUD, inline por cliente)
- [x] Catálogo de itens (CRUD com categorias e unidades)
- [x] Ordens de Trabalho (CRUD + state machine + items + staff hours + status log)
- [x] Técnicos (CRUD, internos/externos, custo/hora)
- [x] Despesas (CRUD por tipo)
- [x] Definições da oficina (6 chaves)
- [x] Faturação/Orçamentos (geração PDF com go-pdf/fpdf)
- [x] 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