Files
sgse-app/packages/backend/convex/logsLogin.ts

240 lines
6.4 KiB
TypeScript

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: 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<string, number> = {};
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<string, { sucesso: number; falha: number }> = {};
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<number, number> = {};
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<string, number> = {};
logs
.filter((l) => l.sucesso)
.forEach((log) => {
if (log.browser) {
porBrowser[log.browser] = (porBrowser[log.browser] || 0) + 1;
}
});
// Dispositivos mais usados
const porDevice: Record<string, number> = {};
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
};
},
});