# 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_`). --- ## 🏗 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:`) — 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_, public ``` --- ## 🏢 Multi-tenancy (Schema-per-tenant) ### Estrutura - **Schema `public`**: `tenants`, `invites`, `super_admins`, `platform_settings` - **Schema `tenant_`** (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_", 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: " 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