This commit is contained in:
parent
cb08a9f48a
commit
b2aa1a7712
87
README.md
Normal file
87
README.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
# Omnichannel Backend
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Backend da plataforma Omnichannel da Sothis, responsavel por autenticacao, atendimento, integracao com WhatsApp, gestao administrativa, contatos, base de conhecimento e servicos de apoio ao painel web.
|
||||||
|
|
||||||
|
Este repositorio contem apenas a API. Para subir o projeto completo com frontend, backend e configuracao de deploy, utilize o repositorio de orquestracao:
|
||||||
|
|
||||||
|
https://chaleiradev.sothistelecom.com/Sothis/omnichannel-deploy
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Node.js
|
||||||
|
- NestJS
|
||||||
|
- TypeScript
|
||||||
|
- PostgreSQL
|
||||||
|
- Socket.IO
|
||||||
|
- whatsapp-web.js
|
||||||
|
- JSON Web Token para autenticacao
|
||||||
|
- LDAP/AD e Microsoft OAuth como provedores de login configuraveis
|
||||||
|
|
||||||
|
## Documentacao da API
|
||||||
|
|
||||||
|
A documentacao detalhada das rotas fica disponivel no Swagger do backend no ambiente publicado.
|
||||||
|
|
||||||
|
Para documentacao tecnica complementar do backend, decisoes, modulos e operacao, acesse a wiki:
|
||||||
|
|
||||||
|
https://chaleiradev.sothistelecom.com/Sothis/omnichannel-backend/wiki
|
||||||
|
|
||||||
|
## Execucao local
|
||||||
|
|
||||||
|
Instale as dependencias:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie o arquivo de ambiente a partir do exemplo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env.development
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure as variaveis de banco, JWT, CORS e integracoes no `.env.development`.
|
||||||
|
|
||||||
|
Execute em modo desenvolvimento:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Por padrao, a API usa a porta configurada em `PORT` ou `BACKEND_PORT`. Se nenhuma variavel for definida, utiliza `3001`.
|
||||||
|
|
||||||
|
## Build e producao
|
||||||
|
|
||||||
|
Gere o build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute a versao compilada:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estrutura principal
|
||||||
|
|
||||||
|
- `src/infra`: configuracao, banco de dados, logger e infraestrutura compartilhada.
|
||||||
|
- `src/modules/auth`: autenticacao, JWT, LDAP/AD e Microsoft OAuth.
|
||||||
|
- `src/modules/admin`: recursos administrativos, base de conhecimento, contatos e controles de acesso.
|
||||||
|
- `src/modules/whatsapp`: conexao WhatsApp, QR Code, mensagens, transferencia e eventos em tempo real.
|
||||||
|
- `src/modules/attendance`: apoio as regras de atendimento.
|
||||||
|
- `src/modules/call`, `src/modules/chat` e `src/modules/home`: modulos de apoio as telas e fluxos do frontend.
|
||||||
|
|
||||||
|
## Observacoes
|
||||||
|
|
||||||
|
- O banco de dados nao e criado por este repositorio. A configuracao deve apontar para uma instancia PostgreSQL existente.
|
||||||
|
- Arquivos locais como `.env*`, logs, `dist`, dumps e sessoes do WhatsApp ficam fora do Git.
|
||||||
|
- Para operacao completa, deploy e atualizacao em producao, consulte o repositorio `omnichannel-deploy`.
|
||||||
@ -1,351 +0,0 @@
|
|||||||
# Controle de Acesso, Areas e Usuarios
|
|
||||||
|
|
||||||
## Visao geral
|
|
||||||
|
|
||||||
O backend agora usa o banco PostgreSQL para manter o usuario interno da aplicacao e suas atribuicoes operacionais.
|
|
||||||
|
|
||||||
A autenticacao continua sendo feita por provedores externos:
|
|
||||||
|
|
||||||
- LDAP / Active Directory
|
|
||||||
- Microsoft OAuth
|
|
||||||
|
|
||||||
Depois que o provedor confirma a identidade, o backend sincroniza esse usuario no banco e consulta:
|
|
||||||
|
|
||||||
- perfis de acesso
|
|
||||||
- areas vinculadas
|
|
||||||
- area principal
|
|
||||||
- status de acesso
|
|
||||||
|
|
||||||
Esse status determina qual experiencia o frontend deve renderizar em `/home`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fluxo de login com banco
|
|
||||||
|
|
||||||
```text
|
|
||||||
Frontend
|
|
||||||
-> POST /auth/login ou OAuth Microsoft
|
|
||||||
-> Backend autentica no provedor externo
|
|
||||||
-> Backend cria/atualiza usuarios
|
|
||||||
-> Backend cria/atualiza usuarios_provedores
|
|
||||||
-> Backend consulta usuarios_perfis e usuarios_areas
|
|
||||||
-> Backend emite JWT com perfis/areas
|
|
||||||
-> Frontend salva authToken/authUser
|
|
||||||
-> Frontend navega para /home
|
|
||||||
```
|
|
||||||
|
|
||||||
O provedor externo valida identidade. O banco define acesso dentro do Omnichannel.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tabelas usadas
|
|
||||||
|
|
||||||
### `usuarios`
|
|
||||||
|
|
||||||
Representa uma pessoa autenticada no produto.
|
|
||||||
|
|
||||||
Campos principais:
|
|
||||||
|
|
||||||
- `id`
|
|
||||||
- `nome`
|
|
||||||
- `email`
|
|
||||||
- `ativo`
|
|
||||||
- `created_at`
|
|
||||||
- `updated_at`
|
|
||||||
|
|
||||||
### `usuarios_provedores`
|
|
||||||
|
|
||||||
Relaciona o usuario interno com o provedor externo usado no login.
|
|
||||||
|
|
||||||
Exemplos:
|
|
||||||
|
|
||||||
- `ldap` + username do AD
|
|
||||||
- `microsoft` + userPrincipalName/email
|
|
||||||
|
|
||||||
### `perfis_acesso`
|
|
||||||
|
|
||||||
Define os niveis de acesso.
|
|
||||||
|
|
||||||
Perfis iniciais:
|
|
||||||
|
|
||||||
- `Agente`
|
|
||||||
- `Supervisor`
|
|
||||||
- `Admin`
|
|
||||||
|
|
||||||
### `usuarios_perfis`
|
|
||||||
|
|
||||||
Relaciona usuarios e perfis.
|
|
||||||
|
|
||||||
Hoje o frontend usa o perfil principal para decidir a experiencia em `/home`.
|
|
||||||
|
|
||||||
### `areas`
|
|
||||||
|
|
||||||
Representa as areas operacionais.
|
|
||||||
|
|
||||||
Areas iniciais:
|
|
||||||
|
|
||||||
- `Suporte`
|
|
||||||
- `Financeiro`
|
|
||||||
- `Comercial`
|
|
||||||
|
|
||||||
### `usuarios_areas`
|
|
||||||
|
|
||||||
Relaciona usuarios e areas.
|
|
||||||
|
|
||||||
Um usuario pode ter mais de uma area, mas a regra atual permite no maximo uma area principal por usuario.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Status de acesso
|
|
||||||
|
|
||||||
O backend devolve `accessStatus` no usuario autenticado.
|
|
||||||
|
|
||||||
Valores:
|
|
||||||
|
|
||||||
- `assigned`: usuario tem pelo menos um perfil e uma area
|
|
||||||
- `unassigned`: usuario existe/autenticou, mas ainda nao tem perfil ou area
|
|
||||||
|
|
||||||
### Comportamento no frontend
|
|
||||||
|
|
||||||
```text
|
|
||||||
assigned + Admin -> /home renderiza painel Admin
|
|
||||||
assigned + Supervisor -> /home renderiza painel Supervisor
|
|
||||||
assigned + Agente -> /home renderiza tela do Atendente
|
|
||||||
unassigned -> /home renderiza tela de acesso aguardando configuracao
|
|
||||||
```
|
|
||||||
|
|
||||||
Esse comportamento permite que um usuario novo entre via AD/OAuth, seja criado no banco e fique bloqueado ate um Admin atribuir perfil e area.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Payload do usuario autenticado
|
|
||||||
|
|
||||||
Exemplo de resposta do login:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"token": "jwt",
|
|
||||||
"user": {
|
|
||||||
"id": "1",
|
|
||||||
"databaseId": 1,
|
|
||||||
"name": "Atendente Demo",
|
|
||||||
"email": "atendente@sothis.com.br",
|
|
||||||
"username": "atendente",
|
|
||||||
"provider": "ldap",
|
|
||||||
"perfis": ["Agente"],
|
|
||||||
"profiles": ["Agente"],
|
|
||||||
"areas": ["Suporte"],
|
|
||||||
"areaPrincipal": "Suporte",
|
|
||||||
"accessStatus": "assigned"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
No JWT tambem entram:
|
|
||||||
|
|
||||||
- `name`
|
|
||||||
- `email`
|
|
||||||
- `provider`
|
|
||||||
- `username`
|
|
||||||
- `perfis`
|
|
||||||
- `profiles`
|
|
||||||
- `areas`
|
|
||||||
- `areaPrincipal`
|
|
||||||
- `accessStatus`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Variaveis de ambiente
|
|
||||||
|
|
||||||
O backend le as variaveis de banco pelo `.env.development` em desenvolvimento.
|
|
||||||
|
|
||||||
```env
|
|
||||||
DB_HOST=10.0.120.75
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_USER=desenvolvimento
|
|
||||||
DB_PASSWORD=********
|
|
||||||
DB_NAME=omnichannel-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Observacao:
|
|
||||||
|
|
||||||
- `npm run dev` usa `NODE_ENV=development` e carrega `.env.development`.
|
|
||||||
- `npm run start` usa `NODE_ENV=production`; nesse caso, configure `.env.production` ou variaveis do ambiente.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Endpoints administrativos
|
|
||||||
|
|
||||||
Base path:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/admin/access
|
|
||||||
```
|
|
||||||
|
|
||||||
### Listar opcoes de acesso
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /admin/access/options
|
|
||||||
```
|
|
||||||
|
|
||||||
Resposta:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"profiles": [
|
|
||||||
{ "id": 3, "nome": "Admin" },
|
|
||||||
{ "id": 1, "nome": "Agente" },
|
|
||||||
{ "id": 2, "nome": "Supervisor" }
|
|
||||||
],
|
|
||||||
"areas": [
|
|
||||||
{ "id": 3, "nome": "Comercial" },
|
|
||||||
{ "id": 2, "nome": "Financeiro" },
|
|
||||||
{ "id": 1, "nome": "Suporte" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Listar usuarios
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /admin/access/users
|
|
||||||
```
|
|
||||||
|
|
||||||
Resposta:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"nome": "Admin Demo",
|
|
||||||
"email": "admin@sothis.com.br",
|
|
||||||
"ativo": true,
|
|
||||||
"perfis": [{ "id": 3, "nome": "Admin" }],
|
|
||||||
"areas": [{ "id": 1, "nome": "Suporte", "principal": true }],
|
|
||||||
"perfilPrincipal": { "id": 3, "nome": "Admin" },
|
|
||||||
"areaPrincipal": { "id": 1, "nome": "Suporte", "principal": true },
|
|
||||||
"accessStatus": "assigned"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Atualizar acesso de usuario
|
|
||||||
|
|
||||||
```http
|
|
||||||
PUT /admin/access/users/:id
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
Body:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"perfilId": 2,
|
|
||||||
"areaId": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Comportamento atual:
|
|
||||||
|
|
||||||
- remove perfis anteriores do usuario
|
|
||||||
- remove areas anteriores do usuario
|
|
||||||
- cria o novo perfil informado
|
|
||||||
- cria a nova area como principal
|
|
||||||
- retorna o usuario atualizado
|
|
||||||
|
|
||||||
Para remover atribuicoes:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"perfilId": null,
|
|
||||||
"areaId": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usuarios de demonstracao
|
|
||||||
|
|
||||||
A migration `003_demo_access.sql` cria usuarios iniciais para demonstracao:
|
|
||||||
|
|
||||||
| Usuario | Email | Perfil | Area |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Admin Demo | `admin@sothis.com.br` | Admin | Suporte |
|
|
||||||
| Supervisor Demo | `supervisor@sothis.com.br` | Supervisor | Suporte |
|
|
||||||
| Atendente Demo | `atendente@sothis.com.br` | Agente | Suporte |
|
|
||||||
|
|
||||||
Esses usuarios existem no banco, mas nao substituem a autenticacao real.
|
|
||||||
|
|
||||||
Para logar pela tela de login, o usuario precisa existir no AD ou no provedor Microsoft configurado.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Como testar
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Endpoints de validacao:
|
|
||||||
|
|
||||||
```text
|
|
||||||
http://localhost:3001/admin/access/options
|
|
||||||
http://localhost:3001/admin/access/users
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend sem depender de AD
|
|
||||||
|
|
||||||
No console do navegador:
|
|
||||||
|
|
||||||
```js
|
|
||||||
localStorage.setItem('authUser', JSON.stringify({
|
|
||||||
username: 'admin@sothis.com.br',
|
|
||||||
name: 'Admin Demo',
|
|
||||||
email: 'admin@sothis.com.br',
|
|
||||||
perfis: ['Admin'],
|
|
||||||
profiles: ['Admin'],
|
|
||||||
areas: ['Suporte'],
|
|
||||||
accessStatus: 'assigned'
|
|
||||||
}));
|
|
||||||
localStorage.setItem('authToken', 'dev-token');
|
|
||||||
location.href = '/home';
|
|
||||||
```
|
|
||||||
|
|
||||||
Para usuario sem atribuicao:
|
|
||||||
|
|
||||||
```js
|
|
||||||
localStorage.setItem('authUser', JSON.stringify({
|
|
||||||
username: 'novo.usuario',
|
|
||||||
name: 'Novo Usuario',
|
|
||||||
email: 'novo.usuario@sothis.com.br',
|
|
||||||
perfis: [],
|
|
||||||
profiles: [],
|
|
||||||
areas: [],
|
|
||||||
accessStatus: 'unassigned'
|
|
||||||
}));
|
|
||||||
localStorage.setItem('authToken', 'dev-token');
|
|
||||||
location.href = '/home';
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Limitacoes atuais
|
|
||||||
|
|
||||||
- Os endpoints `/admin/access/*` ainda nao possuem guard JWT nem checagem de perfil Admin.
|
|
||||||
- A alteracao de acesso substitui perfil/area atuais por um unico perfil e uma unica area principal.
|
|
||||||
- Ainda nao ha auditoria das alteracoes de acesso.
|
|
||||||
- O painel Admin consome os endpoints reais, mas ainda possui fallback visual mockado se o backend estiver indisponivel.
|
|
||||||
- A senha do banco nao deve ser commitada; arquivos `.env.*` seguem ignorados pelo Git.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Proximos passos sugeridos
|
|
||||||
|
|
||||||
- Adicionar `AuthGuard` JWT.
|
|
||||||
- Proteger `/admin/access/*` para `Admin`.
|
|
||||||
- Registrar auditoria em `logs_auditoria`.
|
|
||||||
- Criar endpoints CRUD completos para `areas`.
|
|
||||||
- Permitir multiplas areas por usuario na UI, mantendo uma area principal.
|
|
||||||
268
docs/auth.md
268
docs/auth.md
@ -1,268 +0,0 @@
|
|||||||
# Módulo de Autenticação
|
|
||||||
|
|
||||||
## Visão geral
|
|
||||||
|
|
||||||
O módulo `auth` centraliza toda a lógica de autenticação do Omnichannel. Ele suporta múltiplos provedores de identidade e emite JWT próprio da aplicação, independente de qual provedor foi usado.
|
|
||||||
|
|
||||||
Provedores implementados:
|
|
||||||
|
|
||||||
- **LDAP / Active Directory** — login com usuário e senha do AD corporativo
|
|
||||||
- **Microsoft OAuth (Entra ID)** — login via conta Microsoft com redirect OAuth 2.0
|
|
||||||
|
|
||||||
A arquitetura foi desenhada para facilitar a adição de novos provedores no futuro.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Estrutura de arquivos
|
|
||||||
|
|
||||||
```
|
|
||||||
src/modules/auth/
|
|
||||||
├── auth.module.ts # Registro do módulo no NestJS
|
|
||||||
├── auth.controller.ts # Rotas HTTP
|
|
||||||
├── auth.service.ts # Fachada — delega para os providers
|
|
||||||
├── auth.config.ts # Leitura de variáveis de ambiente
|
|
||||||
├── auth-token.service.ts # Emissão de JWT da aplicação
|
|
||||||
├── auth.types.ts # Interfaces TypeScript compartilhadas
|
|
||||||
└── providers/
|
|
||||||
├── ldap-auth.provider.ts # Autenticação LDAP/AD
|
|
||||||
├── microsoft-oauth.provider.ts # Autenticação Microsoft OAuth
|
|
||||||
└── oauth-state.service.ts # Proteção CSRF para OAuth
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Rotas disponíveis
|
|
||||||
|
|
||||||
| Método | Rota | Descrição |
|
|
||||||
|--------|---------------------------------|------------------------------------------------|
|
|
||||||
| GET | `/auth/config` | Retorna quais provedores estão habilitados |
|
|
||||||
| POST | `/auth/login` | Login com usuário e senha (LDAP/AD) |
|
|
||||||
| GET | `/auth/oauth/microsoft/start` | Inicia o fluxo OAuth com a Microsoft |
|
|
||||||
| GET | `/auth/oauth/microsoft/callback`| Callback que a Microsoft chama após o login |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Variáveis de ambiente
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Servidor
|
|
||||||
PORT=3001
|
|
||||||
FRONTEND_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# JWT
|
|
||||||
JWT_SECRET=uma-chave-longa-e-aleatoria
|
|
||||||
JWT_EXPIRES_IN=8h
|
|
||||||
|
|
||||||
# Provedores ativos (separados por vírgula)
|
|
||||||
AUTH_PROVIDERS=ldap
|
|
||||||
|
|
||||||
# LDAP / Active Directory
|
|
||||||
LDAP_ENABLED=true
|
|
||||||
LDAP_URL=ldaps://servidor-ad:636
|
|
||||||
LDAP_DOMAIN=empresa.com.br
|
|
||||||
LDAP_USER_DN_TEMPLATE={{username}}@empresa.com.br
|
|
||||||
LDAP_SEARCH_BASE=DC=empresa,DC=com
|
|
||||||
LDAP_SEARCH_FILTER=(&(objectClass=user)(sAMAccountName={{username}}))
|
|
||||||
LDAP_TIMEOUT_MS=5000
|
|
||||||
|
|
||||||
# Microsoft Entra ID (desabilitado por padrão)
|
|
||||||
MICROSOFT_ENABLED=false
|
|
||||||
MICROSOFT_TENANT_ID=common
|
|
||||||
MICROSOFT_CLIENT_ID=
|
|
||||||
MICROSOFT_CLIENT_SECRET=
|
|
||||||
MICROSOFT_REDIRECT_URI=http://localhost:3001/auth/oauth/microsoft/callback
|
|
||||||
MICROSOFT_SUCCESS_REDIRECT_URL=http://localhost:3000/login
|
|
||||||
```
|
|
||||||
|
|
||||||
> `JWT_SECRET` deve ser uma string longa e aleatória. Em produção, nunca use o valor padrão do `.env.development`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fluxo LDAP / Active Directory
|
|
||||||
|
|
||||||
```
|
|
||||||
Frontend
|
|
||||||
→ POST /auth/login { username, password }
|
|
||||||
→ AuthController
|
|
||||||
→ AuthService.loginWithLdap()
|
|
||||||
→ LdapAuthProvider.authenticate()
|
|
||||||
→ Conecta no servidor AD (LDAP_URL)
|
|
||||||
→ Faz bind com o usuário e senha
|
|
||||||
→ Se o bind falhar: UnauthorizedException
|
|
||||||
→ Busca dados do usuário no diretório (se LDAP_SEARCH_BASE configurado)
|
|
||||||
→ Monta objeto AuthenticatedUser
|
|
||||||
→ AuthTokenService.issueToken()
|
|
||||||
→ Gera JWT assinado com JWT_SECRET
|
|
||||||
→ Retorna { token, user } para o frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
O AD apenas valida a identidade. O JWT emitido é da aplicação, não do AD.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fluxo Microsoft OAuth
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Frontend redireciona para GET /auth/oauth/microsoft/start
|
|
||||||
→ Backend gera um state assinado (proteção CSRF)
|
|
||||||
→ Backend redireciona para login.microsoftonline.com
|
|
||||||
|
|
||||||
2. Usuário autentica na Microsoft
|
|
||||||
|
|
||||||
3. Microsoft chama GET /auth/oauth/microsoft/callback?code=...&state=...
|
|
||||||
→ Backend valida o state (assinatura + expiração)
|
|
||||||
→ Backend troca o code por access_token (chamada server-to-server)
|
|
||||||
→ Backend consulta Microsoft Graph /me para obter dados do usuário
|
|
||||||
→ AuthTokenService.issueToken() gera JWT próprio
|
|
||||||
→ Backend redireciona para MICROSOFT_SUCCESS_REDIRECT_URL?token=...
|
|
||||||
|
|
||||||
4. Frontend salva o token e navega para /home
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Proteção CSRF com OAuth State
|
|
||||||
|
|
||||||
O `OAuthStateService` protege o fluxo OAuth contra ataques de CSRF.
|
|
||||||
|
|
||||||
**Como funciona:**
|
|
||||||
|
|
||||||
1. No início do fluxo, o backend cria um state:
|
|
||||||
- Gera um nonce aleatório + timestamp
|
|
||||||
- Converte para base64url
|
|
||||||
- Assina com HMAC-SHA256 usando o `JWT_SECRET`
|
|
||||||
- Formato final: `payload.assinatura`
|
|
||||||
|
|
||||||
2. No callback, o backend verifica:
|
|
||||||
- O state tem os dois pedaços (`payload.assinatura`)
|
|
||||||
- A assinatura é válida (recalcula e compara com `timingSafeEqual`)
|
|
||||||
- O state não expirou (padrão: 10 minutos, configurável via `MICROSOFT_STATE_MAX_AGE_MS`)
|
|
||||||
|
|
||||||
Se qualquer verificação falhar, o callback é rejeitado com `400 Bad Request`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## JWT da aplicação
|
|
||||||
|
|
||||||
Após qualquer autenticação bem-sucedida, o `AuthTokenService` emite um JWT com o seguinte payload:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sub": "identificador-do-usuario",
|
|
||||||
"name": "Nome Completo",
|
|
||||||
"email": "usuario@empresa.com",
|
|
||||||
"provider": "ldap",
|
|
||||||
"username": "usuario"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
O `sub` é atualmente o email ou identificador externo. Quando houver banco de dados, deve ser substituído pelo ID interno da tabela `users`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Como adicionar um novo provedor
|
|
||||||
|
|
||||||
1. Crie o arquivo em `src/modules/auth/providers/novo-provedor.provider.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthConfigService } from '../auth.config';
|
|
||||||
import { AuthTokenService } from '../auth-token.service';
|
|
||||||
import { AuthResult } from '../auth.types';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class NovoProvedorProvider {
|
|
||||||
constructor(
|
|
||||||
private readonly authConfig: AuthConfigService,
|
|
||||||
private readonly authToken: AuthTokenService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async authenticate(/* dados necessários */): Promise<AuthResult> {
|
|
||||||
// 1. Valide as credenciais no provedor externo
|
|
||||||
// 2. Monte o objeto AuthenticatedUser
|
|
||||||
// 3. Emita o token com this.authToken.issueToken(user)
|
|
||||||
// 4. Retorne { token, user }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Registre o provider em `auth.module.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
providers: [
|
|
||||||
AuthConfigService,
|
|
||||||
AuthService,
|
|
||||||
AuthTokenService,
|
|
||||||
LdapAuthProvider,
|
|
||||||
MicrosoftOAuthProvider,
|
|
||||||
OAuthStateService,
|
|
||||||
NovoProvedorProvider, // adicione aqui
|
|
||||||
],
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Injete no `AuthService` e exponha o método necessário:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
constructor(
|
|
||||||
private readonly authConfig: AuthConfigService,
|
|
||||||
private readonly ldapAuthProvider: LdapAuthProvider,
|
|
||||||
private readonly microsoftOAuthProvider: MicrosoftOAuthProvider,
|
|
||||||
private readonly novoProvedorProvider: NovoProvedorProvider, // injete aqui
|
|
||||||
) {}
|
|
||||||
|
|
||||||
loginComNovoProvedor(dados: any) {
|
|
||||||
return this.novoProvedorProvider.authenticate(dados);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Adicione a rota no `AuthController`.
|
|
||||||
|
|
||||||
5. Se o provedor precisar de configuração, adicione as variáveis no `AuthConfigService` e no `.env`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Diagnóstico de problemas
|
|
||||||
|
|
||||||
### Login LDAP falha com `UnauthorizedException`
|
|
||||||
|
|
||||||
- Verifique se `LDAP_URL` está acessível a partir do servidor backend
|
|
||||||
- Confirme que `LDAP_DOMAIN` ou `LDAP_USER_DN_TEMPLATE` está correto
|
|
||||||
- Teste a conectividade: `ldapsearch -H ldaps://servidor:636 -x`
|
|
||||||
- Verifique `LDAP_TIMEOUT_MS` — servidores lentos podem estar expirando
|
|
||||||
- O erro é genérico intencionalmente para não vazar informações. Adicione um `console.log(_error)` temporário no `catch` do `ldap-auth.provider.ts` para ver o erro real
|
|
||||||
|
|
||||||
### Login Microsoft falha com `400 Bad Request`
|
|
||||||
|
|
||||||
- O state expirou (padrão: 10 minutos). Se o usuário demorou muito na tela da Microsoft, repita o fluxo
|
|
||||||
- Verifique se `MICROSOFT_REDIRECT_URI` no `.env` é idêntico ao cadastrado no Azure App Registration
|
|
||||||
- Confirme que `MICROSOFT_CLIENT_ID` e `MICROSOFT_CLIENT_SECRET` estão corretos e não expiraram
|
|
||||||
|
|
||||||
### Token inválido no frontend
|
|
||||||
|
|
||||||
- Verifique se `JWT_SECRET` não mudou entre deploys — isso invalida todos os tokens emitidos anteriormente
|
|
||||||
- Confirme que o frontend está enviando o header `Authorization: Bearer <token>`
|
|
||||||
|
|
||||||
### `GET /auth/config` retorna os provedores errados
|
|
||||||
|
|
||||||
- Verifique `LDAP_ENABLED` e `MICROSOFT_ENABLED` no `.env`
|
|
||||||
- Reinicie o servidor — variáveis de ambiente são lidas na inicialização
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## O que ainda falta para produção
|
|
||||||
|
|
||||||
- [ ] Tabela `users` no banco de dados
|
|
||||||
- [ ] Tabela `auth_identities` para vincular provedores externos ao usuário interno
|
|
||||||
- [ ] `sub` do JWT usando ID interno do banco, não email externo
|
|
||||||
- [ ] Guard NestJS para proteger rotas privadas (`@UseGuards(AuthGuard)`)
|
|
||||||
- [ ] Roles e permissões
|
|
||||||
- [ ] Auditoria de login
|
|
||||||
- [ ] Trocar token na query string por cookie HTTP-only (reduz exposição no browser)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentacao complementar
|
|
||||||
|
|
||||||
A sincronizacao de usuarios autenticados com o banco, os perfis de acesso, as areas operacionais e os endpoints administrativos estao documentados em:
|
|
||||||
|
|
||||||
- [`access-control.md`](./access-control.md)
|
|
||||||
@ -1,132 +0,0 @@
|
|||||||
# Modulo de Chat WhatsApp (Backend)
|
|
||||||
|
|
||||||
## Visao geral
|
|
||||||
|
|
||||||
O modulo de WhatsApp do backend e desenvolvido em **NestJS** e utiliza a biblioteca **whatsapp-web.js** (que roda uma instancia headless do Chromium via **Puppeteer**) para integrar a aplicacao diretamente com o WhatsApp Web em tempo real.
|
|
||||||
|
|
||||||
A arquitetura e constituida por quatro pilares:
|
|
||||||
1. **Puppeteer Client**: Controla o WhatsApp Web, gera o QR Code para autenticacao e escuta eventos de novas mensagens.
|
|
||||||
2. **WebSocket Gateway**: Transmite eventos em tempo real (novas mensagens, atualizacao de status) para o frontend usando Socket.io.
|
|
||||||
3. **Persistencia Hibrida**: Mantem conversas indexadas localmente via JSON para performance e o status de atribuicao de atendimento gravado no banco relacional (PostgreSQL).
|
|
||||||
4. **Servico de Atribuicao**: Gerencia o vinculo dos chats com os atendentes (`usuarios`) e suas respectivas `areas` operacionais.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fluxo de Eventos e Mensagens
|
|
||||||
|
|
||||||
### 1. Inicializacao e Conexao
|
|
||||||
* O backend inicia o cliente do `whatsapp-web.js` na porta configurada.
|
|
||||||
* Se nenhuma sessao ativa for encontrada na pasta `/whatsapp-session`, o cliente gera um QR Code em formato Base64.
|
|
||||||
* Esse QR Code e enviado via WebSocket (`qr`) para o frontend configurar o dispositivo.
|
|
||||||
* Uma vez conectado, o status muda para `CONNECTED` e a pasta de sessao e gravada localmente.
|
|
||||||
|
|
||||||
### 2. Captura de Mensagens (`message_create`)
|
|
||||||
Utilizamos o evento global `message_create` para capturar tanto mensagens recebidas do cliente quanto mensagens enviadas pelo proprio atendente (seja pela tela ou por outro dispositivo sincronizado):
|
|
||||||
|
|
||||||
```text
|
|
||||||
Puppeteer (Message)
|
|
||||||
-> Trata de-duplicacao ou broadcast
|
|
||||||
-> Se possuir midia, baixa os buffers em tempo real
|
|
||||||
-> Transmite via WebSocket 'message' para o frontend
|
|
||||||
-> Salva ou atualiza a conversa na persistencia local JSON
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Persistencia e Banco de Dados
|
|
||||||
|
|
||||||
### 1. Cache Local JSON (`whatsapp-chats-persist.json`)
|
|
||||||
Para evitar sobrecarregar o banco relacional e garantir latencias imperceptiveis de scroll, os chats e seus metadados de visualizacao sao armazenados de forma persistente em um arquivo JSON local na raiz do backend.
|
|
||||||
|
|
||||||
### 2. Tabela de Atribuicao de Chat (`whatsapp_chat_atribuicoes`)
|
|
||||||
O controle de quem esta atendendo qual chat e estritamente transacional e reside no banco PostgreSQL.
|
|
||||||
|
|
||||||
A estrutura da tabela e criada pela migration `004_whatsapp.sql`:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS whatsapp_chat_atribuicoes (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
chat_id VARCHAR(255) NOT NULL,
|
|
||||||
user_id SERIAL REFERENCES usuarios(id) ON DELETE CASCADE,
|
|
||||||
area_id SERIAL REFERENCES areas(id) ON DELETE SET NULL,
|
|
||||||
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE(chat_id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Regras de Atribuicao:
|
|
||||||
* `chat_id`: Identificador unico da conversa no WhatsApp (ex: `5511999999999@c.us` ou `...@lid`).
|
|
||||||
* `user_id`: ID inteiro (`usuarios.id`) do atendente que assumiu.
|
|
||||||
* `area_id`: ID inteiro (`areas.id`) do setor sob o qual o atendimento esta sendo prestado (ex: `1` para Suporte).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Endpoints do Modulo
|
|
||||||
|
|
||||||
Base path: `/whatsapp`
|
|
||||||
|
|
||||||
### 1. Enviar Mensagem
|
|
||||||
```http
|
|
||||||
POST /whatsapp/send
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
**Payload**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"to": "5511999999999@c.us",
|
|
||||||
"message": "Ola, tudo bem?",
|
|
||||||
"media": {
|
|
||||||
"data": "base64String...",
|
|
||||||
"mimetype": "image/png",
|
|
||||||
"filename": "comprovante.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Atribuir Chat
|
|
||||||
```http
|
|
||||||
POST /whatsapp/assign
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
**Payload**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"chatId": "5511999999999@c.us",
|
|
||||||
"userId": 4,
|
|
||||||
"areaId": 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Liberar Chat
|
|
||||||
```http
|
|
||||||
DELETE /whatsapp/release/:chatId
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Limitacoes e Solucoes de Bugs
|
|
||||||
|
|
||||||
### 1. Payload Too Large (Upload de Midia)
|
|
||||||
Para permitir o envio de imagens, videos e audios pesados codificados em Base64, o limite padrao de payload do NestJS foi estendido para **50MB** no `main.ts` tanto para formato JSON quanto para `urlencoded`.
|
|
||||||
|
|
||||||
### 2. Nomes Numericos do WhatsApp (Smart Name Resolution)
|
|
||||||
Devido a latencias do WhatsApp Web, as mensagens recebidas as vezes reportam apenas o telefone/JID como nome do contato. O servico de WhatsApp contem uma camada reativa de reparacao automatica:
|
|
||||||
* Checa se o nome e puramente numerico.
|
|
||||||
* Se for, faz uma chamada em background para o Puppeteer (`client.getContactById`) para obter o `notifyName` real do cliente e atualizar o cache local.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Como Rodar e Testar
|
|
||||||
|
|
||||||
### Requisitos locais
|
|
||||||
* Node.js v18+
|
|
||||||
* PostgreSQL ativo com as migrations executadas
|
|
||||||
* Google Chrome ou Chromium instalado (o Puppeteer tentara usar o bundle local se omitido)
|
|
||||||
|
|
||||||
### Executando em desenvolvimento
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Os logs do NestJS no terminal indicarao as fases de inicializacao do Puppeteer, geracao de QR Code ou recuperacao de sessao ativa.
|
|
||||||
Loading…
Reference in New Issue
Block a user