Inicial
This commit is contained in:
+429
@@ -0,0 +1,429 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user