feat: enhance login functionality by adding IP geolocation tracking and advanced filtering options in the audit page, improving user insights and data accuracy

This commit is contained in:
2025-11-30 08:12:46 -03:00
parent e35846103e
commit 334676b860
4 changed files with 610 additions and 38 deletions

View File

@@ -2,6 +2,61 @@ import { v } from "convex/values";
import { mutation, query, QueryCtx, MutationCtx } from "./_generated/server";
import { Doc, Id } from "./_generated/dataModel";
/**
* Obtém geolocalização aproximada por IP usando serviço externo
* Similar ao sistema de ponto
*/
async function obterGeoPorIP(ipAddress: string): Promise<{
latitude: number;
longitude: number;
cidade?: string;
estado?: string;
pais?: string;
endereco?: string;
} | null> {
try {
// Usar ipapi.co (gratuito, sem chave para uso limitado)
const response = await fetch(`https://ipapi.co/${ipAddress}/json/`, {
headers: {
'User-Agent': 'SGSE-App/1.0'
}
});
if (response.ok) {
const data = (await response.json()) as {
latitude?: number;
longitude?: number;
city?: string;
region?: string;
country_name?: string;
error?: boolean;
};
if (!data.error && data.latitude && data.longitude) {
// Montar endereço completo
const partesEndereco: string[] = [];
if (data.city) partesEndereco.push(data.city);
if (data.region) partesEndereco.push(data.region);
if (data.country_name) partesEndereco.push(data.country_name);
const endereco = partesEndereco.length > 0 ? partesEndereco.join(', ') : undefined;
return {
latitude: data.latitude,
longitude: data.longitude,
cidade: data.city,
estado: data.region,
pais: data.country_name,
endereco
};
}
}
} catch (error) {
console.warn('Erro ao obter geolocalização por IP:', error);
}
return null;
}
/**
* Helper para registrar tentativas de login
*/
@@ -52,6 +107,26 @@ export async function registrarLogin(
// Validar e sanitizar IP antes de salvar
const ipAddressValidado = validarIP(dados.ipAddress);
// Obter geolocalização por IP se disponível (de forma assíncrona para não bloquear)
let geolocalizacao: {
latitude?: number;
longitude?: number;
cidade?: string;
estado?: string;
pais?: string;
endereco?: string;
} | null = null;
if (ipAddressValidado) {
// Obter geolocalização por IP (não bloquear se falhar)
try {
geolocalizacao = await obterGeoPorIP(ipAddressValidado);
} catch (error) {
console.warn('Erro ao obter geolocalização por IP:', error);
// Continuar sem localização se houver erro
}
}
await ctx.db.insert("logsLogin", {
usuarioId: dados.usuarioId,
@@ -63,6 +138,13 @@ export async function registrarLogin(
device,
browser,
sistema,
// Informações de Localização
latitude: geolocalizacao?.latitude,
longitude: geolocalizacao?.longitude,
cidade: geolocalizacao?.cidade,
estado: geolocalizacao?.estado,
pais: geolocalizacao?.pais,
endereco: geolocalizacao?.endereco,
timestamp: Date.now(),
});
@@ -280,6 +362,32 @@ function extrairSistema(userAgent: string): string {
return "Desconhecido";
}
/**
* Mutation pública para registrar tentativa de login
* Pode ser chamada do frontend após login bem-sucedido ou falho
*/
export const registrarTentativaLogin = mutation({
args: {
usuarioId: v.optional(v.id("usuarios")),
matriculaOuEmail: v.string(),
sucesso: v.boolean(),
motivoFalha: v.optional(v.string()),
ipAddress: v.optional(v.string()),
userAgent: v.optional(v.string()),
},
handler: async (ctx, args) => {
await registrarLogin(ctx, {
usuarioId: args.usuarioId,
matriculaOuEmail: args.matriculaOuEmail,
sucesso: args.sucesso,
motivoFalha: args.motivoFalha,
ipAddress: args.ipAddress,
userAgent: args.userAgent,
});
return { success: true };
},
});
/**
* Lista histórico de logins de um usuário
*/