feat: add full project - backend, frontend, docker, specs and configs

This commit is contained in:
MatheusAlves96 2026-04-20 23:59:45 -03:00
parent b77c7d5a01
commit e6cb06255b
24489 changed files with 61341 additions and 36 deletions

View file

@ -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`.

View 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 | ✅ | 1150 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)

View 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.

View 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.*

View 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

View 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.

View 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 P1P3 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.

View 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 (US1US4)
- 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 | T001T017 | quickstart.md §4, spec.md §SC-001SC-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) | T001T008, T009T012, 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)