sass-imobiliaria/.specify/features/005-authentication/tasks.md

251 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 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 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 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)