Files
sgse-app/packages/backend/convex/configuracaoRelogio.ts

323 lines
9.8 KiB
TypeScript

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<ConfiguracaoRelogioRetorno> => {
// 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<ConfiguracaoRelogioRetorno> => {
// 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<SincronizacaoRetorno> => {
// 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
});
}
});