1005 lines
27 KiB
TypeScript
1005 lines
27 KiB
TypeScript
import { getLocalIP } from './browserInfo';
|
|
|
|
export interface DadosAcelerometro {
|
|
x: number;
|
|
y: number;
|
|
z: number;
|
|
movimentoDetectado: boolean;
|
|
magnitude: number;
|
|
variacao: number; // Variância entre leituras
|
|
timestamp: number;
|
|
}
|
|
|
|
export interface DadosGiroscopio {
|
|
alpha: number;
|
|
beta: number;
|
|
gamma: number;
|
|
}
|
|
|
|
export interface InformacoesDispositivo {
|
|
ipAddress?: string;
|
|
ipPublico?: string;
|
|
ipLocal?: string;
|
|
userAgent?: string;
|
|
browser?: string;
|
|
browserVersion?: string;
|
|
engine?: string;
|
|
sistemaOperacional?: string;
|
|
osVersion?: string;
|
|
arquitetura?: string;
|
|
plataforma?: string;
|
|
latitude?: number;
|
|
longitude?: number;
|
|
precisao?: number;
|
|
altitude?: number | null;
|
|
altitudeAccuracy?: number | null;
|
|
heading?: number | null;
|
|
speed?: number | null;
|
|
confiabilidadeGPS?: number; // 0-1
|
|
suspeitaSpoofing?: boolean;
|
|
motivoSuspeita?: string;
|
|
endereco?: string;
|
|
cidade?: string;
|
|
estado?: string;
|
|
pais?: string;
|
|
timezone?: string;
|
|
deviceType?: string;
|
|
deviceModel?: string;
|
|
screenResolution?: string;
|
|
coresTela?: number;
|
|
idioma?: string;
|
|
isMobile?: boolean;
|
|
isTablet?: boolean;
|
|
isDesktop?: boolean;
|
|
connectionType?: string;
|
|
memoryInfo?: string;
|
|
acelerometro?: DadosAcelerometro;
|
|
giroscopio?: DadosGiroscopio;
|
|
sensorDisponivel?: boolean; // Indica se o sensor está disponível no dispositivo
|
|
permissaoNegada?: boolean; // Indica se a permissão foi negada pelo usuário
|
|
}
|
|
|
|
/**
|
|
* Detecta informações do navegador
|
|
*/
|
|
function detectarNavegador(): {
|
|
browser: string;
|
|
browserVersion: string;
|
|
engine: string;
|
|
} {
|
|
if (typeof navigator === 'undefined') {
|
|
return { browser: 'Desconhecido', browserVersion: '', engine: '' };
|
|
}
|
|
|
|
const ua = navigator.userAgent;
|
|
let browser = 'Desconhecido';
|
|
let browserVersion = '';
|
|
let engine = '';
|
|
|
|
// Detectar engine
|
|
if (ua.includes('Edg/')) {
|
|
engine = 'EdgeHTML';
|
|
} else if (ua.includes('Chrome/')) {
|
|
engine = 'Blink';
|
|
} else if (ua.includes('Firefox/')) {
|
|
engine = 'Gecko';
|
|
} else if (ua.includes('Safari/') && !ua.includes('Chrome/')) {
|
|
engine = 'WebKit';
|
|
}
|
|
|
|
// Detectar navegador
|
|
if (ua.includes('Edg/')) {
|
|
browser = 'Edge';
|
|
const match = ua.match(/Edg\/(\d+)/);
|
|
browserVersion = match ? match[1]! : '';
|
|
} else if (ua.includes('Chrome/') && !ua.includes('Edg/')) {
|
|
browser = 'Chrome';
|
|
const match = ua.match(/Chrome\/(\d+)/);
|
|
browserVersion = match ? match[1]! : '';
|
|
} else if (ua.includes('Firefox/')) {
|
|
browser = 'Firefox';
|
|
const match = ua.match(/Firefox\/(\d+)/);
|
|
browserVersion = match ? match[1]! : '';
|
|
} else if (ua.includes('Safari/') && !ua.includes('Chrome/')) {
|
|
browser = 'Safari';
|
|
const match = ua.match(/Version\/(\d+)/);
|
|
browserVersion = match ? match[1]! : '';
|
|
} else if (ua.includes('Opera/') || ua.includes('OPR/')) {
|
|
browser = 'Opera';
|
|
const match = ua.match(/(?:Opera|OPR)\/(\d+)/);
|
|
browserVersion = match ? match[1]! : '';
|
|
}
|
|
|
|
return { browser, browserVersion, engine };
|
|
}
|
|
|
|
/**
|
|
* Detecta informações do sistema operacional
|
|
*/
|
|
function detectarSistemaOperacional(): {
|
|
sistemaOperacional: string;
|
|
osVersion: string;
|
|
arquitetura: string;
|
|
plataforma: string;
|
|
} {
|
|
if (typeof navigator === 'undefined') {
|
|
return {
|
|
sistemaOperacional: 'Desconhecido',
|
|
osVersion: '',
|
|
arquitetura: '',
|
|
plataforma: ''
|
|
};
|
|
}
|
|
|
|
const ua = navigator.userAgent;
|
|
const platform = navigator.platform || '';
|
|
let sistemaOperacional = 'Desconhecido';
|
|
let osVersion = '';
|
|
let arquitetura = '';
|
|
const plataforma = platform;
|
|
|
|
// Detectar OS
|
|
if (ua.includes('Windows NT')) {
|
|
sistemaOperacional = 'Windows';
|
|
const match = ua.match(/Windows NT (\d+\.\d+)/);
|
|
if (match) {
|
|
const version = match[1]!;
|
|
const versions: Record<string, string> = {
|
|
'10.0': '10/11',
|
|
'6.3': '8.1',
|
|
'6.2': '8',
|
|
'6.1': '7'
|
|
};
|
|
osVersion = versions[version] || version;
|
|
}
|
|
} else if (ua.includes('Mac OS X') || ua.includes('Macintosh')) {
|
|
sistemaOperacional = 'macOS';
|
|
const match = ua.match(/Mac OS X (\d+[._]\d+)/);
|
|
if (match) {
|
|
osVersion = match[1]!.replace('_', '.');
|
|
}
|
|
} else if (ua.includes('Linux')) {
|
|
sistemaOperacional = 'Linux';
|
|
osVersion = 'Linux';
|
|
} else if (ua.includes('Android')) {
|
|
sistemaOperacional = 'Android';
|
|
const match = ua.match(/Android (\d+(?:\.\d+)?)/);
|
|
osVersion = match ? match[1]! : '';
|
|
} else if (ua.includes('iPhone') || ua.includes('iPad')) {
|
|
sistemaOperacional = 'iOS';
|
|
const match = ua.match(/OS (\d+[._]\d+)/);
|
|
if (match) {
|
|
osVersion = match[1]!.replace('_', '.');
|
|
}
|
|
}
|
|
|
|
// Detectar arquitetura (se disponível)
|
|
if ('cpuClass' in navigator) {
|
|
arquitetura = (navigator as unknown as { cpuClass: string }).cpuClass;
|
|
}
|
|
|
|
return { sistemaOperacional, osVersion, arquitetura, plataforma };
|
|
}
|
|
|
|
/**
|
|
* Detecta tipo de dispositivo
|
|
*/
|
|
function detectarTipoDispositivo(): {
|
|
deviceType: string;
|
|
isMobile: boolean;
|
|
isTablet: boolean;
|
|
isDesktop: boolean;
|
|
} {
|
|
if (typeof navigator === 'undefined') {
|
|
return {
|
|
deviceType: 'Desconhecido',
|
|
isMobile: false,
|
|
isTablet: false,
|
|
isDesktop: true
|
|
};
|
|
}
|
|
|
|
const ua = navigator.userAgent;
|
|
const isMobile = /Mobile|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
|
const isTablet = /iPad|Android(?!.*Mobile)|Tablet/i.test(ua);
|
|
const isDesktop = !isMobile && !isTablet;
|
|
|
|
let deviceType = 'Desktop';
|
|
if (isTablet) {
|
|
deviceType = 'Tablet';
|
|
} else if (isMobile) {
|
|
deviceType = 'Mobile';
|
|
}
|
|
|
|
return { deviceType, isMobile, isTablet, isDesktop };
|
|
}
|
|
|
|
/**
|
|
* Obtém informações da tela
|
|
*/
|
|
function obterInformacoesTela(): {
|
|
screenResolution: string;
|
|
coresTela: number;
|
|
} {
|
|
if (typeof screen === 'undefined') {
|
|
return { screenResolution: 'Desconhecido', coresTela: 0 };
|
|
}
|
|
|
|
const screenResolution = `${screen.width}x${screen.height}`;
|
|
const coresTela = screen.colorDepth || 24;
|
|
|
|
return { screenResolution, coresTela };
|
|
}
|
|
|
|
/**
|
|
* Obtém informações de conexão
|
|
*/
|
|
async function obterInformacoesConexao(): Promise<string> {
|
|
if (typeof navigator === 'undefined' || !('connection' in navigator)) {
|
|
return 'Desconhecido';
|
|
}
|
|
|
|
const connection = (navigator as unknown as { connection?: { effectiveType?: string } })
|
|
.connection;
|
|
if (connection?.effectiveType) {
|
|
return connection.effectiveType;
|
|
}
|
|
|
|
return 'Desconhecido';
|
|
}
|
|
|
|
/**
|
|
* Obtém informações de memória (se disponível)
|
|
*/
|
|
function obterInformacoesMemoria(): string {
|
|
if (typeof navigator === 'undefined' || !('deviceMemory' in navigator)) {
|
|
return 'Desconhecido';
|
|
}
|
|
|
|
const deviceMemory = (navigator as unknown as { deviceMemory?: number }).deviceMemory;
|
|
if (deviceMemory) {
|
|
return `${deviceMemory} GB`;
|
|
}
|
|
|
|
return 'Desconhecido';
|
|
}
|
|
|
|
/**
|
|
* Calcula distância entre duas coordenadas (fórmula de Haversine)
|
|
* Retorna distância em metros
|
|
*/
|
|
function calcularDistancia(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
|
const R = 6371000; // Raio da Terra em metros
|
|
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
|
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
|
const a =
|
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos((lat1 * Math.PI) / 180) *
|
|
Math.cos((lat2 * Math.PI) / 180) *
|
|
Math.sin(dLon / 2) *
|
|
Math.sin(dLon / 2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return R * c;
|
|
}
|
|
|
|
/**
|
|
* Obtém timezone aproximado por coordenadas
|
|
*/
|
|
function obterTimezonePorCoordenadas(latitude: number, longitude: number): string {
|
|
// Pernambuco está em UTC-3 (America/Recife)
|
|
if (longitude >= -45 && longitude <= -30 && latitude >= -10 && latitude <= 5) {
|
|
return 'America/Recife'; // UTC-3
|
|
}
|
|
|
|
// Fallback: usar timezone do sistema
|
|
try {
|
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
} catch {
|
|
return 'America/Recife'; // Default
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Captura uma única leitura de localização com todas as propriedades disponíveis
|
|
*/
|
|
async function capturarLocalizacaoUnica(
|
|
enableHighAccuracy: boolean = true,
|
|
timeout: number = 10000
|
|
): Promise<{
|
|
latitude?: number;
|
|
longitude?: number;
|
|
precisao?: number;
|
|
altitude?: number | null;
|
|
altitudeAccuracy?: number | null;
|
|
heading?: number | null;
|
|
speed?: number | null;
|
|
timestamp?: number;
|
|
confiabilidade: number; // 0-1
|
|
}> {
|
|
return new Promise((resolve) => {
|
|
if (typeof navigator === 'undefined' || !navigator.geolocation) {
|
|
resolve({ confiabilidade: 0 });
|
|
return;
|
|
}
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
resolve({ confiabilidade: 0 });
|
|
}, timeout + 1000);
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
clearTimeout(timeoutId);
|
|
const coords = position.coords;
|
|
const { latitude, longitude, accuracy } = coords;
|
|
|
|
// Validar coordenadas básicas
|
|
if (
|
|
isNaN(latitude) ||
|
|
isNaN(longitude) ||
|
|
latitude === 0 ||
|
|
longitude === 0 ||
|
|
latitude < -90 ||
|
|
latitude > 90 ||
|
|
longitude < -180 ||
|
|
longitude > 180
|
|
) {
|
|
resolve({ confiabilidade: 0 });
|
|
return;
|
|
}
|
|
|
|
// Calcular score de confiabilidade baseado em propriedades do GPS real
|
|
const sinaisGPSReal = {
|
|
temAltitude: coords.altitude !== null && coords.altitude !== 0,
|
|
temAltitudeAccuracy: coords.altitudeAccuracy !== null && coords.altitudeAccuracy > 0,
|
|
temHeading: coords.heading !== null && !isNaN(coords.heading),
|
|
temSpeed: coords.speed !== null && !isNaN(coords.speed),
|
|
precisaoBoa: accuracy < 20, // GPS real geralmente < 20m
|
|
precisaoMedia: accuracy >= 20 && accuracy < 100,
|
|
timestampPreciso: position.timestamp > 0
|
|
};
|
|
|
|
// Calcular confiabilidade: cada sinal adiciona pontos
|
|
let pontos = 0;
|
|
const maxPontos = 7;
|
|
|
|
if (sinaisGPSReal.temAltitude) pontos += 1;
|
|
if (sinaisGPSReal.temAltitudeAccuracy) pontos += 1;
|
|
if (sinaisGPSReal.temHeading) pontos += 0.5;
|
|
if (sinaisGPSReal.temSpeed) pontos += 0.5;
|
|
if (sinaisGPSReal.precisaoBoa) pontos += 2;
|
|
if (sinaisGPSReal.precisaoMedia) pontos += 1;
|
|
if (sinaisGPSReal.timestampPreciso) pontos += 1;
|
|
|
|
const confiabilidade = Math.min(pontos / maxPontos, 1);
|
|
|
|
resolve({
|
|
latitude,
|
|
longitude,
|
|
precisao: accuracy,
|
|
altitude: coords.altitude ?? null,
|
|
altitudeAccuracy: coords.altitudeAccuracy ?? null,
|
|
heading: coords.heading ?? null,
|
|
speed: coords.speed ?? null,
|
|
timestamp: position.timestamp,
|
|
confiabilidade
|
|
});
|
|
},
|
|
(error) => {
|
|
clearTimeout(timeoutId);
|
|
console.warn('Erro ao obter localização:', error.code, error.message);
|
|
resolve({ confiabilidade: 0 });
|
|
},
|
|
{
|
|
enableHighAccuracy,
|
|
timeout,
|
|
maximumAge: 0 // Sempre obter nova leitura
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Obtém localização via GPS com múltiplas leituras para detectar spoofing
|
|
* Apps de spoofing geralmente retornam valores idênticos em todas as leituras
|
|
*/
|
|
async function obterLocalizacaoMultipla(): Promise<{
|
|
latitude?: number;
|
|
longitude?: number;
|
|
precisao?: number;
|
|
altitude?: number | null;
|
|
altitudeAccuracy?: number | null;
|
|
heading?: number | null;
|
|
speed?: number | null;
|
|
confiabilidade: number; // 0-1
|
|
suspeitaSpoofing: boolean;
|
|
motivoSuspeita?: string;
|
|
}> {
|
|
if (typeof navigator === 'undefined' || !navigator.geolocation) {
|
|
return {
|
|
confiabilidade: 0,
|
|
suspeitaSpoofing: true,
|
|
motivoSuspeita: 'Geolocalização não suportada'
|
|
};
|
|
}
|
|
|
|
// Capturar 3 leituras com intervalo de 2 segundos entre elas
|
|
const leituras: Array<{
|
|
lat: number;
|
|
lon: number;
|
|
precisao: number;
|
|
altitude: number | null;
|
|
confiabilidade: number;
|
|
}> = [];
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
const leitura = await capturarLocalizacaoUnica(true, 8000);
|
|
|
|
if (leitura.latitude && leitura.longitude && leitura.confiabilidade > 0) {
|
|
leituras.push({
|
|
lat: leitura.latitude,
|
|
lon: leitura.longitude,
|
|
precisao: leitura.precisao || 999,
|
|
altitude: leitura.altitude ?? null,
|
|
confiabilidade: leitura.confiabilidade
|
|
});
|
|
}
|
|
|
|
// Aguardar 2 segundos entre leituras (exceto na última)
|
|
if (i < 2) {
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
}
|
|
}
|
|
|
|
if (leituras.length === 0) {
|
|
return {
|
|
confiabilidade: 0,
|
|
suspeitaSpoofing: true,
|
|
motivoSuspeita: 'Não foi possível obter localização'
|
|
};
|
|
}
|
|
|
|
// Se tivermos menos de 2 leituras, usar única leitura com baixa confiança
|
|
if (leituras.length < 2) {
|
|
const unica = leituras[0];
|
|
return {
|
|
latitude: unica.lat,
|
|
longitude: unica.lon,
|
|
precisao: unica.precisao,
|
|
altitude: unica.altitude,
|
|
altitudeAccuracy: null,
|
|
heading: null,
|
|
speed: null,
|
|
confiabilidade: unica.confiabilidade * 0.5, // Reduzir confiança por ter apenas 1 leitura
|
|
suspeitaSpoofing: true,
|
|
motivoSuspeita: 'Apenas uma leitura obtida'
|
|
};
|
|
}
|
|
|
|
// Verificar se todas as leituras são idênticas (suspeito de spoofing)
|
|
const primeiraLeitura = leituras[0];
|
|
const todasIguais = leituras.every(
|
|
(l) =>
|
|
Math.abs(l.lat - primeiraLeitura.lat) < 0.00001 && // ~1 metro
|
|
Math.abs(l.lon - primeiraLeitura.lon) < 0.00001
|
|
);
|
|
|
|
if (todasIguais && leituras.length === 3) {
|
|
// GPS real varia alguns metros, se todas são idênticas pode ser spoofing
|
|
return {
|
|
latitude: primeiraLeitura.lat,
|
|
longitude: primeiraLeitura.lon,
|
|
precisao: primeiraLeitura.precisao,
|
|
altitude: primeiraLeitura.altitude,
|
|
altitudeAccuracy: null,
|
|
heading: null,
|
|
speed: null,
|
|
confiabilidade: primeiraLeitura.confiabilidade * 0.4, // Reduzir drasticamente confiança
|
|
suspeitaSpoofing: true,
|
|
motivoSuspeita: 'Todas as leituras são idênticas (GPS real varia alguns metros)'
|
|
};
|
|
}
|
|
|
|
// Calcular média das leituras e variância
|
|
const mediaLat = leituras.reduce((sum, l) => sum + l.lat, 0) / leituras.length;
|
|
const mediaLon = leituras.reduce((sum, l) => sum + l.lon, 0) / leituras.length;
|
|
const mediaConfianca = leituras.reduce((sum, l) => sum + l.confiabilidade, 0) / leituras.length;
|
|
|
|
// Calcular distância máxima entre leituras
|
|
let distanciaMaxima = 0;
|
|
for (let i = 0; i < leituras.length; i++) {
|
|
for (let j = i + 1; j < leituras.length; j++) {
|
|
const dist = calcularDistancia(
|
|
leituras[i].lat,
|
|
leituras[i].lon,
|
|
leituras[j].lat,
|
|
leituras[j].lon
|
|
);
|
|
distanciaMaxima = Math.max(distanciaMaxima, dist);
|
|
}
|
|
}
|
|
|
|
// Se distância máxima for muito grande (> 100m), pode indicar problemas
|
|
const suspeitoPorDistancia = distanciaMaxima > 100;
|
|
|
|
return {
|
|
latitude: mediaLat,
|
|
longitude: mediaLon,
|
|
precisao: primeiraLeitura.precisao,
|
|
altitude: primeiraLeitura.altitude,
|
|
altitudeAccuracy: null,
|
|
heading: null,
|
|
speed: null,
|
|
confiabilidade: suspeitoPorDistancia ? mediaConfianca * 0.6 : mediaConfianca,
|
|
suspeitaSpoofing: suspeitoPorDistancia,
|
|
motivoSuspeita: suspeitoPorDistancia
|
|
? `Variação muito grande entre leituras (${Math.round(distanciaMaxima)}m)`
|
|
: undefined
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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<Response>((_, 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
|
|
*/
|
|
export async function obterLocalizacao(): Promise<{
|
|
latitude?: number;
|
|
longitude?: number;
|
|
precisao?: number;
|
|
altitude?: number | null;
|
|
altitudeAccuracy?: number | null;
|
|
heading?: number | null;
|
|
speed?: number | null;
|
|
confiabilidadeGPS?: number;
|
|
suspeitaSpoofing?: boolean;
|
|
motivoSuspeita?: string;
|
|
endereco?: string;
|
|
cidade?: string;
|
|
estado?: string;
|
|
pais?: string;
|
|
}> {
|
|
if (typeof navigator === 'undefined' || !navigator.geolocation) {
|
|
console.warn('Geolocalização não suportada');
|
|
return {};
|
|
}
|
|
|
|
// Usar múltiplas leituras para detectar spoofing
|
|
const localizacaoMultipla = await obterLocalizacaoMultipla();
|
|
|
|
if (!localizacaoMultipla.latitude || !localizacaoMultipla.longitude) {
|
|
console.warn('Não foi possível obter localização');
|
|
return {
|
|
confiabilidadeGPS: 0,
|
|
suspeitaSpoofing: true,
|
|
motivoSuspeita: 'Não foi possível obter localização'
|
|
};
|
|
}
|
|
|
|
const {
|
|
latitude,
|
|
longitude,
|
|
precisao,
|
|
altitude,
|
|
altitudeAccuracy,
|
|
heading,
|
|
speed,
|
|
confiabilidade,
|
|
suspeitaSpoofing,
|
|
motivoSuspeita
|
|
} = localizacaoMultipla;
|
|
|
|
// Tentar obter endereço via reverse geocoding
|
|
let endereco = '';
|
|
let cidade = '';
|
|
let estado = '';
|
|
let pais = '';
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&zoom=18&addressdetails=1`,
|
|
{
|
|
headers: {
|
|
'User-Agent': 'SGSE-App/1.0'
|
|
}
|
|
}
|
|
);
|
|
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) {
|
|
console.warn('Erro ao obter endereço:', error);
|
|
}
|
|
|
|
// Validar timezone vs localização
|
|
if (typeof navigator !== 'undefined') {
|
|
const timezoneAtual = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
const timezoneEsperado = obterTimezonePorCoordenadas(latitude, longitude);
|
|
|
|
// Se timezone é muito diferente, pode ser suspeito
|
|
if (
|
|
timezoneAtual !== timezoneEsperado &&
|
|
timezoneAtual !== 'America/Recife' &&
|
|
timezoneEsperado !== 'America/Recife'
|
|
) {
|
|
console.warn(`Timezone inconsistente: esperado ${timezoneEsperado}, atual ${timezoneAtual}`);
|
|
}
|
|
}
|
|
|
|
console.log('Localização obtida com validações:', {
|
|
latitude,
|
|
longitude,
|
|
confiabilidade: confiabilidade.toFixed(2),
|
|
suspeitaSpoofing,
|
|
motivoSuspeita
|
|
});
|
|
|
|
return {
|
|
latitude,
|
|
longitude,
|
|
precisao,
|
|
altitude,
|
|
altitudeAccuracy,
|
|
heading,
|
|
speed,
|
|
confiabilidadeGPS: confiabilidade,
|
|
suspeitaSpoofing: suspeitaSpoofing || false,
|
|
motivoSuspeita,
|
|
endereco,
|
|
cidade,
|
|
estado,
|
|
pais
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Obtém IP público
|
|
*/
|
|
export async function obterIPPublico(): Promise<string | undefined> {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Solicita permissão para acesso aos sensores de movimento (iOS 13+)
|
|
*/
|
|
async function solicitarPermissaoSensor(): Promise<PermissionState> {
|
|
if (
|
|
typeof DeviceMotionEvent === 'undefined' ||
|
|
typeof (DeviceMotionEvent as { requestPermission?: () => Promise<PermissionState> })
|
|
.requestPermission !== 'function'
|
|
) {
|
|
// Permissão não necessária ou já concedida (navegadores modernos)
|
|
return 'granted';
|
|
}
|
|
|
|
try {
|
|
const requestPermission = (
|
|
DeviceMotionEvent as { requestPermission: () => Promise<PermissionState> }
|
|
).requestPermission;
|
|
const resultado = await requestPermission();
|
|
return resultado;
|
|
} catch (error) {
|
|
console.warn('Erro ao solicitar permissão de sensor:', error);
|
|
return 'denied';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtém dados de acelerômetro e giroscópio durante um período
|
|
* @param duracaoMs Duração da coleta em milissegundos (padrão: 5000ms = 5 segundos)
|
|
*/
|
|
async function obterDadosAcelerometro(duracaoMs: number = 5000): Promise<{
|
|
acelerometro?: DadosAcelerometro;
|
|
giroscopio?: DadosGiroscopio;
|
|
sensorDisponivel: boolean;
|
|
permissaoNegada: boolean;
|
|
}> {
|
|
// Verificar se DeviceMotionEvent está disponível
|
|
if (typeof DeviceMotionEvent === 'undefined' || typeof DeviceOrientationEvent === 'undefined') {
|
|
return {
|
|
sensorDisponivel: false,
|
|
permissaoNegada: false
|
|
};
|
|
}
|
|
|
|
// Solicitar permissão (especialmente necessário no iOS 13+)
|
|
const permissao = await solicitarPermissaoSensor();
|
|
|
|
if (permissao === 'denied') {
|
|
return {
|
|
sensorDisponivel: true,
|
|
permissaoNegada: true
|
|
};
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
const leiturasAcelerometro: Array<{ x: number; y: number; z: number; timestamp: number }> = [];
|
|
const leiturasGiroscopio: Array<{
|
|
alpha: number;
|
|
beta: number;
|
|
gamma: number;
|
|
timestamp: number;
|
|
}> = [];
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
window.removeEventListener('devicemotion', handleDeviceMotion);
|
|
window.removeEventListener('deviceorientation', handleDeviceOrientation);
|
|
|
|
// Processar dados de acelerômetro
|
|
let acelerometro: DadosAcelerometro | undefined;
|
|
if (leiturasAcelerometro.length > 0) {
|
|
const ultimaLeitura = leiturasAcelerometro[leiturasAcelerometro.length - 1]!;
|
|
|
|
// Calcular magnitude média
|
|
const magnitudes = leiturasAcelerometro.map((l) =>
|
|
Math.sqrt(l.x * l.x + l.y * l.y + l.z * l.z)
|
|
);
|
|
const magnitude = magnitudes.reduce((sum, m) => sum + m, 0) / magnitudes.length;
|
|
|
|
// Calcular variância para detectar movimento
|
|
const mediaX =
|
|
leiturasAcelerometro.reduce((sum, l) => sum + l.x, 0) / leiturasAcelerometro.length;
|
|
const mediaY =
|
|
leiturasAcelerometro.reduce((sum, l) => sum + l.y, 0) / leiturasAcelerometro.length;
|
|
const mediaZ =
|
|
leiturasAcelerometro.reduce((sum, l) => sum + l.z, 0) / leiturasAcelerometro.length;
|
|
|
|
const variacoes = leiturasAcelerometro.map(
|
|
(l) => Math.pow(l.x - mediaX, 2) + Math.pow(l.y - mediaY, 2) + Math.pow(l.z - mediaZ, 2)
|
|
);
|
|
const variacao = variacoes.reduce((sum, v) => sum + v, 0) / variacoes.length;
|
|
|
|
// Detectar movimento: se variância > 0.01, há movimento
|
|
const movimentoDetectado = variacao > 0.01;
|
|
|
|
acelerometro = {
|
|
x: ultimaLeitura.x,
|
|
y: ultimaLeitura.y,
|
|
z: ultimaLeitura.z,
|
|
movimentoDetectado,
|
|
magnitude,
|
|
variacao,
|
|
timestamp: ultimaLeitura.timestamp
|
|
};
|
|
}
|
|
|
|
// Processar dados de giroscópio
|
|
let giroscopio: DadosGiroscopio | undefined;
|
|
if (leiturasGiroscopio.length > 0) {
|
|
const ultimaLeitura = leiturasGiroscopio[leiturasGiroscopio.length - 1]!;
|
|
giroscopio = {
|
|
alpha: ultimaLeitura.alpha || 0,
|
|
beta: ultimaLeitura.beta || 0,
|
|
gamma: ultimaLeitura.gamma || 0
|
|
};
|
|
}
|
|
|
|
resolve({
|
|
acelerometro,
|
|
giroscopio,
|
|
sensorDisponivel: true,
|
|
permissaoNegada: false
|
|
});
|
|
}, duracaoMs);
|
|
|
|
function handleDeviceMotion(event: DeviceMotionEvent) {
|
|
if (event.accelerationIncludingGravity) {
|
|
const acc = event.accelerationIncludingGravity;
|
|
if (acc.x !== null && acc.y !== null && acc.z !== null) {
|
|
leiturasAcelerometro.push({
|
|
x: acc.x,
|
|
y: acc.y,
|
|
z: acc.z,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleDeviceOrientation(event: DeviceOrientationEvent) {
|
|
if (event.alpha !== null && event.beta !== null && event.gamma !== null) {
|
|
leiturasGiroscopio.push({
|
|
alpha: event.alpha,
|
|
beta: event.beta,
|
|
gamma: event.gamma,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
}
|
|
|
|
window.addEventListener('devicemotion', handleDeviceMotion);
|
|
window.addEventListener('deviceorientation', handleDeviceOrientation);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Obtém todas as informações do dispositivo
|
|
*/
|
|
export async function obterInformacoesDispositivo(): Promise<InformacoesDispositivo> {
|
|
const informacoes: InformacoesDispositivo = {};
|
|
|
|
// Informações básicas
|
|
if (typeof navigator !== 'undefined') {
|
|
informacoes.userAgent = navigator.userAgent;
|
|
informacoes.idioma = navigator.language || navigator.languages?.[0];
|
|
informacoes.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
}
|
|
|
|
// Informações do navegador
|
|
const navegador = detectarNavegador();
|
|
informacoes.browser = navegador.browser;
|
|
informacoes.browserVersion = navegador.browserVersion;
|
|
informacoes.engine = navegador.engine;
|
|
|
|
// Informações do sistema
|
|
const sistema = detectarSistemaOperacional();
|
|
informacoes.sistemaOperacional = sistema.sistemaOperacional;
|
|
informacoes.osVersion = sistema.osVersion;
|
|
informacoes.arquitetura = sistema.arquitetura;
|
|
informacoes.plataforma = sistema.plataforma;
|
|
|
|
// Tipo de dispositivo
|
|
const dispositivo = detectarTipoDispositivo();
|
|
informacoes.deviceType = dispositivo.deviceType;
|
|
informacoes.isMobile = dispositivo.isMobile;
|
|
informacoes.isTablet = dispositivo.isTablet;
|
|
informacoes.isDesktop = dispositivo.isDesktop;
|
|
|
|
// Informações da tela
|
|
const tela = obterInformacoesTela();
|
|
informacoes.screenResolution = tela.screenResolution;
|
|
informacoes.coresTela = tela.coresTela;
|
|
|
|
// Informações de conexão, memória e localização (assíncronas)
|
|
const [connectionType, memoryInfo, ipPublico, ipLocal, localizacao, dadosSensores] =
|
|
await Promise.all([
|
|
obterInformacoesConexao(),
|
|
Promise.resolve(obterInformacoesMemoria()),
|
|
obterIPPublico(),
|
|
getLocalIP(),
|
|
obterLocalizacao(),
|
|
obterDadosAcelerometro(5000) // Coletar dados por 5 segundos
|
|
]);
|
|
|
|
informacoes.connectionType = connectionType;
|
|
informacoes.memoryInfo = memoryInfo;
|
|
informacoes.ipPublico = ipPublico;
|
|
informacoes.ipLocal = ipLocal;
|
|
informacoes.latitude = localizacao.latitude;
|
|
informacoes.longitude = localizacao.longitude;
|
|
informacoes.precisao = localizacao.precisao;
|
|
informacoes.altitude = localizacao.altitude ?? null;
|
|
informacoes.altitudeAccuracy = localizacao.altitudeAccuracy ?? null;
|
|
informacoes.heading = localizacao.heading ?? null;
|
|
informacoes.speed = localizacao.speed ?? null;
|
|
informacoes.confiabilidadeGPS = localizacao.confiabilidadeGPS;
|
|
informacoes.suspeitaSpoofing = localizacao.suspeitaSpoofing;
|
|
informacoes.motivoSuspeita = localizacao.motivoSuspeita;
|
|
informacoes.endereco = localizacao.endereco;
|
|
informacoes.cidade = localizacao.cidade;
|
|
informacoes.estado = localizacao.estado;
|
|
informacoes.pais = localizacao.pais;
|
|
|
|
// Dados de sensores
|
|
informacoes.acelerometro = dadosSensores.acelerometro;
|
|
informacoes.giroscopio = dadosSensores.giroscopio;
|
|
informacoes.sensorDisponivel = dadosSensores.sensorDisponivel;
|
|
informacoes.permissaoNegada = dadosSensores.permissaoNegada;
|
|
|
|
// IP address (usar público se disponível, senão local)
|
|
informacoes.ipAddress = ipPublico || ipLocal;
|
|
|
|
return informacoes;
|
|
}
|