monacousa-portal/server/utils/admin-config.ts

246 lines
7.3 KiB
TypeScript

import { readFile, writeFile, mkdir, access, constants } from 'fs/promises';
import { join } from 'path';
import { createCipher, createDecipher } from 'crypto';
import type { NocoDBSettings } from '~/utils/types';
interface AdminConfiguration {
nocodb: NocoDBSettings;
lastUpdated: string;
updatedBy: string;
}
interface EffectiveNocoDB {
url: string;
token: string;
baseId: string;
tableId: string;
}
const CONFIG_DIR = '/app/data';
const CONFIG_FILE = join(CONFIG_DIR, 'admin-config.json');
const BACKUP_FILE = join(CONFIG_DIR, 'admin-config.backup.json');
const LOG_FILE = join(CONFIG_DIR, 'admin-config.log');
// In-memory cache for runtime configuration
let configCache: AdminConfiguration | null = null;
/**
* Ensure configuration directory exists
*/
async function ensureConfigDir(): Promise<void> {
try {
await access(CONFIG_DIR, constants.F_OK);
} catch {
await mkdir(CONFIG_DIR, { recursive: true });
console.log(`[admin-config] Created config directory: ${CONFIG_DIR}`);
}
}
/**
* Encrypt sensitive data using the app's encryption key
*/
function encryptSensitiveData(data: string): string {
const runtimeConfig = useRuntimeConfig();
const encryptionKey = runtimeConfig.encryptionKey;
if (!encryptionKey || encryptionKey.length < 16) {
console.warn('[admin-config] No encryption key found or key too short, storing data as-is');
return data;
}
try {
const cipher = createCipher('aes256', encryptionKey);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return encrypted;
} catch (error) {
console.error('[admin-config] Encryption failed:', error);
return data;
}
}
/**
* Decrypt sensitive data using the app's encryption key
*/
function decryptSensitiveData(encryptedData: string): string {
const runtimeConfig = useRuntimeConfig();
const encryptionKey = runtimeConfig.encryptionKey;
if (!encryptionKey || encryptionKey.length < 16) {
return encryptedData;
}
try {
const decipher = createDecipher('aes256', encryptionKey);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
console.error('[admin-config] Decryption failed:', error);
return encryptedData;
}
}
/**
* Log configuration changes for audit trail
*/
async function logConfigChange(action: string, user: string, details?: any): Promise<void> {
const logEntry = {
timestamp: new Date().toISOString(),
action,
user,
details: details || {}
};
try {
await ensureConfigDir();
const logLine = JSON.stringify(logEntry) + '\n';
await writeFile(LOG_FILE, logLine, { flag: 'a' });
} catch (error) {
console.error('[admin-config] Failed to write log:', error);
}
}
/**
* Create backup of current configuration before changes
*/
async function createBackup(): Promise<void> {
try {
await access(CONFIG_FILE, constants.F_OK);
const currentConfig = await readFile(CONFIG_FILE, 'utf-8');
await writeFile(BACKUP_FILE, currentConfig, 'utf-8');
console.log('[admin-config] Configuration backup created');
} catch (error) {
// No existing config to backup
console.log('[admin-config] No existing config to backup');
}
}
/**
* Load configuration from file
*/
export async function loadAdminConfig(): Promise<AdminConfiguration | null> {
try {
await ensureConfigDir();
const configData = await readFile(CONFIG_FILE, 'utf-8');
const config: AdminConfiguration = JSON.parse(configData);
// Decrypt sensitive data
if (config.nocodb.apiKey) {
config.nocodb.apiKey = decryptSensitiveData(config.nocodb.apiKey);
}
console.log('[admin-config] Configuration loaded from file');
configCache = config;
return config;
} catch (error) {
console.log('[admin-config] No configuration file found, using defaults');
configCache = null;
return null;
}
}
/**
* Save configuration to file
*/
export async function saveAdminConfig(config: NocoDBSettings, updatedBy: string): Promise<void> {
try {
await ensureConfigDir();
await createBackup();
// Prepare configuration with encrypted sensitive data
const adminConfig: AdminConfiguration = {
nocodb: {
...config,
apiKey: config.apiKey ? encryptSensitiveData(config.apiKey) : ''
},
lastUpdated: new Date().toISOString(),
updatedBy
};
const configJson = JSON.stringify(adminConfig, null, 2);
await writeFile(CONFIG_FILE, configJson, 'utf-8');
// Update cache with unencrypted data for runtime use
configCache = {
...adminConfig,
nocodb: { ...config } // Keep original unencrypted data in cache
};
console.log('[admin-config] Configuration saved to file');
await logConfigChange('CONFIG_SAVED', updatedBy, { url: config.url, baseId: config.baseId });
} catch (error) {
console.error('[admin-config] Failed to save configuration:', error);
await logConfigChange('CONFIG_SAVE_FAILED', updatedBy, { error: error instanceof Error ? error.message : String(error) });
throw error;
}
}
/**
* Get effective NocoDB configuration (file config merged with env defaults)
*/
export function getEffectiveNocoDBConfig(): EffectiveNocoDB {
const runtimeConfig = useRuntimeConfig();
// Start with environment variables as defaults
const envConfig = {
url: runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
token: runtimeConfig.nocodb?.token || '',
baseId: runtimeConfig.nocodb?.baseId || '',
tableId: 'members-table-id' // This would be configured
};
// Override with file configuration if available
if (configCache?.nocodb) {
return {
url: configCache.nocodb.url || envConfig.url,
token: configCache.nocodb.apiKey || envConfig.token,
baseId: configCache.nocodb.baseId || envConfig.baseId,
tableId: configCache.nocodb.tableId || envConfig.tableId
};
}
return envConfig;
}
/**
* Get current configuration for display (with masked sensitive data)
*/
export async function getCurrentConfig(): Promise<NocoDBSettings> {
const config = configCache || await loadAdminConfig();
const runtimeConfig = useRuntimeConfig();
if (config?.nocodb) {
return {
url: config.nocodb.url || runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
apiKey: config.nocodb.apiKey ? '••••••••••••••••' : '',
baseId: config.nocodb.baseId || runtimeConfig.nocodb?.baseId || '',
tableId: config.nocodb.tableId || 'members-table-id'
};
}
// Return defaults from environment if no file config
return {
url: runtimeConfig.nocodb?.url || 'https://database.monacousa.org',
apiKey: runtimeConfig.nocodb?.token ? '••••••••••••••••' : '',
baseId: runtimeConfig.nocodb?.baseId || '',
tableId: 'members-table-id'
};
}
/**
* Initialize configuration system on server startup
*/
export async function initAdminConfig(): Promise<void> {
console.log('[admin-config] Initializing admin configuration system...');
try {
await ensureConfigDir();
await loadAdminConfig();
console.log('[admin-config] ✅ Admin configuration system initialized');
} catch (error) {
console.error('[admin-config] ❌ Failed to initialize admin configuration:', error);
}
}