Merge remote-tracking branch 'origin' into feat-pedidos
This commit is contained in:
429
packages/backend/convex/errosServidor.ts
Normal file
429
packages/backend/convex/errosServidor.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import {
|
||||
action,
|
||||
internalMutation,
|
||||
internalAction,
|
||||
internalQuery,
|
||||
query
|
||||
} from './_generated/server';
|
||||
import { v } from 'convex/values';
|
||||
import { api, internal } from './_generated/api';
|
||||
import type { Id, Doc } from './_generated/dataModel';
|
||||
import { getCurrentUserFunction } from './auth';
|
||||
|
||||
/**
|
||||
* Action pública para registrar erro do servidor e notificar equipe técnica
|
||||
* Esta função será chamada pelo handleError do SvelteKit
|
||||
*/
|
||||
export const registrarErroServidor = action({
|
||||
args: {
|
||||
statusCode: v.number(),
|
||||
mensagem: v.string(),
|
||||
stack: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
method: v.optional(v.string()),
|
||||
ipAddress: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
usuarioId: v.optional(v.id('usuarios'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Registrar erro no banco
|
||||
// Anotação explícita de tipo para evitar problemas de tipagem circular ao
|
||||
// chamar uma função registrada neste mesmo módulo (ver docs do Convex).
|
||||
const erroId: Id<'errosServidor'> = await ctx.runMutation(internal.errosServidor.inserirErro, {
|
||||
statusCode: args.statusCode,
|
||||
mensagem: args.mensagem,
|
||||
stack: args.stack,
|
||||
url: args.url,
|
||||
method: args.method,
|
||||
ipAddress: args.ipAddress,
|
||||
userAgent: args.userAgent,
|
||||
usuarioId: args.usuarioId
|
||||
});
|
||||
|
||||
// Notificar equipe técnica (assíncrono)
|
||||
ctx.scheduler
|
||||
.runAfter(0, internal.errosServidor.notificarEquipeTecnica, {
|
||||
erroId
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erro ao agendar notificação de erro:', error);
|
||||
});
|
||||
|
||||
return { sucesso: true, erroId };
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation interna para inserir erro no banco
|
||||
*/
|
||||
export const inserirErro = internalMutation({
|
||||
args: {
|
||||
statusCode: v.number(),
|
||||
mensagem: v.string(),
|
||||
stack: v.optional(v.string()),
|
||||
url: v.optional(v.string()),
|
||||
method: v.optional(v.string()),
|
||||
ipAddress: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
usuarioId: v.optional(v.id('usuarios'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const erroId = await ctx.db.insert('errosServidor', {
|
||||
statusCode: args.statusCode,
|
||||
mensagem: args.mensagem,
|
||||
stack: args.stack,
|
||||
url: args.url,
|
||||
method: args.method,
|
||||
ipAddress: args.ipAddress,
|
||||
userAgent: args.userAgent,
|
||||
usuarioId: args.usuarioId,
|
||||
notificado: false,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
|
||||
return erroId;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Action interna para notificar equipe técnica sobre erro do servidor
|
||||
*/
|
||||
export const notificarEquipeTecnica = internalAction({
|
||||
args: {
|
||||
erroId: v.id('errosServidor')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar detalhes do erro
|
||||
const erro = await ctx.runQuery(internal.errosServidor.obterErroPorId, {
|
||||
erroId: args.erroId
|
||||
});
|
||||
|
||||
if (!erro) {
|
||||
console.error('Erro não encontrado:', args.erroId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Buscar usuários da equipe técnica (roles com nível <= 1)
|
||||
const rolesAdminOuTi: Doc<'roles'>[] = await ctx.runQuery(
|
||||
internal.errosServidor.obterRolesTI,
|
||||
{}
|
||||
);
|
||||
|
||||
if (rolesAdminOuTi.length === 0) {
|
||||
console.warn('Nenhuma role de TI encontrada para notificação de erro');
|
||||
return;
|
||||
}
|
||||
|
||||
const rolesPermitidas = new Set(rolesAdminOuTi.map((r: Doc<'roles'>) => r._id));
|
||||
const usuarios: Doc<'usuarios'>[] = await ctx.runQuery(internal.errosServidor.obterUsuariosTI, {
|
||||
rolesPermitidas: Array.from(rolesPermitidas)
|
||||
});
|
||||
|
||||
if (usuarios.length === 0) {
|
||||
console.warn('Nenhum usuário de TI encontrado para notificação de erro');
|
||||
return;
|
||||
}
|
||||
|
||||
// Preparar informações do erro para notificação
|
||||
const urlFormatada = erro.url || 'N/A';
|
||||
const metodoFormatado = erro.method || 'N/A';
|
||||
const stackFormatado = erro.stack
|
||||
? erro.stack.substring(0, 500) + (erro.stack.length > 500 ? '...' : '')
|
||||
: 'N/A';
|
||||
|
||||
// Notificar via chat (notificações internas)
|
||||
for (const usuario of usuarios) {
|
||||
await ctx.runMutation(internal.errosServidor.criarNotificacaoChat, {
|
||||
usuarioId: usuario._id,
|
||||
statusCode: erro.statusCode,
|
||||
mensagem: erro.mensagem,
|
||||
url: urlFormatada,
|
||||
method: metodoFormatado
|
||||
});
|
||||
}
|
||||
|
||||
// Notificar via email (apenas para usuários com email)
|
||||
const usuariosComEmail = usuarios.filter((u: Doc<'usuarios'>) => u.email);
|
||||
|
||||
for (const usuario of usuariosComEmail) {
|
||||
try {
|
||||
// Determinar código do template baseado no status code
|
||||
const templateCodigo = erro.statusCode === 404 ? 'ERRO_SERVIDOR_404' : 'ERRO_SERVIDOR_500';
|
||||
|
||||
// Verificar se existe template de erro do servidor
|
||||
const templateExiste = await ctx.runQuery(api.templatesMensagens.obterTemplatePorCodigo, {
|
||||
codigo: templateCodigo
|
||||
});
|
||||
|
||||
if (templateExiste) {
|
||||
// Usar template personalizado
|
||||
await ctx.scheduler.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||
destinatario: usuario.email!,
|
||||
destinatarioId: usuario._id,
|
||||
templateCodigo,
|
||||
variaveis: {
|
||||
destinatarioNome: usuario.nome,
|
||||
statusCode: erro.statusCode.toString(),
|
||||
mensagem: erro.mensagem,
|
||||
url: urlFormatada,
|
||||
method: metodoFormatado,
|
||||
stack: stackFormatado,
|
||||
timestamp: new Date(erro.criadoEm).toLocaleString('pt-BR')
|
||||
},
|
||||
enviadoPor: usuario._id // Usar o próprio usuário como remetente
|
||||
});
|
||||
} else {
|
||||
// Template não existe, criar email simples com HTML básico
|
||||
const assunto =
|
||||
erro.statusCode === 404
|
||||
? `⚠️ Erro 404 - Página não encontrada: ${urlFormatada.substring(0, 50)}`
|
||||
: `🚨 Erro ${erro.statusCode} no Servidor - ${urlFormatada.substring(0, 50)}`;
|
||||
const corpo = `<html><body>
|
||||
<h2>${erro.statusCode === 404 ? 'Página Não Encontrada (404)' : 'Erro do Servidor Detectado'}</h2>
|
||||
<p><strong>Código:</strong> ${erro.statusCode}</p>
|
||||
<p><strong>Mensagem:</strong> ${erro.mensagem}</p>
|
||||
<p><strong>URL:</strong> ${urlFormatada}</p>
|
||||
<p><strong>Método:</strong> ${metodoFormatado}</p>
|
||||
<p><strong>Data/Hora:</strong> ${new Date(erro.criadoEm).toLocaleString('pt-BR')}</p>
|
||||
${erro.stack && erro.statusCode !== 404 ? `<p><strong>Stack Trace:</strong><br><pre style="white-space: pre-wrap; word-wrap: break-word;">${stackFormatado}</pre></p>` : ''}
|
||||
</body></html>`;
|
||||
|
||||
await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: usuario.email!,
|
||||
destinatarioId: usuario._id,
|
||||
assunto,
|
||||
corpo,
|
||||
enviadoPor: usuario._id
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Erro ao enviar email de notificação para ${usuario.email}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Marcar erro como notificado
|
||||
await ctx.runMutation(internal.errosServidor.marcarComoNotificado, {
|
||||
erroId: args.erroId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query interna para obter erro por ID
|
||||
*/
|
||||
export const obterErroPorId = internalQuery({
|
||||
args: {
|
||||
erroId: v.id('errosServidor')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
return await ctx.db.get(args.erroId);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query interna para obter roles de TI
|
||||
*/
|
||||
export const obterRolesTI = internalQuery({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db
|
||||
.query('roles')
|
||||
.filter((q) => q.lte(q.field('admin'), true))
|
||||
.collect();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query interna para obter usuários de TI
|
||||
*/
|
||||
export const obterUsuariosTI = internalQuery({
|
||||
args: {
|
||||
rolesPermitidas: v.array(v.id('roles'))
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const usuarios = await ctx.db.query('usuarios').collect();
|
||||
return usuarios.filter((u) => args.rolesPermitidas.includes(u.roleId));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation interna para criar notificação no chat
|
||||
*/
|
||||
export const criarNotificacaoChat = internalMutation({
|
||||
args: {
|
||||
usuarioId: v.id('usuarios'),
|
||||
statusCode: v.number(),
|
||||
mensagem: v.string(),
|
||||
url: v.string(),
|
||||
method: v.string()
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const tituloNotificacao =
|
||||
args.statusCode === 404
|
||||
? `⚠️ Erro 404 - Página não encontrada`
|
||||
: `🚨 Erro ${args.statusCode} no Servidor`;
|
||||
const descricaoNotificacao =
|
||||
args.statusCode === 404
|
||||
? `Página não encontrada: ${args.url} (${args.method})`
|
||||
: `Erro detectado em ${args.url} (${args.method}): ${args.mensagem.substring(0, 100)}`;
|
||||
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId: args.usuarioId,
|
||||
tipo: 'nova_mensagem',
|
||||
titulo: tituloNotificacao,
|
||||
descricao: descricaoNotificacao,
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mutation interna para marcar erro como notificado
|
||||
*/
|
||||
export const marcarComoNotificado = internalMutation({
|
||||
args: {
|
||||
erroId: v.id('errosServidor')
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.patch(args.erroId, {
|
||||
notificado: true,
|
||||
notificadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query pública para listar erros do servidor (apenas para TI)
|
||||
*/
|
||||
export const listarErros = query({
|
||||
args: {
|
||||
limite: v.optional(v.number()),
|
||||
statusCode: v.optional(v.number()),
|
||||
notificado: v.optional(v.boolean()),
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se usuário tem permissão (nível <= 1)
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Não autenticado');
|
||||
}
|
||||
|
||||
const role = await ctx.db.get(usuario.roleId);
|
||||
if (!role || !role.admin) {
|
||||
throw new Error('Acesso negado. Apenas usuários de TI podem visualizar erros do servidor.');
|
||||
}
|
||||
|
||||
// Construir query com filtros
|
||||
let erros;
|
||||
if (args.statusCode !== undefined) {
|
||||
erros = await ctx.db
|
||||
.query('errosServidor')
|
||||
.withIndex('by_status_code', (q) => q.eq('statusCode', args.statusCode!))
|
||||
.collect();
|
||||
} else {
|
||||
erros = await ctx.db.query('errosServidor').withIndex('by_criado_em').collect();
|
||||
}
|
||||
|
||||
// Aplicar filtros adicionais que não são índices
|
||||
if (args.notificado !== undefined) {
|
||||
erros = erros.filter((e) => e.notificado === args.notificado);
|
||||
}
|
||||
|
||||
if (args.dataInicio !== undefined) {
|
||||
erros = erros.filter((e) => e.criadoEm >= args.dataInicio!);
|
||||
}
|
||||
|
||||
if (args.dataFim !== undefined) {
|
||||
erros = erros.filter((e) => e.criadoEm <= args.dataFim!);
|
||||
}
|
||||
|
||||
// Ordenar por data (mais recentes primeiro)
|
||||
erros.sort((a, b) => b.criadoEm - a.criadoEm);
|
||||
|
||||
// Aplicar limite
|
||||
const limite = args.limite || 100;
|
||||
erros = erros.slice(0, limite);
|
||||
|
||||
// Buscar informações do usuário se houver usuarioId
|
||||
const errosComUsuario = await Promise.all(
|
||||
erros.map(async (erro) => {
|
||||
let usuarioNome = null;
|
||||
if (erro.usuarioId) {
|
||||
const usuarioErro = await ctx.db.get(erro.usuarioId);
|
||||
usuarioNome = usuarioErro?.nome || null;
|
||||
}
|
||||
return {
|
||||
...erro,
|
||||
usuarioNome
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return errosComUsuario;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Query pública para obter estatísticas de erros
|
||||
*/
|
||||
export const obterEstatisticasErros = query({
|
||||
args: {
|
||||
dataInicio: v.optional(v.number()),
|
||||
dataFim: v.optional(v.number())
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Verificar se usuário tem permissão (nível <= 1)
|
||||
const usuario = await getCurrentUserFunction(ctx);
|
||||
if (!usuario) {
|
||||
throw new Error('Não autenticado');
|
||||
}
|
||||
|
||||
const role = await ctx.db.get(usuario.roleId);
|
||||
if (!role || !role.admin) {
|
||||
throw new Error(
|
||||
'Acesso negado. Apenas usuários de TI podem visualizar estatísticas de erros.'
|
||||
);
|
||||
}
|
||||
|
||||
// Buscar todos os erros no período
|
||||
let erros = await ctx.db.query('errosServidor').withIndex('by_criado_em').collect();
|
||||
|
||||
// Aplicar filtros de data
|
||||
if (args.dataInicio !== undefined) {
|
||||
erros = erros.filter((e) => e.criadoEm >= args.dataInicio!);
|
||||
}
|
||||
|
||||
if (args.dataFim !== undefined) {
|
||||
erros = erros.filter((e) => e.criadoEm <= args.dataFim!);
|
||||
}
|
||||
|
||||
// Calcular estatísticas
|
||||
const total = erros.length;
|
||||
const porStatus = erros.reduce(
|
||||
(acc, erro) => {
|
||||
const status = erro.statusCode.toString();
|
||||
acc[status] = (acc[status] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
const notificados = erros.filter((e) => e.notificado).length;
|
||||
const naoNotificados = total - notificados;
|
||||
|
||||
// Erros mais recentes (últimas 24 horas)
|
||||
const agora = Date.now();
|
||||
const ultimas24h = erros.filter((e) => agora - e.criadoEm <= 24 * 60 * 60 * 1000).length;
|
||||
|
||||
return {
|
||||
total,
|
||||
porStatus,
|
||||
notificados,
|
||||
naoNotificados,
|
||||
ultimas24h
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user