# 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=` 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/_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 ` 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 ` (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`, `loginUser(data: LoginCredentials): Promise` e `getMe(): Promise`, 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` (chama `loginUser`, salva token, define user) e `register(credentials): Promise` (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 ``; caso contrário renderiza `` — `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 ``; adicionar rota `} />`; adicionar rota `} />`; criar grupo de rotas protegidas com `}>` contendo `} />` (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; `` 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)