feat: enhance call and point registration features with sensor data integration

- Updated the CallWindow component to include connection quality states and reconnection attempts, improving user experience during calls.
- Enhanced the ChatWindow to allow starting audio and video calls in a new window, providing users with more flexibility.
- Integrated accelerometer and gyroscope data collection in the RegistroPonto component, enabling validation of point registration authenticity.
- Improved error handling and user feedback for sensor permissions and data validation, ensuring a smoother registration process.
- Updated backend logic to validate sensor data and adjust confidence scores for point registration, enhancing security against spoofing.
This commit is contained in:
2025-11-22 20:49:52 -03:00
parent fc4b5c5ba5
commit f818756efc
15 changed files with 2100 additions and 275 deletions

View File

@@ -223,6 +223,92 @@ async function validarLocalizacao(
};
}
/**
* Valida dados de acelerômetro para detectar autenticidade do registro
* Retorna informações de validação sem bloquear o registro
*/
function validarAcelerometro(
isDesktop: boolean | undefined,
sensorDisponivel: boolean | undefined,
permissaoSensorNegada: boolean | undefined,
acelerometroX: number | undefined,
acelerometroY: number | undefined,
acelerometroZ: number | undefined,
movimentoDetectado: boolean | undefined,
magnitudeMovimento: number | undefined,
variacaoAcelerometro: number | undefined
): {
valida: boolean;
motivo?: string;
scoreConfianca: number; // 0-1
avisos: string[];
} {
const avisos: string[] = [];
let scoreConfianca = 1.0;
// Se for desktop, ausência de sensor não é suspeito
if (isDesktop === true) {
if (sensorDisponivel === false || !acelerometroX) {
// Desktop não tem sensor - isso é normal, não reduzir confiança
return {
valida: true,
scoreConfianca: 1.0,
avisos: []
};
}
}
// Se permissão foi negada, apenas reduzir score de confiança (não bloqueia registro)
if (permissaoSensorNegada === true) {
scoreConfianca *= 0.9;
avisos.push('Permissão de sensor negada pelo usuário (não bloqueia registro)');
// Continuar validação normalmente
}
// Se sensor não está disponível e não é desktop, pode ser suspeito (mas não bloqueia)
if (sensorDisponivel === false && isDesktop !== true) {
scoreConfianca *= 0.8;
avisos.push('Sensor de movimento não disponível no dispositivo móvel');
}
// Se sensor está disponível mas não há dados, pode ser suspeito
if (sensorDisponivel === true && (!acelerometroX || !acelerometroY || !acelerometroZ)) {
scoreConfianca *= 0.7;
avisos.push('Sensor disponível mas dados de acelerômetro não coletados');
}
// Se há dados de acelerômetro, validar
if (acelerometroX !== undefined && acelerometroY !== undefined && acelerometroZ !== undefined) {
// Verificar se valores são realistas (aceleração geralmente entre -20 e +20 m/s² em uso normal)
const magnitude = magnitudeMovimento || Math.sqrt(acelerometroX * acelerometroX + acelerometroY * acelerometroY + acelerometroZ * acelerometroZ);
if (magnitude > 50) {
// Aceleração muito alta pode indicar leitura errada ou emulador
scoreConfianca *= 0.6;
avisos.push(`Magnitude de movimento muito alta (${magnitude.toFixed(2)} m/s²). Pode indicar leitura incorreta.`);
}
// Se não há movimento detectado quando deveria haver (em móvel), pode ser suspeito
if (isDesktop !== true && movimentoDetectado === false && variacaoAcelerometro !== undefined && variacaoAcelerometro < 0.001) {
// Variância muito baixa pode indicar que o dispositivo está parado ou emulador
scoreConfianca *= 0.9;
avisos.push('Nenhum movimento detectado durante o registro. Pode ser normal se o dispositivo estava parado.');
}
// Se há movimento, aumenta confiança
if (movimentoDetectado === true) {
scoreConfianca = Math.min(scoreConfianca * 1.1, 1.0);
}
}
return {
valida: true, // Sempre retorna true - não bloqueia registro, apenas informa através do score
motivo: avisos.length > 0 ? avisos[0] : undefined,
scoreConfianca: Math.max(0, Math.min(1, scoreConfianca)),
avisos
};
}
/**
* Gera URL para upload de imagem do ponto
*/
@@ -337,6 +423,27 @@ export const registrarPonto = mutation({
isDesktop: v.optional(v.boolean()),
connectionType: v.optional(v.string()),
memoryInfo: v.optional(v.string()),
// Campos de sensores (acelerômetro e giroscópio)
sensorDisponivel: v.optional(v.boolean()),
permissaoNegada: v.optional(v.boolean()),
acelerometro: v.optional(
v.object({
x: v.number(),
y: v.number(),
z: v.number(),
movimentoDetectado: v.boolean(),
magnitude: v.number(),
variacao: v.number(),
timestamp: v.number(),
})
),
giroscopio: v.optional(
v.object({
alpha: v.number(),
beta: v.number(),
gamma: v.number(),
})
),
})
),
timestamp: v.number(),
@@ -564,6 +671,39 @@ export const registrarPonto = mutation({
}
}
// Validar dados de acelerômetro (não bloqueia registro - apenas informa)
const validacaoAcelerometro = validarAcelerometro(
args.informacoesDispositivo?.isDesktop,
args.informacoesDispositivo?.sensorDisponivel,
args.informacoesDispositivo?.permissaoNegada,
args.informacoesDispositivo?.acelerometro?.x,
args.informacoesDispositivo?.acelerometro?.y,
args.informacoesDispositivo?.acelerometro?.z,
args.informacoesDispositivo?.acelerometro?.movimentoDetectado,
args.informacoesDispositivo?.acelerometro?.magnitude,
args.informacoesDispositivo?.acelerometro?.variacao
);
// Nota: A validação de acelerômetro não bloqueia o registro - apenas reduz o score de confiança
// Apenas câmera e localização são obrigatórias para registrar ponto
// Combinar avisos de validação de localização e acelerômetro
const todosAvisos = [
...(validacaoLocalizacao?.avisos || []),
...(validacaoAcelerometro.avisos || [])
];
// Combinar scores de confiança (média ponderada)
let scoreFinalConfianca = 1.0;
if (validacaoLocalizacao && validacaoAcelerometro) {
// GPS tem peso 0.7, acelerômetro tem peso 0.3
scoreFinalConfianca = (validacaoLocalizacao.scoreConfianca * 0.7) + (validacaoAcelerometro.scoreConfianca * 0.3);
} else if (validacaoLocalizacao) {
scoreFinalConfianca = validacaoLocalizacao.scoreConfianca;
} else if (validacaoAcelerometro) {
scoreFinalConfianca = validacaoAcelerometro.scoreConfianca;
}
// Criar registro
const registroId = await ctx.db.insert('registrosPonto', {
funcionarioId: usuario.funcionarioId,
@@ -597,11 +737,11 @@ export const registrarPonto = mutation({
heading: args.informacoesDispositivo?.heading,
speed: args.informacoesDispositivo?.speed,
confiabilidadeGPS: args.informacoesDispositivo?.confiabilidadeGPS,
scoreConfiancaBackend: validacaoLocalizacao?.scoreConfianca,
suspeitaSpoofing: args.informacoesDispositivo?.suspeitaSpoofing || (validacaoLocalizacao ? validacaoLocalizacao.scoreConfianca < 0.5 || !validacaoLocalizacao.valida : undefined),
motivoSuspeita: args.informacoesDispositivo?.motivoSuspeita || validacaoLocalizacao?.motivo || (validacaoLocalizacao && validacaoLocalizacao.avisos.length > 0 ? validacaoLocalizacao.avisos.join('; ') : undefined),
scoreConfiancaBackend: scoreFinalConfianca,
suspeitaSpoofing: args.informacoesDispositivo?.suspeitaSpoofing || (validacaoLocalizacao ? validacaoLocalizacao.scoreConfianca < 0.5 || !validacaoLocalizacao.valida : undefined) || (validacaoAcelerometro ? validacaoAcelerometro.scoreConfianca < 0.5 || !validacaoAcelerometro.valida : undefined),
motivoSuspeita: args.informacoesDispositivo?.motivoSuspeita || validacaoLocalizacao?.motivo || validacaoAcelerometro?.motivo || (todosAvisos.length > 0 ? todosAvisos.join('; ') : undefined),
// Informações detalhadas de validação (sempre salvar quando houver validação)
avisosValidacao: validacaoLocalizacao && validacaoLocalizacao.avisos.length > 0 ? validacaoLocalizacao.avisos : undefined,
avisosValidacao: todosAvisos.length > 0 ? todosAvisos : undefined,
// Informações de Geofencing
enderecoMarcacaoEsperado: validacaoGeofencing?.enderecoMaisProximo,
distanciaEnderecoEsperado: validacaoGeofencing?.distanciaMetros,
@@ -623,6 +763,18 @@ export const registrarPonto = mutation({
isDesktop: args.informacoesDispositivo?.isDesktop,
connectionType: args.informacoesDispositivo?.connectionType,
memoryInfo: args.informacoesDispositivo?.memoryInfo,
// Dados de sensores (Acelerômetro e Giroscópio)
acelerometroX: args.informacoesDispositivo?.acelerometro?.x,
acelerometroY: args.informacoesDispositivo?.acelerometro?.y,
acelerometroZ: args.informacoesDispositivo?.acelerometro?.z,
movimentoDetectado: args.informacoesDispositivo?.acelerometro?.movimentoDetectado,
magnitudeMovimento: args.informacoesDispositivo?.acelerometro?.magnitude,
variacaoAcelerometro: args.informacoesDispositivo?.acelerometro?.variacao,
giroscopioAlpha: args.informacoesDispositivo?.giroscopio?.alpha,
giroscopioBeta: args.informacoesDispositivo?.giroscopio?.beta,
giroscopioGamma: args.informacoesDispositivo?.giroscopio?.gamma,
sensorDisponivel: args.informacoesDispositivo?.sensorDisponivel,
permissaoSensorNegada: args.informacoesDispositivo?.permissaoNegada,
criadoEm: Date.now(),
});