feat: update ESLint and TypeScript configurations across frontend and backend; enhance component structure and improve data handling in various modules

This commit is contained in:
2025-12-02 16:36:02 -03:00
parent f48d28067c
commit d79e6959c3
215 changed files with 29474 additions and 28173 deletions

View File

@@ -1,233 +1,216 @@
"use node";
'use node';
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
import { decryptSMTPPasswordNode } from "./utils/nodeCrypto";
import nodemailer from "nodemailer";
import { action } from '../_generated/server';
import { v } from 'convex/values';
import { internal } from '../_generated/api';
import { decryptSMTPPasswordNode } from './utils/nodeCrypto';
import nodemailer from 'nodemailer';
export const enviar = action({
args: {
emailId: v.id("notificacoesEmail"),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
"use node";
args: {
emailId: v.id('notificacoesEmail')
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
'use node';
let email;
try {
// Buscar email da fila
email = await ctx.runQuery(internal.email.getEmailById, {
emailId: args.emailId,
});
let email;
try {
// Buscar email da fila
email = await ctx.runQuery(internal.email.getEmailById, {
emailId: args.emailId
});
if (!email) {
return { sucesso: false, erro: "Email não encontrado" };
}
if (!email) {
return { sucesso: false, erro: 'Email não encontrado' };
}
// Buscar configuração SMTP ativa
const configRaw = await ctx.runQuery(
internal.email.getActiveEmailConfig,
{}
);
// Buscar configuração SMTP ativa
const configRaw = await ctx.runQuery(internal.email.getActiveEmailConfig, {});
if (!configRaw) {
console.error(
"❌ Configuração SMTP não encontrada ou inativa para email:",
email.destinatario
);
return {
sucesso: false,
erro: "Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.",
};
}
if (!configRaw) {
console.error(
'❌ Configuração SMTP não encontrada ou inativa para email:',
email.destinatario
);
return {
sucesso: false,
erro: 'Configuração de email não encontrada ou inativa. Verifique as configurações SMTP no painel de TI.'
};
}
console.log("📧 Tentando enviar email:", {
para: email.destinatario,
assunto: email.assunto,
servidor: configRaw.servidor,
porta: configRaw.porta,
});
console.log('📧 Tentando enviar email:', {
para: email.destinatario,
assunto: email.assunto,
servidor: configRaw.servidor,
porta: configRaw.porta
});
// Descriptografar senha usando função compatível com Node.js
let senhaDescriptografada: string;
try {
senhaDescriptografada = await decryptSMTPPasswordNode(
configRaw.senhaHash
);
} catch (decryptError) {
const decryptErrorMessage =
decryptError instanceof Error
? decryptError.message
: String(decryptError);
console.error(
"Erro ao descriptografar senha SMTP:",
decryptErrorMessage
);
return {
sucesso: false,
erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`,
};
}
// Descriptografar senha usando função compatível com Node.js
let senhaDescriptografada: string;
try {
senhaDescriptografada = await decryptSMTPPasswordNode(configRaw.senhaHash);
} catch (decryptError) {
const decryptErrorMessage =
decryptError instanceof Error ? decryptError.message : String(decryptError);
console.error('Erro ao descriptografar senha SMTP:', decryptErrorMessage);
return {
sucesso: false,
erro: `Erro ao descriptografar senha SMTP: ${decryptErrorMessage}`
};
}
const config = {
...configRaw,
senha: senhaDescriptografada,
};
const config = {
...configRaw,
senha: senhaDescriptografada
};
// Config já foi validado acima
// Config já foi validado acima
// Avisar mas não bloquear se não foi testado
if (!config.testadoEm) {
console.warn(
"⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim..."
);
}
// Avisar mas não bloquear se não foi testado
if (!config.testadoEm) {
console.warn('⚠️ Configuração SMTP não foi testada. Tentando enviar mesmo assim...');
}
// Marcar como enviando
await ctx.runMutation(internal.email.markEmailEnviando, {
emailId: args.emailId,
});
// Marcar como enviando
await ctx.runMutation(internal.email.markEmailEnviando, {
emailId: args.emailId
});
// Criar transporter do nodemailer com configuração melhorada
const transporterOptions: {
host: string;
port: number;
secure: boolean;
requireTLS?: boolean;
auth: {
user: string;
pass: string;
};
tls?: {
rejectUnauthorized: boolean;
ciphers?: string;
};
connectionTimeout: number;
greetingTimeout: number;
socketTimeout: number;
pool?: boolean;
maxConnections?: number;
maxMessages?: number;
} = {
host: config.servidor,
port: config.porta,
secure: config.usarSSL,
auth: {
user: config.usuario,
pass: config.senha, // Senha já descriptografada
},
connectionTimeout: 15000, // 15 segundos
greetingTimeout: 15000,
socketTimeout: 15000,
pool: true, // Usar pool de conexões
maxConnections: 5,
maxMessages: 100,
};
// Criar transporter do nodemailer com configuração melhorada
const transporterOptions: {
host: string;
port: number;
secure: boolean;
requireTLS?: boolean;
auth: {
user: string;
pass: string;
};
tls?: {
rejectUnauthorized: boolean;
ciphers?: string;
};
connectionTimeout: number;
greetingTimeout: number;
socketTimeout: number;
pool?: boolean;
maxConnections?: number;
maxMessages?: number;
} = {
host: config.servidor,
port: config.porta,
secure: config.usarSSL,
auth: {
user: config.usuario,
pass: config.senha // Senha já descriptografada
},
connectionTimeout: 15000, // 15 segundos
greetingTimeout: 15000,
socketTimeout: 15000,
pool: true, // Usar pool de conexões
maxConnections: 5,
maxMessages: 100
};
// Adicionar TLS apenas se necessário
if (config.usarTLS) {
transporterOptions.requireTLS = true;
transporterOptions.tls = {
rejectUnauthorized: false, // Permitir certificados autoassinados
};
} else if (config.usarSSL) {
transporterOptions.tls = {
rejectUnauthorized: false,
};
}
// Adicionar TLS apenas se necessário
if (config.usarTLS) {
transporterOptions.requireTLS = true;
transporterOptions.tls = {
rejectUnauthorized: false // Permitir certificados autoassinados
};
} else if (config.usarSSL) {
transporterOptions.tls = {
rejectUnauthorized: false
};
}
const transporter = nodemailer.createTransport(transporterOptions);
const transporter = nodemailer.createTransport(transporterOptions);
// Verificar conexão antes de enviar
try {
await transporter.verify();
console.log("✅ Conexão SMTP verificada com sucesso");
} catch (verifyError) {
const verifyErrorMessage =
verifyError instanceof Error
? verifyError.message
: String(verifyError);
console.warn(
"⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:",
verifyErrorMessage
);
// Não bloquear envio por falha na verificação, apenas avisar
}
// Verificar conexão antes de enviar
try {
await transporter.verify();
console.log('✅ Conexão SMTP verificada com sucesso');
} catch (verifyError) {
const verifyErrorMessage =
verifyError instanceof Error ? verifyError.message : String(verifyError);
console.warn(
'⚠️ Falha na verificação SMTP, mas tentando enviar mesmo assim:',
verifyErrorMessage
);
// Não bloquear envio por falha na verificação, apenas avisar
}
// Validar email destinatário antes de enviar
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email.destinatario)) {
throw new Error(`Email destinatário inválido: ${email.destinatario}`);
}
// Validar email destinatário antes de enviar
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email.destinatario)) {
throw new Error(`Email destinatário inválido: ${email.destinatario}`);
}
// Criar versão texto do HTML (remover tags e decodificar entidades básicas)
const textoPlano = email.corpo
.replace(/<[^>]*>/g, "") // Remover tags HTML
.replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.trim();
// Criar versão texto do HTML (remover tags e decodificar entidades básicas)
const textoPlano = email.corpo
.replace(/<[^>]*>/g, '') // Remover tags HTML
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.trim();
// Enviar email
const info = await transporter.sendMail({
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
to: email.destinatario,
subject: email.assunto,
html: email.corpo,
text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML
headers: {
"X-Mailer": "SGSE-Sistema-de-Gerenciamento-de-Secretaria",
"X-Priority": "3",
},
});
// Enviar email
const info = await transporter.sendMail({
from: `"${config.nomeRemetente}" <${config.emailRemetente}>`,
to: email.destinatario,
subject: email.assunto,
html: email.corpo,
text: textoPlano || email.assunto, // Versão texto para clientes que não suportam HTML
headers: {
'X-Mailer': 'SGSE-Sistema-de-Gerenciamento-de-Secretaria',
'X-Priority': '3'
}
});
interface MessageInfo {
messageId?: string;
response?: string;
}
interface MessageInfo {
messageId?: string;
response?: string;
}
const messageInfo = info as MessageInfo;
const messageInfo = info as MessageInfo;
console.log("✅ Email enviado com sucesso!", {
para: email.destinatario,
assunto: email.assunto,
messageId: messageInfo.messageId,
response: messageInfo.response,
});
console.log('✅ Email enviado com sucesso!', {
para: email.destinatario,
assunto: email.assunto,
messageId: messageInfo.messageId,
response: messageInfo.response
});
// Marcar como enviado
await ctx.runMutation(internal.email.markEmailEnviado, {
emailId: args.emailId,
});
// Marcar como enviado
await ctx.runMutation(internal.email.markEmailEnviado, {
emailId: args.emailId
});
return { sucesso: true };
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
return { sucesso: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
console.error("❌ Erro ao enviar email:", {
emailId: args.emailId,
destinatario: email?.destinatario,
erro: errorMessage,
stack: errorStack,
});
console.error('❌ Erro ao enviar email:', {
emailId: args.emailId,
destinatario: email?.destinatario,
erro: errorMessage,
stack: errorStack
});
// Marcar como falha com detalhes completos
const erroCompleto = errorStack
? `${errorMessage}\n\nStack: ${errorStack}`
: errorMessage;
// Marcar como falha com detalhes completos
const erroCompleto = errorStack ? `${errorMessage}\n\nStack: ${errorStack}` : errorMessage;
await ctx.runMutation(internal.email.markEmailFalha, {
emailId: args.emailId,
erro: erroCompleto.substring(0, 2000), // Limitar tamanho do erro
});
await ctx.runMutation(internal.email.markEmailFalha, {
emailId: args.emailId,
erro: erroCompleto.substring(0, 2000) // Limitar tamanho do erro
});
return { sucesso: false, erro: errorMessage };
}
},
return { sucesso: false, erro: errorMessage };
}
}
});

View File

@@ -158,7 +158,7 @@
// // Configurar SSH
// let sshPasswordDecrypted: string | undefined = undefined;
// // Se senha foi fornecida, usar ela. Caso contrário, tentar descriptografar a armazenada
// if (args.sshPassword) {
// sshPasswordDecrypted = args.sshPassword;
@@ -429,4 +429,3 @@
// }
// },
// });

View File

@@ -1,138 +1,155 @@
"use node";
'use node';
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
import { action } from '../_generated/server';
import { v } from 'convex/values';
import { internal } from '../_generated/api';
/**
* Extrair preview de link (metadados Open Graph) - função auxiliar
*/
async function extrairPreviewLinkHelper(url: string) {
try {
// Validar URL
let urlObj: URL;
try {
urlObj = new URL(url);
} catch {
return null;
}
try {
// Validar URL
let urlObj: URL;
try {
urlObj = new URL(url);
} catch {
return null;
}
// Buscar HTML da página
const response = await fetch(url, {
headers: {
"User-Agent": "Mozilla/5.0 (compatible; SGSE-Bot/1.0)",
},
signal: AbortSignal.timeout(5000), // Timeout de 5 segundos
});
// Buscar HTML da página
const response = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; SGSE-Bot/1.0)'
},
signal: AbortSignal.timeout(5000) // Timeout de 5 segundos
});
if (!response.ok) {
return null;
}
if (!response.ok) {
return null;
}
const html = await response.text();
const html = await response.text();
// Extrair metadados Open Graph e Twitter Cards
const metadata: {
titulo?: string;
descricao?: string;
imagem?: string;
site?: string;
} = {};
// Extrair metadados Open Graph e Twitter Cards
const metadata: {
titulo?: string;
descricao?: string;
imagem?: string;
site?: string;
} = {};
// Título (og:title ou twitter:title ou <title>)
const ogTitleMatch = html.match(/<meta\s+property=["']og:title["']\s+content=["']([^"']+)["']/i);
const twitterTitleMatch = html.match(/<meta\s+name=["']twitter:title["']\s+content=["']([^"']+)["']/i);
const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
metadata.titulo = ogTitleMatch?.[1] || twitterTitleMatch?.[1] || titleMatch?.[1] || undefined;
if (metadata.titulo) {
metadata.titulo = metadata.titulo.trim().substring(0, 200);
}
// Título (og:title ou twitter:title ou <title>)
const ogTitleMatch = html.match(
/<meta\s+property=["']og:title["']\s+content=["']([^"']+)["']/i
);
const twitterTitleMatch = html.match(
/<meta\s+name=["']twitter:title["']\s+content=["']([^"']+)["']/i
);
const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
// Descrição (og:description ou twitter:description ou meta description)
const ogDescMatch = html.match(/<meta\s+property=["']og:description["']\s+content=["']([^"']+)["']/i);
const twitterDescMatch = html.match(/<meta\s+name=["']twitter:description["']\s+content=["']([^"']+)["']/i);
const metaDescMatch = html.match(/<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i);
metadata.descricao = ogDescMatch?.[1] || twitterDescMatch?.[1] || metaDescMatch?.[1] || undefined;
if (metadata.descricao) {
metadata.descricao = metadata.descricao.trim().substring(0, 300);
}
metadata.titulo = ogTitleMatch?.[1] || twitterTitleMatch?.[1] || titleMatch?.[1] || undefined;
if (metadata.titulo) {
metadata.titulo = metadata.titulo.trim().substring(0, 200);
}
// Imagem (og:image ou twitter:image)
const ogImageMatch = html.match(/<meta\s+property=["']og:image["']\s+content=["']([^"']+)["']/i);
const twitterImageMatch = html.match(/<meta\s+name=["']twitter:image["']\s+content=["']([^"']+)["']/i);
const imageUrl = ogImageMatch?.[1] || twitterImageMatch?.[1];
if (imageUrl) {
// Resolver URL relativa
try {
metadata.imagem = new URL(imageUrl, url).href;
} catch {
metadata.imagem = imageUrl;
}
}
// Descrição (og:description ou twitter:description ou meta description)
const ogDescMatch = html.match(
/<meta\s+property=["']og:description["']\s+content=["']([^"']+)["']/i
);
const twitterDescMatch = html.match(
/<meta\s+name=["']twitter:description["']\s+content=["']([^"']+)["']/i
);
const metaDescMatch = html.match(
/<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i
);
// Site (og:site_name ou domínio)
const ogSiteMatch = html.match(/<meta\s+property=["']og:site_name["']\s+content=["']([^"']+)["']/i);
metadata.site = ogSiteMatch?.[1] || urlObj.hostname.replace(/^www\./, "");
metadata.descricao =
ogDescMatch?.[1] || twitterDescMatch?.[1] || metaDescMatch?.[1] || undefined;
if (metadata.descricao) {
metadata.descricao = metadata.descricao.trim().substring(0, 300);
}
return {
url,
titulo: metadata.titulo,
descricao: metadata.descricao,
imagem: metadata.imagem,
site: metadata.site,
};
} catch (error) {
console.error("Erro ao extrair preview de link:", error);
return null;
}
// Imagem (og:image ou twitter:image)
const ogImageMatch = html.match(
/<meta\s+property=["']og:image["']\s+content=["']([^"']+)["']/i
);
const twitterImageMatch = html.match(
/<meta\s+name=["']twitter:image["']\s+content=["']([^"']+)["']/i
);
const imageUrl = ogImageMatch?.[1] || twitterImageMatch?.[1];
if (imageUrl) {
// Resolver URL relativa
try {
metadata.imagem = new URL(imageUrl, url).href;
} catch {
metadata.imagem = imageUrl;
}
}
// Site (og:site_name ou domínio)
const ogSiteMatch = html.match(
/<meta\s+property=["']og:site_name["']\s+content=["']([^"']+)["']/i
);
metadata.site = ogSiteMatch?.[1] || urlObj.hostname.replace(/^www\./, '');
return {
url,
titulo: metadata.titulo,
descricao: metadata.descricao,
imagem: metadata.imagem,
site: metadata.site
};
} catch (error) {
console.error('Erro ao extrair preview de link:', error);
return null;
}
}
/**
* Processar preview de link e atualizar mensagem
*/
export const processarPreviewLink = action({
args: {
mensagemId: v.id("mensagens"),
url: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
// Extrair preview
const preview = await extrairPreviewLinkHelper(args.url);
if (preview) {
// Atualizar mensagem com preview
await ctx.runMutation(internal.chat.atualizarLinkPreview, {
mensagemId: args.mensagemId,
linkPreview: preview,
});
}
return null;
},
args: {
mensagemId: v.id('mensagens'),
url: v.string()
},
returns: v.null(),
handler: async (ctx, args) => {
// Extrair preview
const preview = await extrairPreviewLinkHelper(args.url);
if (preview) {
// Atualizar mensagem com preview
await ctx.runMutation(internal.chat.atualizarLinkPreview, {
mensagemId: args.mensagemId,
linkPreview: preview
});
}
return null;
}
});
/**
* Extrair preview de link (metadados Open Graph) - versão pública
*/
export const extrairPreviewLink = action({
args: {
url: v.string(),
},
returns: v.union(
v.object({
url: v.string(),
titulo: v.optional(v.string()),
descricao: v.optional(v.string()),
imagem: v.optional(v.string()),
site: v.optional(v.string()),
}),
v.null()
),
handler: async (ctx, args) => {
return await extrairPreviewLinkHelper(args.url);
},
args: {
url: v.string()
},
returns: v.union(
v.object({
url: v.string(),
titulo: v.optional(v.string()),
descricao: v.optional(v.string()),
imagem: v.optional(v.string()),
site: v.optional(v.string())
}),
v.null()
),
handler: async (ctx, args) => {
return await extrairPreviewLinkHelper(args.url);
}
});

View File

@@ -1,103 +1,102 @@
"use node";
'use node';
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
import { action } from '../_generated/server';
import { v } from 'convex/values';
import { internal } from '../_generated/api';
/**
* Enviar push notification usando Web Push API
*/
export const enviarPush = action({
args: {
subscriptionId: v.id("pushSubscriptions"),
titulo: v.string(),
corpo: v.string(),
data: v.optional(
v.object({
conversaId: v.optional(v.string()),
mensagemId: v.optional(v.string()),
tipo: v.optional(v.string()),
})
),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
try {
// Buscar subscription
const subscription = await ctx.runQuery(internal.pushNotifications.getSubscriptionById, {
subscriptionId: args.subscriptionId,
});
args: {
subscriptionId: v.id('pushSubscriptions'),
titulo: v.string(),
corpo: v.string(),
data: v.optional(
v.object({
conversaId: v.optional(v.string()),
mensagemId: v.optional(v.string()),
tipo: v.optional(v.string())
})
)
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
try {
// Buscar subscription
const subscription = await ctx.runQuery(internal.pushNotifications.getSubscriptionById, {
subscriptionId: args.subscriptionId
});
if (!subscription || !subscription.ativo) {
return { sucesso: false, erro: "Subscription não encontrada ou inativa" };
}
if (!subscription || !subscription.ativo) {
return { sucesso: false, erro: 'Subscription não encontrada ou inativa' };
}
// Web Push requer VAPID keys (deve estar em variáveis de ambiente)
// Por enquanto, vamos usar uma implementação básica
// Em produção, você precisará configurar VAPID keys
// Web Push requer VAPID keys (deve estar em variáveis de ambiente)
// Por enquanto, vamos usar uma implementação básica
// Em produção, você precisará configurar VAPID keys
const webpushModule = await import("web-push");
// web-push pode exportar como default ou named exports
// Usar a declaração de tipo do módulo web-push
interface WebPushType {
setVapidDetails: (subject: string, publicKey: string, privateKey: string) => void;
sendNotification: (
subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
payload: string | Buffer
) => Promise<void>;
}
const webpush: WebPushType = (webpushModule.default || webpushModule) as WebPushType;
const webpushModule = await import('web-push');
// web-push pode exportar como default ou named exports
// Usar a declaração de tipo do módulo web-push
interface WebPushType {
setVapidDetails: (subject: string, publicKey: string, privateKey: string) => void;
sendNotification: (
subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
payload: string | Buffer
) => Promise<void>;
}
const webpush: WebPushType = (webpushModule.default || webpushModule) as WebPushType;
// VAPID keys devem vir de variáveis de ambiente
const publicKey: string | undefined = process.env.VAPID_PUBLIC_KEY;
const privateKey: string | undefined = process.env.VAPID_PRIVATE_KEY;
// VAPID keys devem vir de variáveis de ambiente
const publicKey: string | undefined = process.env.VAPID_PUBLIC_KEY;
const privateKey: string | undefined = process.env.VAPID_PRIVATE_KEY;
if (!publicKey || !privateKey) {
console.warn("⚠️ VAPID keys não configuradas. Push notifications não funcionarão.");
// Em desenvolvimento, podemos retornar sucesso sem enviar
return { sucesso: true };
}
if (!publicKey || !privateKey) {
console.warn('⚠️ VAPID keys não configuradas. Push notifications não funcionarão.');
// Em desenvolvimento, podemos retornar sucesso sem enviar
return { sucesso: true };
}
webpush.setVapidDetails("mailto:suporte@sgse.app", publicKey, privateKey);
webpush.setVapidDetails('mailto:suporte@sgse.app', publicKey, privateKey);
// Preparar payload da notificação
const payload = JSON.stringify({
title: args.titulo,
body: args.corpo,
icon: "/favicon.png",
badge: "/favicon.png",
data: args.data || {},
tag: args.data?.conversaId || "default",
requireInteraction: args.data?.tipo === "mencao", // Menções requerem interação
});
// Preparar payload da notificação
const payload = JSON.stringify({
title: args.titulo,
body: args.corpo,
icon: '/favicon.png',
badge: '/favicon.png',
data: args.data || {},
tag: args.data?.conversaId || 'default',
requireInteraction: args.data?.tipo === 'mencao' // Menções requerem interação
});
// Enviar push notification
await webpush.sendNotification(
{
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth,
},
},
payload
);
// Enviar push notification
await webpush.sendNotification(
{
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth
}
},
payload
);
console.log(`✅ Push notification enviada para ${subscription.endpoint}`);
return { sucesso: true };
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("❌ Erro ao enviar push notification:", errorMessage);
console.log(`✅ Push notification enviada para ${subscription.endpoint}`);
return { sucesso: true };
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('❌ Erro ao enviar push notification:', errorMessage);
// Se subscription inválida, marcar como inativa
if (errorMessage.includes("410") || errorMessage.includes("expired")) {
await ctx.runMutation(internal.pushNotifications.marcarSubscriptionInativa, {
subscriptionId: args.subscriptionId,
});
}
// Se subscription inválida, marcar como inativa
if (errorMessage.includes('410') || errorMessage.includes('expired')) {
await ctx.runMutation(internal.pushNotifications.marcarSubscriptionInativa, {
subscriptionId: args.subscriptionId
});
}
return { sucesso: false, erro: errorMessage };
}
},
return { sucesso: false, erro: errorMessage };
}
}
});

View File

@@ -1,64 +1,61 @@
"use node";
import { action } from "../_generated/server";
import { v } from "convex/values";
'use node';
import { action } from '../_generated/server';
import { v } from 'convex/values';
// Importar nodemailer de forma estática para evitar problemas com caminhos no Windows
import nodemailer from "nodemailer";
import nodemailer from 'nodemailer';
export const testarConexao = action({
args: {
servidor: v.string(),
porta: v.number(),
usuario: v.string(),
senha: v.string(),
usarSSL: v.boolean(),
usarTLS: v.boolean(),
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
args: {
servidor: v.string(),
porta: v.number(),
usuario: v.string(),
senha: v.string(),
usarSSL: v.boolean(),
usarTLS: v.boolean()
},
returns: v.union(
v.object({ sucesso: v.literal(true) }),
v.object({ sucesso: v.literal(false), erro: v.string() })
),
handler: async (ctx, args) => {
try {
// Validações básicas
if (!args.servidor || args.servidor.trim() === '') {
return {
sucesso: false as const,
erro: 'Servidor SMTP não pode estar vazio'
};
}
try {
// Validações básicas
if (!args.servidor || args.servidor.trim() === "") {
return {
sucesso: false as const,
erro: "Servidor SMTP não pode estar vazio",
};
}
if (args.porta < 1 || args.porta > 65535) {
return { sucesso: false as const, erro: 'Porta inválida' };
}
if (args.porta < 1 || args.porta > 65535) {
return { sucesso: false as const, erro: "Porta inválida" };
}
const transporter = nodemailer.createTransport({
host: args.servidor,
port: args.porta,
secure: args.usarSSL,
auth: {
user: args.usuario,
pass: args.senha
},
tls: {
rejectUnauthorized: false
},
connectionTimeout: 10000, // 10 segundos
greetingTimeout: 10000,
socketTimeout: 10000
});
const transporter = nodemailer.createTransport({
host: args.servidor,
port: args.porta,
secure: args.usarSSL,
auth: {
user: args.usuario,
pass: args.senha,
},
tls: {
rejectUnauthorized: false,
},
connectionTimeout: 10000, // 10 segundos
greetingTimeout: 10000,
socketTimeout: 10000,
});
// Verificar conexão
await transporter.verify();
// Verificar conexão
await transporter.verify();
return { sucesso: true as const };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { sucesso: false as const, erro: errorMessage };
}
},
return { sucesso: true as const };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { sucesso: false as const, erro: errorMessage };
}
}
});

View File

@@ -1,4 +1,4 @@
"use node";
'use node';
/**
* Utilitários de criptografia compatíveis com Node.js
@@ -10,65 +10,64 @@
* Esta versão funciona em ambiente Node.js (actions)
*/
export async function decryptSMTPPasswordNode(encryptedPassword: string): Promise<string> {
try {
// Em Node.js, crypto.subtle está disponível globalmente
const crypto = globalThis.crypto;
if (!crypto || !crypto.subtle) {
throw new Error("Web Crypto API não disponível");
}
try {
// Em Node.js, crypto.subtle está disponível globalmente
const crypto = globalThis.crypto;
// Chave base - mesma usada em auth/utils.ts
const keyMaterial = new TextEncoder().encode("SGSE-EMAIL-ENCRYPTION-KEY-2024");
// Importar chave material
const keyMaterialKey = await crypto.subtle.importKey(
"raw",
keyMaterial,
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
);
if (!crypto || !crypto.subtle) {
throw new Error('Web Crypto API não disponível');
}
// Derivar chave de 256 bits usando PBKDF2
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: new TextEncoder().encode("SGSE-SALT"),
iterations: 100000,
hash: "SHA-256",
},
keyMaterialKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
// Decodificar base64 manualmente (compatível com Node.js e browser)
const binaryString = atob(encryptedPassword);
const combined = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
// Chave base - mesma usada em auth/utils.ts
const keyMaterial = new TextEncoder().encode('SGSE-EMAIL-ENCRYPTION-KEY-2024');
// Extrair IV e dados criptografados
const iv = combined.slice(0, 12);
const encrypted = combined.slice(12);
// Importar chave material
const keyMaterialKey = await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
// Descriptografar
const decrypted = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv,
},
key,
encrypted
);
// Derivar chave de 256 bits usando PBKDF2
const key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: new TextEncoder().encode('SGSE-SALT'),
iterations: 100000,
hash: 'SHA-256'
},
keyMaterialKey,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
// Converter para string
const decoder = new TextDecoder();
return decoder.decode(decrypted);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Erro ao descriptografar senha SMTP (Node.js):", errorMessage);
throw new Error(`Falha ao descriptografar senha SMTP: ${errorMessage}`);
}
// Decodificar base64 manualmente (compatível com Node.js e browser)
const binaryString = atob(encryptedPassword);
const combined = Uint8Array.from(binaryString, (c) => c.charCodeAt(0));
// Extrair IV e dados criptografados
const iv = combined.slice(0, 12);
const encrypted = combined.slice(12);
// Descriptografar
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
encrypted
);
// Converter para string
const decoder = new TextDecoder();
return decoder.decode(decrypted);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Erro ao descriptografar senha SMTP (Node.js):', errorMessage);
throw new Error(`Falha ao descriptografar senha SMTP: ${errorMessage}`);
}
}