diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index 55480a2..22805e1 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -13,20 +13,7 @@ import { Menu, User, Home, UserPlus, XCircle, LogIn, Tag, Plus, Check } from 'lucide-svelte'; 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; - } + import { obterIPPublico } from '$lib/utils/deviceInfo'; let { children }: { children: Snippet } = $props(); @@ -137,11 +124,34 @@ erroLogin = ''; carregandoLogin = true; - // 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) - ]); + // Obter IP público e userAgent (rápido, não bloqueia) + const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : undefined; + + // Obter IP público com timeout curto (não bloquear login) + const ipPublicoPromise = obterIPPublico().catch(() => undefined); + const ipPublicoTimeout = new Promise((resolve) => + setTimeout(() => resolve(undefined), 2000) // Timeout de 2 segundos + ); + const ipPublico = await Promise.race([ipPublicoPromise, ipPublicoTimeout]); + + // Função para coletar GPS em background (não bloqueia login) + async function coletarGPS(): Promise { + try { + const { obterLocalizacaoRapida } = await import('$lib/utils/deviceInfo'); + // Usar versão rápida com timeout curto (3 segundos máximo) + const gpsPromise = obterLocalizacaoRapida(); + const gpsTimeout = new Promise<{}>((resolve) => + setTimeout(() => resolve({}), 3000) + ); + return await Promise.race([gpsPromise, gpsTimeout]); + } catch (err) { + console.warn('Erro ao obter GPS (não bloqueia login):', err); + return {}; + } + } + + // Iniciar coleta de GPS em background (não esperar) + const gpsPromise = coletarGPS(); const result = await authClient.signIn.email( { email: matricula.trim(), password: senha }, @@ -149,12 +159,30 @@ onError: async (ctx) => { // Registrar tentativa de login falha try { + // Tentar obter GPS se já estiver disponível (não esperar) + let localizacaoGPS: any = {}; + try { + localizacaoGPS = await Promise.race([ + gpsPromise, + new Promise<{}>((resolve) => setTimeout(() => resolve({}), 100)) + ]); + } catch { + // Ignorar se GPS não estiver pronto + } + await convexClient.mutation(api.logsLogin.registrarTentativaLogin, { matriculaOuEmail: matricula.trim(), sucesso: false, motivoFalha: ctx.error?.message || 'Erro desconhecido', userAgent: userAgent, ipAddress: ipPublico, + latitudeGPS: localizacaoGPS.latitude, + longitudeGPS: localizacaoGPS.longitude, + precisaoGPS: localizacaoGPS.precisao, + enderecoGPS: localizacaoGPS.endereco, + cidadeGPS: localizacaoGPS.cidade, + estadoGPS: localizacaoGPS.estado, + paisGPS: localizacaoGPS.pais, }); } catch (err) { console.error('Erro ao registrar tentativa de login falha:', err); @@ -172,6 +200,17 @@ // Aguardar um pouco para o usuário ser sincronizado no Convex await new Promise((resolve) => setTimeout(resolve, 500)); + // Tentar obter GPS se já estiver disponível (não esperar) + let localizacaoGPS: any = {}; + try { + localizacaoGPS = await Promise.race([ + gpsPromise, + new Promise<{}>((resolve) => setTimeout(() => resolve({}), 100)) + ]); + } catch { + // Ignorar se GPS não estiver pronto + } + // Buscar o usuário no Convex usando getCurrentUser const usuario = await convexClient.query(api.auth.getCurrentUser, {}); @@ -182,6 +221,13 @@ sucesso: true, userAgent: userAgent, ipAddress: ipPublico, + latitudeGPS: localizacaoGPS.latitude, + longitudeGPS: localizacaoGPS.longitude, + precisaoGPS: localizacaoGPS.precisao, + enderecoGPS: localizacaoGPS.endereco, + cidadeGPS: localizacaoGPS.cidade, + estadoGPS: localizacaoGPS.estado, + paisGPS: localizacaoGPS.pais, }); } else { // Se não encontrou o usuário, registrar sem usuarioId (será atualizado depois) @@ -190,6 +236,13 @@ sucesso: true, userAgent: userAgent, ipAddress: ipPublico, + latitudeGPS: localizacaoGPS.latitude, + longitudeGPS: localizacaoGPS.longitude, + precisaoGPS: localizacaoGPS.precisao, + enderecoGPS: localizacaoGPS.endereco, + cidadeGPS: localizacaoGPS.cidade, + estadoGPS: localizacaoGPS.estado, + paisGPS: localizacaoGPS.pais, }); } } catch (err) { diff --git a/apps/web/src/lib/utils/deviceInfo.ts b/apps/web/src/lib/utils/deviceInfo.ts index 6213454..17fdcca 100644 --- a/apps/web/src/lib/utils/deviceInfo.ts +++ b/apps/web/src/lib/utils/deviceInfo.ts @@ -526,10 +526,97 @@ async function obterLocalizacaoMultipla(): Promise<{ }; } +/** + * Obtém localização via GPS de forma rápida (uma única leitura, sem reverse geocoding) + * Usado para login - não bloqueia o fluxo + */ +export async function obterLocalizacaoRapida(): Promise<{ + latitude?: number; + longitude?: number; + precisao?: number; + endereco?: string; + cidade?: string; + estado?: string; + pais?: string; +}> { + if (typeof navigator === 'undefined' || !navigator.geolocation) { + return {}; + } + + try { + // Uma única leitura rápida com timeout curto + const leitura = await capturarLocalizacaoUnica(true, 3000); // 3 segundos máximo + + if (!leitura.latitude || !leitura.longitude || leitura.confiabilidade === 0) { + return {}; + } + + // Tentar obter endereço via reverse geocoding (com timeout curto) + let endereco = ''; + let cidade = ''; + let estado = ''; + let pais = ''; + + try { + const geocodePromise = fetch( + `https://nominatim.openstreetmap.org/reverse?format=json&lat=${leitura.latitude}&lon=${leitura.longitude}&zoom=18&addressdetails=1`, + { + headers: { + 'User-Agent': 'SGSE-App/1.0' + } + } + ); + const geocodeTimeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 2000) + ); + + const response = await Promise.race([geocodePromise, geocodeTimeout]); + + if (response.ok) { + const data = (await response.json()) as { + address?: { + road?: string; + house_number?: string; + city?: string; + town?: string; + state?: string; + country?: string; + }; + }; + if (data.address) { + const addr = data.address; + if (addr.road) { + endereco = `${addr.road}${addr.house_number ? `, ${addr.house_number}` : ''}`; + } + cidade = addr.city || addr.town || ''; + estado = addr.state || ''; + pais = addr.country || ''; + } + } + } catch (error) { + // Ignorar erro de geocoding - não é crítico + console.warn('Erro ao obter endereço (não crítico):', error); + } + + return { + latitude: leitura.latitude, + longitude: leitura.longitude, + precisao: leitura.precisao, + endereco, + cidade, + estado, + pais + }; + } catch (error) { + console.warn('Erro ao obter localização rápida:', error); + return {}; + } +} + /** * Obtém localização via GPS com múltiplas tentativas e validações anti-spoofing */ -async function obterLocalizacao(): Promise<{ +export async function obterLocalizacao(): Promise<{ latitude?: number; longitude?: number; precisao?: number; @@ -644,7 +731,7 @@ async function obterLocalizacao(): Promise<{ /** * Obtém IP público */ -async function obterIPPublico(): Promise { +export async function obterIPPublico(): Promise { try { const response = await fetch('https://api.ipify.org?format=json'); if (response.ok) { diff --git a/apps/web/src/routes/(dashboard)/ti/auditoria/+page.svelte b/apps/web/src/routes/(dashboard)/ti/auditoria/+page.svelte index 8a42d70..6979b04 100644 --- a/apps/web/src/routes/(dashboard)/ti/auditoria/+page.svelte +++ b/apps/web/src/routes/(dashboard)/ti/auditoria/+page.svelte @@ -132,6 +132,37 @@ function formatarLocalizacao(login: any): string { if (!login || typeof login !== 'object') return "-"; + // Priorizar localização GPS (mais precisa) quando disponível + const usarGPS = login.latitudeGPS && login.longitudeGPS; + + if (usarGPS) { + const partes: string[] = []; + if (login.cidadeGPS && typeof login.cidadeGPS === 'string' && login.cidadeGPS.trim()) { + partes.push(login.cidadeGPS.trim()); + } + if (login.estadoGPS && typeof login.estadoGPS === 'string' && login.estadoGPS.trim()) { + partes.push(login.estadoGPS.trim()); + } + if (login.paisGPS && typeof login.paisGPS === 'string' && login.paisGPS.trim()) { + partes.push(login.paisGPS.trim()); + } + + if (partes.length > 0) { + return `${partes.join(", ")} (GPS)`; + } + + // Se não tiver cidade/estado/pais GPS, mas tiver endereco GPS, mostrar endereco + if (login.enderecoGPS && typeof login.enderecoGPS === 'string' && login.enderecoGPS.trim()) { + return `${login.enderecoGPS.trim()} (GPS)`; + } + + // Se tiver coordenadas mas não endereço, mostrar coordenadas + if (login.latitudeGPS && login.longitudeGPS) { + return `${login.latitudeGPS.toFixed(6)}, ${login.longitudeGPS.toFixed(6)} (GPS)`; + } + } + + // Fallback para localização por IP const partes: string[] = []; if (login.cidade && typeof login.cidade === 'string' && login.cidade.trim()) { partes.push(login.cidade.trim()); diff --git a/packages/backend/convex/logsLogin.ts b/packages/backend/convex/logsLogin.ts index 8219b49..ff03f7c 100644 --- a/packages/backend/convex/logsLogin.ts +++ b/packages/backend/convex/logsLogin.ts @@ -2,61 +2,6 @@ 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 */ @@ -98,6 +43,13 @@ export async function registrarLogin( motivoFalha?: string; ipAddress?: string; userAgent?: string; + latitudeGPS?: number; + longitudeGPS?: number; + precisaoGPS?: number; + enderecoGPS?: string; + cidadeGPS?: string; + estadoGPS?: string; + paisGPS?: string; } ) { // Extrair informações do userAgent @@ -108,25 +60,8 @@ 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 - } - } + // Nota: Geolocalização por IP removida porque fetch() não pode ser usado em mutations do Convex + // A localização GPS já é coletada no frontend e enviada diretamente await ctx.db.insert("logsLogin", { usuarioId: dados.usuarioId, @@ -138,13 +73,21 @@ 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, + // Informações de Localização por IP (removido - usar GPS do frontend) + latitude: undefined, + longitude: undefined, + cidade: undefined, + estado: undefined, + pais: undefined, + endereco: undefined, + // Informações de Localização (GPS do navegador) + latitudeGPS: dados.latitudeGPS, + longitudeGPS: dados.longitudeGPS, + precisaoGPS: dados.precisaoGPS, + enderecoGPS: dados.enderecoGPS, + cidadeGPS: dados.cidadeGPS, + estadoGPS: dados.estadoGPS, + paisGPS: dados.paisGPS, timestamp: Date.now(), }); @@ -374,6 +317,13 @@ export const registrarTentativaLogin = mutation({ motivoFalha: v.optional(v.string()), ipAddress: v.optional(v.string()), userAgent: v.optional(v.string()), + latitudeGPS: v.optional(v.number()), + longitudeGPS: v.optional(v.number()), + precisaoGPS: v.optional(v.number()), + enderecoGPS: v.optional(v.string()), + cidadeGPS: v.optional(v.string()), + estadoGPS: v.optional(v.string()), + paisGPS: v.optional(v.string()), }, handler: async (ctx, args) => { await registrarLogin(ctx, { @@ -383,6 +333,13 @@ export const registrarTentativaLogin = mutation({ motivoFalha: args.motivoFalha, ipAddress: args.ipAddress, userAgent: args.userAgent, + latitudeGPS: args.latitudeGPS, + longitudeGPS: args.longitudeGPS, + precisaoGPS: args.precisaoGPS, + enderecoGPS: args.enderecoGPS, + cidadeGPS: args.cidadeGPS, + estadoGPS: args.estadoGPS, + paisGPS: args.paisGPS, }); return { success: true }; }, diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index cfef17e..022cbcf 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -787,13 +787,21 @@ export default defineSchema({ device: v.optional(v.string()), browser: v.optional(v.string()), sistema: v.optional(v.string()), - // Informações de Localização + // Informações de Localização (por IP) 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()), + // Informações de Localização (GPS do navegador) + latitudeGPS: v.optional(v.number()), + longitudeGPS: v.optional(v.number()), + precisaoGPS: v.optional(v.number()), + enderecoGPS: v.optional(v.string()), + cidadeGPS: v.optional(v.string()), + estadoGPS: v.optional(v.string()), + paisGPS: v.optional(v.string()), timestamp: v.number(), }) .index("by_usuario", ["usuarioId"])