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'; /** * Tipo de retorno da configuração do relógio */ type ConfiguracaoRelogioRetorno = { servidorNTP?: string | undefined; portaNTP?: number | undefined; usarServidorExterno: boolean; fallbackParaPC: boolean; ultimaSincronizacao: number | null; offsetSegundos: number | null; gmtOffset: number; }; /** * Obtém a configuração do relógio */ export const obterConfiguracao = query({ args: {}, handler: async (ctx): Promise => { // 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, ultimaSincronizacao: config.ultimaSincronizacao ?? null, // Converter undefined para null offsetSegundos: config.offsetSegundos ?? null, // Converter undefined para null gmtOffset: config.gmtOffset ?? -3 // Padrão GMT-3 para Brasília se não configurado }; } }); /** * Obtém a configuração do relógio (internal) - usado por actions para evitar referência circular */ export const obterConfiguracaoInternal = internalQuery({ args: {}, handler: async (ctx): Promise => { // 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, ultimaSincronizacao: config.ultimaSincronizacao ?? null, // Converter undefined para null offsetSegundos: config.offsetSegundos ?? null, // Converter undefined para null 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() }; } }); /** * Tipo de retorno da sincronização */ type SincronizacaoRetorno = { sucesso: boolean; timestamp: number; usandoServidorExterno: boolean; offsetSegundos: number; aviso?: string; }; /** * 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): Promise => { // Buscar configuração usando query interna para evitar referência circular const config: ConfiguracaoRelogioRetorno = await ctx.runQuery( internal.configuracaoRelogio.obterConfiguracaoInternal, {} ); 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; // Se o servidor configurado for uma URL HTTP/HTTPS, tentar usar diretamente if (servidorNTP.startsWith('http://') || servidorNTP.startsWith('https://')) { try { const response = await fetch(servidorNTP); if (!response.ok) { throw new Error('Falha ao obter tempo do servidor configurado'); } const data = (await response.json()) as { unixtime?: number; unixTime?: number; unixtimestamp?: number; }; // Tentar diferentes formatos de resposta if (data.unixtime) { serverTime = data.unixtime * 1000; // Converter segundos para milissegundos } else if (data.unixTime) { serverTime = data.unixTime * 1000; } else if (data.unixtimestamp) { serverTime = data.unixtimestamp * 1000; } else { throw new Error('Formato de resposta não reconhecido'); } } catch (error) { // Se falhar, tentar APIs genéricas como fallback throw new Error(`Falha ao usar servidor configurado: ${error}`); } } else { // Para servidores NTP tradicionais (sem HTTP), usar APIs genéricas que retornam UTC // Não usar worldtimeapi.org hardcoded - usar timeapi.io como primeira opção 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 { // Fallback: usar tempo do servidor Convex (já está em UTC) // Não usar worldtimeapi.org como fallback automático 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: string = 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 }); } });