24 KiB
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 |
-
T001 Adicionar
"PyJWT>=2.9","bcrypt>=4.2"e"pydantic[email]"às dependências em[project].dependenciese executaruv add "PyJWT>=2.9" "bcrypt>=4.2" "pydantic[email]"para atualizaruv.lock—backend/pyproject.toml- Done when:
import jwt,import bcryptefrom pydantic import EmailStrexecutam sem erro dentro do container;uv.lockreflete as três novas dependências;uv run python -c "import jwt, bcrypt; from pydantic import EmailStr; print('ok')"imprimeok.
- Done when:
-
T002 Adicionar variável
JWT_SECRET_KEY=<valor-gerado>ao arquivobackend/.env(para desenvolvimento local) e como variável de ambiente no serviçobackenddodocker-compose.yml; o valor NUNCA deve ser string vazia ou placeholder óbvio como"secret"—backend/.envedocker-compose.yml- Done when:
docker-compose configmostraJWT_SECRET_KEYdefinida para o serviçobackend;grep -r "JWT_SECRET_KEY" backend/app/não retorna nenhuma ocorrência com valor embutido (apenas leituras viaos.environoucurrent_app.config);backend/.envcontém chave com pelo menos 32 caracteres hexadecimais.
- Done when:
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
ClientUsercom colunasid(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 embackend/app/models/__init__.py—backend/app/models/user.pyebackend/app/models/__init__.py- Done when:
from app.models.user import ClientUserimporta sem erro;ClientUser.__tablename__ == "client_users";ClientUser.emailtemunique=Trueeindex=True;ClientUser.roletemdefault='client';from app.models import ClientUserimporta sem erro (via__init__.py); Flask-Migrate detecta a tabela ao rodarflask db migrate.
- Done when:
-
T004 Gerar e aplicar migration Alembic criando a tabela
client_userscom todas as colunas e índiceix_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 confirmaop.create_table("client_users", ...)com colunasid,name,email,password_hash,role,created_ateop.create_index("ix_client_users_email", "client_users", ["email"], unique=True);flask db upgradeexecuta sem erro;flask db downgrade -1reverte sem erro;flask db upgradere-aplica sem erro.
- Done when:
-
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, AuthTokenOutimporta 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")levantaValidationError(senha < 8 chars);UserOut.model_validate(client_user_instance)serializa sempassword_hash;AuthTokenOut(access_token="tok", user=user_out_instance)serializa corretamente.
- Done when:
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_authembackend/app/utils/auth.py: extrai Bearer token do headerAuthorization; decodifica viajwt.decode(token, current_app.config["JWT_SECRET_KEY"], algorithms=["HS256"]); em caso de token ausente, expirado ou inválido retornajsonify({"error": "Não autorizado."})com status 401; em caso de sucesso, injetag.current_user_id = payload["sub"]e chama o endpoint original —backend/app/utils/auth.py- Done when:
from app.utils.auth import require_authimporta sem erro; rota decorada com@require_authretorna 401 quandoAuthorizationestá ausente; retorna 401 com token manipulado; retorna 401 com token expirado (TTL forçado para 0s em teste);g.current_user_idcontém o UUID como string após autenticação bem-sucedida.
- Done when:
-
T007 Criar blueprint
auth_bpcom prefixo/api/v1/authcontendo três endpoints: (1)POST /register— validaRegisterIn, verifica e-mail duplicado (retorna 409 se existir), faz hash da senha combcrypt.hashpw, persisteClientUser, emite JWT comsub=str(user.id)eexp=utcnow+7dias, retornaAuthTokenOutcom status 201; (2)POST /login— validaLoginIn, busca usuário por email, verifica senha combcrypt.checkpw(retorna 401 genérico se inválido), emite JWT, retornaAuthTokenOutcom status 200; (3)GET /meprotegida por@require_auth— busca usuário porg.current_user_id, retornaUserOutcom status 200 (retorna 401 se usuário não encontrado) —backend/app/routes/auth.py- Done when:
POST /api/v1/auth/registercom dados válidos retorna 201 comaccess_tokeneuser(sempassword_hash); segundo POST com mesmo e-mail retorna 409; POST com senha de 5 chars retorna 422;POST /api/v1/auth/logincom 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/mecom token válido retorna 200 comid,name,email,role,created_at;GET /api/v1/auth/mesem token retorna 401.
- Done when:
-
T008 Registrar
auth_bpna factorycreate_app()embackend/app/__init__.py: adicionarfrom app.routes.auth import auth_bpeapp.register_blueprint(auth_bp); garantir import deClientUserantes dodb.create_all()ou Alembic para que o modelo seja detectado; adicionarapp.config["JWT_SECRET_KEY"] = os.environ["JWT_SECRET_KEY"](raiseKeyErrorse ausente) —backend/app/__init__.py- Done when: Servidor Flask inicia sem erros após a alteração;
GET /api/v1/auth/registerretorna 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 causaKeyErrorexplícito (não silencioso).
- Done when: Servidor Flask inicia sem erros após a alteração;
-
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) eAuthState(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;RegisterCredentialsincluiconfirmPassword(validação apenas no frontend);AuthState.userénullquando não autenticado.
- Done when:
-
T010 [P] Atualizar o arquivo de serviços para exportar instância Axios com
baseURLdo backend; adicionar interceptor de request que injetaAuthorization: Bearer <token>quandolocalStorage.getItem("auth_token")não for null; adicionar interceptor de response que limpalocalStoragee redireciona para/loginem resposta 401 —frontend/src/services/api.ts- Done when:
import api from '@/services/api'compila sem erro; requisição com token nolocalStorageinclui headerAuthorization: Bearer <token>(verificável no DevTools Network); requisição sem token não inclui o header; resposta 401 aciona limpeza dolocalStoragee navegação para/loginsem loop (verificar que/loginem si não dispara o interceptor em loop).
- Done when:
-
T011 [P] Criar serviço de autenticação com funções
registerUser(data: RegisterCredentials): Promise<AuthTokenResponse>,loginUser(data: LoginCredentials): Promise<AuthTokenResponse>egetMe(): Promise<User>, todas chamando os endpoints do blueprintauth_bpvia instânciaapi; nenhuma URL hardcoded —frontend/src/services/auth.ts- Done when:
import { registerUser, loginUser, getMe } from '@/services/auth'compila sem erro TypeScript;registerUserchamaPOST /api/v1/auth/register;loginUserchamaPOST /api/v1/auth/login;getMechamaGET /api/v1/auth/me; erros 4xx/5xx são propagados para o caller sem silenciamento; nenhuma URL hardcoded (usa instânciaapideapi.ts).
- Done when:
-
T012 Criar
AuthContextcomAuthProviderexportado que: inicializa comisLoading: true; emuseEffectinicial tentagetMe()(selocalStoragetemauth_token), defineusereisAuthenticated: trueem sucesso, limpa token e defineisAuthenticated: falseem falha; expõe funçõeslogin(credentials): Promise<void>(chamaloginUser, salva token, define user) eregister(credentials): Promise<void>(chamaregisterUser, salva token, define user) elogout(): void(remove token, limpa user, navega para/login); exportar hookuseAuth()que consome o contexto —frontend/src/contexts/AuthContext.tsx- Done when:
import { AuthProvider, useAuth } from '@/contexts/AuthContext'compila sem erro;useAuth()fora doAuthProviderlança erro descritivo; apóslogin()bem-sucedidoisAuthenticated === trueeusercontém dados do servidor;logout()limpa estado e navega para/login; recarregar página com token válido nolocalStoragerestaura sessão (isAuthenticated: true); recarregar com token expirado resulta emisAuthenticated: false.
- Done when:
-
T015 [US1] Criar
RegisterPagecom formulário contendo camposname(obrigatório),email(obrigatório, formato),password(obrigatório, mín. 8 chars),confirmPassword(obrigatório, deve igualarpassword); validaçãoconfirmPassword !== passwordimpede envio e exibe mensagem no campo; ao submeter chamaregister()douseAuth(); 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/loginpara 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;
confirmPassworddiferente 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 duranteisSubmitting;npm run buildnão apresenta erros TypeScript; estilos correspondem ao design system vigente.
- Done when: Senha < 8 chars exibe erro de validação sem enviar requisição;
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/loginfoi implementado em T007 (Phase 3). Esta fase foca na interface do cliente.
- T014 [US2] Criar
LoginPagecom formulário contendo camposemail(obrigatório, formato) epassword(obrigatório); ao submeter chamalogin()douseAuth(); 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/cadastropara 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 duranteisSubmitting;npm run buildnão apresenta erros TypeScript; estilos correspondem ao design system vigente.
- 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
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_authfoi 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
ProtectedRouteque consomeuseAuth(); enquantoisLoading === truerenderiza spinner ounull(evita redirect prematuro); se!isAuthenticatedretorna<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; duranteisLoading(verificação inicial) não há redirect prematuro;npm run buildnão apresenta erros TypeScript.
- Done when: Sem token no
-
T016 [US3] Atualizar
App.tsxpara: 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:
/loginrenderizaLoginPage;/cadastrorenderizaRegisterPage;/area-do-clientesem autenticação redireciona para/login;/area-do-clientecom autenticação renderiza conteúdo (pode ser placeholder);npm run buildsem erros TypeScript;<AuthProvider>envolve todas as rotas.
- Done when:
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/mefoi implementado em T007 (Phase 3). Esta fase foca na Navbar.
- T017 [US4] Atualizar
Navbarpara consumiruseAuth(); quando!isAuthenticatedexibe botão/link "Entrar" apontando para/login; quandoisAuthenticatedexibe nome do usuário (user.name) truncado se necessário + botão "Sair" que chamalogout(); duranteisLoadingexibe 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; duranteisLoadingnão há flash de "Entrar"/"Sair";npm run buildnão apresenta erros TypeScript.
- Done when: Navbar exibe "Entrar" para visitante; após login exibe nome do usuário e "Sair"; clicar em "Sair" chama
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 |
-
T018 [P] Auditar
backend/app/routes/auth.pyebackend/app/utils/auth.pypara garantir: (a) nenhuma resposta contémpassword_hash; (b) mensagens de erro de login são idênticas para e-mail inexistente e senha incorreta (SC-006); (c)JWT_SECRET_KEYnunca 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.pyebackend/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 viacurrent_app.config, nunca o valor em si; revisão manual confirmabcrypt.checkpwexecutado mesmo para usuário não encontrado (dummy hash).
- Done when:
-
T019 Executar o roteiro completo do
quickstart.md: instalar dependências, gerar/aplicar migration, testar os três endpoints viaInvoke-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 /registerretorna 201;POST /loginretorna 200;GET /mecom token retorna 200; frontend emhttp://localhost:5173/cadastrocompleta cadastro e redireciona;http://localhost:5173/logincompleta login e redireciona;http://localhost:5173/area-do-clientesem sessão redireciona para/login.
- Done when: Todos os comandos do quickstart executam sem erro;
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)