mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat: implement server configuration persistence to ~/.hapi/settings.json
Configuration now loads with priority: environment variable > settings.json > default value. When values are read from environment variables and not present in settings.json, they are automatically saved for future use. This eliminates the need to repeatedly set environment variables. - New serverSettings.ts module handles loading/saving with persistence logic - Async createConfiguration() factory for proper initialization ordering - Configuration sources tracked and displayed in startup logs - Exported Settings interface and read/write functions from cliApiToken.ts - Updated index.ts to display configuration sources in log output
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 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 { join } from 'node:path'
|
||||
import { readSettings, writeSettings, type Settings } from './web/cliApiToken'
|
||||
|
||||
export interface ServerSettings {
|
||||
telegramBotToken: string | null
|
||||
allowedChatIds: number[]
|
||||
webappPort: number
|
||||
webappUrl: string
|
||||
corsOrigins: string[]
|
||||
}
|
||||
|
||||
export interface ServerSettingsResult {
|
||||
settings: ServerSettings
|
||||
sources: {
|
||||
telegramBotToken: 'env' | 'file' | 'default'
|
||||
allowedChatIds: 'env' | 'file' | 'default'
|
||||
webappPort: 'env' | 'file' | 'default'
|
||||
webappUrl: 'env' | 'file' | 'default'
|
||||
corsOrigins: 'env' | 'file' | 'default'
|
||||
}
|
||||
savedToFile: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse comma-separated chat IDs from string
|
||||
*/
|
||||
function parseChatIds(str: string): number[] {
|
||||
return str
|
||||
.split(',')
|
||||
.map(id => parseInt(id.trim(), 10))
|
||||
.filter(id => !isNaN(id))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = join(dataDir, 'settings.json')
|
||||
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',
|
||||
allowedChatIds: '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'
|
||||
}
|
||||
|
||||
// allowedChatIds: env > file > []
|
||||
let allowedChatIds: number[] = []
|
||||
if (process.env.ALLOWED_CHAT_IDS) {
|
||||
allowedChatIds = parseChatIds(process.env.ALLOWED_CHAT_IDS)
|
||||
sources.allowedChatIds = 'env'
|
||||
if (settings.allowedChatIds === undefined) {
|
||||
settings.allowedChatIds = allowedChatIds
|
||||
needsSave = true
|
||||
}
|
||||
} else if (settings.allowedChatIds !== undefined) {
|
||||
allowedChatIds = settings.allowedChatIds
|
||||
sources.allowedChatIds = '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,
|
||||
allowedChatIds,
|
||||
webappPort,
|
||||
webappUrl,
|
||||
corsOrigins,
|
||||
},
|
||||
sources,
|
||||
savedToFile: needsSave,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user