-
Notifications
You must be signed in to change notification settings - Fork 0
Documentação do Projeto
Documentação técnica completa da arquitetura, fluxos de uso e funcionamento da plataforma Valida AI.
- Visão Geral
- Arquitetura da Aplicação
- Stack Tecnológica
- Backend — Como Funciona
- Frontend — Como Funciona
- Banco de Dados
- Armazenamento de Arquivos (S3)
- Fluxos de Uso
- Autenticação e Autorização
- Multi-Tenancy
- Sistema de Eventos
- Geração e Verificação de Certificados
- Monitoramento e Observabilidade
- Infraestrutura e Deploy
- CI/CD
O Valida AI é uma plataforma web para validação de horas complementares acadêmicas. Estudantes submetem comprovantes em PDF, coordenadores de curso avaliam (aprovando, reprovando ou solicitando revisão) e, ao aprovar, o sistema gera automaticamente um certificado em PDF com QR Code, verificável publicamente sem login.
| Ator | Papel |
|---|---|
| Estudante | Submete documentos comprobatórios, acompanha status e baixa certificados |
| Coordenador | Avalia documentos dos cursos que coordena (aprova, reprova, solicita revisão) |
| Administrador | Gerencia instituições, cursos, usuários e vínculos de coordenadores |
| Visitante | Verifica a autenticidade de certificados pela página pública (sem login) |
flowchart LR
Estudante(["Estudante"])
Coordenador(["Coordenador"])
Admin(["Administrador"])
Visitante(["Visitante"])
ValidaAI["Valida AI\nFrontend + Backend + DB"]
S3[("MinIO / AWS S3\nArmazenamento de\nPDFs e Certificados")]
Estudante -- "HTTPS" --> ValidaAI
Coordenador -- "HTTPS" --> ValidaAI
Admin -- "HTTPS" --> ValidaAI
Visitante -- "HTTPS" --> ValidaAI
ValidaAI -- "S3 API" --> S3
A aplicação segue uma arquitetura cliente-servidor com separação clara entre frontend (SPA) e backend (API REST), comunicando-se via JSON sobre HTTPS.
flowchart TB
subgraph ValidaAI["Valida AI"]
SPA["Frontend SPA\nReact 19 · Vite · TailwindCSS 4\nReact Router 7"]
API["API REST\nNode.js · Express 5 · TypeScript\nJWT Auth · Barramento de Eventos"]
DB[("PostgreSQL 16")]
S3[("MinIO / AWS S3\nArmazenamento de Objetos")]
Prometheus["Prometheus"]
Grafana["Grafana"]
end
SPA <-- "JSON / HTTPS" --> API
API -- "SQL / TCP" --> DB
API -- "S3 API" --> S3
Prometheus -. "Scrape /metrics" .-> API
Grafana -. "Query" .-> Prometheus
-
Modular por domínio: backend organizado em módulos (
auth,usuarios,documentos,validacao,certificados,cursos,instituicoes), cada um com rotas e repositórios próprios. - Orientada a eventos: transições de status de documentos disparam eventos processados assincronamente por handlers dedicados.
- Multi-tenant hierárquico: isolamento por instituição → curso → usuário.
- Stateless: autenticação via JWT, sem estado de sessão no servidor.
-
SQL direto: queries explícitas via driver
pg(sem ORM).
| Camada | Tecnologias |
|---|---|
| Linguagem | TypeScript (backend e frontend) |
| Backend | Node.js 18+, Express 5, JWT (jsonwebtoken), bcryptjs, zod, helmet, multer, pdfkit, qrcode, winston
|
| Frontend | React 19, Vite 8, TailwindCSS 4, React Router 7, React Hook Form, TanStack Query, Axios |
| Banco de Dados | PostgreSQL 16 (driver pg, SQL direto) |
| Armazenamento | MinIO (dev) / AWS S3 (prod) via @aws-sdk/client-s3
|
| Containerização | Docker, Docker Compose |
| Proxy Reverso | Nginx (SSL/TLS termination) |
| SSL/TLS | Let's Encrypt via Certbot |
| CI/CD | GitHub Actions |
| Qualidade | ESLint, Prettier, SonarCloud |
| Testes | Jest + Supertest (backend), Vitest + Testing Library (frontend) |
| Monitoramento | Prometheus (métricas), Grafana (dashboards) |
| Logging | Winston (structured logging com rotação de arquivos) |
| Cache | Redis (provisionado para uso futuro) |
backend/src/
├── servidor.ts # Inicialização do servidor HTTP (porta 3000)
├── aplicativo.ts # Configuração do Express (middlewares, rotas, error handling)
├── configuracao.ts # Leitura de variáveis de ambiente
│
├── banco/
│ ├── conexao.ts # Pool de conexões PostgreSQL
│ └── migrations/ # Scripts de migração SQL incrementais
│
├── middleware/
│ ├── autenticacao.ts # Verificação do token JWT (Bearer)
│ └── autorizacao.ts # Controle de acesso por perfil (RBAC)
│
├── modulos/ # Módulos de domínio
│ ├── auth/
│ │ └── rotas.ts # POST /api/auth/login, /api/auth/cadastro
│ ├── usuarios/
│ │ ├── rotas.ts # CRUD de usuários
│ │ └── repositorio.ts # Queries SQL de usuários
│ ├── documentos/
│ │ ├── rotas.ts # Submissão, listagem, download de documentos
│ │ └── repositorio.ts # Queries SQL de documentos
│ ├── validacao/
│ │ ├── rotas.ts # Aprovar, reprovar, solicitar revisão
│ │ └── repositorio.ts # Queries SQL de validação
│ ├── certificados/
│ │ ├── rotas.ts # Listagem e download de certificados
│ │ └── repositorio.ts # Queries SQL de certificados
│ ├── cursos/
│ │ ├── rotas.ts # CRUD de cursos
│ │ └── repositorio.ts # Queries SQL de cursos
│ └── instituicoes/
│ ├── rotas.ts # CRUD de instituições
│ └── repositorio.ts # Queries SQL de instituições
│
├── eventos/
│ ├── barramento.ts # EventEmitter (barramento de eventos)
│ ├── tipos.ts # Tipos de eventos
│ ├── registrar.ts # Registro de handlers
│ └── handlers/
│ ├── documento-submetido.ts # Notifica coordenadores
│ ├── documento-aprovado.ts # Gera certificado + notifica estudante
│ ├── documento-reprovado.ts # Notifica estudante
│ └── documento-revisao-solicitada.ts # Notifica estudante
│
├── servicos/
│ ├── armazenamento.ts # Upload/download S3 + URLs assinadas
│ ├── gerador-certificado.ts # Geração de PDF com QR Code
│ └── notificacao.ts # Serviço de notificações (stub)
│
├── utils/
│ ├── metricas.ts # Métricas Prometheus
│ ├── registrador.ts # Winston logger
│ └── erros.ts # Utilitários de erro
│
└── routes/
├── publica.ts # Verificação pública de certificados
└── verificacao.ts # Health check
Cada requisição HTTP passa pelas seguintes camadas, em ordem:
flowchart TD
A["Requisição HTTP"] --> B["Helmet\nCabeçalhos de segurança"]
B --> C["CORS\nValidação de origem"]
C --> D["express.json()\nParse do body JSON — limite 10 MB"]
D --> E["Métricas\nRegistra duração e status — Prometheus"]
E --> F["Logger\nLog da requisição — Winston"]
F --> G["autenticar()\nVerifica token JWT — rotas protegidas"]
G --> H["exigirPerfil()\nVerifica papel do usuário — RBAC"]
H --> I["Rota / Handler\nLógica de negócio do módulo"]
I --> J["Repositório\nQuery SQL via Pool PostgreSQL"]
J --> K["Resposta JSON\nRetorno ao cliente"]
| Método | Rota | Descrição |
|---|---|---|
POST |
/api/auth/login |
Login com e-mail e senha, retorna token JWT |
POST |
/api/auth/cadastro |
Auto-cadastro de estudante (4 etapas) |
| Método | Rota | Perfil | Descrição |
|---|---|---|---|
GET |
/api/documentos |
Todos | Lista documentos (filtrado por perfil) |
POST |
/api/documentos |
Estudante | Submete um documento PDF |
GET |
/api/documentos/:id/download |
Todos | URL assinada para download do PDF original |
| Método | Rota | Perfil | Descrição |
|---|---|---|---|
PATCH |
/api/documentos/:id/aprovar |
Coordenador/Admin | Aprova o documento, dispara geração de certificado |
PATCH |
/api/documentos/:id/reprovar |
Coordenador/Admin | Reprova com observações obrigatórias |
PATCH |
/api/documentos/:id/solicitar-revisao |
Coordenador/Admin | Solicita revisão com observações |
| Método | Rota | Perfil | Descrição |
|---|---|---|---|
GET |
/api/certificados |
Estudante | Lista certificados do estudante |
GET |
/api/certificados/:id/download |
Todos | URL assinada para download do certificado PDF |
| Método | Rota | Perfil | Descrição |
|---|---|---|---|
GET |
/api/cursos |
Público | Lista cursos ativos (para cadastro) |
GET |
/api/cursos/admin |
Admin | Lista todos os cursos com contagem de alunos |
GET |
/api/cursos/meus |
Coordenador | Cursos que o coordenador gerencia |
POST |
/api/cursos |
Admin | Cria um novo curso |
| Método | Rota | Perfil | Descrição |
|---|---|---|---|
GET |
/api/instituicoes |
Todos | Lista instituições |
POST |
/api/instituicoes |
Admin | Cria uma nova instituição |
| Método | Rota | Perfil | Descrição |
|---|---|---|---|
GET |
/api/usuarios |
Admin | Lista todos os usuários |
POST |
/api/usuarios |
Admin | Cria um novo usuário |
PATCH |
/api/usuarios/:id |
Admin | Atualiza dados de um usuário |
| Método | Rota | Descrição |
|---|---|---|
GET |
/api/publica/verificar/:hash |
Verifica autenticidade de um certificado (sem login) |
GET |
/verificacao |
Health check |
GET |
/metrics |
Métricas Prometheus |
frontend/src/
├── main.tsx # Entry point (React DOM + providers)
├── App.tsx # Roteamento principal (React Router)
│
├── pages/ # Páginas da aplicação
│ ├── Apresentacao.tsx # Landing page pública
│ ├── Login.tsx # Login em 2 etapas (e-mail → senha)
│ ├── Cadastro.tsx # Cadastro em 4 etapas
│ ├── Dashboard.tsx # Painel principal (conteúdo por perfil)
│ ├── Documentos.tsx # Lista de documentos com filtros
│ ├── DetalheDocumento.tsx # Detalhes de um documento
│ ├── SubmeterDocumento.tsx# Formulário de submissão de PDF
│ ├── MeusCertificados.tsx # Certificados do estudante
│ ├── Perfil.tsx # Edição de perfil
│ ├── Usuarios.tsx # CRUD de usuários (admin)
│ ├── Alunos.tsx # Lista de alunos (coordenador)
│ ├── Instituicoes.tsx # CRUD de instituições (admin)
│ ├── Cursos.tsx # CRUD de cursos (admin)
│ ├── Verificar.tsx # Verificação pública de certificado
│ └── NaoEncontrado.tsx # Página 404
│
├── components/
│ ├── Layout.tsx # Layout principal com sidebar responsiva
│ ├── PrivateRoute.tsx # Guard de rota (auth + perfil)
│ └── ui/ # Componentes reutilizáveis (botões, inputs, etc.)
│
├── contexts/
│ ├── AuthContext.tsx # Estado de autenticação (token, usuário, login/logout)
│ ├── ToastContext.tsx # Notificações toast
│ └── ThemeContext.tsx # Tema claro/escuro
│
├── services/
│ ├── api.ts # Instância Axios com interceptor de auth
│ ├── auth.ts # Chamadas de login e cadastro
│ ├── documentos.ts # Chamadas da API de documentos
│ ├── certificados.ts # Chamadas da API de certificados
│ ├── cursos.ts # Chamadas da API de cursos
│ ├── usuarios.ts # Chamadas da API de usuários
│ └── instituicoes.ts # Chamadas da API de instituições
│
├── hooks/ # Hooks customizados
├── types/ # Interfaces TypeScript
└── lib/
└── queryClient.ts # Configuração do TanStack Query
| Rota | Página | Acesso | Descrição |
|---|---|---|---|
/ |
Apresentacao | Público | Landing page |
/login |
Login | Público | Tela de login |
/cadastro |
Cadastro | Público | Auto-cadastro de estudante |
/verificar/:hash |
Verificar | Público | Verificação de certificado |
/dashboard |
Dashboard | Autenticado | Painel principal |
/documentos |
Documentos | Autenticado | Lista de documentos |
/documentos/:id |
DetalheDocumento | Autenticado | Detalhes do documento |
/documentos/novo |
SubmeterDocumento | Estudante | Submissão de documento |
/certificados |
MeusCertificados | Estudante | Certificados do estudante |
/alunos |
Alunos | Coordenador | Lista de alunos do curso |
/usuarios |
Usuarios | Admin | Gerenciamento de usuários |
/instituicoes |
Instituicoes | Admin | Gerenciamento de instituições |
/cursos |
Cursos | Admin | Gerenciamento de cursos |
/perfil |
Perfil | Autenticado | Edição de perfil |
flowchart TB
subgraph Providers["Providers — main.tsx"]
Auth["AuthProvider\ntoken, usuário, login/logout"]
Query["QueryClientProvider\nTanStack Query — cache do servidor"]
Toast["ToastProvider\nNotificações visuais"]
Theme["ThemeProvider\nTema claro/escuro"]
end
Providers --> App["App.tsx\nRoteamento + Páginas"]
-
AuthContext: armazena token JWT e dados do usuário no
localStorage. Fornecelogin(),logout()eusuariopara toda a aplicação. - TanStack Query: gerencia estado do servidor (documentos, certificados, usuários, cursos) com cache automático, refetch e invalidação.
- ToastContext: sistema de notificações visuais.
- ThemeContext: alternância de tema.
O arquivo services/api.ts cria uma instância Axios centralizada:
-
Base URL: lida a partir de
VITE_API_URL(variável de ambiente do Vite). -
Interceptor de requisição: injeta automaticamente o header
Authorization: Bearer <token>em todas as chamadas. -
Interceptor de resposta: ao receber
401 Unauthorized, limpa olocalStoragee redireciona para/login.
flowchart TD
A["Componente React"] --> B["useQuery() / useMutation()\nTanStack Query"]
B --> C["services/documentos.ts\nCamada de serviço"]
C --> D["services/api.ts — Axios\nInterceptors: auth + error handling"]
D --> E["Backend API — Express"]
erDiagram
instituicoes ||--o{ cursos : "1:N"
cursos ||--o{ usuarios : "1:N"
cursos ||--o{ documentos : "1:N"
cursos ||--o{ coordenadores_cursos : "N:N"
usuarios ||--o{ coordenadores_cursos : "N:N"
usuarios ||--o{ documentos : "estudante"
documentos ||--o| certificados : "1:1"
documentos ||--o{ historico_validacoes : "1:N"
instituicoes {
uuid id PK
varchar nome
varchar sigla
varchar cnpj
text[] dominios_email
boolean ativa
}
cursos {
uuid id PK
uuid instituicao_id FK
varchar nome
varchar codigo
int carga_horaria_complementar
varchar turno
varchar modalidade
boolean ativo
}
usuarios {
uuid id PK
varchar nome
varchar email UK
varchar senha_hash
perfil_usuario perfil
varchar matricula
varchar cpf
uuid curso_id FK
boolean ativo
}
coordenadores_cursos {
uuid coordenador_id FK
uuid curso_id FK
}
documentos {
uuid id PK
varchar titulo
tipo_documento_enum tipo
int carga_horaria
uuid estudante_id FK
uuid curso_id FK
status_documento status
uuid coordenador_id FK
text observacoes
varchar caminho_arquivo
}
certificados {
uuid id PK
uuid documento_id FK
uuid estudante_id FK
varchar hash UK
varchar caminho_arquivo
}
historico_validacoes {
uuid id PK
uuid documento_id FK
uuid usuario_id FK
varchar status_anterior
varchar status_novo
text observacoes
jsonb metadados
}
| Tabela | Propósito |
|---|---|
instituicoes |
Instituições de ensino, com domínios de e-mail permitidos |
cursos |
Cursos vinculados a instituições |
usuarios |
Estudantes, coordenadores e admins |
coordenadores_cursos |
Relação N:N entre coordenadores e cursos |
documentos |
Documentos comprobatórios submetidos |
certificados |
Certificados gerados (1:1 com documento aprovado) |
historico_validacoes |
Trilha de auditoria de todas as decisões |
-
perfil_usuario:estudante,coordenador,admin -
status_documento:pendente,em_analise,aprovado,reprovado,revisao_solicitada,cancelado -
tipo_documento_enum:certificado_curso,certificado_evento,declaracao_participacao,comprovante_atividade,artigo_publicado,outro
O schema base está em infra/banco.sql e as migrações incrementais em backend/src/banco/migrations/:
| Arquivo | Descrição |
|---|---|
001_add_certificados.sql |
Criação da tabela certificados
|
002_add_senha.sql |
Adição da coluna senha_hash com senha padrão |
003_update_tipo_documento_enum.sql |
Atualização dos tipos de documento |
004_add_coordenadores_cursos.sql |
Relação N:N coordenador-curso |
O backend não usa ORM. Cada módulo possui um repositorio.ts com queries SQL explícitas executadas via Pool do driver pg:
flowchart LR
A["Rota — Express"] --> B["Repositório — SQL"] --> C["Pool PostgreSQL"] --> D[("Banco")]
O sistema usa armazenamento compatível com S3 para PDFs de documentos e certificados.
| Ambiente | Serviço | Endpoint |
|---|---|---|
| Desenvolvimento | MinIO | http://localhost:9000 |
| Produção | AWS S3 | https://s3.amazonaws.com |
| Bucket | Variável de Ambiente | Conteúdo |
|---|---|---|
| Documentos | AWS_S3_BUCKET |
PDFs originais submetidos pelos estudantes |
| Certificados | AWS_S3_BUCKET_CERTIFICADOS |
PDFs de certificados gerados pelo sistema |
flowchart TD
A["Estudante envia PDF"] --> B["Multer — Express\nRecebe multipart/form-data\nlimite 10 MB"]
B --> C["armazenamento.ts\nPutObjectCommand"]
C --> D[("MinIO / AWS S3\ndocumentos/id/filename")]
flowchart TD
A["Usuário clica em Baixar"] --> B["GET /api/documentos/:id/download"]
B --> C["armazenamento.ts\nGetObjectCommand + S3 Presigner"]
C --> D["URL assinada\nexpira em 1 hora"]
D --> E["Navegador baixa o PDF\ndireto do S3"]
flowchart LR
E1["Etapa 1\nSelecionar\nInstituição"] --> E2["Etapa 2\nSelecionar\nCurso + Turno"]
E2 --> E3["Etapa 3\nNome, E-mail,\nMatrícula"]
E3 --> V{"Validação do\ndomínio de e-mail"}
V -->|Válido| E4["Etapa 4\nDefinir Senha\nmin 6 chars"]
V -->|Inválido| Erro["Erro:\ndomínio não autorizado"]
E4 --> OK["Cadastro concluído\nToken JWT retornado"]
O que acontece por trás:
- Frontend chama
GET /api/instituicoespara listar instituições. - Ao selecionar instituição, chama
GET /api/cursos?instituicao_id=Xpara listar cursos. - Ao preencher o formulário completo, chama
POST /api/auth/cadastro. - Backend valida domínio do e-mail, cria hash bcrypt da senha, insere o usuário com
perfil: 'estudante'. - Backend retorna token JWT e dados do usuário. Frontend armazena no
localStoragee redireciona ao Dashboard.
flowchart LR
E1["Etapa 1\nInformar E-mail"] --> E2["Etapa 2\nInformar Senha"]
E2 --> D["Dashboard"]
O que acontece por trás:
- Frontend chama
POST /api/auth/logincom{ email, senha }. - Backend busca usuário pelo e-mail, verifica senha com
bcrypt.compare(). - Valida domínio do e-mail contra a instituição.
- Gera token JWT com payload:
{ sub, email, nome, perfil, matricula, curso_id, curso_ids, instituicao_id, instituicao_nome }. - Retorna
{ token, usuario }ao frontend. - Frontend armazena no
localStorage, atualiza oAuthContexte redireciona ao/dashboard.
flowchart LR
A["Estudante preenche\nformulário:\n• Título\n• Tipo\n• Carga horária\n• Arquivo PDF"] --> B["Backend recebe\ne processa:\n• Valida com Zod\n• Upload S3\n• Insert no BD\n• Emite evento"]
B --> C["Coordenadores\nsão notificados"]
Fluxo detalhado:
- Estudante acessa
/documentos/novoe preenche: título, tipo de documento, carga horária, arquivo PDF. - Frontend envia
POST /api/documentoscommultipart/form-data. - Backend (Multer) recebe o arquivo, valida tipo (PDF) e tamanho (máx 10 MB).
- Faz upload do PDF para o bucket S3.
- Insere registro na tabela
documentoscomstatus: 'pendente'. - Emite evento
documento_submetidono barramento. - Handler do evento busca coordenadores ativos do curso e envia notificação.
- Retorna
201 Createdao frontend com os dados do documento.
flowchart LR
C["Coordenador\navalia\ndocumento"] -->|Aprovar| AP["APROVADO"]
C -->|Reprovar| RE["REPROVADO"]
C -->|Solicitar revisão| RS["REVISÃO SOLICITADA"]
AP --> CERT["Gera certificado PDF + QR Code\nNotifica estudante"]
RE --> NOTI1["Notifica estudante\nobservações obrigatórias"]
RS --> NOTI2["Notifica estudante\nobservações obrigatórias"]
Fluxo de aprovação detalhado:
- Coordenador acessa
/documentose vê documentos dos cursos que coordena. - Clica em um documento para ver detalhes em
/documentos/:id. - Faz download do PDF original para analisar.
- Decide: aprovar, reprovar ou solicitar revisão.
-
Se aprova (
PATCH /api/documentos/:id/aprovar):- Backend atualiza
status → 'aprovado'ecoordenador_id. - Insere registro em
historico_validacoes. - Emite evento
documento_aprovado. - Handler gera certificado PDF com QR Code (PDFKit).
- Calcula hash SHA-256 único para o certificado.
- Faz upload do certificado PDF para bucket S3.
- Insere registro em
certificados. - Notifica o estudante.
- Backend atualiza
flowchart LR
A["Visitante\nescaneia\nQR Code"] --> B["Frontend carrega\n/verificar/:hash"]
B --> C["GET /api/publica/verificar/:hash"]
C -->|Hash encontrado| D["Certificado Válido\nNome, título, data"]
C -->|Hash não encontrado| E["Erro:\ncertificado inválido"]
O que acontece:
- Qualquer pessoa escaneia o QR Code impresso no certificado.
- QR Code redireciona para
{FRONTEND_URL}/verificar/{hash}. - Frontend faz
GET /api/publica/verificar/:hash(sem autenticação). - Backend busca o certificado pelo hash SHA-256.
- Se encontrado: retorna nome do estudante, título do documento, tipo, carga horária e data de aprovação.
- Frontend exibe card com selo "Certificado Válido".
- Se não encontrado: exibe mensagem de erro.
stateDiagram-v2
[*] --> PENDENTE : Estudante submete
PENDENTE --> EM_ANALISE : Coordenador visualiza
PENDENTE --> CANCELADO : Estudante cancela
EM_ANALISE --> APROVADO : Coordenador aprova
EM_ANALISE --> REPROVADO : Coordenador reprova
EM_ANALISE --> REVISAO_SOLICITADA : Coordenador solicita revisão
APROVADO --> CERTIFICADO_GERADO : Geração automática PDF + QR
REVISAO_SOLICITADA --> PENDENTE : Estudante reenvia
CERTIFICADO_GERADO --> [*]
REPROVADO --> [*]
CANCELADO --> [*]
Status possíveis:
-
pendente— documento submetido, aguardando análise -
em_analise— coordenador visualizou o documento -
aprovado— coordenador aprovou, certificado gerado automaticamente -
reprovado— coordenador reprovou com justificativa -
revisao_solicitada— coordenador pediu correções -
cancelado— estudante cancelou a submissão (apenas se ainda pendente)
sequenceDiagram
participant F as Frontend
participant B as Backend
F->>B: POST /api/auth/login { email, senha }
B-->>F: { token, usuario }
Note over F: Armazena token no localStorage
F->>B: GET /api/documentos<br/>Authorization: Bearer token
Note over B: Middleware valida JWT
B-->>F: { documentos: [...] }
Payload do Token JWT:
{
"sub": "uuid-do-usuario",
"email": "estudante@instituicao.edu.br",
"nome": "Nome do Estudante",
"perfil": "estudante",
"matricula": "2024001",
"curso_id": "uuid-do-curso",
"curso_ids": [],
"instituicao_id": "uuid-da-instituicao",
"instituicao_nome": "Nome da Instituição"
}O middleware exigirPerfil() verifica se o perfil do token corresponde aos perfis autorizados para a rota:
| Perfil | Permissões |
|---|---|
| Estudante | Submeter documentos, ver próprios documentos, ver próprios certificados, cancelar submissão pendente, editar perfil |
| Coordenador | Avaliar documentos dos cursos que coordena, ver alunos dos seus cursos, editar perfil |
| Admin | Tudo: CRUD de instituições, cursos, usuários, vincular coordenadores, ver todos os documentos |
- Senhas armazenadas com hash
bcrypt(custo 10) - Token JWT assinado com
JWT_SECRET(obrigatório em produção) - Expiração configurável via
JWT_EXPIRES_IN(padrão: 7 dias) - Headers de segurança via
helmet - CORS restrito a allowlist (
CORS_ORIGIN) - Validação de entrada com
zod - HTTPS obrigatório em produção (Nginx + Let's Encrypt)
A aplicação suporta múltiplas instituições de ensino de forma isolada, seguindo um modelo hierárquico:
flowchart TB
I["Instituição\nex: Católica SC\ndominios_email: catolicasc.org.br"]
subgraph CA["Curso A — Engenharia de Software"]
C1["Coordenador 1\nvê apenas docs do Curso A"]
E1["Estudante 1\nvê apenas próprios docs"]
E2["Estudante 2\nvê apenas próprios docs"]
end
subgraph CB["Curso B — Ciência da Computação"]
C2["Coordenador 2\nvê apenas docs do Curso B"]
E3["Estudante 3\nvê apenas próprios docs"]
end
I --> CA
I --> CB
-
Domínio de e-mail: no cadastro e login, o domínio do e-mail é validado contra
instituicoes.dominios_email. Apenas e-mails com domínios autorizados podem acessar o sistema. -
Filtragem por curso (Coordenador): coordenadores possuem vínculo N:N com cursos via
coordenadores_cursos. Ao listar documentos, o backend filtra:WHERE curso_id IN (cursos_do_coordenador). -
Filtragem por propriedade (Estudante): estudantes veem apenas documentos onde
estudante_id = usuario_logado. -
Acesso total (Admin): admins veem todos os recursos sem filtro.
A aplicação usa um barramento de eventos baseado no EventEmitter do Node.js para desacoplar ações assíncronas da resposta HTTP.
sequenceDiagram
participant Client as Cliente
participant Rota as Rota de Validação
participant BD as Banco de Dados
participant Bus as Barramento de Eventos
participant Handler as Handler Assíncrono
participant S3 as MinIO / S3
Client->>Rota: PATCH /documentos/:id/aprovar
Rota->>BD: UPDATE status → aprovado
Rota->>Bus: emitir('documento_aprovado', payload)
Rota-->>Client: 200 OK (resposta imediata)
Note over Bus,Handler: Processamento assíncrono
Bus->>Handler: documento-aprovado.ts
Handler->>Handler: Gera PDF (PDFKit + QR Code)
Handler->>S3: Upload certificado PDF
Handler->>BD: INSERT certificados
Handler->>Handler: Notifica estudante
| Evento | Gatilho | Handler |
|---|---|---|
documento_submetido |
Estudante submete um documento | Notifica coordenadores do curso |
documento_aprovado |
Coordenador aprova um documento | Gera certificado PDF e notifica estudante |
documento_reprovado |
Coordenador reprova um documento | Notifica estudante com motivo |
documento_revisao_solicitada |
Coordenador solicita revisão | Notifica estudante com observações |
Quando um documento é aprovado, o handler documento-aprovado.ts aciona o gerador-certificado.ts:
flowchart TD
A["Documento aprovado"] --> B["PDFKit cria PDF A4 Landscape"]
B --> B1["Bordas decorativas — tema azul"]
B --> B2["Título: CERTIFICADO"]
B --> B3["Nome do estudante"]
B --> B4["Tipo de atividade + Carga horária"]
B --> B5["Data de aprovação + Coordenador"]
B --> B6["QR Code 80x80px\nURL: /verificar/hash"]
B1 & B2 & B3 & B4 & B5 & B6 --> C["Hash SHA-256 gerado\ndocumentoId + timestamp + 16 bytes"]
C --> D["Upload para S3\ncertificados/docId/hash.pdf"]
D --> E["INSERT na tabela certificados"]
Qualquer pessoa pode verificar a autenticidade de um certificado:
- Escaneia o QR Code ou acessa
{URL}/verificar/{hash}. - O frontend chama
GET /api/publica/verificar/:hash. - O backend busca na tabela
certificadospelo hash. - Se encontrado, retorna dados públicos (nome, título, data).
- Nenhuma autenticação necessária.
Métricas coletadas automaticamente pelo middleware medirRequisicoes:
| Métrica | Tipo | Labels | Descrição |
|---|---|---|---|
http_request_duration_seconds |
Histogram | method, route, status | Duração das requisições HTTP |
http_requests_total |
Counter | method, route, status | Total de requisições HTTP |
| Métricas padrão do Node.js | Diversos | — | CPU, memória, event loop, GC |
Endpoint: GET /metrics (formato texto Prometheus)
- Acesso:
http://localhost:3001(dev) ou via SSH tunnel (prod) - Datasource: Prometheus
- Dashboards provisionados automaticamente via
infra/grafana/provisioning/
| Destino | Conteúdo | Rotação |
|---|---|---|
| Console | Todos os níveis (colorizado) | — |
logs/erro.log |
Apenas erros | 5 MB, até 5 arquivos |
logs/combinado.log |
Todos os níveis | 5 MB, até 5 arquivos |
Nível configurável via LOG_LEVEL (padrão: info).
docker compose -f docker-compose.dev.yml up -d| Serviço | Porta | Descrição |
|---|---|---|
| PostgreSQL | 5432 | Banco de dados (inicializado com infra/banco.sql + seed) |
| Redis | 6379 | Cache (provisionado) |
| MinIO | 9000 (API), 9001 (Console) | Armazenamento S3 local |
| Prometheus | 9090 | Coleta de métricas |
| Grafana | 3001 | Dashboards |
Após os containers, rodar backend e frontend localmente:
npm run dev # Inicia backend (porta 3000) e frontend (porta 5173) simultaneamenteflowchart TB
Internet(["Internet"]) --> Nginx
subgraph Servidor["Servidor de Produção"]
Nginx["Nginx\nPorta 80 → 301 HTTPS\nPorta 443 — TLS"]
Nginx --> Frontend["Frontend\nNginx estático\nservindo build React"]
Nginx --> Backend["Backend\nExpress API\nporta 3000"]
Backend --> PG[("PostgreSQL\nporta 5432")]
Backend --> Redis[("Redis\nporta 6379")]
Prometheus["Prometheus\nlocalhost:9090"] -. scrape .-> Backend
Grafana["Grafana\nlocalhost:3001"] -. query .-> Prometheus
Certbot["Certbot\nRenovação automática SSL"]
Certbot -. "Let's Encrypt" .-> Nginx
end
| Variável | Obrigatória | Descrição |
|---|---|---|
DB_HOST |
Sim | Host do PostgreSQL |
DB_PORT |
Sim | Porta do PostgreSQL (5432) |
DB_USER |
Sim | Usuário do banco |
DB_PASSWORD |
Sim | Senha do banco |
DB_NAME |
Sim | Nome do banco |
JWT_SECRET |
Sim | Segredo para assinar tokens JWT |
JWT_EXPIRES_IN |
Não | Tempo de expiração do token (padrão: 7d) |
AWS_REGION |
Sim | Região AWS / MinIO |
AWS_ACCESS_KEY_ID |
Sim | Chave de acesso S3 |
AWS_SECRET_ACCESS_KEY |
Sim | Segredo de acesso S3 |
AWS_S3_BUCKET |
Sim | Bucket para documentos |
AWS_S3_BUCKET_CERTIFICADOS |
Sim | Bucket para certificados |
S3_ENDPOINT |
Dev | Endpoint MinIO (ex: http://localhost:9000) |
S3_FORCE_PATH_STYLE |
Dev |
true para MinIO |
CORS_ORIGIN |
Sim | Origem permitida para CORS |
FRONTEND_URL |
Sim | URL do frontend (para QR Codes) |
VITE_API_URL |
Sim | URL da API para o frontend |
LOG_LEVEL |
Não | Nível de log Winston (padrão: info) |
O projeto usa GitHub Actions para integração e deploy contínuos.
flowchart TD
A["Push / Pull Request"] --> B["Testes\nBackend — Jest\nFrontend — Vitest"]
B --> C["Lint + Format\nESLint · Prettier"]
C --> D["SonarCloud\nAnálise estática de qualidade"]
D --> E["Build Docker\nFrontend image · Backend image"]
E --> F["Push Images\nDocker Hub / Registry"]
F --> G["Deploy via SSH\ndocker compose pull & up"]
- ESLint: linting TypeScript para backend e frontend
- Prettier: formatação consistente
- SonarCloud: análise estática (bugs, vulnerabilidades, code smells, cobertura)
- Testes automatizados: Jest + Supertest (backend), Vitest + Testing Library (frontend)
Valida AI — Plataforma de validação de horas complementares acadêmicas com emissão automática de certificados verificáveis.