import { v } from 'convex/values'; import { action, internalMutation, internalQuery, mutation, query } from './_generated/server'; import { getCurrentUserFunction } from './auth'; import type { Id } from './_generated/dataModel'; import { api, internal } from './_generated/api'; /** * Obtém a configuração do relógio */ export const obterConfiguracao = query({ args: {}, handler: async (ctx) => { // Buscar todas as configurações e pegar a mais recente (por atualizadoEm) const configs = await ctx.db .query('configuracaoRelogio') .collect(); // Pegar a configuração mais recente (ordenar por atualizadoEm desc) const config = configs.length > 0 ? configs.sort((a, b) => (b.atualizadoEm || 0) - (a.atualizadoEm || 0))[0] : null; if (!config) { // Retornar configuração padrão (GMT-3 para Brasília) return { servidorNTP: 'pool.ntp.org', portaNTP: 123, usarServidorExterno: false, fallbackParaPC: true, ultimaSincronizacao: null, offsetSegundos: null, gmtOffset: -3, // GMT-3 para Brasília }; } return { ...config, gmtOffset: config.gmtOffset ?? -3, // Padrão GMT-3 para Brasília se não configurado }; }, }); /** * Salva configuração do relógio (apenas TI) */ export const salvarConfiguracao = mutation({ args: { servidorNTP: v.optional(v.string()), portaNTP: v.optional(v.number()), usarServidorExterno: v.boolean(), fallbackParaPC: v.boolean(), gmtOffset: v.optional(v.number()), }, handler: async (ctx, args) => { const usuario = await getCurrentUserFunction(ctx); if (!usuario) { throw new Error('Usuário não autenticado'); } // TODO: Verificar se usuário tem permissão de TI // Validar servidor NTP se usar servidor externo if (args.usarServidorExterno) { if (!args.servidorNTP || args.servidorNTP.trim() === '') { throw new Error('Servidor NTP é obrigatório quando usar servidor externo'); } if (!args.portaNTP || args.portaNTP < 1 || args.portaNTP > 65535) { throw new Error('Porta NTP deve estar entre 1 e 65535'); } } // Buscar configuração existente (pegar a mais recente) const configs = await ctx.db .query('configuracaoRelogio') .collect(); const configExistente = configs.length > 0 ? configs.sort((a, b) => (b.atualizadoEm || 0) - (a.atualizadoEm || 0))[0] : null; if (configExistente) { // Atualizar configuração existente await ctx.db.patch(configExistente._id, { servidorNTP: args.servidorNTP, portaNTP: args.portaNTP, usarServidorExterno: args.usarServidorExterno, fallbackParaPC: args.fallbackParaPC, gmtOffset: args.gmtOffset ?? -3, // Padrão GMT-3 para Brasília atualizadoPor: usuario._id as Id<'usuarios'>, atualizadoEm: Date.now(), }); return { configId: configExistente._id }; } else { // Criar nova configuração const configId = await ctx.db.insert('configuracaoRelogio', { servidorNTP: args.servidorNTP, portaNTP: args.portaNTP, usarServidorExterno: args.usarServidorExterno, fallbackParaPC: args.fallbackParaPC, gmtOffset: args.gmtOffset ?? -3, // Padrão GMT-3 para Brasília atualizadoPor: usuario._id as Id<'usuarios'>, atualizadoEm: Date.now(), }); return { configId }; } }, }); /** * Obtém tempo do servidor (timestamp atual) */ export const obterTempoServidor = query({ args: {}, handler: async () => { return { timestamp: Date.now(), data: new Date().toISOString(), }; }, }); /** * Sincroniza tempo com servidor NTP (via action) * Nota: NTP real requer biblioteca específica, aqui fazemos uma aproximação */ export const sincronizarTempo = action({ args: {}, handler: async (ctx) => { // Buscar configuração diretamente do banco usando query pública const config = await ctx.runQuery(api.configuracaoRelogio.obterConfiguracao, {}); if (!config.usarServidorExterno) { return { sucesso: true, timestamp: Date.now(), usandoServidorExterno: false, offsetSegundos: 0, }; } // Tentar obter tempo de um servidor NTP público via HTTP // Nota: NTP real requer protocolo UDP na porta 123, aqui usamos APIs HTTP que retornam UTC // O GMT offset será aplicado no frontend try { const servidorNTP = config.servidorNTP || 'pool.ntp.org'; let serverTime: number; // Mapear servidores NTP conhecidos para APIs HTTP que retornam UTC // Todos os servidores NTP retornam UTC, então usamos APIs que retornam UTC if (servidorNTP.includes('pool.ntp.org') || servidorNTP.includes('ntp.org') || servidorNTP.includes('ntp.br')) { // pool.ntp.org e servidores .org/.br - usar API que retorna UTC const response = await fetch('https://worldtimeapi.org/api/timezone/Etc/UTC'); if (!response.ok) { throw new Error('Falha ao obter tempo do servidor'); } const data = (await response.json()) as { unixtime: number; datetime: string }; // unixtime está em segundos, converter para milissegundos serverTime = data.unixtime * 1000; } else if (servidorNTP.includes('time.google.com') || servidorNTP.includes('google')) { // Google NTP - usar API que retorna UTC try { const response = await fetch('https://worldtimeapi.org/api/timezone/Etc/UTC'); if (!response.ok) { throw new Error('Falha ao obter tempo'); } const data = (await response.json()) as { unixtime: number }; serverTime = data.unixtime * 1000; } catch { // Fallback para outra API UTC const response = await fetch('https://timeapi.io/api/Time/current/zone?timeZone=UTC'); if (!response.ok) { throw new Error('Falha ao obter tempo do servidor'); } const data = (await response.json()) as { unixTime: number }; serverTime = data.unixTime * 1000; } } else if (servidorNTP.includes('time.windows.com') || servidorNTP.includes('windows')) { // Windows NTP - usar API que retorna UTC const response = await fetch('https://worldtimeapi.org/api/timezone/Etc/UTC'); if (!response.ok) { throw new Error('Falha ao obter tempo do servidor'); } const data = (await response.json()) as { unixtime: number }; serverTime = data.unixtime * 1000; } else { // Para outros servidores NTP, usar API genérica que retorna UTC // Tentar worldtimeapi primeiro try { const response = await fetch('https://worldtimeapi.org/api/timezone/Etc/UTC'); if (!response.ok) { throw new Error('Falha ao obter tempo'); } const data = (await response.json()) as { unixtime: number }; serverTime = data.unixtime * 1000; } catch { // Fallback para timeapi.io try { const response = await fetch('https://timeapi.io/api/Time/current/zone?timeZone=UTC'); if (!response.ok) { throw new Error('Falha ao obter tempo'); } const data = (await response.json()) as { unixTime: number }; serverTime = data.unixTime * 1000; } catch { // Último fallback: usar tempo do servidor Convex (já está em UTC) serverTime = Date.now(); } } } const localTime = Date.now(); const offsetSegundos = Math.floor((serverTime - localTime) / 1000); // Atualizar configuração com offset // Buscar configuração diretamente usando query interna const configs = await ctx.runQuery(internal.configuracaoRelogio.listarConfiguracoes, {}); const configExistente = configs.find( (c: { usarServidorExterno: boolean; _id: Id<'configuracaoRelogio'> }) => c.usarServidorExterno === config.usarServidorExterno ); if (configExistente) { await ctx.runMutation(internal.configuracaoRelogio.atualizarSincronizacao, { configId: configExistente._id, offsetSegundos, }); } return { sucesso: true, timestamp: serverTime, // Retorna UTC (sem GMT offset aplicado) usandoServidorExterno: true, offsetSegundos, }; } catch (error) { // Sempre usar fallback como última opção, mesmo se desabilitado // Isso evita que o sistema trave completamente se o servidor externo não estiver disponível const aviso = config.fallbackParaPC ? 'Falha ao sincronizar com servidor externo, usando relógio do PC' : 'Falha ao sincronizar com servidor externo. Fallback desabilitado, mas usando relógio do PC como última opção.'; console.warn('Erro ao sincronizar tempo com servidor externo:', error); return { sucesso: true, timestamp: Date.now(), usandoServidorExterno: false, offsetSegundos: 0, aviso, }; } }, }); /** * Lista configurações (internal) */ export const listarConfiguracoes = internalQuery({ args: {}, handler: async (ctx) => { return await ctx.db.query('configuracaoRelogio').collect(); }, }); /** * Atualiza informações de sincronização (internal) */ export const atualizarSincronizacao = internalMutation({ args: { configId: v.id('configuracaoRelogio'), offsetSegundos: v.number(), }, handler: async (ctx, args) => { await ctx.db.patch(args.configId, { ultimaSincronizacao: Date.now(), offsetSegundos: args.offsetSegundos, }); }, });