Pular para o conteúdo
Voltar aos projetos

Estudo de caso · Fábrica de Software do Unipê

ADM4All

Sistema de gestão acadêmica que ajudei a construir nas três camadas — banco, API e interface — e a colocar em produção.

Período
mar–set/2026 · concluído
Papel
Desenvolvedor full-stack · 2º maior contribuidor (50 commits)
Equipe
Fábrica de Software do Unipê, com PO e cliente real
Status
Em produção em adm4all.extensao-fs.com.br
  • TypeScript
  • Next.js 16
  • React 19
  • Express
  • PostgreSQL 16
  • Docker Compose
  • JWT
  • Jest
endpoints REST
96
endpoints REST
tabelas no PostgreSQL
18
tabelas no PostgreSQL
perfis de usuário
3
perfis de usuário
meus commits
50
meus commits

01 /

O problema

O projeto de extensão Administração para Todos oferece cursos à comunidade e controlava matrícula, frequência e certificados sem um sistema.

Eram três públicos com necessidades diferentes: a coordenação precisava de indicadores e relatórios; os instrutores, de chamada, cronograma e materiais; os alunos, de acompanhar o próprio progresso e baixar o certificado.

02 /

Uma plataforma, três perfis

Coordenador

  • Dashboard com indicadores reais
  • Cursos, turmas, alunos e instrutores
  • Relatórios com exportação em PDF e CSV
  • Período letivo editável sem deploy

Instrutor

  • Dashboard da turma
  • Cronograma de aulas
  • Registro de presença
  • Materiais didáticos

Aluno

  • Progresso e frequência
  • Download de materiais
  • Certificado emitido pelo sistema

Minha parte: o banco, a autenticação, o backend e a integração da área do coordenador, o dashboard do instrutor e a preparação para produção.

adm4all.extensao-fs.com.br/coordenador/dashboard
Painel do coordenador do ADM4All: alunos, turmas ativas, frequência média e certificados
Painel do coordenador, no sistema em produção.
adm4all.extensao-fs.com.br/coordenador/cursos
Tela de cursos do ADM4All com carga horária, período letivo e status calculado a partir das turmas
Cursos: o status sai das turmas a cada consulta, não de uma coluna.

03 /

Arquitetura

O front-end em Next.js fala com uma API em Express organizada em arquitetura limpa. O caso de uso depende da interface AuthRepository, não do Postgres: a implementação concreta, PostgresAuthRepository, mora na camada de infraestrutura.

Na prática, trocar o banco seria escrever outro repositório — a regra de negócio não mudaria. O preço é mais arquivos e mais cerimônia; o ganho aparece nos testes: os casos de uso rodam com repositórios falsos (jest.Mocked<AuthRepository>), sem subir banco.

docker composeNavegador3 perfis de usuárioNext.js 16 · React 19telas dos três perfisrota /api/auth/loginroda no servidor e gravaa sessão em cookie httpOnlyAPI Express · 96 endpointsinfrastructurerepositórios · SMTP · PDF · storageapplicationcasos de uso · JWT · trava de logindomainentidades · interfaces de repositórioPostgreSQL 1618 tabelasAdminerSMTPe-mails de senha
Tudo sobe com docker compose up: banco, Adminer, API e front-end. No celular, arraste o diagrama para o lado.

04 /

Modelagem: o banco antes do primeiro endpoint

usuarios centraliza a autenticação e cada perfil guarda só o que é dele. O status do curso não é coluna: ele é calculado a partir das turmas a cada consulta, para não existirem duas fontes da verdade.

Em 18/09 removi duas tabelas que nenhuma linha de código lia ou escrevia. Uma delas guardaria documento pessoal de aluno — tirar tabela vazia também é minimização de dados, não só faxina.

Acesso e segurançaPessoasAcadêmicoEmissão e gestãoperfisusuariosrecuperacoes_senhaativacoes_contacpfs_bloqueadosalunosinstrutorescoordenadorestreinamentosturmasturma_instrutoresaulasmatriculasfrequenciasmateriaiscertificadosrelatorios_geradosconfiguracoes_sistema
Passe o mouse (ou toque) numa tabela para ver com quem ela se relaciona.

05 /

Decisões técnicas e o que cada uma custou

Login por e-mail ou CPF no mesmo campo

Ganho

O aluno entra com o dado que lembra, sem precisar escolher o tipo de login.

Custo

A busca precisa normalizar o CPF, tirando pontos e traços. Foi exatamente essa normalização que criou o bug descrito na próxima seção.

JWT próprio, sem sessão no banco

Ganho

Cada requisição carrega um token assinado, guardado em cookie httpOnly pela rota do Next. O backend não precisa consultar sessão.

Custo

O logout apaga o cookie, não o token: um token assinado vale até expirar, e a validade fica em 8 horas.

Trava de força bruta por identificador, não por IP

Ganho

O login passa pela rota do Next, então o backend enxerga o mesmo IP para todo mundo. Limitar por IP travaria o sistema inteiro depois de algumas senhas erradas somadas.

Custo

Limitar também por IP exige confiar no IP repassado pelo proxy, o que só é seguro quando a API aceita conexões apenas dele.

CPF bloqueado guardado como HMAC-SHA256

Ganho

A lista de bloqueio guardava CPF, nome e e-mail para sempre, depois da exclusão — a exclusão não excluía. Agora ela responde só “este CPF está bloqueado?”.

Custo

HMAC e não SHA-256 puro: CPF tem cerca de 10⁹ valores, e um hash sem segredo cairia por força bruta. O segredo precisa ficar no ambiente do backend e ser o mesmo em toda migração.

06 /

O bug que autenticava o usuário errado

Sintoma

A busca por e-mail ou CPF em buscarUsuarioPorIdentificador podia devolver uma conta diferente da que tentava entrar.

Causa

  1. 1.Um e-mail sem números, depois de regexp_replace($1, '[^0-9]', ''), vira string vazia.
  2. 2.Quem não tem aluno vinculado (instrutor, coordenador) tem CPF nulo, e coalesce(a.cpf, '') também vira string vazia.
  3. 3.Vazio igual a vazio é verdadeiro para todas essas linhas, e o LIMIT 1 devolvia uma qualquer — não necessariamente a dona do e-mail.

Correção

Comparar CPF só quando o identificador tem dígitos, e sem o coalesce: CPF nulo não casa com nada.

O que ficou: normalização que transforma entrada inválida em string vazia precisa de guarda. Vazio sempre casa com vazio.

PostgresAuthRepository.ts
 LEFT JOIN coordenadores c ON c.usuario_id = u.id WHERE lower(u.email) = lower($1)-   OR regexp_replace(coalesce(a.cpf, ''), '[^0-9]', '', 'g')-      = regexp_replace($1, '[^0-9]', '', 'g')+   OR (+     length(regexp_replace($1, '[^0-9]', '', 'g')) > 0+     AND regexp_replace(a.cpf, '[^0-9]', '', 'g')+       = regexp_replace($1, '[^0-9]', '', 'g')+   ) LIMIT 1

b65780b fix: busca por identificador podia autenticar o usuário errado

07 /

Go-live: do código à produção

Conduzi a preparação para produção: auditoria de segurança, adequação à LGPD, remoção de código morto, proteção de credenciais e limpeza dos dados de teste. Os commits abaixo são meus e estão no repositório.

08 /

O reconhecimento

No fim do ciclo 2026.1, recebi da Fábrica de Software o certificado de destaque Liderança & Produtividade.

Certificado Liderança & Produtividade — Fábrica de Software do Unipê
“Destacou-se pela liderança servidora, visão técnica sólida e foco em entrega, conduzindo sua equipe a vencer barreiras e superar as expectativas do cliente.”