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:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -779,11 +779,21 @@ export default defineSchema({
|
||||
matriculaOuEmail: v.string(), // tentativa de login
|
||||
sucesso: v.boolean(),
|
||||
motivoFalha: v.optional(v.string()), // "senha_incorreta", "usuario_bloqueado", "usuario_inexistente"
|
||||
// Informações de Rede
|
||||
ipAddress: v.optional(v.string()),
|
||||
ipPublico: v.optional(v.string()),
|
||||
ipLocal: v.optional(v.string()),
|
||||
userAgent: v.optional(v.string()),
|
||||
device: v.optional(v.string()),
|
||||
browser: v.optional(v.string()),
|
||||
sistema: v.optional(v.string()),
|
||||
// Informações de Localização
|
||||
latitude: v.optional(v.number()),
|
||||
longitude: v.optional(v.number()),
|
||||
endereco: v.optional(v.string()),
|
||||
cidade: v.optional(v.string()),
|
||||
estado: v.optional(v.string()),
|
||||
pais: v.optional(v.string()),
|
||||
timestamp: v.number(),
|
||||
})
|
||||
.index("by_usuario", ["usuarioId"])
|
||||
|
||||
Reference in New Issue
Block a user