Skip to content

Módulo Configurações

Localização: backend/src/modules/config/

Responsável por toda a configuração operacional do sistema: lojas, usuários, papéis (roles), integrações de comunicação (e-mail, WhatsApp, SMTP) e parâmetros de campanha. O módulo é composto pelos sub-módulos settings, roles, users e store-aliases, cada um com seu próprio controller e service.


Arquitetura geral

config/
├── settings/
│   ├── settings.controller.ts   — HTTP layer
│   └── settings.service.ts      — Lógica de negócio
├── roles/
│   ├── roles.controller.ts
│   └── roles.service.ts
├── users/
│   ├── users.controller.ts
│   ├── users.service.ts
│   ├── webhooks.controller.ts   — Webhooks do Clerk
│   └── clerk-bootstrap.service.ts
└── store-aliases/
    ├── store-aliases.controller.ts
    └── store-aliases.service.ts

Dependências transversais:

  • EncryptionService — criptografia AES-256-GCM de credenciais sensíveis
  • PrismaService — acesso ao banco de dados
  • ClerkAuthGuard — autenticação JWT via Clerk
  • PermissionsGuard — autorização por permissão declarada no decorator @Permissions(...)
  • TenantInterceptor — isola o contexto de organização por request
  • BullMQ queue whatsapp-send — acesso ao cliente Redis via queue.client
  • ChatwootDbService — operações diretas no banco do Chatwoot (desbloqueio de agentes)

Sub-módulo: Settings

Classe: SettingsService

Arquivo: backend/src/modules/config/settings/settings.service.ts

Controller: SettingsController em backend/src/modules/config/settings/settings.controller.ts

Permissão base: app:settings (rotas de loja) ou app:integrations (rotas de integração)


Método getEmailSettings(): Promise<EmailSettings | null>

Retorna as configurações de e-mail da primeira loja cadastrada. A senha é sempre mascarada como '********' antes de retornar ao frontend — o valor real nunca trafega pela API.

Fluxo:

  1. Busca a primeira loja via Store.findFirst()
  2. Se não há loja, retorna null
  3. Busca EmailSettings com where: { storeId: store.id }
  4. Se settings.pass existe, substitui pelo placeholder '********'
  5. Retorna o objeto

Tabelas afetadas: Store (leitura), EmailSettings (leitura)


Método upsertEmailSettings(data: UpdateEmailSettingsDto): Promise<EmailSettings>

Cria ou atualiza a configuração de e-mail da loja. A senha é criptografada com AES-256-GCM antes de persistir.

Parâmetros:

  • data.pass — senha SMTP em texto puro (opcional na atualização)
  • Demais campos do UpdateEmailSettingsDto

Fluxo:

  1. Busca a primeira loja; lança NotFoundException se não existe
  2. Copia data para encryptedData
  3. Se data.pass foi fornecida, chama encryptionService.encrypt(data.pass) e substitui em encryptedData
  4. Executa emailSettings.upsert({ where: { storeId } }) — cria se não existe, atualiza se existe

Segurança: A senha nunca é salva em plaintext. O formato armazenado é iv:encrypted:tag (veja EncryptionService).

Tabelas afetadas: Store (leitura), EmailSettings (escrita/upsert)


Método listWhatsappInstances(organizationId?: string): Promise<MaskedInstance[]>

Lista todas as instâncias WhatsApp da organização. Tokens, access tokens e app secrets são sempre mascarados como '********'.

Parâmetros:

  • organizationId — opcional; sem filtro lista todas as instâncias do sistema

Fluxo:

  1. Busca StoreWhatsappNumber.findMany() com join em Store (id, name, tradeName)
  2. Ordena por isDefault DESC (padrão aparece primeiro)
  3. Mapeia cada instância substituindo token, accessToken e appSecret por '********' (ou null se não configurado)

Tabelas afetadas: StoreWhatsappNumber (leitura), Store (leitura via include)


Método addWhatsappInstance(data: CreateWhatsappInstanceDto): Promise<CreatedInstance>

Adiciona uma nova instância WhatsApp. É o único momento em que o webhookVerifyToken em texto puro é retornado — o usuário deve copiá-lo imediatamente pois não é recuperável.

Parâmetros do DTO:

  • name — nome da instância
  • number — número de telefone
  • provider'meta_cloud' ou outro provider
  • token — token de acesso da instância (opcional)
  • wabaId — ID da WABA no Meta (opcional)
  • phoneNumberId — ID do número no Meta (opcional)
  • accessToken — access token de longa duração do Meta (opcional)
  • appSecret — app secret do aplicativo Meta (opcional)
  • webhookVerifyToken — token para validação de webhook Meta (opcional; se omitido, gera UUID)
  • isDefault — se deve ser a instância padrão da loja

Fluxo:

  1. Busca a primeira loja; lança NotFoundException se não existe
  2. Se isDefault = true, rebaixa todas as demais instâncias da loja para isDefault = false
  3. Criptografa token, accessToken e appSecret (se fornecidos) via EncryptionService.encrypt()
  4. Gera o webhookVerifyToken:
    • Usa o valor fornecido (após .trim()) ou gera um UUID via randomUUID()
    • Armazena o hash SHA-256 do token (não o plaintext)
  5. Cria o registro em StoreWhatsappNumber com status: 'CONNECTED'
  6. Retorna o objeto com webhookVerifyToken em plaintext (única vez), mas accessToken e appSecret mascarados

Segurança:

  • token, accessToken, appSecret → criptografados com AES-256-GCM
  • webhookVerifyToken → armazenado como SHA-256 (suficiente para comparação, impossível de reverter)
  • Token retornado em plaintext apenas nesta resposta de criação

Tabelas afetadas: Store (leitura), StoreWhatsappNumber (escrita)


Método updateWhatsappInstance(id: string, data: UpdateWhatsappInstanceDto): Promise<MaskedInstance>

Atualiza campos de uma instância WhatsApp existente. Re-criptografa credenciais somente quando um novo valor real é enviado — enviar '********' sinaliza "manter como está".

Parâmetros:

  • id — UUID da instância
  • data — campos editáveis: name, number, wabaId, phoneNumberId, isDefault, chatwootInboxId, accessToken, appSecret

Fluxo:

  1. Valida que a instância existe; lança NotFoundException se não
  2. Se isDefault = true, rebaixa as demais instâncias da mesma loja
  3. Monta updateData com apenas os campos presentes no DTO (campos undefined são ignorados)
  4. Para accessToken e appSecret: só re-criptografa se o valor é diferente de '********'
  5. Executa StoreWhatsappNumber.update()
  6. Retorna o objeto com accessToken e appSecret mascarados

Tabelas afetadas: StoreWhatsappNumber (escrita)


Método deleteWhatsappInstance(id: string): Promise<StoreWhatsappNumber>

Remove uma instância WhatsApp e seus dados dependentes em cascata manual (Prisma não faz cascade automático nessas relações).

Fluxo:

  1. Remove todos os WhatsappTemplate vinculados à instância
  2. Remove todas as WhatsappMessage vinculadas à instância
  3. Remove o registro StoreWhatsappNumber

Tabelas afetadas: WhatsappTemplate (deleção), WhatsappMessage (deleção), StoreWhatsappNumber (deleção)


Método exchangeMetaCode(code: string, redirectUri: string): Promise<CreatedInstance>

Implementa o fluxo OAuth do Meta para onboarding de WhatsApp Business. Troca o authorization code por um access token e descobre automaticamente a WABA associada.

Parâmetros:

  • code — authorization code retornado pelo Meta OAuth dialog
  • redirectUri — URI de redirecionamento usada no OAuth (deve ser idêntica à usada na abertura do dialog)

Fluxo completo:

Passo 1 — Troca do code por access_token:

  • GET https://graph.facebook.com/v19.0/oauth/access_token com client_id, client_secret, code, redirect_uri
  • Valida que a resposta contém access_token; caso contrário lança erro descritivo

Passo 2 — Descoberta do WABA ID (4 estratégias em cascata):

EstratégiaEndpointCondição de uso
Adebug_tokengranular_scopesFunciona quando o app é Tech Provider
B/me/whatsapp_business_accountsFallback genérico
C/me/businessesowned_whatsapp_business_accountsPara contas com Business Manager
D/{userId}/whatsapp_business_accounts (app token)Último recurso via user_id do debug_token

Se nenhuma estratégia encontrar um WABA ID, lança erro explicativo orientando o usuário a configurar o Meta Business Manager.

Passo 3 — Busca dados da WABA e números:

  • GET /{wabaId}?fields=id,name,phone_numbers{id,display_phone_number,verified_name}
  • Valida que há ao menos um número de telefone; caso contrário lança erro
  • Usa o primeiro número encontrado

Passo 4 — Criação da instância:

  • Delega para addWhatsappInstance() com provider: 'meta_cloud' e dados da WABA

Segurança: O appSecret nunca é logado em plaintext. Logs usam maskValue(appSecret) que exibe {primeiros4}***{últimos4}.

Tabelas afetadas: Idem a addWhatsappInstance()


Método getStore(): Promise<Store | null>

Retorna os dados da loja padrão (primeira loja encontrada). Não aplica filtro por organização — assume single-tenant por loja.

Tabelas afetadas: Store (leitura)


Método updateStore(data: { name?, cnpj?, cityNormalized? }): Promise<Store>

Atualiza nome, CNPJ e cidade normalizada da loja padrão.

Fluxo:

  1. Busca a primeira loja; lança NotFoundException se não existe
  2. Atualiza apenas os campos name, cnpj e cityNormalized

Tabelas afetadas: Store (escrita)


Método getStores(organizationId: string): Promise<Store[]>

Lista todas as lojas da organização com campos resumidos: id, name, tradeName, isEcommerce, isActive. Ordenadas alfabeticamente por nome.

Tabelas afetadas: Store (leitura)


Método getBusinessHours(storeId: string, organizationId: string): Promise<{ businessHours: Json | null }>

Retorna a configuração de horários de funcionamento de uma loja específica. Valida pertencimento à organização antes de retornar.

Tabelas afetadas: Store (leitura)


Método setBusinessHours(storeId, businessHours, organizationId): Promise<{ updated: true }>

Persiste a configuração de horários e dispara a limpeza de flags Redis para forçar reavaliação imediata.

Parâmetros:

  • storeId — UUID da loja
  • businessHours — objeto JSON com o schedule por dia da semana
  • organizationId — UUID da organização (validação de tenant)

Fluxo:

  1. Valida que a loja existe e pertence à organização
  2. Salva businessHours como Prisma.InputJsonValue no campo Store.businessHours
  3. Chama clearBusinessHoursFlags() para limpar o estado Redis

Tabelas afetadas: Store (escrita)


Método privado clearBusinessHoursFlags(storeId, organizationId, businessHours)

Limpa as flags Redis de business hours após uma atualização de horários, garantindo que o sistema reavalie o status de abertura imediatamente.

Fluxo:

  1. Busca todas as instâncias WhatsApp da loja que têm chatwootInboxId configurado
  2. Se não há instâncias com inbox, retorna sem fazer nada
  3. Calcula a data atual em horário de Brasília (UTC-3)
  4. Chama isTodayClosePast() para saber se o horário de fechamento de hoje já passou
  5. Para cada inbox, marca para deleção as chaves Redis:
    • bh:warned:{orgId}:{inboxId}:{date} — sempre deletada
    • bh:blocked:{orgId}:{inboxId}:{date} — deletada apenas se o fechamento não passou
    • bh:rotated:{orgId}:{inboxId}:{date} — deletada apenas se o fechamento não passou
  6. Executa redis.del(...keys) em batch
  7. Se o fechamento não passou (closeAlreadyPast = false), chama ChatwootDbService.unblockInboxAgents() para cada inbox — permite que agentes façam login imediatamente sem esperar o cron

Redis keys afetadas:

  • bh:warned:{organizationId}:{chatwootInboxId}:{YYYY-MM-DD}
  • bh:blocked:{organizationId}:{chatwootInboxId}:{YYYY-MM-DD}
  • bh:rotated:{organizationId}:{chatwootInboxId}:{YYYY-MM-DD}

Erros: Falhas são logadas como warn mas não propagadas — o update do banco já foi confirmado.


Método privado isTodayClosePast(businessHours): boolean

Determina se o horário de fechamento do dia atual (em BRT) já passou, considerando uma tolerância de 60 minutos após o horário configurado.

Lógica:

  • Converte UTC para BRT (UTC-3) manualmente (sem biblioteca de timezone)
  • Determina o dia da semana local
  • schedule[localDay].close do objeto de horários
  • Compara o horário atual (em minutos desde meia-noite) com closeHour * 60 + closeMinute + 60
  • Retorna false em caso de qualquer erro (comportamento seguro: assume que ainda está aberto)

Método setEcommerceStore(storeId, isEcommerce, organizationId): Promise<Store>

Define qual loja é a loja e-commerce da organização. Apenas uma loja pode ser e-commerce por vez.

Fluxo:

  1. Valida que a loja existe e pertence à organização
  2. Se isEcommerce = true, rebaixa todas as outras lojas da organização para isEcommerce = false
  3. Atualiza a loja alvo

Tabelas afetadas: Store (escrita múltipla via updateMany + update)


Método getOrg(organizationId: string): Promise<{ id, name, logoUrl } | null>

Retorna o nome e URL do logo da organização.

Tabelas afetadas: Organization (leitura)


Método updateOrg(organizationId, data: { logoUrl? }): Promise<{ id, name, logoUrl }>

Atualiza a URL do logo da organização.

Tabelas afetadas: Organization (escrita)


Método getCampaignSettings(organizationId: string): Promise<CampaignSettings>

Retorna as configurações de campanha da organização. Todos os campos têm valores padrão garantidos quando não configurados.

Valores padrão:

CampoPadrão
attributionWindowDays7
replyWindowHours48
optOutMessageMensagem padrão de descadastro
redirectGreetingSaudação padrão
redirectConfirmationConfirmação padrão com e
messageLimitDaysnull (desabilitado)
messageLimitCountnull (desabilitado)
blockMultipleCampaignsfalse

Tabelas afetadas: Organization (leitura do campo JSON campaignSettings)


Método updateCampaignSettings(organizationId, data): Promise<CampaignSettings>

Atualiza as configurações de campanha com validação e sanitização de valores. Aplica merge com os valores existentes — campos não enviados são preservados.

Validações e limites:

CampoMínimoMáximoTratamento
attributionWindowDays190Math.min(Math.max(1, val), 90)
replyWindowHours1720Math.min(Math.max(1, val), 720)
optOutMessage1000 chars.trim().slice(0, 1000)
redirectGreeting1000 chars.trim().slice(0, 1000)
redirectConfirmation2000 chars.trim().slice(0, 2000)
messageLimitDays1 ou null365null = desabilita
messageLimitCount1 ou null100null = desabilita
blockMultipleCampaignsboolean direto

Tabelas afetadas: Organization (escrita do campo JSON campaignSettings)


Método getOrgSmtp(organizationId: string): Promise<SmtpConfig | null>

Retorna a configuração SMTP da organização com a senha parcialmente mascarada usando maskValue() (exibe {primeiros4}***{últimos4}).

Tabelas afetadas: OrganizationSmtpSettings (leitura)


Método upsertOrgSmtp(organizationId, data): Promise<{ saved: true }>

Cria ou atualiza a configuração SMTP da organização. A senha é criptografada com AES-256-GCM.

Parâmetros:

  • senderName, senderEmail, host, port, user, secure — obrigatórios
  • pass — opcional; se omitida, mantém a senha atual

Fluxo:

  1. Busca a configuração existente
  2. Se data.pass foi fornecida: encryptedPass = encryptionService.encrypt(data.pass)
  3. Caso contrário: reutiliza existing.pass (já criptografada)
  4. Executa OrganizationSmtpSettings.upsert()

Tabelas afetadas: OrganizationSmtpSettings (escrita/upsert)


Método privado maskValue(value?, visibleStart = 4, visibleEnd = 4): string | null

Utilitário de mascaramento para exibição de credenciais na UI.

Comportamento:

  • Se value é nulo/vazio: retorna null
  • Se value.length <= visibleStart + visibleEnd: retorna '*'.repeat(value.length) (mascara tudo)
  • Caso contrário: retorna {primeiros4}***{últimos4}

Exemplo: 'sk_live_abcdef1234''sk_l***1234'


Sub-módulo: EncryptionService

Arquivo: backend/src/common/services/encryption.service.ts

Serviço singleton responsável por criptografia simétrica de credenciais. Inicializado no onModuleInit do NestJS.

Algoritmo

AES-256-GCM — cifra autenticada que garante confidencialidade e integridade dos dados.

Inicialização (onModuleInit)

  1. Busca a MASTER_ENCRYPTION_KEY via SecretProvider (Azure Key Vault em produção)
  2. Deriva uma chave de 32 bytes via SHA-256 da string da key — garante que qualquer string se torna uma chave válida para AES-256

Método encrypt(text: string): string

Criptografa um texto puro e retorna no formato {iv_hex}:{encrypted_hex}:{auth_tag_hex}.

Fluxo:

  1. Gera IV aleatório de 16 bytes via crypto.randomBytes(16)
  2. Cria cipher AES-256-GCM com a masterKey e o IV
  3. Cifra o texto (UTF-8 → HEX)
  4. Obtém o auth tag GCM (16 bytes)
  5. Concatena: iv.hex + ':' + encrypted.hex + ':' + tag.hex

Método decrypt(hash: string): string

Descriptografa um hash no formato iv:encrypted:tag.

Fluxo:

  1. Split por : extrai os 3 componentes
  2. Valida que os 3 existem; lança erro em formato inválido
  3. Cria decipher, define auth tag, descriptografa
  4. Em caso de falha (key incorreta, dados corrompidos): lança InternalServerErrorException

Sub-módulo: Roles

Classe: RolesService

Arquivo: backend/src/modules/config/roles/roles.service.ts

Permissão: app:roles

O sistema de papéis usa níveis hierárquicos numéricos onde menor número = maior autoridade. Level 0 é o Super Admin e tem poderes irrestri tos.

Método findAll(organizationId: string): Promise<Role[]>

Lista todos os papéis da organização ordenados por level ASC (do mais poderoso ao menos poderoso).

Tabelas afetadas: Role (leitura)


Método findOne(organizationId: string, roleId: string): Promise<Role>

Busca um papel pelo ID e valida pertencimento à organização. Lança NotFoundException se não existe ou pertence a outra organização.

Tabelas afetadas: Role (leitura)


Método create(organizationId, userLevel, data): Promise<Role>

Cria um novo papel. Aplica proteção de escalonamento: usuários não podem criar papéis com nível igual ou superior ao seu.

Parâmetros do data:

  • name — nome do papel
  • description — descrição opcional
  • level — nível hierárquico (inteiro positivo)
  • permissions — array de strings de permissão
  • readOnly — boolean (padrão false)

Validação de escalonamento:

  • Se userLevel !== 0 (não é Super Admin) e data.level <= userLevel: lança ForbiddenException
  • Super Admin (userLevel === 0): sem restrições

Tabelas afetadas: Role (criação)


Método update(organizationId, roleId, userLevel, data): Promise<Role>

Atualiza um papel existente. Aplica três camadas de proteção:

Camada 1 — Proteção do Super Admin:

  • Se o papel a editar tem level === 0 e userLevel !== 0: ForbiddenException ("Você não pode editar o cargo de Administrador")

Camada 2 — Proteção de hierarquia (papel alvo):

  • Se userLevel !== 0 e role.level <= userLevel: ForbiddenException (papel alvo tem nível igual ou superior ao do editor)

Camada 3 — Proteção de escalonamento (nível destino):

  • Se userLevel !== 0 e data.level !== undefined e data.level <= userLevel: ForbiddenException (tentativa de elevar o papel acima do próprio nível)

Tabelas afetadas: Role (escrita)


Método remove(organizationId, roleId, userLevel): Promise<Role>

Remove um papel. Aplica verificações de proteção e valida que o papel não está em uso.

Validações:

  1. Papel existe e pertence à organização (findOne)
  2. Proteção do Super Admin: level === 0 só pode ser deletado por outro Super Admin
  3. Proteção de hierarquia: não pode deletar papel com nível ≤ ao do executante
  4. Verifica se há usuários vinculados (User.count({ where: { roleId } })); se > 0: lança ForbiddenException

Tabelas afetadas: Role (deleção), User (contagem)


Sub-módulo: Users

Classe: UsersService

Arquivo: backend/src/modules/config/users/users.service.ts

Permissão: app:team

Gerencia o ciclo de vida dos usuários: convite, alteração de papel, remoção e reset de senha. Integra com a API do Clerk para todas as operações de identidade.

Método getOrganizationDetails(organizationId: string): Promise<{ name, syncStatus } | null>

Retorna nome e status de sincronização da organização. Usado pelo endpoint GET /users/me.

Tabelas afetadas: Organization (leitura)


Método findAll(currentUser): Promise<User[]>

Lista usuários. O comportamento varia conforme o nível do usuário:

Super Admin (role.level === 0):

  • Retorna todos os usuários do sistema (sem filtro de organização)
  • Inclui campos extras: organizationId, clerkId

Outros usuários:

  • Retorna apenas usuários da mesma organização
  • Campos retornados: id, name, email, role, roleId, createdAt, status

Tabelas afetadas: User (leitura), Role (via include implícito em role)


Método inviteUser(currentUser, email, roleId, name?, storeIds?, targetOrganizationId?): Promise<User>

Convida um novo usuário para a organização. Cria o convite no Clerk e um registro local com status INVITED.

Parâmetros:

  • currentUser — usuário que está convidando
  • email — e-mail do convidado
  • roleId — UUID do papel a ser atribuído
  • name — nome opcional
  • storeIds — lojas às quais o usuário terá acesso (opcional)
  • targetOrganizationId — organização alvo (apenas Super Admin pode especificar outra organização)

Validações:

  1. Papel (roleId) existe no banco; NotFoundException se não
  2. currentUserLevel > 10: apenas gerentes (level ≤ 10) e Super Admin podem convidar
  3. currentUserLevel !== 0 && roleData.level <= currentUserLevel: não pode convidar para papel igual ou superior ao seu
  4. Determina finalOrgId: Super Admin pode usar targetOrganizationId; outros usam currentUser.organizationId
  5. E-mail já existe na organização: ConflictException

Fluxo de integração com Clerk:

  1. Busca a Organization local e seu clerkOrgId
  2. Se a organização ainda não tem clerkOrgId: cria a organização no Clerk via clerkClient.organizations.createOrganization() e persiste o clerkOrgId
  3. Cria o convite via clerkClient.organizations.createOrganizationInvitation() com role: 'org:member' e redirectUrl: {FRONTEND_URL}/sign-up
  4. Cria o registro local User com status INVITED, clerkInvitationId e vínculos de loja (se informados)

Tratamento de erros do Clerk:

  • HTTP 403: erro descritivo sobre permissão de admin da organização no Clerk
  • HTTP 404 / 409: repropaga a mensagem do Clerk
  • Outros: mensagem genérica de falha

Tabelas afetadas: Role (leitura), User (leitura + criação), Organization (leitura + possível escrita de clerkOrgId)

Rate limit: 30 requests por 60 segundos


Método updateRole(currentUser, targetUserId, newRoleId, storeIds?): Promise<User>

Altera o papel de um usuário. Aplica múltiplas camadas de proteção contra escalação de privilégios.

Validações:

  1. Auto-edição bloqueada (exceto Super Admin)
  2. currentUserLevel > 10: apenas gerentes e Super Admin podem alterar perfis
  3. Novo papel existe: NotFoundException se não
  4. Usuário alvo existe: NotFoundException se não
  5. Isolamento de tenant: usuário de outra organização → ConflictException
  6. Proteção de escalonamento (apenas para não-Super Admin):
    • Novo papel tem level <= currentUserLevel: não pode promover acima de si
    • Usuário alvo tem role.level <= currentUserLevel: não pode alterar superior ou par

Tabelas afetadas: Role (leitura), User (leitura + escrita)

Rate limit: 20 requests por 60 segundos


Método remove(currentUser, id): Promise<User>

Remove um usuário do sistema. Executa deleção em cascata manual e revoga o acesso no Clerk.

Validações:

  1. Auto-deleção bloqueada
  2. Usuário existe
  3. Isolamento de tenant (exceto Super Admin)
  4. Proteção de escalonamento: não pode remover superior ou par hierárquico

Fluxo de remoção no Clerk:

  • Se user.clerkId existe: clerkClient.users.deleteUser(user.clerkId)
  • Se status INVITED e tem clerkInvitationId: busca clerkOrgId da organização e chama clerkClient.organizations.revokeOrganizationInvitation()
  • Erros no Clerk são logados mas não propagados (o banco local é a fonte de verdade)

Limpeza de dados associados (antes de deletar o usuário):

  1. AccessLog.deleteMany({ where: { userId: id } }) — evita erro de FK
  2. Segment.updateMany({ where: { updatedById: id }, data: { updatedById: null } }) — preserva segmentos
  3. User.delete({ where: { id } })

Tabelas afetadas: AccessLog (deleção), Segment (atualização), User (deleção)


Método adminResetPassword(currentUser, targetUserId): Promise<{ success, message }>

Revoga todas as sessões ativas do usuário alvo no Clerk, forçando que ele use "Esqueci minha senha" no próximo acesso.

Validações:

  1. Usuário alvo existe
  2. Isolamento de tenant (exceto Super Admin)
  3. Usuário alvo deve ter clerkId (não pode ser status INVITED)

Fluxo:

  1. Busca todas as sessões ativas: clerkClient.sessions.getSessionList({ userId: targetUser.clerkId })
  2. Revoga cada sessão: clerkClient.sessions.revokeSession(session.id)
  3. Registra o evento diretamente em AccessLog com actionType: 'ADMIN_PASSWORD_RESET'

Tabelas afetadas: User (leitura), AccessLog (criação direta)


Sub-módulo: TenantInterceptor

Arquivo: backend/src/common/interceptors/tenant.interceptor.ts

Interceptor global que estabelece o contexto de tenant (organização) para cada request autenticado. Usa AsyncLocalStorage via tenantContext.run() para isolar o contexto entre requests concorrentes.

Comportamento:

  1. Extrai user do request (populado pelo ClerkAuthGuard)
  2. Se user tem role: determina activeOrgId = user.organizationId
  3. Override de Super Admin: Se user.role.level === 0 e o header x-org-id contém um UUID válido: substitui activeOrgId pelo valor do header (permite Super Admin operar em qualquer organização)
  4. Chama tenantContext.run({ organizationId, role }, callback) envolvendo o handler

Casos de warning (sem contexto):

  • user.organizationId é nulo: log de aviso, request continua sem contexto de tenant
  • user.role é nulo: log de aviso, request continua sem contexto de tenant

Header especial: x-org-id — exclusivo para Super Admin; UUIDs inválidos são ignorados


Horários de funcionamento — Estrutura JSON

O campo Store.businessHours armazena um objeto JSON com a seguinte estrutura:

json
{
  "schedule": {
    "monday":    { "active": true,  "open": "09:00", "close": "18:00" },
    "tuesday":   { "active": true,  "open": "09:00", "close": "18:00" },
    "wednesday": { "active": true,  "open": "09:00", "close": "18:00" },
    "thursday":  { "active": true,  "open": "09:00", "close": "18:00" },
    "friday":    { "active": true,  "open": "09:00", "close": "17:00" },
    "saturday":  { "active": true,  "open": "09:00", "close": "13:00" },
    "sunday":    { "active": false, "open": "00:00", "close": "00:00" }
  }
}

Regras de negócio:

  • Ausência de configuração = sempre aberto (sem restrição)
  • Turnos overnight suportados (ex: open: "23:30", close: "02:00")
  • Timezone: BRT (UTC-3) — calculado manualmente no backend
  • Ao salvar: flags Redis de business hours são limpas imediatamente para reavaliação instantânea
  • Tolerância de fechamento: o sistema considera a loja fechada 60 minutos após o close configurado

Tabelas do banco afetadas pelo módulo Config

TabelaOperações
StoreCRUD completo (nome, CNPJ, businessHours, isEcommerce)
EmailSettingsUpsert (configurações de e-mail SMTP por loja)
StoreWhatsappNumberCRUD completo (instâncias WhatsApp)
WhatsappTemplateDeleção (cascata ao remover instância)
WhatsappMessageDeleção (cascata ao remover instância)
OrganizationAtualização (logoUrl, campaignSettings, clerkOrgId)
OrganizationSmtpSettingsUpsert (SMTP global da organização)
RoleCRUD completo (papéis da organização)
UserCRUD completo (usuários, convites, papéis)
AccessLogCriação (reset de senha) e deleção (ao remover usuário)
SegmentAtualização (desvincular updatedById ao remover usuário)

Padrões de segurança resumidos

DadoTécnicaOnde
Senha SMTP (e-mail por loja)AES-256-GCMEmailSettings.pass
Senha SMTP (organização)AES-256-GCMOrganizationSmtpSettings.pass
WhatsApp tokenAES-256-GCMStoreWhatsappNumber.token
WhatsApp accessTokenAES-256-GCMStoreWhatsappNumber.accessToken
WhatsApp appSecretAES-256-GCMStoreWhatsappNumber.appSecret
WhatsApp webhookVerifyTokenSHA-256 (hash unidirecional)StoreWhatsappNumber.webhookVerifyToken
Exibição de credenciais na APISempre '********' ou maskValue()Todos os endpoints GET
webhookVerifyToken plaintextRetornado apenas uma vez na criaçãoaddWhatsappInstance()
Token Meta em logsmaskValue() — parcialmente mascaradoLogs do exchangeMetaCode()

Documentação interna — Galdix CRM