Projeto de estudo desenvolvido durante o estágio em desenvolvimento back end, com o objetivo de praticar lógica de programação em Python e, na sequência, os fundamentos de desenvolvimento de APIs com FastAPI.
O projeto contém duas versões, que representam etapas diferentes do aprendizado:
- Versão terminal — um sistema de cadastro, login e perfil de usuário rodando via linha de comando.
- Versão API (em andamento) — adaptação do mesmo sistema para rodar como uma API web usando FastAPI.
🚧 Em desenvolvimento. A versão terminal está funcional. A versão API já tem rotas próprias por ação, modelos Pydantic separados da classe de estado, salva a senha como hash (bcrypt), migrou o armazenamento para múltiplos usuários (dicionário indexado por email), e o /login já gera um token JWT assinado (SECRET_KEY guardada em .env, fora do código). A rota /perfil já está protegida por token JWT (Authorization: Bearer), sem mais aceitar o email diretamente na URL, e trata token inválido/expirado com 401. O /login também trata email inexistente (404) e senha incorreta (401).
Foi adicionada autorização por papel (user/admin): o cadastro define "user" por padrão, e o papel não é mais informado no login — o /login busca o papel diretamente do cadastro e o inclui dentro do token JWT gerado, já que essa informação já existe desde o cadastro e não faz sentido pedir de novo. A rota /admin é restrita a usuários com papel "admin" (403 para quem não é) e tem conteúdo próprio: retorna a lista de todos os usuários cadastrados (nome e email de cada um), sem expor senha nem papel.
A API agora também cobre um CRUD mais completo de administração e de autogerenciamento de conta: um admin pode alterar o papel de qualquer usuário, excluir um usuário e resetar a senha de alguém que esqueceu; qualquer usuário logado pode editar o próprio perfil (nome, email e senha) ou deletar a própria conta. O tratamento de exceções de token também ficou mais específico: em vez de um except: genérico (que podia "engolir" até um HTTPException de 403 lançado dentro do mesmo try), agora cada rota protegida usa except jwt.ExpiredSignatureError e except jwt.InvalidTokenError separados, e a lógica de autorização (checar papel) ficou fora do bloco try, garantindo que 401 (token inválido) e 403 (sem permissão) nunca se confundam.
A edição do próprio perfil (PATCH /perfil/usuario) deixou de exigir todos os três campos de uma vez: agora usa um modelo Pydantic próprio (UsuarioAlterarPerfil), separado do modelo de cadastro, com nome, email e senha todos opcionais — a pessoa escolhe alterar só um, dois ou os três, e cada campo só é sobrescrito se for realmente enviado. O reset de senha por um admin (PATCH /admin/usuario) também ganhou uma validação: a nova senha é comparada (via bcrypt.checkpw) com o hash já salvo antes de qualquer coisa ser sobrescrita, e a rota recusa (409) se a senha nova for igual à anterior.
Os logs das rotas administrativas (/admin/papel, e as duas de /admin/usuario — exclusão e reset de senha) agora registram a trilha de auditoria completa: quem executou a ação (o admin, extraído do token), o que foi feito, em quem (o usuário-alvo) e o resultado — em vez de mensagens genéricas que não identificavam as partes envolvidas.
O email do usuário agora é criptografado em repouso: ele é salvo de forma reversível (Fernet, criptografia simétrica) dentro do objeto Usuario, diferente da senha (hash bcrypt, irreversível). O dicionário usuario_cadastro continua indexado pelo email em texto puro (necessário, já que a criptografia gera um resultado diferente a cada execução), enquanto o campo .email guardado dentro de cada objeto Usuario fica sempre criptografado, sendo descriptografado apenas nas rotas que precisam devolvê-lo (como /admin).
A suíte de testes automatizados com pytest foi expandida e agora cobre o fluxo completo: login (sucesso, senha errada, email inexistente), acesso a rota protegida com e sem token válido, autorização por papel (usuário comum barrado de ações de admin), e as ações de admin e de autogerenciamento de perfil — tudo passando (11 testes, isolamento de estado entre eles).
O código também passou a seguir um padrão de estilo verificado por linter (flake8): o api.py foi reorganizado (imports agrupados no topo, 2 linhas em branco entre definições, sem espaço em parâmetro nomeado, sem linha comprida) e roda hoje com zero avisos de lint.
- Cadastro de usuário (nome, email e senha), com papel
"user"atribuído por padrão - Login com verificação de credenciais e geração de token JWT contendo o papel do usuário (obtido do cadastro, não informado no login)
- Visualização de perfil do usuário logado, protegida por token JWT
- Edição do próprio perfil (nome, email e/ou senha, todos opcionais — só é alterado o que for enviado), protegida por token JWT, com verificação de conflito de email
- Exclusão da própria conta, protegida por token JWT
- Rota administrativa (
/admin), acessível apenas para usuários com papel"admin", que lista todos os usuários cadastrados (nome e email, sem senha) - Alteração do papel de um usuário por um admin, sem precisar editar o dicionário manualmente
- Exclusão de qualquer usuário por um admin
- Reset de senha de qualquer usuário por um admin (fluxo de "esqueci minha senha")
- Reset de senha recusa (409) se a senha nova for igual à anterior, verificado com
bcrypt.checkpwantes de sobrescrever o hash salvo - Tratamento de erros HTTP específicos: 404 (usuário não encontrado), 401 (senha, token inválido ou expirado), 403 (sem permissão de admin), 409 (email já em uso ou senha repetida)
- Registro de eventos via
logging, incluindo trilha de auditoria completa nas ações administrativas (quem executou, o que foi feito, em quem, e o resultado) - Criptografia reversível do email em repouso (
Fernet), descriptografado apenas quando precisa ser exibido - Testes automatizados com
pytesteTestClient, cobrindo login, autenticação, autorização por papel e as ações de admin/perfil - Padrão de estilo verificado por
flake8, com configuração própria (.flake8) e código passando sem nenhum aviso
- Python 3
- FastAPI (versão API)
- Pydantic (validação de dados de entrada na versão API)
- bcrypt (hash de senhas na versão API)
- PyJWT (geração e validação de tokens JWT)
- cryptography (criptografia simétrica reversível do email, via
Fernet) - python-dotenv (carregamento de
SECRET_KEYeFERNET_KEYa partir de.env) - pytest (testes automatizados)
- httpx (requisitado internamente pelo
TestClientdo FastAPI/Starlette para simular requisições nos testes) - flake8 (linter de estilo e checagem estática, combinando
pycodestyle,pyflakesemccabe) - autopep8 (formatação automática, usado para corrigir a maior parte dos avisos do flake8)
- Módulo
loggingda biblioteca padrão
Sistema com menu interativo no terminal, controlado por um loop while True com estrutura if/elif/else. Os dados do usuário são armazenados em memória durante a execução (sem persistência em banco de dados).
python terminal.pyO programa vai pedir seu nome e, em seguida, exibir um menu com as opções:
1. Cadastro
2. Login
3. Perfil
4. Sair
Adaptação do mesmo sistema para o formato de rotas HTTP com FastAPI, como parte do aprendizado de desenvolvimento de APIs REST.
| Método | Rota | Descrição |
|---|---|---|
GET |
/ |
Rota inicial, mensagem de boas-vindas |
POST |
/usuario |
Cadastro de novo usuário (nome, email e senha; papel "user" por padrão). O email é criptografado antes de ser salvo no objeto Usuario |
POST |
/login |
Login do usuário (email e senha). O papel é obtido do cadastro e incluído no token JWT gerado. 404 se o email não existir, 401 se a senha estiver errada |
GET |
/perfil |
Visualização de perfil do usuário logado, protegida por token JWT (Authorization: Bearer). 401 se o token for inválido/expirado |
PATCH |
/perfil/usuario |
Edição do próprio perfil (nome, email e/ou senha — todos opcionais, só altera o que for enviado), protegida por token JWT. O usuário editado é sempre o dono do token (nunca um email vindo do corpo). 409 se o novo email já pertencer a outro usuário. Se o email for alterado, o novo é criptografado antes de ser salvo |
DELETE |
/perfil/usuario |
Exclusão da própria conta, protegida por token JWT. Não recebe nada no corpo — o usuário deletado é sempre o dono do token |
GET |
/admin |
Rota administrativa, protegida por token JWT e restrita a usuários com papel "admin". Retorna a lista de todos os usuários cadastrados (nome e email descriptografado, sem senha). 401 se o token for inválido/expirado, 403 se o usuário não for admin |
PATCH |
/admin/papel |
Altera o papel (user/admin) de um usuário especificado por email. Restrita a admins. 404 se o email não existir, 403 se quem chama não for admin |
DELETE |
/admin/usuario |
Exclui um usuário especificado por email. Restrita a admins. 404 se o email não existir, 403 se quem chama não for admin |
PATCH |
/admin/usuario |
Reseta a senha de um usuário especificado por email (fluxo de "esqueci minha senha"). Restrita a admins. 404 se o email não existir, 403 se quem chama não for admin, 409 se a senha nova for igual à anterior |
Cada ação tem seu próprio path, o que resolveu o conflito de rotas duplicadas que existia quando
cadastro,logineperfildisputavam o mesmo endereço.
A rota /login depende de uma variável de ambiente SECRET_KEY, usada para assinar os tokens JWT. A criptografia do email depende de uma variável FERNET_KEY, usada para criptografar/descriptografar esse dado em repouso. Ambas devem ser definidas em um arquivo .env na raiz do projeto (não incluído no repositório):
SECRET_KEY=<string aleatória gerada com secrets.token_hex(32)>
FERNET_KEY=<chave gerada com Fernet.generate_key()>
uvicorn api:app --reloadDepois de rodar, a documentação interativa gerada automaticamente pelo FastAPI fica disponível em:
http://127.0.0.1:8000/docs
A pasta tests/ contém a suíte de testes escrita com pytest, usando o TestClient do FastAPI (que depende de httpx) para simular requisições HTTP reais contra a aplicação, sem precisar do servidor rodando.
Cada teste começa limpando o dicionário usuario_cadastro (função auxiliar limpar_cadastro()), garantindo que um teste não interfira no resultado do outro — isolamento de estado entre testes.
- Login com senha correta: confirma status
200e presença de"token"na resposta - Login com senha incorreta: confirma status
401 - Login com email não cadastrado: confirma status
404 - Acesso a rota protegida sem token: confirma status
401 - Acesso a rota protegida com token inválido: confirma status
401 - Login de um usuário comum (fluxo base para os testes de autorização): confirma status
200 - Usuário comum tentando acessar
/admin: confirma status403 - Usuário comum tentando
PATCH /admin/papel: confirma status403 - Usuário comum tentando
DELETE /admin/usuario: confirma status403 - Fluxo de login de um admin (base para o teste de promoção de papel)
- Edição do próprio perfil (
PATCH /perfil/usuario): confirma status200e que a alteração não afeta o perfil de outra pessoa
Total: 11 testes, todos passando.
pip install pytest httpx
pytest -v- Cobrir cadastro (sucesso e email duplicado)
- Formalizar a limpeza de estado como fixture (
@pytest.fixture) em vez de chamada manual - Testar de fato a promoção de um usuário a admin por outro admin (hoje o teste só cobre o login do admin, sem chamar
PATCH /admin/papel) - Cobrir a edição parcial de perfil (só nome, só email, só senha, e combinações entre os três)
- Cobrir a recusa (409) do reset de senha por admin quando a senha nova é igual à anterior
O projeto usa flake8 para checagem de estilo e alguns erros estáticos, combinando três ferramentas por baixo dos panos: pycodestyle (estilo/PEP 8), pyflakes (erros lógicos, como import não usado) e mccabe (complexidade). A configuração fica no arquivo .flake8, na raiz do projeto, com o limite de linha ajustado para 100 caracteres.
pip install flake8
flake8 api.pyBoa parte dos avisos de estilo (espaçamento, linhas em branco, indentação) pode ser corrigida sem edição manual, com o autopep8:
pip install autopep8
autopep8 --in-place --aggressive --max-line-length 100 api.pyO que sobra depois disso costuma ser erro de lógica (import/variável não usada, função redefinida) ou linha comprida por causa de uma condição booleana — esses o autopep8 não corrige sozinho, de propósito, porque mudariam o comportamento do código ou a legibilidade dele.
api.py roda com zero avisos de flake8.
Este projeto foi usado como base prática para consolidar conceitos de:
- Fundamentos de testes automatizados com
pytest: convenção de nomes de arquivo (test_*.py, nãoteste_*.py) e de função (def test_*()) exigida para o pytest descobrir e coletar os testes - Uso do
TestClient(FastAPI/Starlette) para simular requisições HTTP reais contra a aplicação nos testes, em vez de chamar as funções de rota diretamente - Isolamento entre testes: por que um teste não pode depender de estado deixado por outro, e como uma função auxiliar de limpeza (embrião do conceito de fixture) resolve isso
- Estrutura básica de um teste (
assertpara conferir o resultado de uma ação) e o padrão ação → conferência - Diferença entre instalar um pacote dentro de uma virtualenv (
.venv) e no Python do sistema operacional, e por quesudo pip installé um anti-padrão que pode mascarar o problema real (PATH apontando para o pip errado) em vez de resolvê-lo - Criptografia simétrica reversível (
Fernet) versus hash irreversível (bcrypt): quando usar cada uma, dependendo se o dado precisa ser lido de volta em algum momento - Por que um valor criptografado com
Fernetmuda a cada execução (mesmo para o mesmo texto de entrada), e por que isso impede usá-lo como chave de busca em um dicionário — a chave precisa continuar em texto puro, e só o valor guardado dentro do objeto é que fica criptografado - Gerenciamento de segredos com variáveis de ambiente (
.env), incluindo geração de chaves de criptografia e o cuidado de nunca commitar esse arquivo - Conversão entre
strebytes(.encode()/.decode()) como pré-requisito para operações de criptografia, que trabalham a nível de bytes - Escopo de variáveis em Python (locais, de classe e de módulo/
global) e compartilhamento de estado entre requisições diferentes - Diferença entre um modelo Pydantic (
BaseModel, usado para validar o corpo da requisição) e uma classe comum usada para guardar estado em memória - Por que gravar em atributo de classe (
Classe.atributo = valor) compartilha o dado entre todas as instâncias/requisições, e por que isso é um anti-padrão para estado por usuário - Por que uma única variável global (mesmo guardando uma instância corretamente) só suporta um usuário por vez, e como um dicionário indexado por uma chave única (email) resolve isso
- Hash de senhas com
bcrypt: por que é irreversível, por que usa salt, e por que a verificação (checkpw) nunca "descriptografa" a senha salva - Autenticação vs. autorização: "quem você é" vs. "o que você pode fazer/ver"
- Estrutura e propósito de um JWT (header, payload, signature) e por que ele permite autenticação stateless
- Por que o papel do usuário deve ser obtido de uma fonte confiável (o cadastro) e nunca informado livremente pelo próprio usuário no momento do login
- Trade-off entre incluir dados como o papel dentro do payload do token (mais rápido, mas "engessado" até o token expirar) versus consultar a fonte de dados a cada requisição (mais lento, porém sempre atualizado)
- Uso de
DependseHTTPBearerdo FastAPI para extrair e validar o token do headerAuthorization, protegendo rotas sem depender de parâmetros na URL try/exceptcomo estratégia para lidar com falhas que só podem ser detectadas na hora de executar (como decodificar um token inválido), em vez de checadas antecipadamente comif- Diferença entre
exceptgenérico eexceptespecífico por tipo de exceção (jwt.ExpiredSignatureError,jwt.InvalidTokenError), incluindo a ordem de precedência quando uma exceção é subclasse de outra, e o risco de umexceptgenérico capturar até umHTTPExceptionlançado dentro do própriotry - Diferença entre
raise(interrompe a execução e propaga um erro) ereturn(devolve um valor normalmente) - Uso de
HTTPExceptionpara devolver códigos de status HTTP apropriados a cada tipo de falha (404, 401, 403, 409), em vez de mensagens de erro genéricas com status 200 - Diferença entre os códigos 401 (não autenticado), 403 (autenticado, mas sem permissão) e 409 (conflito de dado, como email duplicado ou senha repetida)
- Controle de acesso baseado em papel (role-based access control): por que o papel de um usuário deve ser definido no cadastro (pela aplicação) e nunca escolhido livremente pelo próprio usuário
- Diferença entre uma ação sobre "si mesmo" (identificar o usuário pelo email do token) e uma ação de admin sobre "outra pessoa" (identificar o usuário-alvo por um email recebido no corpo da requisição)
KeyErrorao acessar uma chave inexistente em um dicionário, e por que checar a existência da chave antes (if chave in dicionario) sempre precisa vir antes de qualquer outro acesso a essa mesma chave, mesmo dentro de uma condição diferente (como uma comparação de senha)delpara remover uma entrada de um dicionário- Por que, num dicionário indexado por email, alterar apenas o atributo
.emailde um objeto não move o registro — é preciso criar a entrada na nova chave e apagar a antiga — e por que é necessário checar conflito de email antes de permitir a troca - Como percorrer um dicionário com
forpara acumular resultados em uma lista com.append(), e por que declarar a lista antes do loop (não dentro dele) é essencial para não perder os dados de cada volta - Por que um
returndentro de um loop interrompe a execução na primeira volta, e por que ele deve ficar fora doforquando o objetivo é processar todos os itens - Por que segredos (como
SECRET_KEYeFERNET_KEY) não devem ficar no código-fonte, e o papel de variáveis de ambiente (.env) nisso - Boas práticas de segurança básica (nunca logar ou armazenar senhas em texto puro)
- Níveis de log (
INFO,WARNING) e configuração do módulologging - Por que uma linha de log/
raisecolocada depois de umreturnou de umraiseno mesmo bloco nunca é executada (código morto), e por que a ordem das linhas dentro de uma função importa tanto quanto a lógica em si - Como montar uma mensagem de log de auditoria completa (quem executou, o que fez, em quem, e o resultado), em vez de uma frase genérica que não identifica as partes envolvidas — e por que isso muda dependendo se a rota age sobre o próprio usuário ou sobre outra pessoa
- Separar um modelo Pydantic exclusivo para uma rota de edição parcial (
Optional[str] = Noneem todos os campos), em vez de reaproveitar o modelo obrigatório do cadastro — e por que reaproveitar quebraria a validação da rota original - Pegadinha de comparar uma senha nova com o hash salvo usando
bcrypt.checkpw()antes de sobrescrever esse hash — comparar depois de já ter trocado o valor faz a checagem sempre darTrue, porque a senha estaria sendo comparada com o hash dela mesma - Diferença entre usar
try/exceptpara um erro que o próprio Python pode lançar em tempo de execução e usarif/raisepara uma condição de negócio que já se sabe checar de antemão (como comparar duas senhas) — e por que todo blocotry/exceptprecisa ter corpo, nunca ficar vazio - Quebra de uma condição booleana longa (
andencadeado) em várias linhas usando parênteses, como alternativa manual quando oautopep8não mexe em condições de código - Risco de um import "morto" entrar no arquivo sem querer via autocomplete do editor, e como o
flake8(F401) sinaliza isso - Fundamentos de APIs REST: rotas, métodos HTTP (
GET,POST,PATCH,DELETE), path parameters e por que cada combinação verbo+path deve representar uma única ação - Diagnóstico de testes de API por código de status: como a progressão de um erro (404 → 405 → 422 → 200/403) durante o debug aponta, nessa ordem, para "rota não existe" → "método errado" → "corpo da requisição errado" → "lógica de autorização", útil pra saber onde procurar antes mesmo de ler o traceback inteiro
- Diferença entre passar um dicionário Python válido como corpo da requisição (
json={"campo": valor}) e escrever o nome da classe do modelo Pydantic dentro do dicionário por engano — o nome do modelo é só documentação/validação do lado do servidor, nunca faz parte do JSON enviado - Por que o atalho
TestClient.delete()não aceita o argumentojson(por padrão um DELETE "não deveria" ter corpo), e como usarclient.request("DELETE", ..., json=...)para contornar isso quando a rota exige dados no corpo - Uso de
ast.parse()para isolar rapidamente umSyntaxErrorde um arquivo, sem precisar rodar o pytest inteiro por cima - O que é um linter e como o
flake8funciona por baixo dos panos, combinandopycodestyle(estilo),pyflakes(erros lógicos) emccabe(complexidade) — e por que os códigos de erro têm essa cara (E501,F401,C901) - Diferença entre configurar um linter (
.flake8,max-line-length,exclude) e silenciar um aviso pontual com# noqa: CÓDIGOnuma linha específica, e por que abusar do# noqaé sinal de que a regra deveria mudar globalmente, não ser abafada linha a linha - Diferença entre erros de estilo (linha em branco, espaçamento, comprimento de linha — resolvíveis por formatação automática) e erros de lógica (
globalsem uso real, função redefinida — que exigem decisão humana, por isso nenhuma ferramenta de formatação mexe neles sozinha) - Uso de
autopep8 --aggressivepara corrigir automaticamente a maior parte dos avisos de estilo de um arquivo já escrito, em vez de corrigir um por um manualmente - Por que
globalsó é necessário quando uma variável de módulo é reatribuída dentro de uma função (variavel = novo_valor), e não quando ela é apenas mutada (variavel[chave] = valor) — nesse segundo caso a declaraçãoglobalnão tem efeito nenhum - Reconhecer quando duas funções com o mesmo nome no mesmo arquivo (uma sobrescrevendo a outra silenciosamente) é um bug de nomenclatura, mesmo quando o programa continua funcionando sem erro aparente
E402(import fora do topo do arquivo): por que o PEP 8 exige que todo código executável (como instanciarapp = FastAPI()) venha depois de todos os imports, e não misturado entre eles
Luiza Souza (@lu-izah19)