Files
hapi/server/src/config/serverSettings.ts
T
2026-01-13 20:32:47 +08:00

203 lines
6.2 KiB
TypeScript

/**
* Server Settings Management
*
* Handles loading and persistence of server configuration.
* Priority: environment variable > settings.json > default value
*
* When a value is loaded from environment variable and not present in settings.json,
* it will be saved to settings.json for future use
*/
import { getSettingsFile, readSettings, writeSettings } from './settings'
export interface ServerSettings {
telegramBotToken: string | null
telegramNotification: boolean
webappHost: string
webappPort: number
webappUrl: string
corsOrigins: string[]
}
export interface ServerSettingsResult {
settings: ServerSettings
sources: {
telegramBotToken: 'env' | 'file' | 'default'
telegramNotification: 'env' | 'file' | 'default'
webappHost: 'env' | 'file' | 'default'
webappPort: 'env' | 'file' | 'default'
webappUrl: 'env' | 'file' | 'default'
corsOrigins: 'env' | 'file' | 'default'
}
savedToFile: boolean
}
/**
* Parse and normalize CORS origins
*/
function parseCorsOrigins(str: string): string[] {
const entries = str
.split(',')
.map(origin => origin.trim())
.filter(Boolean)
if (entries.includes('*')) {
return ['*']
}
const normalized: string[] = []
for (const entry of entries) {
try {
normalized.push(new URL(entry).origin)
} catch {
// Keep raw value if it's already an origin-like string
normalized.push(entry)
}
}
return normalized
}
/**
* Derive CORS origins from webapp URL
*/
function deriveCorsOrigins(webappUrl: string): string[] {
try {
return [new URL(webappUrl).origin]
} catch {
return []
}
}
/**
* Load server settings with priority: env > file > default
* Saves new env values to file when not already present
*/
export async function loadServerSettings(dataDir: string): Promise<ServerSettingsResult> {
const settingsFile = getSettingsFile(dataDir)
const settings = await readSettings(settingsFile)
// If settings file exists but couldn't be parsed, fail fast
if (settings === null) {
throw new Error(
`Cannot read ${settingsFile}. Please fix or remove the file and restart.`
)
}
let needsSave = false
const sources: ServerSettingsResult['sources'] = {
telegramBotToken: 'default',
telegramNotification: 'default',
webappHost: 'default',
webappPort: 'default',
webappUrl: 'default',
corsOrigins: 'default',
}
// telegramBotToken: env > file > null
let telegramBotToken: string | null = null
if (process.env.TELEGRAM_BOT_TOKEN) {
telegramBotToken = process.env.TELEGRAM_BOT_TOKEN
sources.telegramBotToken = 'env'
if (settings.telegramBotToken === undefined) {
settings.telegramBotToken = telegramBotToken
needsSave = true
}
} else if (settings.telegramBotToken !== undefined) {
telegramBotToken = settings.telegramBotToken
sources.telegramBotToken = 'file'
}
// telegramNotification: env > file > true (default enabled for backward compatibility)
let telegramNotification = true
if (process.env.TELEGRAM_NOTIFICATION !== undefined) {
telegramNotification = process.env.TELEGRAM_NOTIFICATION === 'true'
sources.telegramNotification = 'env'
if (settings.telegramNotification === undefined) {
settings.telegramNotification = telegramNotification
needsSave = true
}
} else if (settings.telegramNotification !== undefined) {
telegramNotification = settings.telegramNotification
sources.telegramNotification = 'file'
}
// webappHost: env > file > 127.0.0.1
let webappHost = '127.0.0.1'
if (process.env.WEBAPP_HOST) {
webappHost = process.env.WEBAPP_HOST
sources.webappHost = 'env'
if (settings.webappHost === undefined) {
settings.webappHost = webappHost
needsSave = true
}
} else if (settings.webappHost !== undefined) {
webappHost = settings.webappHost
sources.webappHost = 'file'
}
// webappPort: env > file > 3006
let webappPort = 3006
if (process.env.WEBAPP_PORT) {
const parsed = parseInt(process.env.WEBAPP_PORT, 10)
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error('WEBAPP_PORT must be a valid port number')
}
webappPort = parsed
sources.webappPort = 'env'
if (settings.webappPort === undefined) {
settings.webappPort = webappPort
needsSave = true
}
} else if (settings.webappPort !== undefined) {
webappPort = settings.webappPort
sources.webappPort = 'file'
}
// webappUrl: env > file > http://localhost:{port}
let webappUrl = `http://localhost:${webappPort}`
if (process.env.WEBAPP_URL) {
webappUrl = process.env.WEBAPP_URL
sources.webappUrl = 'env'
if (settings.webappUrl === undefined) {
settings.webappUrl = webappUrl
needsSave = true
}
} else if (settings.webappUrl !== undefined) {
webappUrl = settings.webappUrl
sources.webappUrl = 'file'
}
// corsOrigins: env > file > derived from webappUrl
let corsOrigins: string[]
if (process.env.CORS_ORIGINS) {
corsOrigins = parseCorsOrigins(process.env.CORS_ORIGINS)
sources.corsOrigins = 'env'
if (settings.corsOrigins === undefined) {
settings.corsOrigins = corsOrigins
needsSave = true
}
} else if (settings.corsOrigins !== undefined) {
corsOrigins = settings.corsOrigins
sources.corsOrigins = 'file'
} else {
corsOrigins = deriveCorsOrigins(webappUrl)
}
// Save settings if any new values were added
if (needsSave) {
await writeSettings(settingsFile, settings)
}
return {
settings: {
telegramBotToken,
telegramNotification,
webappHost,
webappPort,
webappUrl,
corsOrigins,
},
sources,
savedToFile: needsSave,
}
}