diff --git a/server/api/admin/nocodb-config.get.ts b/server/api/admin/nocodb-config.get.ts index fb4dd13..fe06c2a 100644 --- a/server/api/admin/nocodb-config.get.ts +++ b/server/api/admin/nocodb-config.get.ts @@ -28,17 +28,9 @@ export default defineEventHandler(async (event) => { console.log('[api/admin/nocodb-config.get] Admin access confirmed for:', session.user.email); - // Get current runtime configuration - const runtimeConfig = useRuntimeConfig(); - const nocodbConfig = runtimeConfig.nocodb; - - // For security, we don't return the actual API key, just indicate if it's set - const settings: NocoDBSettings = { - url: nocodbConfig.url || 'https://database.monacousa.org', - apiKey: nocodbConfig.token ? '••••••••••••••••' : '', // Masked for security - baseId: nocodbConfig.baseId || '', - tableId: 'members-table-id' // This would come from database in real implementation - }; + // Get current configuration using the new admin config system + const { getCurrentConfig } = await import('~/server/utils/admin-config'); + const settings = await getCurrentConfig(); console.log('[api/admin/nocodb-config.get] ✅ Settings retrieved successfully'); diff --git a/server/api/admin/nocodb-config.post.ts b/server/api/admin/nocodb-config.post.ts index 4e86ca9..402b808 100644 --- a/server/api/admin/nocodb-config.post.ts +++ b/server/api/admin/nocodb-config.post.ts @@ -53,21 +53,15 @@ export default defineEventHandler(async (event) => { console.log('[api/admin/nocodb-config.post] Table ID:', body.tableId); console.log('[api/admin/nocodb-config.post] API Key: [REDACTED]'); - // In a real application, you would save these settings to a secure database - // For now, we'll just validate the structure and log success - - // TODO: Implement actual persistence (database or secure config store) - // This could be saved to: - // 1. A separate admin_settings table in the database - // 2. Environment variable overrides - // 3. A secure configuration service - - // For demonstration, we'll simulate success + // Save configuration using the new admin config system + const { saveAdminConfig } = await import('~/server/utils/admin-config'); + await saveAdminConfig(body, session.user.email); + console.log('[api/admin/nocodb-config.post] ✅ Configuration saved successfully'); return { success: true, - message: 'NocoDB configuration saved successfully' + message: 'NocoDB configuration saved successfully and will be applied immediately' }; } catch (error: any) { diff --git a/server/api/admin/nocodb-test.post.ts b/server/api/admin/nocodb-test.post.ts index aa5fb81..d192b45 100644 --- a/server/api/admin/nocodb-test.post.ts +++ b/server/api/admin/nocodb-test.post.ts @@ -28,7 +28,7 @@ export default defineEventHandler(async (event) => { console.log('[api/admin/nocodb-test.post] Admin access confirmed for:', session.user.email); - // Get request body + // Get request body - this contains the settings to test const body = await readBody(event) as NocoDBSettings; // Validate required fields diff --git a/server/plugins/admin-config-init.ts b/server/plugins/admin-config-init.ts new file mode 100644 index 0000000..1f9a6b3 --- /dev/null +++ b/server/plugins/admin-config-init.ts @@ -0,0 +1,13 @@ +import { initAdminConfig } from '~/server/utils/admin-config'; + +export default defineNitroPlugin(async (nitroApp) => { + console.log('[server/plugins/admin-config-init] Initializing admin configuration system...'); + + try { + await initAdminConfig(); + console.log('[server/plugins/admin-config-init] ✅ Admin configuration system initialized successfully'); + } catch (error) { + console.error('[server/plugins/admin-config-init] ❌ Failed to initialize admin configuration system:', error); + // Don't throw error to prevent server startup failure + } +}); diff --git a/server/utils/admin-config.ts b/server/utils/admin-config.ts new file mode 100644 index 0000000..3ac68ff --- /dev/null +++ b/server/utils/admin-config.ts @@ -0,0 +1,245 @@ +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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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); + } +} diff --git a/server/utils/nocodb.ts b/server/utils/nocodb.ts index cd1eb39..fc3036c 100644 --- a/server/utils/nocodb.ts +++ b/server/utils/nocodb.ts @@ -26,6 +26,27 @@ export interface EntityResponse { PageInfo: PageInfo; } +// Dynamic table ID getter - will use configured table ID from admin panel +export const getTableId = (tableName: 'Members'): string => { + try { + // Try to get table ID from persistent configuration + const { getEffectiveNocoDBConfig } = require('./admin-config'); + const effectiveConfig = getEffectiveNocoDBConfig(); + + if (tableName === 'Members' && effectiveConfig.tableId) { + console.log(`[nocodb] Using configured table ID for ${tableName}:`, effectiveConfig.tableId); + return effectiveConfig.tableId; + } + } catch (error) { + console.log(`[nocodb] Admin config not available for table ID, using fallback`); + } + + // Fallback to default + const defaultTableId = 'members-table-id'; + console.log(`[nocodb] Using fallback table ID for ${tableName}:`, defaultTableId); + return defaultTableId; +}; + // Table ID enumeration - Replace with your actual table IDs export enum Table { Members = "members-table-id", // Will be configured via admin panel @@ -82,18 +103,42 @@ export const formatNationalitiesAsString = (nationalities: string[]): string => }; export const getNocoDbConfiguration = () => { - const config = useRuntimeConfig().nocodb; - // Use the new database URL - const updatedConfig = { - ...config, - url: 'https://database.monacousa.org' - }; - console.log('[nocodb] Configuration URL:', updatedConfig.url); - return updatedConfig; + try { + // Try to use the persistent configuration system + const { getEffectiveNocoDBConfig } = require('./admin-config'); + const effectiveConfig = getEffectiveNocoDBConfig(); + + const config = { + url: effectiveConfig.url, + token: effectiveConfig.token, + baseId: effectiveConfig.baseId + }; + + console.log('[nocodb] Using effective configuration - URL:', config.url); + return config; + } catch (error) { + // Fallback to runtime config if admin config is not available + console.log('[nocodb] Admin config not available, using runtime config'); + const config = useRuntimeConfig().nocodb; + const fallbackConfig = { + ...config, + url: config.url || 'https://database.monacousa.org' + }; + console.log('[nocodb] Fallback configuration URL:', fallbackConfig.url); + return fallbackConfig; + } }; -export const createTableUrl = (table: Table) => { - const url = `${getNocoDbConfiguration().url}/api/v2/tables/${table}/records`; +export const createTableUrl = (table: Table | string) => { + let tableId: string; + + if (table === Table.Members || table === 'Members') { + tableId = getTableId('Members'); + } else { + tableId = table.toString(); + } + + const url = `${getNocoDbConfiguration().url}/api/v2/tables/${tableId}/records`; console.log('[nocodb] Table URL:', url); return url; };