diff --git a/apps/web/src/lib/components/Sidebar.svelte b/apps/web/src/lib/components/Sidebar.svelte index 7c1b6aa..8ec8bdd 100644 --- a/apps/web/src/lib/components/Sidebar.svelte +++ b/apps/web/src/lib/components/Sidebar.svelte @@ -10,6 +10,7 @@ import NotificationBell from "$lib/components/chat/NotificationBell.svelte"; import ChatWidget from "$lib/components/chat/ChatWidget.svelte"; import PresenceManager from "$lib/components/chat/PresenceManager.svelte"; + import { getBrowserInfo } from "$lib/utils/browserInfo"; let { children }: { children: Snippet } = $props(); @@ -100,9 +101,14 @@ carregandoLogin = true; try { + // Capturar informações do navegador + const browserInfo = await getBrowserInfo(); + const resultado = await convex.mutation(api.autenticacao.login, { matriculaOuEmail: matricula.trim(), senha: senha, + userAgent: browserInfo.userAgent || undefined, + ipAddress: browserInfo.ipAddress, }); if (resultado.sucesso) { diff --git a/apps/web/src/lib/utils/browserInfo.ts b/apps/web/src/lib/utils/browserInfo.ts new file mode 100644 index 0000000..8030aae --- /dev/null +++ b/apps/web/src/lib/utils/browserInfo.ts @@ -0,0 +1,98 @@ +/** + * Função utilitária para obter informações do navegador + * Sem usar APIs externas + */ + +/** + * Obtém o User-Agent do navegador + */ +export function getUserAgent(): string { + if (typeof window === 'undefined' || !window.navigator) { + return ''; + } + return window.navigator.userAgent || ''; +} + +/** + * Tenta obter o IP local usando WebRTC + * Esta função não usa API externa, mas pode falhar em alguns navegadores + * Retorna undefined se não conseguir obter + */ +export async function getLocalIP(): Promise { + return new Promise((resolve) => { + // Verificar se está em ambiente browser + if (typeof window === 'undefined' || typeof RTCPeerConnection === 'undefined') { + resolve(undefined); + return; + } + + try { + const pc = new RTCPeerConnection({ + iceServers: [] + }); + + let resolved = false; + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + pc.close(); + resolve(undefined); + } + }, 3000); + + pc.onicecandidate = (event) => { + if (event.candidate && !resolved) { + const candidate = event.candidate.candidate; + // Regex para extrair IP local (IPv4) + const ipMatch = candidate.match(/([0-9]{1,3}(\.[0-9]{1,3}){3})/); + if (ipMatch && ipMatch[1]) { + const ip = ipMatch[1]; + // Verificar se não é IP localhost (127.0.0.1 ou ::1) + if (!ip.startsWith('127.') && !ip.startsWith('::1')) { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + pc.close(); + resolve(ip); + } + } + } + } + }; + + // Criar um data channel para forçar a criação de candidatos + pc.createDataChannel(''); + pc.createOffer() + .then((offer) => pc.setLocalDescription(offer)) + .catch(() => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + pc.close(); + resolve(undefined); + } + }); + } catch (error) { + resolve(undefined); + } + }); +} + +/** + * Obtém informações completas do navegador + */ +export interface BrowserInfo { + userAgent: string; + ipAddress?: string; +} + +export async function getBrowserInfo(): Promise { + const userAgent = getUserAgent(); + const ipAddress = await getLocalIP(); + + return { + userAgent, + ipAddress, + }; +} + diff --git a/packages/backend/convex/logsLogin.ts b/packages/backend/convex/logsLogin.ts index acb377d..27de8ac 100644 --- a/packages/backend/convex/logsLogin.ts +++ b/packages/backend/convex/logsLogin.ts @@ -37,26 +37,182 @@ export async function registrarLogin( // Helpers para extrair informações do userAgent function extrairDevice(userAgent: string): string { - if (/mobile/i.test(userAgent)) return "Mobile"; - if (/tablet/i.test(userAgent)) return "Tablet"; + 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 { - if (/edg/i.test(userAgent)) return "Edge"; - if (/chrome/i.test(userAgent)) return "Chrome"; - if (/firefox/i.test(userAgent)) return "Firefox"; - if (/safari/i.test(userAgent)) return "Safari"; - if (/opera/i.test(userAgent)) return "Opera"; + 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 { - if (/windows/i.test(userAgent)) return "Windows"; - if (/mac/i.test(userAgent)) return "MacOS"; - if (/linux/i.test(userAgent)) return "Linux"; - if (/android/i.test(userAgent)) return "Android"; - if (/ios/i.test(userAgent)) return "iOS"; + 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"; }