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 — adaptação do mesmo sistema para rodar como uma API web usando FastAPI, já com todas as rotas migradas para banco de dados real.
🚧 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), suporta múltiplos usuários via banco de dados, e o /login já gera um token JWT assinado (SECRET_KEY guardada em .env, fora do código). A rota /perfil está protegida por token JWT (Authorization: Bearer), sem 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).
Há autorização por papel (user/admin): o cadastro define "user" por padrão, e o papel não é informado no login — o /login busca o papel diretamente do cadastro e o inclui dentro do token JWT gerado. A rota /admin é restrita a usuários com papel "admin" ou ao root (403 para quem não é) e retorna a lista de todos os usuários cadastrados (nome e email de cada um), sem expor senha nem papel.
A API cobre um CRUD 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/ou senha, de forma independente) ou desativar a própria conta.
O email do usuário é criptografado em repouso: ele é salvo de forma reversível (Fernet, criptografia simétrica), diferente da senha (hash bcrypt, irreversível). Como a criptografia com Fernet não é determinística (o mesmo email gera um resultado diferente a cada vez que é criptografado), não é possível localizar um usuário no banco com um WHERE email = %s comparando o email puro digitado com o valor salvo. A solução adotada, sem alterar a estrutura da tabela, foi trazer todos os usuários (SELECT sem WHERE) e descriptografar o email de cada linha em um loop, até achar a que bate com o email recebido na requisição — esse padrão se repete em toda rota que precisa localizar um usuário pelo email, incluindo agora a limpeza de dados entre testes.
O projeto migrou do armazenamento em dicionário (em memória) para um banco de dados MariaDB real, usando mysql.connector para rodar as queries SQL diretamente (sem ORM). Todas as rotas de escrita e leitura estão migradas e usando o padrão de busca por loop com descriptografia de email.
Rotas migradas e revisadas:
POST /usuario(cadastro) —INSERT INTO usuarioscom email criptografado (Fernet) e senha em hashbcrypt,commit(), e tratamento de email duplicado viaexcept mysql.connector.errors.IntegrityError(409).POST /login— busca por loop com descriptografia de email.PATCH /admin/papel— checagem de permissão feita direto pelopayload["papel"]do token (sem busca extra),UPDATEusando o email criptografado da linha encontrada.GET /admin— corrigido o bug em que oreturnda listagem estava indentado dentro dofor, fazendo a rota devolver só o primeiro usuário; agora lista todos corretamente, nos dois branches (root e admin).DELETE /admin/usuario— migrada para o padrão de busca por loop; corrigida a sintaxe SQL (DELETE FROM ... WHERE), o uso do email criptografado noWHERE, e a adição docommit()que faltava.PATCH /admin/usuario(reset de senha) — corrigida a busca, que comparava email puro com a coluna criptografada; agora usa o padrão de loop.PATCH /perfil/desativar— corrigida a busca por email (estava usando um parâmetro inexistente,dados.email, em vez do email de quem está logado) e oWHEREdoUPDATE.GET /perfil— corrigida a busca por email; adicionado tratamento explícito para o caso em que o usuário não é encontrado (404), evitando um erro não tratado quando a conta some do banco com o token ainda válido.PATCH /perfil/usuario(editar o próprio perfil) — reescrita: os três campos opcionais (nome, senha, email) agora são totalmente independentes entre si, cada alteração tem seu própriocommit()ereturn; a checagem de "esse novo email já pertence a outro usuário" foi implementada de fato contra o banco; e foi tratado o caso-limite em que o usuário reenvia o próprio email sem alterá-lo (retorna sucesso sem tentar um update desnecessário).
Por orientação da supervisora, o projeto está sendo reorganizado em múltiplos arquivos, seguindo o mesmo padrão usado por outra colega de estágio, em vez de manter tudo em um único api.py.
Nova estrutura:
config/config.py— variáveis de ambiente e configurações (chaves, credenciais do banco, incluindo agora host e porta)database/database.py— conexão com o MariaDBmodels.py— classes Pydantic, e agora também o modelo real do SQLAlchemy (Usuario(Base)) usado pelo Alembic para versionar o schemaauth/auth.py— criptografia/descriptografia de email (Fernet), decodificação de token JWT e busca de usuário por emailmain/main.py— instância do FastAPI e todas as rotas
O antigo api.py monolítico foi descontinuado em favor dessa separação por responsabilidade.
A suíte de testes também foi dividida por domínio e migrada para trabalhar com o banco real, em vez do antigo dicionário usuario_cadastro:
tests/conftest.py— cliente de teste e funções auxiliares compartilhadas. A funçãolimpar_cadastro(email)agora localiza o usuário de teste pelo mesmo padrão de busca por loop com descriptografia de email usado no resto do projeto e só executa oDELETEse encontrar o usuário correspondente, evitando erro quando o email ainda não existe no banco.tests/test_auth.py— login, token e acesso a rotas protegidastests/test_admin.py— permissões e ações administrativastests/test_perfil.py— edição do próprio perfil
Toda a suíte de testes está passando contra a nova estrutura e o banco de dados real.
O schema do banco passou a ser versionado com Alembic, em vez de alterações manuais direto no MariaDB via HeidiSQL.
- Criado um modelo real do SQLAlchemy (
Usuario(Base), comdeclarative_base()) emmodels.py— substituindo a antiga classeUsuario, que só guardava atributos num__init__e não era reconhecida como modelo pelo Alembic. env.pyealembic.iniconfigurados para ler a URL de conexão a partir do.env(variávelDATABASE_URL), em vez de deixar a credencial do banco exposta noalembic.iniversionado.- Como a tabela
usuariosjá existia no banco (criada manualmente antes do Alembic entrar no projeto), o histórico foi "adotado" comalembic stamp head, em vez de deixar o Alembic tentar recriá-la do zero e falhar com "tabela já existe". - Primeira migration de verdade aplicada com sucesso, adicionando a coluna
ativo(usada pela rota de desativação de conta), que ainda não existia nessa tabela.
As rotas protegidas tinham dois blocos de código repetidos em quase toda rota: a decodificação manual do token JWT (com try/except) e a busca de um usuário pelo email criptografado (loop com descriptografar_email). Esses blocos foram extraídos em funções centralizadas no auth.py:
validar_usuario— dependency do FastAPI que decodifica o token JWT e devolve opayload, tratandotoken expirado/token inválidonum único lugar. Todas as rotas protegidas trocaramcredenciais=Depends(HTTPBearer())+ blocotry/exceptmanual porpayload=Depends(validar_usuario).buscar_usuario_por_email(email)— recebe um email como string e devolve a linha do usuário correspondente (ouNone), substituindo o loop de busca repetido. É chamada manualmente dentro de cada rota (não encadeada viaDepends(), já que o email de origem varia: às vezes é o de quem está logado,payload["email"], às vezes é o de um usuário-alvo vindo do corpo da requisição,dados.email).
Ainda falta extrair a checagem de permissão (root/admin), que também se repete nas rotas administrativas, numa terceira função.
- Extrair a checagem de permissão admin/root (repetida nas rotas administrativas) numa dependency separada.
- Rodar
flake8/autopep8na nova estrutura de arquivos. - Reconferir a suíte de testes depois do refactor com
Dependse da migração para Alembic.
- Cadastro de usuário (nome, email e senha), com papel
"user"atribuído por padrão — grava no banco com email criptografado - Login com verificação de credenciais e geração de token JWT contendo o papel do usuário, com expiração de 30 minutos (
exp) - Usuário root fixo, definido por variáveis de ambiente e não persistido junto com os demais usuários
- 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 e independentes), protegida por token JWT
- Desativação da própria conta (em vez de exclusão), protegida por token JWT
- Rota administrativa (
/admin), acessível para usuários com papel"admin"ou para o root, que lista todos os usuários cadastrados - Alteração do papel de um usuário por um admin, com checagem de permissão baseada no papel de quem pediu (não do alvo)
- Exclusão de qualquer usuário por um admin
- Reset de senha de qualquer usuário por um admin
- Tratamento de erros HTTP específicos: 404, 401, 403, 409
- Registro de eventos via
logging, incluindo trilha de auditoria nas ações administrativas - Criptografia reversível do email em repouso (
Fernet), com busca por descriptografia em loop (já que Fernet não é determinístico) - Versionamento do schema do banco com Alembic, a partir de um modelo real do SQLAlchemy
- Autenticação e busca de usuário centralizadas em dependencies reutilizáveis do FastAPI, em vez de código repetido em cada rota
- Testes automatizados com
pytesteTestClient, organizados por domínio, rodando contra o banco de dados real - Padrão de estilo verificado por
flake8
- Python 3
- FastAPI
- Pydantic
- SQLAlchemy
- Alembic
- bcrypt
- PyJWT
- cryptography (Fernet)
- python-dotenv
- mysql-connector-python
- PyMySQL (driver usado pelo SQLAlchemy/Alembic)
- Módulo
datetimeda biblioteca padrão - pytest
- httpx
- flake8
- autopep8
- Módulo
loggingda biblioteca padrão
- Por que criptografia simétrica não-determinística (
Fernet) impede localizar um registro com umWHERE coluna = %sdireto: o mesmo email criptografado duas vezes gera dois resultados diferentes, então a busca precisa trazer tudo e comparar após descriptografar, em vez de filtrar no SQL - Diferença entre a sintaxe de um
INSERT(INSERT INTO tabela (colunas) VALUES (valores)), a de umUPDATE(SET coluna = valor WHERE) e a de umDELETE(DELETE FROM tabela WHERE) — misturarSETnumDELETE, por exemplo, gera um erro de sintaxe - Por que a ordem dos valores numa tupla de parâmetros precisa bater exatamente com a ordem das colunas listadas na query — inverter a ordem grava os dados nos campos errados, sem erro nenhum sendo levantado
- Risco de um
except:genérico (sem tipo especificado) mascarar qualquer erro, não só o esperado — e por que capturar o tipo específico (mysql.connector.errors.IntegrityError) é mais seguro - Diferença entre
.encode()e.decode(): só bytes têm.decode(), só strings têm.encode()— confundir os dois estouraAttributeError - Risco de segurança de checar permissão de admin usando os dados do usuário alvo de uma ação em vez dos dados de quem fez a requisição — a diferença entre validar credenciais de quem pediu (
payload) e credenciais de quem vai sofrer a ação (dados) - Por que um
returncolocado dentro de umfor(em vez de depois dele) interrompe o loop na primeira volta — fazendo uma listagem devolver só o primeiro item, mesmo sem erro nenhum aparecer - Diferença entre acessar um campo de um dicionário (
usuario["campo"]) e de um objeto (usuario.campo) — o resultado decursor.fetchone()/fetchall()(comdictionary=True) nunca tem atributos, só chaves - Por que uma variável de resultado de uma busca anterior (tipo
usuario, resultado de umSELECTfeito antes doUPDATE) não se atualiza sozinha depois que oUPDATEroda — devolver o "valor antigo" em vez do valor que acabou de ser salvo - Diferença entre inicializar uma variável de controle com
None(ausência real de valor, testável comis not None) e usar uma string como"None"ou um número como placeholder — só oNoneverdadeiro do Python evita erros ao tentar tratar essa variável como se fosse sempre um dicionário encontrado - Por que múltiplos
ifindependentes, cada um terminando emreturn, garantem que campos opcionais de uma rota (nome, senha, email) sejam realmente tratados de forma isolada — sem um cobrir ou pular o outro por engano - Diferença entre reatribuir a mesma variável para dois propósitos diferentes (ex: usar
usuariotanto para "quem está logado" quanto para "quem já tem esse email") e usar duas variáveis com nomes distintos — reaproveitar o nome apaga a referência anterior antes dela ser usada - Uma função Python que termina sem bater em nenhum
returnexplícito devolveNonesilenciosamente — o que pode fazer uma API responder "sucesso" vazio quando na real nada foi processado - Por que uma função de limpeza de dados de teste precisa proteger uma operação de escrita (como um
DELETE) contra o caso em que a busca anterior não encontrou nada — sem esseif, tentar acessar uma chave deNoneestouraTypeError - Diferença entre importar um módulo (usado só pra organizar/agrupar código) e uma classe de verdade usável como tipo — anotar um parâmetro com o nome de um módulo (
dados: models) não funciona como tipo - Por que o
target_metadatado Alembic precisa apontar pra umaBasereal do SQLAlchemy (com tabelas mapeadas viaColumn/__tablename__), e não pra uma classe Python comum, mesmo que ela tenha o mesmo nome do modelo esperado - Por que rodar
alembic upgrade headcontra uma tabela criada manualmente antes do Alembic existir tenta recriá-la do zero e falha com "tabela já existe" — e comoalembic stamp headresolve isso, marcando a migration como aplicada sem executar o SQL dela - O que é uma dependency do FastAPI (
Depends) na prática: uma função comum que o FastAPI chama automaticamente antes da rota, cujo retorno vira o parâmetro da rota — e que uma dependency pode, ela mesma, depender de outra dependency - Por que uma função auxiliar que recebe um email como string simples (em vez do objeto/dicionário de onde ele veio) pode ser reaproveitada em contextos diferentes — pra quem está logado (
payload["email"]) e pra um usuário-alvo (dados.email) — sem precisar de duas funções quase idênticas
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.py| Método | Rota | Descrição |
|---|---|---|
GET |
/ |
Rota inicial, mensagem de boas-vindas |
POST |
/usuario |
Cadastro de novo usuário. ✅ |
POST |
/login |
Login do usuário ou do root. ✅ |
GET |
/perfil |
Visualização de perfil do usuário logado. ✅ |
PATCH |
/perfil/usuario |
Edição do próprio perfil (nome, email e/ou senha, independentes). ✅ |
PATCH |
/perfil/desativar |
Desativação da própria conta. ✅ |
GET |
/admin |
Lista todos os usuários cadastrados. ✅ |
PATCH |
/admin/papel |
Altera o papel de um usuário. ✅ |
DELETE |
/admin/usuario |
Exclui um usuário. ✅ |
PATCH |
/admin/usuario |
Reseta a senha de um usuário. ✅ |
SECRET_KEY=<string aleatória gerada com secrets.token_hex(32)>
FERNET_KEY=<chave gerada com Fernet.generate_key()>
USUARIO_ROOT=<nome de exibição do usuário root>
SENHA_ROOT=<senha do usuário root>
EMAIL_ROOT=<email do usuário root>
MARIADB_HOST=<host do banco MariaDB>
MARIADB_PORT=<porta do banco MariaDB>
MARIADB_USER=<usuário do banco MariaDB>
MARIADB_PASSWORD=<senha do banco MariaDB>
MARIADB_DATABASE=<nome do banco MariaDB>
DATABASE_URL=<string de conexão usada pelo Alembic/SQLAlchemy, ex: mariadb+pymysql://usuario:senha@host:porta/banco?charset=utf8mb4>
uvicorn main.main:app --reloadalembic revision --autogenerate -m "descrição da mudança"
alembic upgrade headA suíte de testes roda contra o banco MariaDB real e está organizada por domínio. A limpeza de dados entre testes é feita via DELETE no banco (em vez do antigo dicionário usuario_cadastro), usando o mesmo padrão de busca por descriptografia de email do restante do projeto.
Toda a suíte está passando.
pytestPara rodar tudo, de qualquer subpasta:
pytestPara rodar uma pasta ou arquivo específico:
pytest tests/
pytest tests/test_admin.pypip install flake8
flake8 .Luiza Souza (@lu-izah19)