import { v } from "convex/values"; import { mutation, query, QueryCtx, MutationCtx } from "./_generated/server"; import { Doc, Id } from "./_generated/dataModel"; /** * Helper para registrar tentativas de login */ export async function registrarLogin( ctx: QueryCtx | MutationCtx, dados: { usuarioId?: Id<"usuarios">; matriculaOuEmail: string; sucesso: boolean; motivoFalha?: string; ipAddress?: string; userAgent?: string; } ) { // Extrair informações do userAgent const device = dados.userAgent ? extrairDevice(dados.userAgent) : undefined; const browser = dados.userAgent ? extrairBrowser(dados.userAgent) : undefined; const sistema = dados.userAgent ? extrairSistema(dados.userAgent) : undefined; await ctx.db.insert("logsLogin", { usuarioId: dados.usuarioId, matriculaOuEmail: dados.matriculaOuEmail, sucesso: dados.sucesso, motivoFalha: dados.motivoFalha, ipAddress: dados.ipAddress, userAgent: dados.userAgent, device, browser, sistema, timestamp: Date.now(), }); } // Helpers para extrair informações do userAgent function extrairDevice(userAgent: string): string { if (/mobile/i.test(userAgent)) return "Mobile"; if (/tablet/i.test(userAgent)) return "Tablet"; return "Desktop"; } function extrairBrowser(userAgent: string): string { if (/edg/i.test(userAgent)) return "Edge"; if (/chrome/i.test(userAgent)) return "Chrome"; if (/firefox/i.test(userAgent)) return "Firefox"; if (/safari/i.test(userAgent)) return "Safari"; if (/opera/i.test(userAgent)) return "Opera"; return "Desconhecido"; } function extrairSistema(userAgent: string): string { if (/windows/i.test(userAgent)) return "Windows"; if (/mac/i.test(userAgent)) return "MacOS"; if (/linux/i.test(userAgent)) return "Linux"; if (/android/i.test(userAgent)) return "Android"; if (/ios/i.test(userAgent)) return "iOS"; return "Desconhecido"; } /** * Lista histórico de logins de um usuário */ export const listarLoginsUsuario = query({ args: { usuarioId: v.id("usuarios"), limite: v.optional(v.number()), }, handler: async (ctx, args) => { const logs = await ctx.db .query("logsLogin") .withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId)) .order("desc") .take(args.limite || 50); return logs; }, }); /** * Lista todos os logins do sistema */ export const listarTodosLogins = query({ args: { limite: v.optional(v.number()), }, handler: async (ctx, args) => { const logs = await ctx.db .query("logsLogin") .withIndex("by_timestamp") .order("desc") .take(args.limite || 50); return logs; }, }); /** * Lista tentativas de login falhadas */ export const listarTentativasFalhas = query({ args: { horasAtras: v.optional(v.number()), // padrão 24h limite: v.optional(v.number()), }, handler: async (ctx, args) => { const horasAtras = args.horasAtras || 24; const dataLimite = Date.now() - horasAtras * 60 * 60 * 1000; const logs = await ctx.db .query("logsLogin") .withIndex("by_sucesso", (q) => q.eq("sucesso", false)) .filter((q) => q.gte(q.field("timestamp"), dataLimite)) .order("desc") .take(args.limite || 100); // Agrupar por IP para detectar possíveis ataques const porIP: Record = {}; logs.forEach((log) => { if (log.ipAddress) { porIP[log.ipAddress] = (porIP[log.ipAddress] || 0) + 1; } }); return { logs, tentativasPorIP: porIP, total: logs.length, }; }, }); /** * Obtém estatísticas de login */ export const obterEstatisticasLogin = query({ args: { dias: v.optional(v.number()), // padrão 30 dias }, handler: async (ctx, args) => { const dias = args.dias || 30; const dataInicio = Date.now() - dias * 24 * 60 * 60 * 1000; const logs = await ctx.db .query("logsLogin") .withIndex("by_timestamp") .filter((q) => q.gte(q.field("timestamp"), dataInicio)) .collect(); // Total de logins bem-sucedidos vs falhos const sucessos = logs.filter((l) => l.sucesso).length; const falhas = logs.filter((l) => !l.sucesso).length; // Logins por dia const porDia: Record = {}; logs.forEach((log) => { const data = new Date(log.timestamp); const dia = data.toISOString().split("T")[0]; if (!porDia[dia]) { porDia[dia] = { sucesso: 0, falha: 0 }; } if (log.sucesso) { porDia[dia].sucesso++; } else { porDia[dia].falha++; } }); // Logins por horário (hora do dia) const porHorario: Record = {}; logs.filter((l) => l.sucesso).forEach((log) => { const hora = new Date(log.timestamp).getHours(); porHorario[hora] = (porHorario[hora] || 0) + 1; }); // Browser mais usado const porBrowser: Record = {}; logs.filter((l) => l.sucesso).forEach((log) => { if (log.browser) { porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1; } }); // Dispositivos mais usados const porDevice: Record = {}; logs.filter((l) => l.sucesso).forEach((log) => { if (log.device) { porDevice[log.device] = (porDevice[log.device] || 0) + 1; } }); return { total: logs.length, sucessos, falhas, taxaSucesso: logs.length > 0 ? (sucessos / logs.length) * 100 : 0, porDia, porHorario, porBrowser, porDevice, }; }, }); /** * Verifica se um IP está sendo suspeito (muitas tentativas falhas) */ export const verificarIPSuspeito = query({ args: { ipAddress: v.string(), minutosAtras: v.optional(v.number()), // padrão 15 minutos }, handler: async (ctx, args) => { const minutosAtras = args.minutosAtras || 15; const dataLimite = Date.now() - minutosAtras * 60 * 1000; const tentativas = await ctx.db .query("logsLogin") .withIndex("by_ip", (q) => q.eq("ipAddress", args.ipAddress)) .filter((q) => q.gte(q.field("timestamp"), dataLimite)) .collect(); const falhas = tentativas.filter((t) => !t.sucesso).length; return { tentativasTotal: tentativas.length, tentativasFalhas: falhas, suspeito: falhas >= 5, // 5 ou mais tentativas falhas em 15 minutos }; }, });