- Integrated browser information capture in the login process, including user agent and IP address. - Enhanced device and browser detection logic to provide more detailed insights into user environments. - Improved system detection for various operating systems and devices, ensuring accurate reporting during authentication.
396 lines
9.5 KiB
TypeScript
396 lines
9.5 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 {
|
|
const ua = userAgent.toLowerCase();
|
|
|
|
// Detectar dispositivos móveis primeiro
|
|
if (/mobile|android|iphone|ipod|blackberry|opera mini|iemobile|wpdesktop/i.test(ua)) {
|
|
// Verificar se é tablet
|
|
if (/ipad|tablet|playbook|silk|(android(?!.*mobile))/i.test(ua)) {
|
|
return "Tablet";
|
|
}
|
|
return "Mobile";
|
|
}
|
|
|
|
// Detectar outros dispositivos
|
|
if (/smart-tv|smarttv|googletv|appletv|roku|chromecast/i.test(ua)) {
|
|
return "Smart TV";
|
|
}
|
|
|
|
if (/watch|wear/i.test(ua)) {
|
|
return "Smart Watch";
|
|
}
|
|
|
|
// Padrão: Desktop
|
|
return "Desktop";
|
|
}
|
|
|
|
function extrairBrowser(userAgent: string): string {
|
|
const ua = userAgent.toLowerCase();
|
|
|
|
// Ordem de detecção é importante (Edge deve vir antes de Chrome)
|
|
if (/edgios/i.test(ua)) {
|
|
return "Edge iOS";
|
|
}
|
|
|
|
if (/edg/i.test(ua)) {
|
|
// Extrair versão do Edge
|
|
const match = ua.match(/edg[e\/]([\d.]+)/i);
|
|
return match ? `Edge ${match[1]}` : "Edge";
|
|
}
|
|
|
|
if (/opr|opera/i.test(ua)) {
|
|
const match = ua.match(/(?:opr|opera)[\/\s]([\d.]+)/i);
|
|
return match ? `Opera ${match[1]}` : "Opera";
|
|
}
|
|
|
|
if (/chrome|crios/i.test(ua) && !/edg|opr|opera/i.test(ua)) {
|
|
const match = ua.match(/chrome[/\s]([\d.]+)/i);
|
|
return match ? `Chrome ${match[1]}` : "Chrome";
|
|
}
|
|
|
|
if (/firefox|fxios/i.test(ua)) {
|
|
const match = ua.match(/firefox[/\s]([\d.]+)/i);
|
|
return match ? `Firefox ${match[1]}` : "Firefox";
|
|
}
|
|
|
|
if (/safari/i.test(ua) && !/chrome|crios|android/i.test(ua)) {
|
|
const match = ua.match(/version[/\s]([\d.]+)/i);
|
|
return match ? `Safari ${match[1]}` : "Safari";
|
|
}
|
|
|
|
if (/msie|trident/i.test(ua)) {
|
|
const match = ua.match(/(?:msie |rv:)([\d.]+)/i);
|
|
return match ? `Internet Explorer ${match[1]}` : "Internet Explorer";
|
|
}
|
|
|
|
if (/samsungbrowser/i.test(ua)) {
|
|
return "Samsung Internet";
|
|
}
|
|
|
|
if (/ucbrowser/i.test(ua)) {
|
|
return "UC Browser";
|
|
}
|
|
|
|
if (/micromessenger/i.test(ua)) {
|
|
return "WeChat";
|
|
}
|
|
|
|
if (/baiduboxapp/i.test(ua)) {
|
|
return "Baidu Browser";
|
|
}
|
|
|
|
return "Desconhecido";
|
|
}
|
|
|
|
function extrairSistema(userAgent: string): string {
|
|
const ua = userAgent.toLowerCase();
|
|
|
|
// Windows
|
|
if (/windows nt 10.0/i.test(ua)) {
|
|
return "Windows 10/11";
|
|
}
|
|
if (/windows nt 6.3/i.test(ua)) {
|
|
return "Windows 8.1";
|
|
}
|
|
if (/windows nt 6.2/i.test(ua)) {
|
|
return "Windows 8";
|
|
}
|
|
if (/windows nt 6.1/i.test(ua)) {
|
|
return "Windows 7";
|
|
}
|
|
if (/windows nt 6.0/i.test(ua)) {
|
|
return "Windows Vista";
|
|
}
|
|
if (/windows nt 5.1/i.test(ua)) {
|
|
return "Windows XP";
|
|
}
|
|
if (/windows/i.test(ua)) {
|
|
return "Windows";
|
|
}
|
|
|
|
// macOS
|
|
if (/macintosh|mac os x/i.test(ua)) {
|
|
const match = ua.match(/mac os x ([\d_]+)/i);
|
|
if (match) {
|
|
const version = match[1].replace(/_/g, '.');
|
|
return `macOS ${version}`;
|
|
}
|
|
return "macOS";
|
|
}
|
|
|
|
// iOS
|
|
if (/iphone|ipad|ipod/i.test(ua)) {
|
|
const match = ua.match(/os ([\d_]+)/i);
|
|
if (match) {
|
|
const version = match[1].replace(/_/g, '.');
|
|
return `iOS ${version}`;
|
|
}
|
|
return "iOS";
|
|
}
|
|
|
|
// Android
|
|
if (/android/i.test(ua)) {
|
|
const match = ua.match(/android ([\d.]+)/i);
|
|
if (match) {
|
|
return `Android ${match[1]}`;
|
|
}
|
|
return "Android";
|
|
}
|
|
|
|
// Linux
|
|
if (/linux/i.test(ua)) {
|
|
// Tentar identificar distribuição
|
|
if (/ubuntu/i.test(ua)) {
|
|
return "Ubuntu";
|
|
}
|
|
if (/debian/i.test(ua)) {
|
|
return "Debian";
|
|
}
|
|
if (/fedora/i.test(ua)) {
|
|
return "Fedora";
|
|
}
|
|
if (/centos/i.test(ua)) {
|
|
return "CentOS";
|
|
}
|
|
if (/redhat/i.test(ua)) {
|
|
return "Red Hat";
|
|
}
|
|
if (/suse/i.test(ua)) {
|
|
return "SUSE";
|
|
}
|
|
return "Linux";
|
|
}
|
|
|
|
// Chrome OS
|
|
if (/cros/i.test(ua)) {
|
|
return "Chrome OS";
|
|
}
|
|
|
|
// BlackBerry
|
|
if (/blackberry/i.test(ua)) {
|
|
return "BlackBerry OS";
|
|
}
|
|
|
|
// Windows Phone
|
|
if (/windows phone/i.test(ua)) {
|
|
return "Windows Phone";
|
|
}
|
|
|
|
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
|
|
};
|
|
},
|
|
});
|