From 334676b860f87f4fa756b82c483f3771baf0970a Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sun, 30 Nov 2025 08:12:46 -0300 Subject: [PATCH] feat: enhance login functionality by adding IP geolocation tracking and advanced filtering options in the audit page, improving user insights and data accuracy --- apps/web/src/lib/components/Sidebar.svelte | 70 ++- .../(dashboard)/ti/auditoria/+page.svelte | 460 ++++++++++++++++-- packages/backend/convex/logsLogin.ts | 108 ++++ packages/backend/convex/schema.ts | 10 + 4 files changed, 610 insertions(+), 38 deletions(-) diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index c381611..55480a2 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -4,7 +4,7 @@ import logo from '$lib/assets/logo_governo_PE.png'; import type { Snippet } from 'svelte'; import { loginModalStore } from '$lib/stores/loginModal.svelte'; - import { useQuery } from 'convex-svelte'; + import { useQuery, useConvexClient } from 'convex-svelte'; import { api } from '@sgse-app/backend/convex/_generated/api'; import NotificationBell from '$lib/components/chat/NotificationBell.svelte'; import ChatWidget from '$lib/components/chat/ChatWidget.svelte'; @@ -14,11 +14,26 @@ import { authClient } from '$lib/auth'; import { resolve } from '$app/paths'; + // Função para obter IP público (similar ao sistema de ponto) + async function obterIPPublico(): Promise { + try { + const response = await fetch('https://api.ipify.org?format=json'); + if (response.ok) { + const data = (await response.json()) as { ip: string }; + return data.ip; + } + } catch (error) { + console.warn('Erro ao obter IP público:', error); + } + return undefined; + } + let { children }: { children: Snippet } = $props(); const currentPath = $derived(page.url.pathname); const currentUser = useQuery(api.auth.getCurrentUser, {}); + const convexClient = useConvexClient(); // Função para obter a URL do avatar/foto do usuário const avatarUrlDoUsuario = $derived(() => { @@ -122,18 +137,67 @@ erroLogin = ''; carregandoLogin = true; - // const browserInfo = await getBrowserInfo(); + // Obter IP público e userAgent antes do login + const [ipPublico, userAgent] = await Promise.all([ + obterIPPublico().catch(() => undefined), + Promise.resolve(typeof navigator !== 'undefined' ? navigator.userAgent : undefined) + ]); const result = await authClient.signIn.email( { email: matricula.trim(), password: senha }, { - onError: (ctx) => { + onError: async (ctx) => { + // Registrar tentativa de login falha + try { + await convexClient.mutation(api.logsLogin.registrarTentativaLogin, { + matriculaOuEmail: matricula.trim(), + sucesso: false, + motivoFalha: ctx.error?.message || 'Erro desconhecido', + userAgent: userAgent, + ipAddress: ipPublico, + }); + } catch (err) { + console.error('Erro ao registrar tentativa de login falha:', err); + } alert(ctx.error.message); } } ); if (result.data) { + // Registrar tentativa de login bem-sucedida + // Fazer de forma assíncrona para não bloquear o login + (async () => { + try { + // Aguardar um pouco para o usuário ser sincronizado no Convex + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Buscar o usuário no Convex usando getCurrentUser + const usuario = await convexClient.query(api.auth.getCurrentUser, {}); + + if (usuario && usuario._id) { + await convexClient.mutation(api.logsLogin.registrarTentativaLogin, { + usuarioId: usuario._id, + matriculaOuEmail: matricula.trim(), + sucesso: true, + userAgent: userAgent, + ipAddress: ipPublico, + }); + } else { + // Se não encontrou o usuário, registrar sem usuarioId (será atualizado depois) + await convexClient.mutation(api.logsLogin.registrarTentativaLogin, { + matriculaOuEmail: matricula.trim(), + sucesso: true, + userAgent: userAgent, + ipAddress: ipPublico, + }); + } + } catch (err) { + console.error('Erro ao registrar tentativa de login:', err); + // Não bloquear o login se houver erro ao registrar + } + })(); + closeLoginModal(); goto(resolve('/')); } else { diff --git a/apps/web/src/routes/(dashboard)/ti/auditoria/+page.svelte b/apps/web/src/routes/(dashboard)/ti/auditoria/+page.svelte index 85e4d2e..6f345dd 100644 --- a/apps/web/src/routes/(dashboard)/ti/auditoria/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/auditoria/+page.svelte @@ -1,26 +1,149 @@
@@ -163,13 +447,38 @@
- - +
+ + + {#if mostrarFiltros} +
+
+
+ + +
+
+ + +
+
+ + +
+ {#if abaAtiva === "logins"} +
+ + +
+
+ + +
+ {/if} +
+ +
+
+ {/if} @@ -191,17 +569,17 @@ Atividades Recentes - {#if atividades?.data} -
{atividades.data.length} registro{atividades.data.length !== 1 ? 's' : ''}
+ {#if atividades} +
{atividades.length} registro{atividades.length !== 1 ? 's' : ''}
{/if} - {#if !atividades?.data} + {#if atividadesRaw === undefined}

Carregando atividades...

- {:else if atividades.data.length === 0} + {:else if atividades.length === 0}
@@ -222,7 +600,7 @@ - {#each atividades.data as atividade} + {#each atividades as atividade}
@@ -277,17 +655,17 @@ Histórico de Logins - {#if logins?.data} -
{logins.data.length} registro{logins.data.length !== 1 ? 's' : ''}
+ {#if logins} +
{logins.length} registro{logins.length !== 1 ? 's' : ''}
{/if}
- {#if !logins?.data} + {#if loginsRaw === undefined}

Carregando logins...

- {:else if logins.data.length === 0} + {:else if logins.length === 0}
@@ -304,13 +682,14 @@ Usuário/Email Status IP + Localização Dispositivo Navegador Sistema - {#each logins.data as login} + {#each logins as login}
@@ -353,6 +732,17 @@ {login.ipAddress || "-"} + +
+ + + + + + {formatarLocalizacao(login)} + +
+
{login.device || "-"}
diff --git a/packages/backend/convex/logsLogin.ts b/packages/backend/convex/logsLogin.ts index 4969c56..3911716 100644 --- a/packages/backend/convex/logsLogin.ts +++ b/packages/backend/convex/logsLogin.ts @@ -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 */ diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 21afec4..cfef17e 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -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"])