import { v } from 'convex/values'; import { mutation, query, action, internalMutation } from './_generated/server'; import { registrarAtividade } from './logsAtividades'; import { api, internal } from './_generated/api'; import { getCurrentUserFunction, authComponent, toGenericCtx } from './auth'; import { encryptJWTSecret, decryptJWTSecret } from './auth/utils'; import type { Id } from './_generated/dataModel'; /** * Codificar base64url (sem padding, com substituição de caracteres) */ function base64UrlEncode(data: Uint8Array): string { const base64 = btoa(String.fromCharCode(...data)); return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } /** * Gerar token JWT para Jitsi (função helper interna) * Especificação: https://github.com/jitsi/lib-jitsi-meet/blob/master/doc/tokens.md */ async function criarTokenJWTJitsi( secret: string, payload: { iss: string; // Issuer (appId) aud: string; // Audience (domínio do servidor) sub: string; // Subject (ID do usuário ou email) room: string; // Nome da sala moderator: boolean; // Se é moderador exp?: number; // Expiração (timestamp, padrão: 1 hora) nbf?: number; // Not before (timestamp, padrão: agora) } ): Promise { // Header JWT const header = { alg: 'HS256', typ: 'JWT' }; // Payload com valores padrão const now = Math.floor(Date.now() / 1000); const jwtPayload = { ...payload, exp: payload.exp || now + 3600, // 1 hora por padrão nbf: payload.nbf || now }; // Codificar header e payload const headerEncoded = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header))); const payloadEncoded = base64UrlEncode(new TextEncoder().encode(JSON.stringify(jwtPayload))); // Criar assinatura const message = `${headerEncoded}.${payloadEncoded}`; const keyData = new TextEncoder().encode(secret); const key = await crypto.subtle.importKey( 'raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ); const signature = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message)); const signatureEncoded = base64UrlEncode(new Uint8Array(signature)); // Retornar token completo return `${headerEncoded}.${payloadEncoded}.${signatureEncoded}`; } /** * Helper para verificar se usuário tem permissão de TI (TI_MASTER, TI_USUARIO ou ADMIN) */ async function verificarPermissaoTI( ctx: Parameters[0], usuarioId: Id<'usuarios'> ): Promise { const usuario = await ctx.db.get(usuarioId); if (!usuario || !usuario.roleId) { return false; } const role = await ctx.db.get(usuario.roleId); if (!role) { return false; } const rolesPermitidas = ['ti_master', 'ti_usuario', 'admin']; return rolesPermitidas.includes(role.nome); } /** * Validar formato de domínio (FQDN ou localhost:porta) */ function validarFormatoDominio(domain: string): { valido: boolean; erro?: string } { const trimmed = domain.trim(); if (trimmed.length === 0) { return { valido: false, erro: 'Domínio não pode estar vazio' }; } // Padrão para localhost:porta (ex: localhost:8443, 127.0.0.1:8080) const localhostPattern = /^(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?$/i; if (localhostPattern.test(trimmed)) { return { valido: true }; } // Padrão para FQDN (ex: meet.example.com, subdomain.example.com:8443) const fqdnPattern = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(:\d+)?$/; if (fqdnPattern.test(trimmed)) { return { valido: true }; } return { valido: false, erro: 'Domínio deve ser um FQDN válido (ex: meet.example.com) ou localhost:porta (ex: localhost:8443)' }; } /** * Validar formato de App ID (alfanumérico, sem espaços) */ function validarAppId(appId: string): { valido: boolean; erro?: string } { const trimmed = appId.trim(); if (trimmed.length === 0) { return { valido: false, erro: 'App ID não pode estar vazio' }; } const appIdPattern = /^[a-zA-Z0-9_-]+$/; if (!appIdPattern.test(trimmed)) { return { valido: false, erro: 'App ID deve conter apenas letras, números, hífens e underscores' }; } return { valido: true }; } /** * Validar JWT Secret (mínimo 32 caracteres recomendado) */ function validarJWTSecret(secret: string): { valido: boolean; erro?: string; aviso?: string } { if (secret.length === 0) { return { valido: true }; // JWT secret é opcional } if (secret.length < 16) { return { valido: false, erro: 'JWT Secret deve ter no mínimo 16 caracteres' }; } if (secret.length < 32) { return { valido: true, aviso: 'Recomendado usar JWT Secret com pelo menos 32 caracteres para maior segurança' }; } return { valido: true }; } /** * Obter configuração de Jitsi ativa (compatibilidade com código existente) */ export const obterConfigJitsi = query({ args: {}, handler: async (ctx) => { const config = await ctx.db .query('configuracaoJitsi') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .first(); if (!config) { return null; } return { _id: config._id, domain: config.domain, appId: config.appId, roomPrefix: config.roomPrefix, useHttps: config.useHttps, acceptSelfSignedCert: config.acceptSelfSignedCert ?? false, ambiente: config.ambiente ?? null, ativo: config.ativo, testadoEm: config.testadoEm, atualizadoEm: config.atualizadoEm, jwtSecret: config.jwtSecret, jwtIssuer: config.jwtIssuer, jwtAudience: config.jwtAudience }; } }); /** * Obter configuração de Jitsi por ambiente */ export const obterConfigJitsiPorAmbiente = query({ args: { ambiente: v.optional(v.string()) }, handler: async ( ctx, args ): Promise<{ _id: Id<'configuracaoJitsi'>; domain: string; appId: string; roomPrefix: string; useHttps: boolean; acceptSelfSignedCert: boolean; ambiente: string | null; ativo: boolean; testadoEm: number | undefined; atualizadoEm: number; jwtSecret: string | undefined; jwtIssuer: string | undefined; jwtAudience: string | undefined; } | null> => { // Se ambiente não especificado, buscar configuração ativa if (!args.ambiente) { return await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {}); } const config = await ctx.db .query('configuracaoJitsi') .withIndex('by_ativo_ambiente', (q) => q.eq('ativo', true).eq('ambiente', args.ambiente)) .first(); if (!config) { return null; } return { _id: config._id, domain: config.domain, appId: config.appId, roomPrefix: config.roomPrefix, useHttps: config.useHttps, acceptSelfSignedCert: config.acceptSelfSignedCert ?? false, ambiente: config.ambiente ?? null, ativo: config.ativo, testadoEm: config.testadoEm, atualizadoEm: config.atualizadoEm, jwtSecret: config.jwtSecret, jwtIssuer: config.jwtIssuer, jwtAudience: config.jwtAudience }; } }); /** * Listar todas as configurações de Jitsi */ export const listarConfiguracoesJitsi = query({ args: {}, handler: async (ctx) => { const configs = await ctx.db.query('configuracaoJitsi').collect(); return configs.map((config) => ({ _id: config._id, domain: config.domain, appId: config.appId, roomPrefix: config.roomPrefix, useHttps: config.useHttps, acceptSelfSignedCert: config.acceptSelfSignedCert ?? false, ambiente: config.ambiente ?? null, ativo: config.ativo, testadoEm: config.testadoEm, atualizadoEm: config.atualizadoEm, configuradoPor: config.configuradoPor })); } }); /** * Salvar configuração de Jitsi (apenas TI_MASTER/TI_USUARIO/ADMIN) * Suporta múltiplos ambientes */ export const salvarConfigJitsi = mutation({ args: { domain: v.string(), appId: v.string(), roomPrefix: v.string(), useHttps: v.boolean(), acceptSelfSignedCert: v.boolean(), ambiente: v.optional(v.string()), jwtSecret: v.optional(v.string()), // JWT Secret em texto plano (será criptografado) jwtAudience: v.optional(v.string()), jwtIssuer: v.optional(v.string()), configuradoPorId: v.id('usuarios'), ativar: v.optional(v.boolean()) // Se true, desativa outras configs do mesmo ambiente }, returns: v.union( v.object({ sucesso: v.literal(true), configId: v.id('configuracaoJitsi') }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { // Verificar permissão const temPermissao = await verificarPermissaoTI(ctx, args.configuradoPorId); if (!temPermissao) { return { sucesso: false as const, erro: 'Apenas usuários de TI ou administradores podem configurar Jitsi' }; } // Validar domínio const validacaoDominio = validarFormatoDominio(args.domain); if (!validacaoDominio.valido) { return { sucesso: false as const, erro: validacaoDominio.erro || 'Domínio inválido' }; } // Validar appId const validacaoAppId = validarAppId(args.appId); if (!validacaoAppId.valido) { return { sucesso: false as const, erro: validacaoAppId.erro || 'App ID inválido' }; } // Validar roomPrefix if (!args.roomPrefix || args.roomPrefix.trim().length === 0) { return { sucesso: false as const, erro: 'Prefixo de sala não pode estar vazio' }; } const roomPrefixRegex = /^[a-zA-Z0-9-]+$/; if (!roomPrefixRegex.test(args.roomPrefix.trim())) { return { sucesso: false as const, erro: 'Prefixo de sala deve conter apenas letras, números e hífens' }; } // Validar JWT Secret (se fornecido) let jwtSecretCriptografado: string | undefined = undefined; if (args.jwtSecret && args.jwtSecret.trim().length > 0) { const validacaoJWT = validarJWTSecret(args.jwtSecret); if (!validacaoJWT.valido) { return { sucesso: false as const, erro: validacaoJWT.erro || 'JWT Secret inválido' }; } // Criptografar JWT secret try { jwtSecretCriptografado = await encryptJWTSecret(args.jwtSecret.trim()); } catch (error) { console.error('Erro ao criptografar JWT secret:', error); return { sucesso: false as const, erro: 'Erro ao criptografar JWT secret' }; } } // Se ativar=true, desativar outras configs do mesmo ambiente (ou todas se ambiente não especificado) if (args.ativar !== false) { if (args.ambiente) { // Desativar outras configs do mesmo ambiente const configsMesmoAmbiente = await ctx.db .query('configuracaoJitsi') .withIndex('by_ambiente', (q) => q.eq('ambiente', args.ambiente)) .filter((q) => q.eq(q.field('ativo'), true)) .collect(); for (const config of configsMesmoAmbiente) { await ctx.db.patch(config._id, { ativo: false }); } } else { // Desativar todas as configs ativas (comportamento antigo) const configsAntigas = await ctx.db .query('configuracaoJitsi') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .collect(); for (const config of configsAntigas) { await ctx.db.patch(config._id, { ativo: false }); } } } // Criar nova config const configId = await ctx.db.insert('configuracaoJitsi', { domain: args.domain.trim(), appId: args.appId.trim(), roomPrefix: args.roomPrefix.trim(), useHttps: args.useHttps, acceptSelfSignedCert: args.acceptSelfSignedCert, ambiente: args.ambiente?.trim() || undefined, jwtSecret: jwtSecretCriptografado, jwtAudience: args.jwtAudience?.trim() || undefined, jwtIssuer: args.jwtIssuer?.trim() || undefined, ativo: args.ativar !== false, configuradoPor: args.configuradoPorId, atualizadoEm: Date.now() }); // Log de atividade await registrarAtividade( ctx, args.configuradoPorId, 'configurar', 'jitsi', JSON.stringify({ domain: args.domain, appId: args.appId, ambiente: args.ambiente }), configId ); return { sucesso: true as const, configId }; } }); /** * Atualizar configuração existente */ export const atualizarConfigJitsi = mutation({ args: { configId: v.id('configuracaoJitsi'), domain: v.optional(v.string()), appId: v.optional(v.string()), roomPrefix: v.optional(v.string()), useHttps: v.optional(v.boolean()), acceptSelfSignedCert: v.optional(v.boolean()), ambiente: v.optional(v.string()), jwtSecret: v.optional(v.string()), // Se fornecido, será criptografado e atualizado jwtAudience: v.optional(v.string()), jwtIssuer: v.optional(v.string()), atualizadoPorId: v.id('usuarios') }, returns: v.union( v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { // Verificar permissão const temPermissao = await verificarPermissaoTI(ctx, args.atualizadoPorId); if (!temPermissao) { return { sucesso: false as const, erro: 'Apenas usuários de TI ou administradores podem atualizar configuração Jitsi' }; } const config = await ctx.db.get(args.configId); if (!config) { return { sucesso: false as const, erro: 'Configuração não encontrada' }; } // Validar campos se fornecidos if (args.domain !== undefined) { const validacaoDominio = validarFormatoDominio(args.domain); if (!validacaoDominio.valido) { return { sucesso: false as const, erro: validacaoDominio.erro || 'Domínio inválido' }; } } if (args.appId !== undefined) { const validacaoAppId = validarAppId(args.appId); if (!validacaoAppId.valido) { return { sucesso: false as const, erro: validacaoAppId.erro || 'App ID inválido' }; } } if (args.roomPrefix !== undefined) { const roomPrefixRegex = /^[a-zA-Z0-9-]+$/; if (!roomPrefixRegex.test(args.roomPrefix.trim())) { return { sucesso: false as const, erro: 'Prefixo de sala deve conter apenas letras, números e hífens' }; } } // Criptografar JWT secret se fornecido let jwtSecretCriptografado: string | undefined = config.jwtSecret; if (args.jwtSecret !== undefined) { if (args.jwtSecret.trim().length > 0) { const validacaoJWT = validarJWTSecret(args.jwtSecret); if (!validacaoJWT.valido) { return { sucesso: false as const, erro: validacaoJWT.erro || 'JWT Secret inválido' }; } try { jwtSecretCriptografado = await encryptJWTSecret(args.jwtSecret.trim()); } catch (error) { console.error('Erro ao criptografar JWT secret:', error); return { sucesso: false as const, erro: 'Erro ao criptografar JWT secret' }; } } else { // Se string vazia, remover JWT secret jwtSecretCriptografado = undefined; } } // Atualizar campos const updates: Partial = { atualizadoEm: Date.now() }; if (args.domain !== undefined) updates.domain = args.domain.trim(); if (args.appId !== undefined) updates.appId = args.appId.trim(); if (args.roomPrefix !== undefined) updates.roomPrefix = args.roomPrefix.trim(); if (args.useHttps !== undefined) updates.useHttps = args.useHttps; if (args.acceptSelfSignedCert !== undefined) updates.acceptSelfSignedCert = args.acceptSelfSignedCert; if (args.ambiente !== undefined) updates.ambiente = args.ambiente?.trim() || undefined; if (args.jwtSecret !== undefined) updates.jwtSecret = jwtSecretCriptografado; if (args.jwtAudience !== undefined) updates.jwtAudience = args.jwtAudience?.trim() || undefined; if (args.jwtIssuer !== undefined) updates.jwtIssuer = args.jwtIssuer?.trim() || undefined; await ctx.db.patch(args.configId, updates); // Log de atividade await registrarAtividade( ctx, args.atualizadoPorId, 'atualizar', 'jitsi', JSON.stringify({ configId: args.configId }), args.configId ); return { sucesso: true as const }; } }); /** * Ativar/desativar configuração */ export const ativarDesativarConfigJitsi = mutation({ args: { configId: v.id('configuracaoJitsi'), ativo: v.boolean(), atualizadoPorId: v.id('usuarios') }, returns: v.union( v.object({ sucesso: v.literal(true) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async (ctx, args) => { // Verificar permissão const temPermissao = await verificarPermissaoTI(ctx, args.atualizadoPorId); if (!temPermissao) { return { sucesso: false as const, erro: 'Apenas usuários de TI ou administradores podem ativar/desativar configuração Jitsi' }; } const config = await ctx.db.get(args.configId); if (!config) { return { sucesso: false as const, erro: 'Configuração não encontrada' }; } // Se ativando, desativar outras configs do mesmo ambiente (ou todas se ambiente não especificado) if (args.ativo) { if (config.ambiente) { const configsMesmoAmbiente = await ctx.db .query('configuracaoJitsi') .withIndex('by_ambiente', (q) => q.eq('ambiente', config.ambiente)) .filter((q) => q.eq(q.field('ativo'), true)) .filter((q) => q.neq(q.field('_id'), args.configId)) .collect(); for (const outraConfig of configsMesmoAmbiente) { await ctx.db.patch(outraConfig._id, { ativo: false }); } } else { // Desativar todas as outras configs ativas const configsAntigas = await ctx.db .query('configuracaoJitsi') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .filter((q) => q.neq(q.field('_id'), args.configId)) .collect(); for (const outraConfig of configsAntigas) { await ctx.db.patch(outraConfig._id, { ativo: false }); } } } await ctx.db.patch(args.configId, { ativo: args.ativo, atualizadoEm: Date.now() }); // Log de atividade await registrarAtividade( ctx, args.atualizadoPorId, args.ativo ? 'ativar' : 'desativar', 'jitsi', JSON.stringify({ configId: args.configId }), args.configId ); return { sucesso: true as const }; } }); /** * Mutation interna para atualizar testadoEm */ export const atualizarTestadoEm = internalMutation({ args: { configId: v.id('configuracaoJitsi') }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.patch(args.configId, { testadoEm: Date.now() }); return null; } }); /** * Testar conexão com servidor Jitsi (melhorado) */ export const testarConexaoJitsi = action({ args: { domain: v.string(), useHttps: v.boolean(), acceptSelfSignedCert: v.optional(v.boolean()), configId: v.optional(v.id('configuracaoJitsi')) }, returns: v.union( v.object({ sucesso: v.literal(true), aviso: v.optional(v.string()) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async ( ctx, args ): Promise<{ sucesso: true; aviso?: string } | { sucesso: false; erro: string }> => { // Validar formato de domínio const validacaoDominio = validarFormatoDominio(args.domain); if (!validacaoDominio.valido) { return { sucesso: false as const, erro: validacaoDominio.erro || 'Domínio inválido' }; } try { const protocol = args.useHttps ? 'https' : 'http'; // Extrair host e porta do domain const [host, portStr] = args.domain.split(':'); const port = portStr ? parseInt(portStr, 10) : args.useHttps ? 443 : 80; const url = `${protocol}://${host}:${port}/http-bind`; // Tentar fazer uma requisição HTTP para verificar se o servidor está acessível const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 segundos de timeout try { const response = await fetch(url, { method: 'GET', signal: controller.signal, headers: { 'Content-Type': 'application/xml' } }); clearTimeout(timeoutId); // Qualquer resposta indica que o servidor está acessível if (response.status >= 200 && response.status < 600) { // Se o teste foi bem-sucedido e há uma config, atualizar testadoEm if (args.configId) { await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, { configId: args.configId }); } else { // Tentar atualizar config ativa const configAtiva = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {}); if (configAtiva) { await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, { configId: configAtiva._id }); } } return { sucesso: true as const, aviso: undefined }; } else { return { sucesso: false as const, erro: `Servidor retornou status ${response.status}` }; } } catch (fetchError: unknown) { clearTimeout(timeoutId); const errorMessage = fetchError instanceof Error ? fetchError.message : String(fetchError); // Se for erro de timeout if (errorMessage.includes('aborted') || errorMessage.includes('timeout')) { return { sucesso: false as const, erro: 'Timeout: Servidor não respondeu em 10 segundos' }; } // Verificar se é erro de certificado SSL autoassinado const isSSLError = errorMessage.includes('CERTIFICATE_VERIFY_FAILED') || errorMessage.includes('self signed certificate') || errorMessage.includes('self-signed certificate') || errorMessage.includes('certificate') || errorMessage.includes('SSL') || errorMessage.includes('certificate verify failed'); // Se for erro de certificado e aceitar autoassinado está configurado if (isSSLError && args.acceptSelfSignedCert) { if (args.configId) { await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, { configId: args.configId }); } else { const configAtiva = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {}); if (configAtiva) { await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, { configId: configAtiva._id }); } } return { sucesso: true as const, aviso: 'Servidor acessível com certificado autoassinado. No navegador, você precisará aceitar o certificado manualmente na primeira conexão.' }; } // Para servidores Jitsi, pode ser normal receber erro 405 (Method Not Allowed) // para GET em /http-bind, pois esse endpoint espera POST (BOSH) if (errorMessage.includes('405') || errorMessage.includes('Method Not Allowed')) { if (args.configId) { await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, { configId: args.configId }); } else { const configAtiva = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {}); if (configAtiva) { await ctx.runMutation(internal.configuracaoJitsi.atualizarTestadoEm, { configId: configAtiva._id }); } } return { sucesso: true as const, aviso: undefined }; } // Se for erro de certificado SSL e não está configurado para aceitar if (isSSLError) { return { sucesso: false as const, erro: `Erro de certificado SSL: O servidor está usando um certificado não confiável (provavelmente autoassinado). Para desenvolvimento local, habilite "Aceitar Certificados Autoassinados" nas configurações de segurança. Em produção, use um certificado válido (ex: Let's Encrypt).` }; } return { sucesso: false as const, erro: `Erro ao conectar: ${errorMessage}` }; } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); return { sucesso: false as const, erro: errorMessage || 'Erro ao conectar com o servidor Jitsi' }; } } }); /** * Marcar que a configuração foi testada com sucesso */ export const marcarConfigTestada = mutation({ args: { configId: v.id('configuracaoJitsi') }, handler: async (ctx, args) => { await ctx.db.patch(args.configId, { testadoEm: Date.now() }); } }); /** * Mutation interna para marcar que a configuração foi aplicada no servidor */ export const marcarConfiguradoNoServidor = internalMutation({ args: { configId: v.id('configuracaoJitsi') }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.patch(args.configId, { configuradoNoServidor: true, configuradoNoServidorEm: Date.now(), configuradoEm: Date.now() }); return null; } }); /** * Gerar token JWT para Jitsi * Valida que o usuário é participante da conversa e retorna token assinado */ export const gerarTokenJitsi = action({ args: { roomName: v.string(), conversaId: v.id('conversas'), chamadaId: v.id('chamadas'), ambiente: v.optional(v.string()) }, returns: v.union( v.object({ sucesso: v.literal(true), token: v.string(), payload: v.object({ iss: v.string(), aud: v.string(), sub: v.string(), room: v.string(), moderator: v.boolean(), exp: v.number(), nbf: v.number() }) }), v.object({ sucesso: v.literal(false), erro: v.string() }) ), handler: async ( ctx, args ): Promise< | { sucesso: true; token: string; payload: { iss: string; aud: string; sub: string; room: string; moderator: boolean; exp: number; nbf: number; }; } | { sucesso: false; erro: string } > => { // Obter usuário autenticado (em actions, precisamos usar authComponent) const authUser = await authComponent.safeGetAuthUser(toGenericCtx(ctx)); if (!authUser) { return { sucesso: false as const, erro: 'Não autenticado' }; } // Buscar usuário no banco usando query const usuarioAtual = await ctx.runQuery(api.auth.getCurrentUser, {}); if (!usuarioAtual) { return { sucesso: false as const, erro: 'Usuário não encontrado' }; } // Verificar se usuário participa da conversa const conversa = await ctx.runQuery(api.chamadas.obterChamada, { chamadaId: args.chamadaId }); if (!conversa) { return { sucesso: false as const, erro: 'Chamada não encontrada ou você não tem acesso' }; } // Verificar se conversaId corresponde if (conversa.conversaId !== args.conversaId) { return { sucesso: false as const, erro: 'Conversa não corresponde à chamada' }; } // Verificar se chamada existe (já obtida acima como conversa) const chamada = conversa; if (!chamada) { return { sucesso: false as const, erro: 'Chamada não encontrada' }; } // Verificar se roomName corresponde if (chamada.roomName !== args.roomName) { return { sucesso: false as const, erro: 'Room name não corresponde à chamada' }; } // Buscar configuração Jitsi let config; if (args.ambiente) { config = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsiPorAmbiente, { ambiente: args.ambiente }); } else { config = await ctx.runQuery(api.configuracaoJitsi.obterConfigJitsi, {}); } if (!config) { return { sucesso: false as const, erro: 'Configuração Jitsi não encontrada' }; } // Se não há JWT secret configurado, retornar erro if (!config.jwtSecret) { return { sucesso: false as const, erro: 'JWT Secret não configurado. Configure o JWT Secret nas configurações do Jitsi.' }; } // Descriptografar JWT secret let jwtSecret: string; try { jwtSecret = await decryptJWTSecret(config.jwtSecret); } catch (error) { console.error('Erro ao descriptografar JWT secret:', error); return { sucesso: false as const, erro: 'Erro ao descriptografar JWT secret' }; } // Determinar se usuário é moderador (anfitrião da chamada) const ehModerador = chamada.criadoPor === usuarioAtual._id; // Preparar claims do JWT const iss: string = config.jwtIssuer || config.appId; const aud: string = config.jwtAudience || config.domain; const sub: string = usuarioAtual.email || usuarioAtual._id; // Gerar token try { const token = await criarTokenJWTJitsi(jwtSecret, { iss, aud, sub, room: args.roomName, moderator: ehModerador }); // Retornar token e payload decodificado para debug const now = Math.floor(Date.now() / 1000); const payload: { iss: string; aud: string; sub: string; room: string; moderator: boolean; exp: number; nbf: number; } = { iss, aud, sub, room: args.roomName, moderator: ehModerador, exp: now + 3600, // 1 hora nbf: now }; return { sucesso: true as const, token, payload }; } catch (error) { console.error('Erro ao gerar token JWT:', error); return { sucesso: false as const, erro: 'Erro ao gerar token JWT' }; } } });