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

24 KiB
Raw Blame History

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
  • 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.lockbackend/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.
  • 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
  • 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__.pybackend/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.
  • T004 Gerar e aplicar migration Alembic criando a tabela client_users com todas as colunas e índice ix_client_users_emailbackend/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.
  • 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
  • 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.
  • 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.
  • 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).
  • 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.
  • 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).
  • 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).
  • 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.
  • 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.

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

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

  • 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
  • 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).
  • 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)