mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-10-26 06:19:53 +00:00
9d7a08e7ab
Modernize Commercialization on packages/sessions IA: Settings Time tab with BILLING_TIMEZONE, group-only packages, monthly period history with midnight split, Web Remote report overlay, pending_report in Sessions, and package preview as a centered modal. Includes panel form polish and desktop-client API notes. Refs #456 Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow); muopc (@muopc)
258 lines
8.3 KiB
JavaScript
258 lines
8.3 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const net = require('net');
|
|
const path = require('path');
|
|
const config = require('../config/config');
|
|
const { upsertEnvKey } = require('../lib/envMerge');
|
|
const updateService = require('./updateService');
|
|
const serverConnectionConfigService = require('./serverConnectionConfigService');
|
|
const { apiClient } = require('./betterdeskApi');
|
|
|
|
const CONSOLE_ROOT = path.join(__dirname, '..');
|
|
const ENV_PATH = path.join(CONSOLE_ROOT, '.env');
|
|
|
|
const ENV_KEYS = {
|
|
ntp_servers: 'NTP_SERVERS',
|
|
max_skew_ms: 'BILLING_MAX_CLOCK_SKEW_MS',
|
|
require_synced_clock: 'BILLING_REQUIRE_SYNCED_CLOCK',
|
|
trust_os_ntp: 'BILLING_TRUST_OS_NTP',
|
|
billing_timezone: 'BILLING_TIMEZONE',
|
|
};
|
|
|
|
/** Common IANA zones for the Settings picker (custom values still allowed if valid). */
|
|
const COMMON_TIMEZONES = [
|
|
'UTC',
|
|
'Europe/Warsaw',
|
|
'Europe/Berlin',
|
|
'Europe/London',
|
|
'Europe/Paris',
|
|
'Europe/Amsterdam',
|
|
'Europe/Madrid',
|
|
'Europe/Rome',
|
|
'Europe/Prague',
|
|
'Europe/Stockholm',
|
|
'Europe/Helsinki',
|
|
'Europe/Athens',
|
|
'Europe/Bucharest',
|
|
'Europe/Istanbul',
|
|
'Europe/Kyiv',
|
|
'America/New_York',
|
|
'America/Chicago',
|
|
'America/Denver',
|
|
'America/Los_Angeles',
|
|
'America/Sao_Paulo',
|
|
'Asia/Tokyo',
|
|
'Asia/Shanghai',
|
|
'Asia/Hong_Kong',
|
|
'Asia/Singapore',
|
|
'Asia/Kolkata',
|
|
'Asia/Dubai',
|
|
'Australia/Sydney',
|
|
'Pacific/Auckland',
|
|
];
|
|
|
|
const DEFAULTS = {
|
|
ntp_servers: 'pool.ntp.org,time.google.com,time.cloudflare.com',
|
|
max_skew_ms: 2000,
|
|
require_synced_clock: true,
|
|
trust_os_ntp: process.platform === 'linux',
|
|
billing_timezone: 'UTC',
|
|
};
|
|
|
|
function detectOsTimezone() {
|
|
try {
|
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
if (tz && isValidIanaTimezone(tz)) return tz;
|
|
} catch (_) { /* fall through */ }
|
|
const envTz = String(process.env.TZ || '').trim();
|
|
if (envTz && isValidIanaTimezone(envTz)) return envTz;
|
|
return 'UTC';
|
|
}
|
|
|
|
function isValidIanaTimezone(tz) {
|
|
const value = String(tz || '').trim();
|
|
if (!value || value.length > 64) return false;
|
|
try {
|
|
Intl.DateTimeFormat('en-US', { timeZone: value });
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function parseEnvFile(content) {
|
|
const out = {};
|
|
if (!content) return out;
|
|
for (const line of String(content).split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
const eq = trimmed.indexOf('=');
|
|
if (eq <= 0) continue;
|
|
out[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim();
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function envToBool(value, defaultValue) {
|
|
if (value === undefined || value === null || value === '') return defaultValue;
|
|
const v = String(value).trim().toUpperCase();
|
|
if (['Y', 'YES', '1', 'TRUE', 'ON'].includes(v)) return true;
|
|
if (['N', 'NO', '0', 'FALSE', 'OFF'].includes(v)) return false;
|
|
return defaultValue;
|
|
}
|
|
|
|
function boolToEnv(value) {
|
|
return value ? '1' : '0';
|
|
}
|
|
|
|
function isValidNTPServerEntry(entry) {
|
|
const s = String(entry || '').trim();
|
|
if (!s || s.length > 253) return false;
|
|
if (net.isIP(s)) return true;
|
|
return /^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(s);
|
|
}
|
|
|
|
function normalizeSettings(raw = {}) {
|
|
const servers = String(raw.ntp_servers ?? DEFAULTS.ntp_servers)
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
const maxSkew = Number.parseInt(raw.max_skew_ms, 10);
|
|
let billingTimezone = String(raw.billing_timezone ?? DEFAULTS.billing_timezone).trim();
|
|
if (!billingTimezone) billingTimezone = DEFAULTS.billing_timezone;
|
|
return {
|
|
ntp_servers: servers.join(','),
|
|
max_skew_ms: Number.isFinite(maxSkew) && maxSkew > 0 ? maxSkew : DEFAULTS.max_skew_ms,
|
|
require_synced_clock: raw.require_synced_clock !== undefined
|
|
? !!raw.require_synced_clock
|
|
: DEFAULTS.require_synced_clock,
|
|
trust_os_ntp: raw.trust_os_ntp !== undefined
|
|
? !!raw.trust_os_ntp
|
|
: DEFAULTS.trust_os_ntp,
|
|
billing_timezone: billingTimezone,
|
|
};
|
|
}
|
|
|
|
function validateSettings(settings) {
|
|
const normalized = normalizeSettings(settings);
|
|
const servers = normalized.ntp_servers.split(',').map((s) => s.trim()).filter(Boolean);
|
|
if (!servers.length) {
|
|
throw new Error('invalid_ntp_servers');
|
|
}
|
|
for (const server of servers) {
|
|
if (!isValidNTPServerEntry(server)) {
|
|
throw new Error('invalid_ntp_servers');
|
|
}
|
|
}
|
|
if (normalized.max_skew_ms <= 0 || normalized.max_skew_ms > 600000) {
|
|
throw new Error('invalid_max_skew');
|
|
}
|
|
if (!isValidIanaTimezone(normalized.billing_timezone)) {
|
|
throw new Error('invalid_billing_timezone');
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function settingsFromEnvContent(content) {
|
|
const env = parseEnvFile(content);
|
|
const fromEnv = env[ENV_KEYS.billing_timezone];
|
|
return normalizeSettings({
|
|
ntp_servers: env[ENV_KEYS.ntp_servers] || DEFAULTS.ntp_servers,
|
|
max_skew_ms: env[ENV_KEYS.max_skew_ms] || DEFAULTS.max_skew_ms,
|
|
require_synced_clock: envToBool(env[ENV_KEYS.require_synced_clock], DEFAULTS.require_synced_clock),
|
|
trust_os_ntp: envToBool(env[ENV_KEYS.trust_os_ntp], DEFAULTS.trust_os_ntp),
|
|
billing_timezone: fromEnv || detectOsTimezone(),
|
|
});
|
|
}
|
|
|
|
function getClockSettings() {
|
|
const content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf8') : '';
|
|
return settingsFromEnvContent(content);
|
|
}
|
|
|
|
function isDockerSplitDeployment() {
|
|
return config.isDocker === true
|
|
&& String(process.env.BETTERDESK_DOCKER_LAYOUT || '').trim().toLowerCase() === 'split';
|
|
}
|
|
|
|
async function getRuntimeClockSettings() {
|
|
const local = getClockSettings();
|
|
if (!isDockerSplitDeployment()) return local;
|
|
const response = await apiClient.get('/timesync/config');
|
|
const remote = response.data || {};
|
|
return normalizeSettings({
|
|
...remote,
|
|
// Prefer remote timezone when Go persists it; else keep console .env / OS detect.
|
|
billing_timezone: remote.billing_timezone || local.billing_timezone,
|
|
});
|
|
}
|
|
|
|
function writeClockSettingsToEnv(settings) {
|
|
const normalized = validateSettings(settings);
|
|
let content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf8') : '';
|
|
content = upsertEnvKey(content, ENV_KEYS.ntp_servers, normalized.ntp_servers);
|
|
content = upsertEnvKey(content, ENV_KEYS.max_skew_ms, String(normalized.max_skew_ms));
|
|
content = upsertEnvKey(content, ENV_KEYS.require_synced_clock, boolToEnv(normalized.require_synced_clock));
|
|
content = upsertEnvKey(content, ENV_KEYS.trust_os_ntp, boolToEnv(normalized.trust_os_ntp));
|
|
content = upsertEnvKey(content, ENV_KEYS.billing_timezone, normalized.billing_timezone);
|
|
fs.writeFileSync(ENV_PATH, content, { encoding: 'utf8', mode: 0o600 });
|
|
return normalized;
|
|
}
|
|
|
|
function restoreClockSettings(settings) {
|
|
return writeClockSettingsToEnv(settings);
|
|
}
|
|
|
|
async function saveClockSettings(settings, opts = {}) {
|
|
const normalized = validateSettings(settings);
|
|
if (isDockerSplitDeployment()) {
|
|
const response = await apiClient.put('/timesync/config', normalized);
|
|
const data = response.data || {};
|
|
if (data.error) throw new Error(data.error);
|
|
try {
|
|
writeClockSettingsToEnv(normalized);
|
|
} catch (_) { /* console .env may be read-only in some layouts */ }
|
|
return {
|
|
settings: normalizeSettings(data.config || normalized),
|
|
serviceConfig: null,
|
|
status: data.status || null,
|
|
restart: null,
|
|
dockerMode: true,
|
|
};
|
|
}
|
|
|
|
writeClockSettingsToEnv(normalized);
|
|
const serviceConfig = updateService.sanitizeGoServerServiceConfig();
|
|
const result = {
|
|
settings: normalized,
|
|
serviceConfig,
|
|
restart: null,
|
|
};
|
|
if (opts.restart !== false) {
|
|
result.restart = serverConnectionConfigService.restartServer();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULTS,
|
|
COMMON_TIMEZONES,
|
|
ENV_PATH,
|
|
ENV_KEYS,
|
|
parseEnvFile,
|
|
envToBool,
|
|
boolToEnv,
|
|
isValidNTPServerEntry,
|
|
isValidIanaTimezone,
|
|
detectOsTimezone,
|
|
normalizeSettings,
|
|
validateSettings,
|
|
settingsFromEnvContent,
|
|
getClockSettings,
|
|
getRuntimeClockSettings,
|
|
isDockerSplitDeployment,
|
|
restoreClockSettings,
|
|
saveClockSettings,
|
|
};
|