235 lines
6.3 KiB
TypeScript
235 lines
6.3 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: 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<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
|
|
};
|
|
},
|
|
});
|
|
|