feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
|
|
@ -0,0 +1,37 @@
|
|||
# Specification Quality Checklist: Sistema de Autenticação de Clientes
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-04-13
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- Todas as decisões técnicas (JWT, bcrypt, React context, localStorage) foram fornecidas pelo autor como decisões já tomadas — o spec as descreve em linguagem de negócio/comportamento, não expondo a tecnologia.
|
||||
- Verificação de e-mail, recuperação de senha e rate limiting foram explicitamente excluídos do escopo e registrados em Assumptions.
|
||||
- API Contract incluída na seção de Requisitos como contrato comportamental (endpoints e formatos), sem mencionar implementação.
|
||||
- Spec aprovada para prosseguir para `/speckit.plan`.
|
||||
175
.specify/features/005-authentication/contracts/auth-api.md
Normal file
175
.specify/features/005-authentication/contracts/auth-api.md
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
# API Contract: Auth Endpoints
|
||||
|
||||
**Prefixo**: `/api/v1/auth`
|
||||
**Blueprint**: `auth_bp` em `backend/app/routes/auth.py`
|
||||
**Content-Type**: `application/json`
|
||||
**Autenticação**: Bearer token via header `Authorization` (onde indicado)
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/auth/register
|
||||
|
||||
Cria uma nova conta de cliente. Token de acesso emitido imediatamente na resposta.
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "João Silva",
|
||||
"email": "joao@exemplo.com",
|
||||
"password": "minhasenha123"
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Obrigatório | Regras |
|
||||
|-------|------|-------------|--------|
|
||||
| `name` | string | ✅ | 1–150 caracteres |
|
||||
| `email` | string (RFC 5321) | ✅ | Email válido; normalizado para lowercase antes de persistir |
|
||||
| `password` | string | ✅ | Mínimo 8 caracteres |
|
||||
|
||||
### Respostas
|
||||
|
||||
**201 Created**
|
||||
```json
|
||||
{
|
||||
"access_token": "<jwt_string>",
|
||||
"user": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "João Silva",
|
||||
"email": "joao@exemplo.com",
|
||||
"role": "client",
|
||||
"created_at": "2026-04-13T15:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**409 Conflict** — e-mail já cadastrado
|
||||
```json
|
||||
{ "error": "E-mail já cadastrado." }
|
||||
```
|
||||
|
||||
**422 Unprocessable Entity** — falha de validação Pydantic
|
||||
```json
|
||||
{
|
||||
"error": "Dados inválidos.",
|
||||
"details": [
|
||||
{ "loc": ["body", "password"], "msg": "String should have at least 8 characters", "type": "string_too_short" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/auth/login
|
||||
|
||||
Autentica um cliente existente e emite token de acesso.
|
||||
|
||||
### Request Body
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "joao@exemplo.com",
|
||||
"password": "minhasenha123"
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Obrigatório |
|
||||
|-------|------|-------------|
|
||||
| `email` | string (email válido) | ✅ |
|
||||
| `password` | string | ✅ |
|
||||
|
||||
### Respostas
|
||||
|
||||
**200 OK**
|
||||
```json
|
||||
{
|
||||
"access_token": "<jwt_string>",
|
||||
"user": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "João Silva",
|
||||
"email": "joao@exemplo.com",
|
||||
"role": "client",
|
||||
"created_at": "2026-04-13T15:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**401 Unauthorized** — e-mail não encontrado **ou** senha incorreta (mesma resposta para não revelar qual campo falhou — FR-007, SC-006)
|
||||
```json
|
||||
{ "error": "Credenciais inválidas." }
|
||||
```
|
||||
|
||||
**422 Unprocessable Entity** — falha de validação Pydantic
|
||||
```json
|
||||
{ "error": "Dados inválidos.", "details": [...] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /api/v1/auth/me
|
||||
|
||||
Retorna dados do usuário autenticado. Requer `Authorization: Bearer <token>`.
|
||||
|
||||
### Headers
|
||||
|
||||
```
|
||||
Authorization: Bearer <jwt_string>
|
||||
```
|
||||
|
||||
### Respostas
|
||||
|
||||
**200 OK**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "João Silva",
|
||||
"email": "joao@exemplo.com",
|
||||
"role": "client",
|
||||
"created_at": "2026-04-13T15:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
**401 Unauthorized** — sem token, token inválido ou token expirado
|
||||
```json
|
||||
{ "error": "Token de acesso inválido ou expirado." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JWT Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "<uuid-string do ClientUser>",
|
||||
"exp": <unix timestamp — now() + 7 dias>
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| Algorithm | HS256 |
|
||||
| Secret | `JWT_SECRET_KEY` (variável de ambiente) |
|
||||
| TTL | 7 dias (604800 s) |
|
||||
| Claim `sub` | UUID do `ClientUser` como string |
|
||||
|
||||
---
|
||||
|
||||
## Envelope de Erro Padrão
|
||||
|
||||
Todas as respostas de erro seguem o envelope:
|
||||
|
||||
```json
|
||||
{ "error": "<mensagem legível>" }
|
||||
```
|
||||
|
||||
Erros de validação 422 incluem `details` com a lista de erros Pydantic.
|
||||
Respostas de erro **nunca** incluem informações que permitam distinguir e-mail vs. senha incorretos (SC-006).
|
||||
|
||||
---
|
||||
|
||||
## Notas de Segurança
|
||||
|
||||
- Nenhuma resposta inclui `password_hash`
|
||||
- `401` em credenciais inválidas NÃO DEVE indicar qual campo está incorreto
|
||||
- `JWT_SECRET_KEY` NUNCA deve aparecer em logs ou respostas da API
|
||||
- CORS configurado explicitamente em `create_app()` (sem wildcard em produção)
|
||||
163
.specify/features/005-authentication/data-model.md
Normal file
163
.specify/features/005-authentication/data-model.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Data Model: 005-authentication
|
||||
|
||||
**Fase 1 — Modelo de Dados**
|
||||
**Data**: 2026-04-13
|
||||
|
||||
---
|
||||
|
||||
## Entidades
|
||||
|
||||
### ClientUser
|
||||
|
||||
**Tabela**: `client_users`
|
||||
**Módulo**: `backend/app/models/user.py`
|
||||
**Classe**: `ClientUser`
|
||||
|
||||
| Campo | Tipo SQLAlchemy | Tipo Python | Nullable | Constraints | Notas |
|
||||
|-------|-----------------|-------------|----------|-------------|-------|
|
||||
| `id` | `UUID(as_uuid=True)` | `uuid.UUID` | NOT NULL | PK, `default=uuid.uuid4` | Gerado no Python antes do flush |
|
||||
| `name` | `String(150)` | `str` | NOT NULL | — | Nome completo |
|
||||
| `email` | `String(254)` | `str` | NOT NULL | UNIQUE, INDEX | Normalizado para lowercase via schema Pydantic |
|
||||
| `password_hash` | `String(100)` | `str` | NOT NULL | — | Hash bcrypt (60 chars); String(100) com margem de segurança |
|
||||
| `role` | `String(20)` | `str` | NOT NULL | `default='client'` | Único papel ativo nesta versão |
|
||||
| `created_at` | `DateTime` | `datetime` | NOT NULL | `server_default=func.now()` | Timezone-naive UTC |
|
||||
|
||||
**Indexes**: `email` (único + índice explícito — frequente em queries de login)
|
||||
**Relacionamentos**: nenhum nesta versão
|
||||
|
||||
**DDL esperado** (gerenciado via Alembic, não escrever manualmente):
|
||||
```sql
|
||||
CREATE TABLE client_users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(150) NOT NULL,
|
||||
email VARCHAR(254) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(100) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'client',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX ix_client_users_email ON client_users (email);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schemas Pydantic
|
||||
|
||||
**Módulo**: `backend/app/schemas/auth.py`
|
||||
|
||||
### `RegisterIn`
|
||||
|
||||
Valida o corpo da requisição `POST /api/v1/auth/register`.
|
||||
|
||||
```python
|
||||
class RegisterIn(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=150)
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=8)
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def normalize_email(cls, v: str) -> str:
|
||||
return v.lower().strip()
|
||||
```
|
||||
|
||||
### `LoginIn`
|
||||
|
||||
Valida o corpo da requisição `POST /api/v1/auth/login`.
|
||||
|
||||
```python
|
||||
class LoginIn(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
@field_validator("email")
|
||||
@classmethod
|
||||
def normalize_email(cls, v: str) -> str:
|
||||
return v.lower().strip()
|
||||
```
|
||||
|
||||
### `UserOut`
|
||||
|
||||
Resposta segura com dados do usuário (sem `password_hash`).
|
||||
|
||||
```python
|
||||
class UserOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: UUID
|
||||
name: str
|
||||
email: str
|
||||
role: str
|
||||
created_at: datetime
|
||||
```
|
||||
|
||||
### `AuthTokenOut`
|
||||
|
||||
Resposta de register e login bem-sucedidos.
|
||||
|
||||
```python
|
||||
class AuthTokenOut(BaseModel):
|
||||
access_token: str
|
||||
user: UserOut
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Regras de Validação
|
||||
|
||||
| Regra | Campo | Resposta em falha |
|
||||
|-------|-------|------------------|
|
||||
| `min_length=8` | `password` em `RegisterIn` | HTTP 422 |
|
||||
| `min_length=1, max_length=150` | `name` em `RegisterIn` | HTTP 422 |
|
||||
| `EmailStr` | `email` em `RegisterIn` / `LoginIn` | HTTP 422 |
|
||||
| Email único | `email` em `ClientUser` | HTTP 409 (capturado na rota via `IntegrityError`) |
|
||||
| Normalize lowercase | `email` (ambos os schemas) | Aplicado silenciosamente via `field_validator` |
|
||||
|
||||
---
|
||||
|
||||
## Transições de Estado
|
||||
|
||||
O `ClientUser` não possui máquina de estados nesta versão:
|
||||
|
||||
- **Created**: via `POST /api/v1/auth/register`
|
||||
- **Read**: via `POST /api/v1/auth/login` + `GET /api/v1/auth/me`
|
||||
- **Update / Delete**: fora do escopo desta feature
|
||||
|
||||
---
|
||||
|
||||
## Tipos Frontend
|
||||
|
||||
**Módulo**: `frontend/src/types/auth.ts`
|
||||
|
||||
```typescript
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
isAuthenticated: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contexto de Autenticação Frontend
|
||||
|
||||
**Módulo**: `frontend/src/contexts/AuthContext.tsx`
|
||||
|
||||
```typescript
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
token: string | null;
|
||||
isAuthenticated: boolean;
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
register: (name: string, email: string, password: string) => Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
**Inicialização**: ao montar `AuthProvider`, carregar `imob_token` do `localStorage` e chamar `GET /api/v1/auth/me` para hidratar `user`. Se o token estiver expirado/inválido, limpar o `localStorage` e definir estado unauthenticated.
|
||||
91
.specify/features/005-authentication/plan.md
Normal file
91
.specify/features/005-authentication/plan.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Implementation Plan: Sistema de Autenticação de Clientes
|
||||
|
||||
**Branch**: `005-authentication` | **Date**: 2026-04-13 | **Spec**: [spec.md](spec.md)
|
||||
**Input**: Sistema de autenticação para clientes: login com e-mail + senha (JWT, sem OAuth), auto-cadastro público.
|
||||
|
||||
## Summary
|
||||
|
||||
Adiciona autenticação JWT ao SaaS imobiliário. Backend: novo model `ClientUser` na tabela `client_users`, hashing de senha com bcrypt, emissão de token PyJWT com TTL de 7 dias, blueprint `/api/v1/auth` com endpoints register/login/me, e decorator `require_auth` reutilizável para proteger rotas. Frontend: `AuthContext` com persistência em `localStorage`, páginas Login e Cadastro no tema Linear escuro do projeto, interceptor Axios automático para injeção do header `Authorization`, e `ProtectedRoute` que guarda `/area-do-cliente/*`.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: Python 3.12 (backend) · TypeScript 5.5 (frontend)
|
||||
**Primary Dependencies**: Flask 3.x, SQLAlchemy 2.x, Pydantic v2, PyJWT>=2.9, bcrypt>=4.2, pydantic[email]; React 18, react-router-dom v6, Axios
|
||||
**Storage**: PostgreSQL 16 via Flask-SQLAlchemy — nova tabela `client_users` via migration Alembic
|
||||
**Testing**: pytest + pytest-flask (backend); nenhum teste automatizado de frontend nesta fase
|
||||
**Target Platform**: Linux/Docker (servidor) + navegadores modernos (SPA)
|
||||
**Project Type**: Web service REST + Single Page Application
|
||||
**Performance Goals**: <200ms p95 nos endpoints de autenticação
|
||||
**Constraints**: Stateless JWT, TTL de 7 dias, sem refresh token, sem verificação de e-mail, sem rate limiting (escopo MVP definido na spec)
|
||||
**Scale/Scope**: MVP, ~100 usuários iniciais, single-tenant
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
|
||||
| Princípio | Status | Justificativa |
|
||||
|-----------|--------|---------------|
|
||||
| **I. Design-First** | ✅ PASS | LoginPage e RegisterPage seguem DESIGN.md: fundo `#08090a`, panel `#0f1011`, tipografia Inter Variable, accent `#5e6ad2`/`#7170ff`, bordas `rgba(255,255,255,0.06)`. Mesmo estilo do PropertiesPage existente. |
|
||||
| **II. Separation of Concerns** | ✅ PASS | Flask retorna JSON puro; React é SPA. CORS configurado explicitamente em `create_app()`. Sem Jinja2 nas rotas da API. |
|
||||
| **III. Spec-Driven** | ✅ PASS | spec.md aprovado. Ciclo spec → plan → tasks → implement respeitado. |
|
||||
| **IV. Data Integrity** | ✅ PASS | Todos os inputs validados por Pydantic (`RegisterIn`/`LoginIn`). Migration Alembic. Tipos corretos (`String`, `UUID`, `DateTime`). `nullable=False` declarado explicitamente em todos os campos obrigatórios. |
|
||||
| **V. Security** | ✅ PASS | `JWT_SECRET_KEY` lido exclusivamente de variável de ambiente. bcrypt irreversível. 401 genérico em credenciais inválidas (SC-006). `password_hash` ausente em todas as respostas. |
|
||||
| **VI. Simplicity First** | ✅ PASS | 2 novos pacotes com razão clara (PyJWT para JWT, bcrypt para hashing). Sem refresh token, verificação de e-mail ou rate limiting (YAGNI — fora do escopo MVP conforme assumptions da spec). Sem flask-jwt-extended (overhead desnecessário). |
|
||||
|
||||
**Gate Result**: ✅ PASS — nenhuma violação. Prosseguir para Phase 0.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
.specify/features/005-authentication/
|
||||
├── plan.md # Este arquivo
|
||||
├── research.md # Fase 0 — resolução de unknowns (gerado)
|
||||
├── data-model.md # Fase 1 — modelo de dados (gerado)
|
||||
├── quickstart.md # Fase 1 — como rodar e testar (gerado)
|
||||
├── contracts/
|
||||
│ └── auth-api.md # Contratos dos endpoints /api/v1/auth (gerado)
|
||||
└── tasks.md # Fase 2 — gerado pelo /speckit.tasks
|
||||
```
|
||||
|
||||
### Source Code
|
||||
|
||||
```text
|
||||
backend/
|
||||
├── app/
|
||||
│ ├── models/
|
||||
│ │ └── user.py # ClientUser — NOVO
|
||||
│ ├── schemas/
|
||||
│ │ └── auth.py # RegisterIn, LoginIn, UserOut, AuthTokenOut — NOVO
|
||||
│ ├── routes/
|
||||
│ │ └── auth.py # auth_bp (/api/v1/auth) — NOVO
|
||||
│ ├── utils/
|
||||
│ │ └── auth.py # require_auth decorator — NOVO
|
||||
│ └── __init__.py # ATUALIZADO: user model import + auth blueprint + JWT_SECRET_KEY
|
||||
├── migrations/versions/
|
||||
│ └── <hash>_add_client_users.py # NOVO — gerado via flask db migrate
|
||||
└── pyproject.toml # ATUALIZADO: PyJWT>=2.9, bcrypt>=4.2, pydantic[email]
|
||||
|
||||
frontend/
|
||||
└── src/
|
||||
├── types/
|
||||
│ └── auth.ts # User, AuthState — NOVO
|
||||
├── contexts/
|
||||
│ └── AuthContext.tsx # AuthProvider, useAuth — NOVO
|
||||
├── services/
|
||||
│ ├── api.ts # ATUALIZADO: interceptor Authorization header
|
||||
│ └── auth.ts # registerUser, loginUser, getMe — NOVO
|
||||
├── pages/
|
||||
│ ├── LoginPage.tsx # rota /login — NOVO
|
||||
│ └── RegisterPage.tsx # rota /cadastro — NOVO
|
||||
├── components/
|
||||
│ ├── ProtectedRoute.tsx # redireciona se !isAuthenticated — NOVO
|
||||
│ └── Navbar.tsx # ATUALIZADO: Entrar / avatar + Sair
|
||||
└── App.tsx # ATUALIZADO: AuthProvider wrap + novas rotas
|
||||
```
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
*Sem violações de constituição — esta seção não se aplica.*
|
||||
137
.specify/features/005-authentication/quickstart.md
Normal file
137
.specify/features/005-authentication/quickstart.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Quickstart: 005-authentication
|
||||
|
||||
**Como configurar, migrar e testar o sistema de autenticação localmente.**
|
||||
|
||||
---
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
- Docker + Docker Compose rodando (`.\start.ps1` ou `docker-compose up`)
|
||||
- Backend acessível em `http://localhost:5000`
|
||||
- Frontend acessível em `http://localhost:5173`
|
||||
|
||||
---
|
||||
|
||||
## 1. Variáveis de Ambiente
|
||||
|
||||
Adicione ao `backend/.env` (e ao `docker-compose.yml` se o container não herdar o `.env`):
|
||||
|
||||
```
|
||||
JWT_SECRET_KEY=sua-chave-secreta-longa-e-aleatoria-aqui
|
||||
```
|
||||
|
||||
> Gere uma chave segura no terminal:
|
||||
> ```powershell
|
||||
> python -c "import secrets; print(secrets.token_hex(32))"
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## 2. Instalar Novas Dependências (backend)
|
||||
|
||||
```powershell
|
||||
cd backend
|
||||
uv add "PyJWT>=2.9" "bcrypt>=4.2" "pydantic[email]"
|
||||
```
|
||||
|
||||
Verifique que `uv.lock` foi atualizado e commit junto com o `pyproject.toml` atualizado.
|
||||
|
||||
---
|
||||
|
||||
## 3. Gerar e Aplicar a Migration
|
||||
|
||||
```powershell
|
||||
# Com DATABASE_URL definida ou container rodando:
|
||||
uv run flask db migrate -m "add client_users"
|
||||
uv run flask db upgrade
|
||||
```
|
||||
|
||||
Teste o ciclo completo antes de commitar:
|
||||
```powershell
|
||||
uv run flask db downgrade
|
||||
uv run flask db upgrade
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Testar os Endpoints Manualmente
|
||||
|
||||
### Cadastro
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "http://localhost:5000/api/v1/auth/register" `
|
||||
-Method POST `
|
||||
-ContentType "application/json" `
|
||||
-Body '{"name":"Teste","email":"teste@exemplo.com","password":"senha1234"}'
|
||||
```
|
||||
|
||||
### Login
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "http://localhost:5000/api/v1/auth/login" `
|
||||
-Method POST `
|
||||
-ContentType "application/json" `
|
||||
-Body '{"email":"teste@exemplo.com","password":"senha1234"}'
|
||||
```
|
||||
|
||||
### Perfil autenticado
|
||||
```powershell
|
||||
$TOKEN = "<access_token da resposta acima>"
|
||||
Invoke-WebRequest -Uri "http://localhost:5000/api/v1/auth/me" `
|
||||
-Headers @{ Authorization = "Bearer $TOKEN" }
|
||||
```
|
||||
|
||||
### Rota protegida sem token (deve retornar 401)
|
||||
```powershell
|
||||
Invoke-WebRequest -Uri "http://localhost:5000/api/v1/auth/me"
|
||||
# Espera: StatusCode 401
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Rodar os Testes (backend)
|
||||
|
||||
```powershell
|
||||
cd backend
|
||||
uv run pytest tests/ -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Frontend — Verificar os Fluxos
|
||||
|
||||
Com backend + frontend rodando:
|
||||
|
||||
| URL | Comportamento esperado |
|
||||
|-----|------------------------|
|
||||
| `http://localhost:5173/login` | Exibe formulário de login com estilo Linear dark |
|
||||
| `http://localhost:5173/cadastro` | Exibe formulário de cadastro |
|
||||
| `http://localhost:5173/area-do-cliente` | Redireciona para `/login` se não autenticado |
|
||||
| Após login bem-sucedido | Redireciona para `/area-do-cliente` |
|
||||
| Navbar sem autenticação | Botão "Entrar" visível |
|
||||
| Navbar autenticado | Inicial do usuário + botão "Sair" visível |
|
||||
|
||||
---
|
||||
|
||||
## 7. Verificar Token no localStorage
|
||||
|
||||
No console do navegador (após login):
|
||||
|
||||
```javascript
|
||||
localStorage.getItem('imob_token') // deve retornar o JWT string
|
||||
```
|
||||
|
||||
Após logout:
|
||||
|
||||
```javascript
|
||||
localStorage.getItem('imob_token') // deve retornar null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Checklist de Segurança Pós-implementação
|
||||
|
||||
- [ ] `JWT_SECRET_KEY` não aparece em nenhum arquivo versionado (`.env.example` pode ter placeholder)
|
||||
- [ ] `grep -r "JWT_SECRET" backend/app/` retorna apenas referências a `os.environ` ou `app.config`
|
||||
- [ ] Login com senha errada retorna 401 com mensagem genérica (não indica se e-mail ou senha está incorreto)
|
||||
- [ ] Cadastro com e-mail duplicado retorna 409 (não 500)
|
||||
- [ ] Acesso a `/api/v1/auth/me` sem token retorna 401 (não 500)
|
||||
- [ ] `password_hash` nunca aparece em nenhuma resposta JSON da API
|
||||
135
.specify/features/005-authentication/research.md
Normal file
135
.specify/features/005-authentication/research.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# Research: 005-authentication
|
||||
|
||||
**Fase 0 — Resolução de Unknowns**
|
||||
**Data**: 2026-04-13
|
||||
|
||||
---
|
||||
|
||||
## Decision Log
|
||||
|
||||
### 1. Biblioteca de hashing de senha
|
||||
|
||||
**Decision**: `bcrypt>=4.2`
|
||||
**Rationale**: Bcrypt é o padrão da indústria para hashing de senhas — adaptativo, lento por design, resistente a ataques de força bruta com GPUs. Usar diretamente (sem passar por `werkzeug.security`) mantém a cadeia de dependências mínima (Princípio VI) e está alinhado com a instrução de design.
|
||||
**Alternatives considered**:
|
||||
- `werkzeug.security.generate_password_hash` — encapsula bcrypt mas adiciona dependência desnecessária
|
||||
- `argon2-cffi` — mais forte que bcrypt, porém não justificado para MVP com ~100 usuários
|
||||
- `passlib` — camada de abstração extra (YAGNI)
|
||||
|
||||
**Usage pattern**:
|
||||
```python
|
||||
import bcrypt
|
||||
|
||||
# Hash
|
||||
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||
password_hash = hashed.decode("utf-8") # armazenar como String(100)
|
||||
|
||||
# Verify
|
||||
bcrypt.checkpw(password.encode("utf-8"), stored_hash.encode("utf-8"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Biblioteca JWT
|
||||
|
||||
**Decision**: `PyJWT>=2.9`
|
||||
**Rationale**: API limpa, amplamente adotada no ecossistema Flask, suporte nativo a expiração via claim `exp`. Leveza da dependência cumpre Princípio VI.
|
||||
**Alternatives considered**:
|
||||
- `python-jose` — orientado a JWE/JWKS, mais pesado (YAGNI)
|
||||
- `authlib` — voltado a OAuth2 / OpenID Connect (fora do escopo)
|
||||
- `flask-jwt-extended` — abstração extra útil para refresh tokens; sem refresh no escopo MVP, não se justifica
|
||||
|
||||
**Usage pattern**:
|
||||
```python
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Encode
|
||||
payload = {"sub": str(user.id), "exp": datetime.utcnow() + timedelta(days=7)}
|
||||
token = jwt.encode(payload, JWT_SECRET_KEY, algorithm="HS256")
|
||||
|
||||
# Decode (lança jwt.ExpiredSignatureError ou jwt.InvalidTokenError em falha)
|
||||
data = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. UUID como chave primária do ClientUser
|
||||
|
||||
**Decision**: `default=uuid.uuid4` (gerado no Python)
|
||||
**Rationale**: Consistência com o padrão já adotado no model `Property`. O ID gerado em Python fica disponível antes do flush, simplificando testes e a construção da resposta de registro.
|
||||
**Alternatives considered**:
|
||||
- `server_default=text("gen_random_uuid()")` — gerado no banco, menos consistente com padrão existente
|
||||
|
||||
---
|
||||
|
||||
### 4. Normalização de e-mail
|
||||
|
||||
**Decision**: `email.lower().strip()` como `@field_validator` nos schemas Pydantic (`RegisterIn` e `LoginIn`)
|
||||
**Rationale**: FR-005 exige normalização para minúsculas. Fazê-la na camada de schema garante que o banco nunca armazene variantes de case, e que a comparação no login seja sempre consistente — sem precisar de `LOWER()` em queries SQL.
|
||||
|
||||
---
|
||||
|
||||
### 5. Armazenamento do token no frontend
|
||||
|
||||
**Decision**: `localStorage` com chave `imob_token`
|
||||
**Rationale**: Escolha MVP pragmática. HttpOnly cookies seriam mais seguros contra XSS, mas exigiriam configuração adicional de CORS e CSRF. O projeto usa Axios com requisições XHR; consistência com o padrão SPA do projeto.
|
||||
**Alternatives considered**:
|
||||
- `sessionStorage` — perde estado ao fechar aba; viola SC-005
|
||||
- HttpOnly cookie — mais seguro para produção, mas exigiria mudança na política CORS/CSRF (fora do escopo MVP)
|
||||
|
||||
**Risk noted**: Token em `localStorage` é vulnerável a XSS. Mitigação via CSP headers fica na responsabilidade de infraestrutura (fora do escopo desta feature). Documentado como débito de segurança para versão futura.
|
||||
|
||||
---
|
||||
|
||||
### 6. Axios interceptor vs. wrapper manual
|
||||
|
||||
**Decision**: `axios.interceptors.request.use` na instância existente em `services/api.ts`
|
||||
**Rationale**: Centraliza a injeção do header `Authorization` em um único ponto. FR-015 é satisfeito sem nenhuma alteração nas chamadas existentes. Adicionar ao objeto Axios existente evita fragmentação de instâncias (Princípio VI).
|
||||
**Alternatives considered**:
|
||||
- Wrapper manual por serviço — duplicação de código
|
||||
- Novo arquivo `axiosInstance.ts` — fragmenta a instância sem ganho
|
||||
|
||||
---
|
||||
|
||||
### 7. Estrutura do `require_auth` decorator
|
||||
|
||||
**Decision**: Decorator simples em `backend/app/utils/auth.py` usando `flask.g`
|
||||
**Rationale**: Abordagem mínima e idiomática no Flask. `g` é o mecanismo nativo de context-local para guardar estado por requisição. Sem nova dependência.
|
||||
**Pattern**:
|
||||
```python
|
||||
from functools import wraps
|
||||
from flask import g, request, jsonify
|
||||
import jwt
|
||||
|
||||
def require_auth(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
token = _extract_token(request)
|
||||
if not token:
|
||||
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
|
||||
g.current_user = ClientUser.query.get(payload["sub"])
|
||||
if g.current_user is None:
|
||||
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
|
||||
except jwt.PyJWTError:
|
||||
return jsonify({"error": "Token de acesso inválido ou expirado."}), 401
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. `UserOut` sem `password_hash`
|
||||
|
||||
**Decision**: `UserOut` exclui `password_hash`
|
||||
**Rationale**: Nunca expor hash de senha em resposta da API. Princípio de menor privilégio (Constituição V). `UserOut` = {id, name, email, role, created_at}.
|
||||
|
||||
---
|
||||
|
||||
### 9. `pydantic[email]` para `EmailStr`
|
||||
|
||||
**Decision**: Adicionar `pydantic[email]` nas dependências (ou verificar se `email-validator` já está presente)
|
||||
**Rationale**: `pydantic.EmailStr` requer o pacote `email-validator`. Verificar `pyproject.toml` antes da tarefa de implementação — se ausente, adicionar junto com PyJWT e bcrypt.
|
||||
**Action**: A tarefa de instalação de dependências deve incluir `"pydantic[email]"` ou checar se `email-validator>=2.0` já está listado.
|
||||
140
.specify/features/005-authentication/spec.md
Normal file
140
.specify/features/005-authentication/spec.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Feature Specification: Sistema de Autenticação de Clientes
|
||||
|
||||
**Feature Branch**: `005-authentication`
|
||||
**Created**: 2026-04-13
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Sistema de autenticação para clientes: login com e-mail + senha (JWT, sem OAuth), auto-cadastro público."
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Cadastro Público de Novo Cliente (Priority: P1)
|
||||
|
||||
Qualquer pessoa pode criar uma conta no sistema fornecendo nome, e-mail e senha. Após o cadastro, o usuário recebe imediatamente um token de acesso e pode utilizar o sistema sem etapas adicionais de verificação.
|
||||
|
||||
**Why this priority**: É o ponto de entrada do sistema. Sem cadastro funcional, nenhuma outra funcionalidade de autenticação é utilizável. Entregar apenas P1 já permite que um novo usuário se registre e acesse a plataforma.
|
||||
|
||||
**Independent Test**: Pode ser testado isoladamente realizando um cadastro com dados válidos e verificando que o sistema retorna token de acesso e dados do usuário criado; e tentando cadastrar com e-mail duplicado ou senha fraca para verificar as rejeições.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** um visitante não autenticado, **When** ele submete nome, e-mail válido e senha com pelo menos 8 caracteres, **Then** o sistema cria a conta, retorna status 201 e fornece um token de acesso junto com os dados básicos do usuário (id, nome, e-mail).
|
||||
2. **Given** um visitante tenta se cadastrar com um e-mail já existente no sistema, **When** ele submete o formulário, **Then** o sistema retorna status 409 indicando conflito, sem revelar informações sobre a conta existente.
|
||||
3. **Given** um visitante tenta se cadastrar com senha contendo menos de 8 caracteres, **When** ele submete o formulário, **Then** o sistema retorna status 422 indicando que a senha não atende aos requisitos mínimos.
|
||||
4. **Given** o formulário de cadastro com campo "confirmar senha" preenchido de forma diferente da senha, **When** o usuário tenta submeter, **Then** o sistema exibe mensagem de validação no formulário sem enviar a requisição.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Login de Cliente Existente (Priority: P2)
|
||||
|
||||
Um cliente já cadastrado pode acessar o sistema fornecendo seu e-mail e senha. Após autenticação bem-sucedida, recebe um token de acesso e é redirecionado para sua área pessoal.
|
||||
|
||||
**Why this priority**: É o fluxo principal de acesso recorrente ao sistema. Depende de P1 (a conta precisa existir), mas pode ser desenvolvido e testado de forma independente com dados pré-cadastrados.
|
||||
|
||||
**Independent Test**: Pode ser testado isoladamente realizando login com credenciais válidas (espera token + dados do usuário) e com credenciais inválidas (espera 401 com mensagem genérica). Erro de rede deve exibir mensagem amigável.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** um cliente com conta cadastrada, **When** ele fornece e-mail e senha corretos, **Then** o sistema retorna status 200 com token de acesso e informações básicas do usuário (id, nome, e-mail).
|
||||
2. **Given** um visitante, **When** ele fornece e-mail não cadastrado ou senha incorreta, **Then** o sistema retorna status 401 com mensagem genérica — sem indicar qual campo está incorreto.
|
||||
3. **Given** um usuário no formulário de login, **When** ocorre erro de rede na requisição, **Then** o formulário exibe mensagem de erro amigável sem expor detalhes técnicos e permite nova tentativa.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Acesso a Rotas Protegidas com Token (Priority: P3)
|
||||
|
||||
Rotas marcadas como protegidas só podem ser acessadas por clientes autenticados. Requisições sem token ou com token inválido/expirado são rejeitadas automaticamente. A interface redireciona usuários não autenticados para o login.
|
||||
|
||||
**Why this priority**: Garante a integridade do sistema. Sem esse mecanismo, dados privados estariam acessíveis publicamente. Depende de P1 e P2 para que haja tokens válidos no sistema.
|
||||
|
||||
**Independent Test**: Pode ser testado tentando acessar uma rota protegida sem token (espera 401 e redirecionamento para /login), com token válido (espera dados normais), e com token expirado ou adulterado (espera 401).
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** um cliente autenticado com token válido, **When** ele acessa uma rota protegida enviando o token no cabeçalho de autorização, **Then** o sistema processa a requisição normalmente e retorna os dados solicitados.
|
||||
2. **Given** uma requisição sem token de autorização, **When** tenta acessar uma rota protegida, **Then** o sistema retorna status 401.
|
||||
3. **Given** uma requisição com token expirado ou adulterado, **When** tenta acessar uma rota protegida, **Then** o sistema retorna status 401.
|
||||
4. **Given** um usuário não autenticado navegando na interface, **When** tenta acessar uma página protegida, **Then** a interface redireciona automaticamente para a tela de login.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Visualização do Perfil do Cliente Autenticado (Priority: P4)
|
||||
|
||||
Um cliente autenticado pode consultar seus próprios dados de perfil: nome, e-mail, papel e data de criação da conta, sem precisar informar o próprio identificador na URL.
|
||||
|
||||
**Why this priority**: Funcionalidade de suporte à identidade do usuário logado. Depende dos fluxos P1–P3 para ter sentido prático.
|
||||
|
||||
**Independent Test**: Pode ser testado acessando o endpoint de perfil com token válido (espera dados completos do usuário autenticado) e sem token (espera 401).
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** um cliente autenticado, **When** ele consulta o endpoint de perfil próprio, **Then** o sistema retorna id, nome, e-mail, papel (role) e data de criação da conta.
|
||||
2. **Given** uma requisição sem token de autorização, **When** tenta consultar o perfil, **Then** o sistema retorna status 401.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- **E-mail duplicado no cadastro**: Sistema retorna 409 Conflict sem revelar dados da conta existente.
|
||||
- **Senha abaixo do mínimo**: Sistema retorna 422 Unprocessable Entity com descrição do critério não atendido.
|
||||
- **Token JWT adulterado ou expirado**: Sistema retorna 401 Unauthorized em qualquer rota protegida.
|
||||
- **Credenciais inválidas no login**: Sistema retorna 401 com mensagem genérica — não revela se o e-mail ou a senha está errado.
|
||||
- **Erro de rede nos formulários**: Interface exibe mensagem de erro amigável e permite nova tentativa, sem expor erros técnicos.
|
||||
- **Usuário não autenticado em rota protegida**: Interface redireciona para /login preservando a intenção de navegação.
|
||||
- **Senha de confirmação não coincide**: Validação no formulário impede envio e exibe mensagem explicativa.
|
||||
- **Token armazenado localmente e sessão do servidor**: Como o sistema é stateless, não há sessão a invalidar; o logout apenas remove o token do armazenamento do navegador.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: O sistema DEVE permitir que qualquer visitante crie uma conta fornecendo nome, e-mail e senha, sem necessidade de aprovação prévia.
|
||||
- **FR-002**: O sistema DEVE validar que senhas tenham no mínimo 8 caracteres no momento do cadastro e retornar erro de validação (422) quando o critério não for atendido.
|
||||
- **FR-003**: O sistema DEVE rejeitar cadastros com e-mail já existente retornando resposta 409, sem revelar informações sobre a conta existente.
|
||||
- **FR-004**: O sistema DEVE armazenar senhas de forma irreversível — nunca em texto puro.
|
||||
- **FR-005**: O sistema DEVE normalizar e-mails para letras minúsculas tanto no armazenamento quanto na comparação durante o login.
|
||||
- **FR-006**: O sistema DEVE emitir um token de acesso com validade de 7 dias após cadastro ou login bem-sucedido.
|
||||
- **FR-007**: O sistema DEVE retornar resposta genérica (401) em caso de credenciais inválidas no login, sem indicar qual campo (e-mail ou senha) está incorreto.
|
||||
- **FR-008**: O sistema DEVE proteger as rotas designadas como privadas, rejeitando com 401 qualquer requisição que não apresente token válido no cabeçalho de autorização.
|
||||
- **FR-009**: O sistema DEVE disponibilizar endpoint para o cliente autenticado consultar seus próprios dados de perfil (id, nome, e-mail, papel, data de criação).
|
||||
- **FR-010**: O sistema DEVE carregar o segredo de assinatura do token a partir de variável de ambiente — nunca embutido no código-fonte.
|
||||
- **FR-011**: A interface DEVE manter o estado de autenticação do usuário entre navegações na aplicação durante a vigência do token.
|
||||
- **FR-012**: A interface DEVE redirecionar automaticamente usuários não autenticados para a tela de login ao tentar acessar áreas protegidas.
|
||||
- **FR-013**: A interface DEVE redirecionar usuários para a área do cliente após login ou cadastro bem-sucedido.
|
||||
- **FR-014**: A interface DEVE exibir opção de "Entrar" na barra de navegação para visitantes não autenticados, e o nome do usuário com opção de "Sair" quando autenticado.
|
||||
- **FR-015**: A interface DEVE adicionar o token de autenticação automaticamente em todas as requisições para rotas protegidas, sem necessidade de configuração manual por página.
|
||||
- **FR-016**: A interface DEVE exibir mensagem de erro amigável quando ocorrer falha de rede nos formulários de login ou cadastro.
|
||||
|
||||
### API Contract
|
||||
|
||||
| Endpoint | Método | Corpo da Requisição | Resposta de Sucesso | Respostas de Erro |
|
||||
|----------|--------|---------------------|---------------------|-------------------|
|
||||
| `/api/v1/auth/register` | POST | `{name, email, password}` | 201 `{access_token, user: {id, name, email}}` | 409 (e-mail duplicado), 422 (validação) |
|
||||
| `/api/v1/auth/login` | POST | `{email, password}` | 200 `{access_token, user: {id, name, email}}` | 401 (credenciais inválidas) |
|
||||
| `/api/v1/auth/me` | GET | — (Bearer token no header) | 200 `{id, name, email, role, created_at}` | 401 (sem token ou token inválido) |
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **ClientUser**: Representa um cliente cadastrado no sistema. Atributos: identificador único (UUID), nome completo (até 150 caracteres), e-mail (único no sistema, até 254 caracteres), senha protegida (hash irreversível), papel no sistema (padrão: 'client'), data e hora de criação da conta.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: Um novo visitante consegue criar uma conta e receber token de acesso em menos de 2 minutos ao utilizar o formulário de cadastro.
|
||||
- **SC-002**: Um cliente cadastrado consegue realizar login e ser redirecionado para sua área em menos de 30 segundos.
|
||||
- **SC-003**: 100% das tentativas de acesso a rotas protegidas sem token válido resultam em rejeição (401) — nenhuma rota protegida é acessível sem autenticação.
|
||||
- **SC-004**: 100% das senhas armazenadas no sistema são protegidas — nenhuma é recuperável em texto puro a partir do banco de dados.
|
||||
- **SC-005**: O estado de autenticação do usuário persiste entre recarregamentos de página durante o período de validade do token.
|
||||
- **SC-006**: Nenhuma resposta do sistema revela se a falha de login se deve ao e-mail ou à senha incorretos — todas as falhas de credencial retornam a mesma mensagem genérica.
|
||||
- **SC-007**: O segredo de assinatura de token nunca aparece no código-fonte versionado — auditoria do repositório não encontra nenhuma ocorrência da chave em texto puro.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- **Sem verificação de e-mail**: A confirmação de conta via link de e-mail está fora do escopo desta versão (MVP). Toda conta criada é imediatamente ativa.
|
||||
- **Sem recuperação de senha**: O fluxo "esqueci minha senha" está fora do escopo desta versão.
|
||||
- **Apenas papel 'client'**: O papel 'admin' é reservado para uso futuro e não possui proteção ou fluxo de acesso implementados nesta versão.
|
||||
- **Sem refresh token**: A renovação automática do token de acesso está fora do escopo desta versão. O usuário precisará fazer login novamente após a expiração (7 dias).
|
||||
- **Sem rate limiting de login**: Limitação de tentativas de acesso é responsabilidade de infraestrutura e está fora do escopo desta feature.
|
||||
- **HTTPS em produção**: A segurança do token em trânsito é garantida pela camada de transporte. O sistema pressupõe ambiente HTTPS em produção.
|
||||
- **Token no armazenamento do navegador**: O token é mantido no armazenamento local do navegador; o logout é realizado removendo-o localmente (o servidor não invalida tokens emitidos, pois o sistema é stateless).
|
||||
- **Design conforme sistema vigente**: Telas de login e cadastro seguem o design system definido em `DESIGN.md` — tema Linear escuro com paleta de cores e tipografia já estabelecidas no projeto.
|
||||
251
.specify/features/005-authentication/tasks.md
Normal file
251
.specify/features/005-authentication/tasks.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# Tasks: Sistema de Autenticação de Clientes
|
||||
|
||||
**Feature**: `005-authentication`
|
||||
**Branch**: `005-authentication`
|
||||
**Input**: `spec.md`, `plan.md`, `data-model.md`, `contracts/auth-api.md`, `quickstart.md`
|
||||
**Generated**: 2026-04-13
|
||||
**Status**: Ready for implementation
|
||||
|
||||
---
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
- [ ] T[NNN] [P?] [USN?] Description — path/to/file.ext
|
||||
```
|
||||
|
||||
- **[P]** — Paralelizável (arquivo diferente, sem dependência de tarefa incompleta)
|
||||
- **[USN]** — User Story associada (US1–US4)
|
||||
- IDs sequenciais na ordem de execução recomendada
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Setup — Infraestrutura e Dependências
|
||||
|
||||
**Objetivo**: Instalar as bibliotecas necessárias e configurar a variável de ambiente do segredo JWT. Sem estas tarefas nenhum código de autenticação pode ser executado.
|
||||
|
||||
**⚠️ CRÍTICO**: Todas as fases seguintes dependem de T001 e T002 estarem concluídas.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T001 | S | — | plan.md §Primary Dependencies, quickstart.md §2 |
|
||||
| T002 | S | — | plan.md §Constraints, spec.md §FR-010, SC-007 |
|
||||
|
||||
- [X] T001 Adicionar `"PyJWT>=2.9"`, `"bcrypt>=4.2"` e `"pydantic[email]"` às dependências em `[project].dependencies` e executar `uv add "PyJWT>=2.9" "bcrypt>=4.2" "pydantic[email]"` para atualizar `uv.lock` — `backend/pyproject.toml`
|
||||
- **Done when**: `import jwt`, `import bcrypt` e `from pydantic import EmailStr` executam sem erro dentro do container; `uv.lock` reflete as três novas dependências; `uv run python -c "import jwt, bcrypt; from pydantic import EmailStr; print('ok')"` imprime `ok`.
|
||||
|
||||
- [X] T002 Adicionar variável `JWT_SECRET_KEY=<valor-gerado>` ao arquivo `backend/.env` (para desenvolvimento local) e como variável de ambiente no serviço `backend` do `docker-compose.yml`; o valor NUNCA deve ser string vazia ou placeholder óbvio como `"secret"` — `backend/.env` e `docker-compose.yml`
|
||||
- **Done when**: `docker-compose config` mostra `JWT_SECRET_KEY` definida para o serviço `backend`; `grep -r "JWT_SECRET_KEY" backend/app/` não retorna nenhuma ocorrência com valor embutido (apenas leituras via `os.environ` ou `current_app.config`); `backend/.env` contém chave com pelo menos 32 caracteres hexadecimais.
|
||||
|
||||
**Checkpoint Phase 1**: `uv run python -c "import jwt, bcrypt"` executa sem erro no container; `JWT_SECRET_KEY` disponível como variável de ambiente.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational — Modelo, Migration e Schemas Base
|
||||
|
||||
**Objetivo**: Criar a tabela `client_users` no banco de dados e os schemas Pydantic compartilhados que todas as user stories utilizam. Estas tarefas bloqueiam a implementação de qualquer endpoint.
|
||||
|
||||
**⚠️ CRÍTICO**: T005 (rotas de auth) e T009 (frontend types) não podem avançar sem T003 e T004 concluídos.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T003 | S | T001 | data-model.md §ClientUser, spec.md §Key Entities |
|
||||
| T004 | M | T003 | data-model.md §DDL, quickstart.md §3 |
|
||||
| T005 | S | T001 | data-model.md §Schemas Pydantic |
|
||||
|
||||
- [X] T003 Criar modelo SQLAlchemy `ClientUser` com colunas `id` (UUID PK, `default=uuid.uuid4`), `name` (String 150, NOT NULL), `email` (String 254, NOT NULL, unique=True, index=True), `password_hash` (String 100, NOT NULL), `role` (String 20, NOT NULL, `default='client'`), `created_at` (DateTime, NOT NULL, `server_default=func.now()`); importar o modelo em `backend/app/models/__init__.py` — `backend/app/models/user.py` e `backend/app/models/__init__.py`
|
||||
- **Done when**: `from app.models.user import ClientUser` importa sem erro; `ClientUser.__tablename__ == "client_users"`; `ClientUser.email` tem `unique=True` e `index=True`; `ClientUser.role` tem `default='client'`; `from app.models import ClientUser` importa sem erro (via `__init__.py`); Flask-Migrate detecta a tabela ao rodar `flask db migrate`.
|
||||
|
||||
- [X] T004 Gerar e aplicar migration Alembic criando a tabela `client_users` com todas as colunas e índice `ix_client_users_email` — `backend/migrations/versions/<hash>_add_client_users.py`
|
||||
- **Done when**: `uv run flask db migrate -m "add client_users"` cria arquivo de migration; revisão manual confirma `op.create_table("client_users", ...)` com colunas `id`, `name`, `email`, `password_hash`, `role`, `created_at` e `op.create_index("ix_client_users_email", "client_users", ["email"], unique=True)`; `flask db upgrade` executa sem erro; `flask db downgrade -1` reverte sem erro; `flask db upgrade` re-aplica sem erro.
|
||||
|
||||
- [X] T005 [P] Criar schemas Pydantic `RegisterIn` (name: str min=1/max=150, email: EmailStr normalizado lowercase, password: str min=8), `LoginIn` (email: EmailStr normalizado lowercase, password: str), `UserOut` (id: UUID, name: str, email: str, role: str, created_at: datetime; `model_config = ConfigDict(from_attributes=True)`), `AuthTokenOut` (access_token: str, user: UserOut) — `backend/app/schemas/auth.py`
|
||||
- **Done when**: `from app.schemas.auth import RegisterIn, LoginIn, UserOut, AuthTokenOut` importa sem erro; `RegisterIn(name="A", email="TESTE@EX.COM", password="12345678").email == "teste@ex.com"` (normalização lowercase); `RegisterIn(name="A", email="ok@ok.com", password="1234567")` levanta `ValidationError` (senha < 8 chars); `UserOut.model_validate(client_user_instance)` serializa sem `password_hash`; `AuthTokenOut(access_token="tok", user=user_out_instance)` serializa corretamente.
|
||||
|
||||
**Checkpoint Phase 2**: Tabela `client_users` existe no banco; `from app.schemas.auth import RegisterIn` funciona; `flask db upgrade` passa sem erro.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: US1 — Cadastro Público de Novo Cliente (Priority: P1) 🎯 MVP
|
||||
|
||||
**Goal**: Qualquer visitante pode criar conta (POST /register); o sistema valida os dados, armazena senha com bcrypt e retorna token JWT + dados do usuário. O formulário de cadastro no frontend permite o fluxo completo.
|
||||
|
||||
**Independent Test**: `POST /api/v1/auth/register` com dados válidos retorna 201 + token; e-mail duplicado retorna 409; senha < 8 chars retorna 422; formulário no frontend completa o cadastro e redireciona para `/area-do-cliente`.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T006 | S | T005 | plan.md §require_auth decorator, spec.md §FR-008, FR-010 |
|
||||
| T007 | M | T003, T005, T006 | spec.md §API Contract, contracts/auth-api.md §register |
|
||||
| T008 | S | T007 | plan.md §backend/__init__.py |
|
||||
| T009 | S | — | plan.md §frontend types, spec.md §FR-011 |
|
||||
| T010 | S | T009 | plan.md §api.ts interceptor, spec.md §FR-015 |
|
||||
| T011 | S | T009 | plan.md §auth service, contracts/auth-api.md |
|
||||
| T012 | M | T009, T011 | plan.md §AuthContext, spec.md §FR-011, FR-013 |
|
||||
| T015 | M | T009, T011, T012 | spec.md §US1, FR-013, SC-001 |
|
||||
|
||||
- [X] T006 Criar decorator `require_auth` em `backend/app/utils/auth.py`: extrai Bearer token do header `Authorization`; decodifica via `jwt.decode(token, current_app.config["JWT_SECRET_KEY"], algorithms=["HS256"])`; em caso de token ausente, expirado ou inválido retorna `jsonify({"error": "Não autorizado."})` com status 401; em caso de sucesso, injeta `g.current_user_id = payload["sub"]` e chama o endpoint original — `backend/app/utils/auth.py`
|
||||
- **Done when**: `from app.utils.auth import require_auth` importa sem erro; rota decorada com `@require_auth` retorna 401 quando `Authorization` está ausente; retorna 401 com token manipulado; retorna 401 com token expirado (TTL forçado para 0s em teste); `g.current_user_id` contém o UUID como string após autenticação bem-sucedida.
|
||||
|
||||
- [X] T007 Criar blueprint `auth_bp` com prefixo `/api/v1/auth` contendo três endpoints: (1) `POST /register` — valida `RegisterIn`, verifica e-mail duplicado (retorna 409 se existir), faz hash da senha com `bcrypt.hashpw`, persiste `ClientUser`, emite JWT com `sub=str(user.id)` e `exp=utcnow+7dias`, retorna `AuthTokenOut` com status 201; (2) `POST /login` — valida `LoginIn`, busca usuário por email, verifica senha com `bcrypt.checkpw` (retorna 401 genérico se inválido), emite JWT, retorna `AuthTokenOut` com status 200; (3) `GET /me` protegida por `@require_auth` — busca usuário por `g.current_user_id`, retorna `UserOut` com status 200 (retorna 401 se usuário não encontrado) — `backend/app/routes/auth.py`
|
||||
- **Done when**: `POST /api/v1/auth/register` com dados válidos retorna 201 com `access_token` e `user` (sem `password_hash`); segundo POST com mesmo e-mail retorna 409; POST com senha de 5 chars retorna 422; `POST /api/v1/auth/login` com credenciais corretas retorna 200 + token; login com e-mail errado retorna 401; login com senha errada retorna 401 (mesma mensagem genérica); `GET /api/v1/auth/me` com token válido retorna 200 com `id`, `name`, `email`, `role`, `created_at`; `GET /api/v1/auth/me` sem token retorna 401.
|
||||
|
||||
- [X] T008 Registrar `auth_bp` na factory `create_app()` em `backend/app/__init__.py`: adicionar `from app.routes.auth import auth_bp` e `app.register_blueprint(auth_bp)`; garantir import de `ClientUser` antes do `db.create_all()` ou Alembic para que o modelo seja detectado; adicionar `app.config["JWT_SECRET_KEY"] = os.environ["JWT_SECRET_KEY"]` (raise `KeyError` se ausente) — `backend/app/__init__.py`
|
||||
- **Done when**: Servidor Flask inicia sem erros após a alteração; `GET /api/v1/auth/register` retorna 405 (rota existe, método não permitido) — confirmando registro do blueprint; `app.config["JWT_SECRET_KEY"]` está definido em runtime; ausência da variável de ambiente no startup causa `KeyError` explícito (não silencioso).
|
||||
|
||||
- [X] T009 [P] Criar interfaces TypeScript `User` (id: string, name: string, email: string, role: string, created_at: string), `AuthTokenResponse` (access_token: string, user: User), `LoginCredentials` (email: string, password: string), `RegisterCredentials` (name: string, email: string, password: string, confirmPassword: string) e `AuthState` (user: User | null, isAuthenticated: boolean, isLoading: boolean) — `frontend/src/types/auth.ts`
|
||||
- **Done when**: `import { User, AuthState, AuthTokenResponse, LoginCredentials, RegisterCredentials } from '@/types/auth'` compila sem erro TypeScript; `RegisterCredentials` inclui `confirmPassword` (validação apenas no frontend); `AuthState.user` é `null` quando não autenticado.
|
||||
|
||||
- [X] T010 [P] Atualizar o arquivo de serviços para exportar instância Axios com `baseURL` do backend; adicionar interceptor de request que injeta `Authorization: Bearer <token>` quando `localStorage.getItem("auth_token")` não for null; adicionar interceptor de response que limpa `localStorage` e redireciona para `/login` em resposta 401 — `frontend/src/services/api.ts`
|
||||
- **Done when**: `import api from '@/services/api'` compila sem erro; requisição com token no `localStorage` inclui header `Authorization: Bearer <token>` (verificável no DevTools Network); requisição sem token não inclui o header; resposta 401 aciona limpeza do `localStorage` e navegação para `/login` sem loop (verificar que `/login` em si não dispara o interceptor em loop).
|
||||
|
||||
- [X] T011 [P] Criar serviço de autenticação com funções `registerUser(data: RegisterCredentials): Promise<AuthTokenResponse>`, `loginUser(data: LoginCredentials): Promise<AuthTokenResponse>` e `getMe(): Promise<User>`, todas chamando os endpoints do blueprint `auth_bp` via instância `api`; nenhuma URL hardcoded — `frontend/src/services/auth.ts`
|
||||
- **Done when**: `import { registerUser, loginUser, getMe } from '@/services/auth'` compila sem erro TypeScript; `registerUser` chama `POST /api/v1/auth/register`; `loginUser` chama `POST /api/v1/auth/login`; `getMe` chama `GET /api/v1/auth/me`; erros 4xx/5xx são propagados para o caller sem silenciamento; nenhuma URL hardcoded (usa instância `api` de `api.ts`).
|
||||
|
||||
- [X] T012 Criar `AuthContext` com `AuthProvider` exportado que: inicializa com `isLoading: true`; em `useEffect` inicial tenta `getMe()` (se `localStorage` tem `auth_token`), define `user` e `isAuthenticated: true` em sucesso, limpa token e define `isAuthenticated: false` em falha; expõe funções `login(credentials): Promise<void>` (chama `loginUser`, salva token, define user) e `register(credentials): Promise<void>` (chama `registerUser`, salva token, define user) e `logout(): void` (remove token, limpa user, navega para `/login`); exportar hook `useAuth()` que consome o contexto — `frontend/src/contexts/AuthContext.tsx`
|
||||
- **Done when**: `import { AuthProvider, useAuth } from '@/contexts/AuthContext'` compila sem erro; `useAuth()` fora do `AuthProvider` lança erro descritivo; após `login()` bem-sucedido `isAuthenticated === true` e `user` contém dados do servidor; `logout()` limpa estado e navega para `/login`; recarregar página com token válido no `localStorage` restaura sessão (`isAuthenticated: true`); recarregar com token expirado resulta em `isAuthenticated: false`.
|
||||
|
||||
- [X] T015 [US1] Criar `RegisterPage` com formulário contendo campos `name` (obrigatório), `email` (obrigatório, formato), `password` (obrigatório, mín. 8 chars), `confirmPassword` (obrigatório, deve igualar `password`); validação `confirmPassword !== password` impede envio e exibe mensagem no campo; ao submeter chama `register()` do `useAuth()`; em sucesso redireciona para `/area-do-cliente`; em erro de rede exibe mensagem amigável; exibe spinner no botão durante requisição; link para `/login` para usuários já cadastrados; segue design system DESIGN.md (fundo `#08090a`, painel `#0f1011`, accent `#5e6ad2`/`#7170ff`, tipografia Inter Variable) — `frontend/src/pages/RegisterPage.tsx`
|
||||
- **Done when**: Senha < 8 chars exibe erro de validação sem enviar requisição; `confirmPassword` diferente exibe "As senhas não coincidem" sem enviar; POST inválido (e-mail duplicado) exibe mensagem de erro amigável; POST válido redireciona para `/area-do-cliente`; botão desabilitado durante `isSubmitting`; `npm run build` não apresenta erros TypeScript; estilos correspondem ao design system vigente.
|
||||
|
||||
**Checkpoint Phase 3**: `POST /api/v1/auth/register` funciona end-to-end; formulário de cadastro no frontend completa o fluxo; usuário pode ver `/area-do-cliente` após cadastro.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: US2 — Login de Cliente Existente (Priority: P2)
|
||||
|
||||
**Goal**: Um cliente cadastrado pode autenticar-se com e-mail e senha, receber token JWT e ser redirecionado para `/area-do-cliente`. Erros de credencial retornam mensagem genérica.
|
||||
|
||||
**Independent Test**: Login com credenciais corretas redireciona para `/area-do-cliente`; e-mail errado retorna 401 com mensagem genérica; senha errada retorna 401 com mesma mensagem genérica; erro de rede exibe mensagem amigável.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T014 | M | T009, T011, T012 | spec.md §US2, FR-013, SC-002 |
|
||||
|
||||
> **Nota**: O endpoint `POST /api/v1/auth/login` foi implementado em T007 (Phase 3). Esta fase foca na interface do cliente.
|
||||
|
||||
- [X] T014 [US2] Criar `LoginPage` com formulário contendo campos `email` (obrigatório, formato) e `password` (obrigatório); ao submeter chama `login()` do `useAuth()`; em sucesso redireciona para `/area-do-cliente`; em erro 401 exibe "E-mail ou senha incorretos." sem indicar qual campo falhou; em erro de rede exibe "Erro de conexão. Tente novamente."; exibe spinner no botão durante requisição; link para `/cadastro` para novos usuários; segue design system DESIGN.md — `frontend/src/pages/LoginPage.tsx`
|
||||
- **Done when**: E-mail inválido (formato) exibe erro de validação sem enviar requisição; credenciais erradas exibem "E-mail ou senha incorretos." (mesmo texto para e-mail ou senha incorretos); credenciais corretas redirecionam para `/area-do-cliente`; botão desabilitado durante `isSubmitting`; `npm run build` não apresenta erros TypeScript; estilos correspondem ao design system vigente.
|
||||
|
||||
**Checkpoint Phase 4**: Login end-to-end funcional — cliente existente autentica-se e é redirecionado; 401 exibe mensagem genérica; erro de rede tratado.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: US3 — Acesso a Rotas Protegidas com Token (Priority: P3)
|
||||
|
||||
**Goal**: Rotas protegidas no frontend redirecionam para `/login` usuários não autenticados. Rotas protegidas no backend rejeitam requisições sem token válido com 401.
|
||||
|
||||
**Independent Test**: Acessar `/area-do-cliente` sem token redireciona para `/login`; com token válido renderiza conteúdo; token adulterado/expirado retorna 401 do backend e redireciona para `/login` no frontend.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T013 | S | T009, T012 | spec.md §US3, FR-012 |
|
||||
| T016 | M | T012, T013, T014, T015 | spec.md §FR-011, FR-012, FR-013, FR-015 |
|
||||
|
||||
> **Nota**: O decorator `@require_auth` foi implementado em T006 (Phase 3). Esta fase foca no roteamento protegido do frontend e na integração final do App.tsx.
|
||||
|
||||
- [X] T013 [P] [US3] Criar componente `ProtectedRoute` que consome `useAuth()`; enquanto `isLoading === true` renderiza spinner ou `null` (evita redirect prematuro); se `!isAuthenticated` retorna `<Navigate to="/login" replace />`; caso contrário renderiza `<Outlet />` — `frontend/src/components/ProtectedRoute.tsx`
|
||||
- **Done when**: Sem token no `localStorage`, acessar rota protegida redireciona para `/login`; com token válido, a rota protegida renderiza normalmente; durante `isLoading` (verificação inicial) não há redirect prematuro; `npm run build` não apresenta erros TypeScript.
|
||||
|
||||
- [X] T016 [US3] Atualizar `App.tsx` para: envolver toda a árvore de rotas com `<AuthProvider>`; adicionar rota `<Route path="/login" element={<LoginPage />} />`; adicionar rota `<Route path="/cadastro" element={<RegisterPage />} />`; criar grupo de rotas protegidas com `<Route element={<ProtectedRoute />}>` contendo `<Route path="/area-do-cliente" element={<ClientAreaPage />} />` (pode ser página placeholder); importar todos os novos componentes e páginas — `frontend/src/App.tsx`
|
||||
- **Done when**: `/login` renderiza `LoginPage`; `/cadastro` renderiza `RegisterPage`; `/area-do-cliente` sem autenticação redireciona para `/login`; `/area-do-cliente` com autenticação renderiza conteúdo (pode ser placeholder); `npm run build` sem erros TypeScript; `<AuthProvider>` envolve todas as rotas.
|
||||
|
||||
**Checkpoint Phase 5**: Fluxo completo de proteção funcional — usuário não autenticado é redirecionado; token inválido é tratado; `ProtectedRoute` garante isolamento.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: US4 — Visualização do Perfil do Cliente Autenticado (Priority: P4)
|
||||
|
||||
**Goal**: Cliente autenticado pode consultar seus dados de perfil via `GET /api/v1/auth/me`. A Navbar exibe nome do usuário + opção de logout quando autenticado.
|
||||
|
||||
**Independent Test**: `GET /api/v1/auth/me` com token válido retorna `id, name, email, role, created_at`; sem token retorna 401; Navbar exibe "Entrar" para visitantes e nome + "Sair" para autenticados.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T017 | S | T012 | spec.md §US4, FR-014, SC-005 |
|
||||
|
||||
> **Nota**: O endpoint `GET /api/v1/auth/me` foi implementado em T007 (Phase 3). Esta fase foca na Navbar.
|
||||
|
||||
- [X] T017 [US4] Atualizar `Navbar` para consumir `useAuth()`; quando `!isAuthenticated` exibe botão/link "Entrar" apontando para `/login`; quando `isAuthenticated` exibe nome do usuário (`user.name`) truncado se necessário + botão "Sair" que chama `logout()`; durante `isLoading` exibe placeholder neutro (evita flash de estado incorreto); garantir que o estado mude reativamente sem reload — `frontend/src/components/Navbar.tsx`
|
||||
- **Done when**: Navbar exibe "Entrar" para visitante; após login exibe nome do usuário e "Sair"; clicar em "Sair" chama `logout()`, limpa sessão e exibe "Entrar" novamente; durante `isLoading` não há flash de "Entrar"/"Sair"; `npm run build` não apresenta erros TypeScript.
|
||||
|
||||
**Checkpoint Phase 6**: Perfil consultável via API; Navbar reflete estado de autenticação reativamente.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Polish & Cross-Cutting Concerns
|
||||
|
||||
**Objetivo**: Validações finais de segurança, conformidade com requisitos não-funcionais e verificação do quickstart.
|
||||
|
||||
| ID | Complexidade | Deps | spec_ref |
|
||||
|----|-------------|------|----------|
|
||||
| T018 | S | T007 | spec.md §SC-006, SC-007, FR-007 |
|
||||
| T019 | S | T001–T017 | quickstart.md §4, spec.md §SC-001–SC-003 |
|
||||
|
||||
- [X] T018 [P] Auditar `backend/app/routes/auth.py` e `backend/app/utils/auth.py` para garantir: (a) nenhuma resposta contém `password_hash`; (b) mensagens de erro de login são idênticas para e-mail inexistente e senha incorreta (SC-006); (c) `JWT_SECRET_KEY` nunca aparece em log ou resposta; (d) `bcrypt.checkpw` é chamado mesmo quando o usuário não existe (para evitar timing attack de enumeração de e-mail) — `backend/app/routes/auth.py` e `backend/app/utils/auth.py`
|
||||
- **Done when**: `grep -r "password_hash" backend/app/routes/ backend/app/schemas/` retorna apenas declarações de model, nunca em serialização de resposta; curl de login com e-mail inexistente e com senha errada retornam exatamente a mesma resposta JSON; `grep -r "JWT_SECRET_KEY" backend/app/` mostra apenas leituras via `current_app.config`, nunca o valor em si; revisão manual confirma `bcrypt.checkpw` executado mesmo para usuário não encontrado (dummy hash).
|
||||
|
||||
- [X] T019 Executar o roteiro completo do `quickstart.md`: instalar dependências, gerar/aplicar migration, testar os três endpoints via `Invoke-WebRequest` (register, login, me), verificar que o frontend completa o fluxo cadastro → área do cliente e login → área do cliente — `quickstart.md` (verificação, sem edição de código)
|
||||
- **Done when**: Todos os comandos do quickstart executam sem erro; `POST /register` retorna 201; `POST /login` retorna 200; `GET /me` com token retorna 200; frontend em `http://localhost:5173/cadastro` completa cadastro e redireciona; `http://localhost:5173/login` completa login e redireciona; `http://localhost:5173/area-do-cliente` sem sessão redireciona para `/login`.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
T001 ──┬── T003 ──── T004
|
||||
│ └── T007 ──────┐
|
||||
└── T005 ──── T007 │
|
||||
│
|
||||
T002 ──────────── T008 ◄────────┘
|
||||
|
||||
T006 ──────────── T007
|
||||
|
||||
T009 ──┬── T010
|
||||
├── T011 ──┬── T012 ──┬── T013 ──┐
|
||||
│ └── T015 ├── T014 │
|
||||
│ └── T015 │
|
||||
└── (via T012) │
|
||||
│
|
||||
T013 ──────────── T016 ◄────────────────┘
|
||||
T014 ──────────── T016
|
||||
T015 ──────────── T016
|
||||
|
||||
T012 ──────────── T017
|
||||
|
||||
T007, T016, T017 ── T018, T019
|
||||
```
|
||||
|
||||
## Parallel Execution Examples
|
||||
|
||||
### Backend e Frontend em paralelo
|
||||
|
||||
```
|
||||
# Terminal 1 — Backend
|
||||
T001 → T002 → T003 → T004 → T005 (paralelo com T003) →
|
||||
T006 → T007 → T008 → auditar T018
|
||||
|
||||
# Terminal 2 — Frontend (pode iniciar após T001)
|
||||
T009 → T010 (paralelo), T011 (paralelo) →
|
||||
T012 → T013 (paralelo), T014 (paralelo), T015 (paralelo) →
|
||||
T016 → T017
|
||||
```
|
||||
|
||||
### Componentes frontend em paralelo (após T012)
|
||||
|
||||
```
|
||||
# T013, T014, T015 podem ser implementados em paralelo
|
||||
# pois estão em arquivos distintos e dependem apenas de T012
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy (MVP Scope)
|
||||
|
||||
| Prioridade | Fase | User Story | Tarefas | Entregável |
|
||||
|---|---|---|---|---|
|
||||
| **MVP** | Setup + Foundational + Phase 3 | US1 (Cadastro) | T001–T008, T009–T012, T015 | Usuário pode se cadastrar e acessar área do cliente |
|
||||
| **Incremental** | Phase 4 | US2 (Login) | T014 | Usuário existente pode fazer login |
|
||||
| **Incremental** | Phase 5 | US3 (Proteção) | T013, T016 | Rotas protegidas e redirecionamento |
|
||||
| **Completude** | Phase 6 | US4 (Perfil) | T017 | Navbar com estado de autenticação |
|
||||
|
||||
**MVP mínimo**: T001 → T002 → T003 → T004 → T005 → T006 → T007 → T008 → T009 → T010 → T011 → T012 → T015 → T016 (básico)
|
||||
Loading…
Add table
Add a link
Reference in a new issue