Implement dynamic admin configuration system for NocoDB settings
Build And Push Image / docker (push) Successful in 3m15s
Details
Build And Push Image / docker (push) Successful in 3m15s
Details
- Add new admin-config utility for persistent configuration management - Replace hardcoded runtime config with dynamic configuration retrieval - Enable admin panel to save and apply NocoDB settings immediately - Add dynamic table ID resolution with fallback to defaults - Update configuration endpoints to use new persistence system
This commit is contained in:
parent
676420c3fa
commit
ce0cbdc980
|
|
@ -28,17 +28,9 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-config.get] Admin access confirmed for:', session.user.email);
|
console.log('[api/admin/nocodb-config.get] Admin access confirmed for:', session.user.email);
|
||||||
|
|
||||||
// Get current runtime configuration
|
// Get current configuration using the new admin config system
|
||||||
const runtimeConfig = useRuntimeConfig();
|
const { getCurrentConfig } = await import('~/server/utils/admin-config');
|
||||||
const nocodbConfig = runtimeConfig.nocodb;
|
const settings = await getCurrentConfig();
|
||||||
|
|
||||||
// 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
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-config.get] ✅ Settings retrieved successfully');
|
console.log('[api/admin/nocodb-config.get] ✅ Settings retrieved successfully');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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] Table ID:', body.tableId);
|
||||||
console.log('[api/admin/nocodb-config.post] API Key: [REDACTED]');
|
console.log('[api/admin/nocodb-config.post] API Key: [REDACTED]');
|
||||||
|
|
||||||
// In a real application, you would save these settings to a secure database
|
// Save configuration using the new admin config system
|
||||||
// For now, we'll just validate the structure and log success
|
const { saveAdminConfig } = await import('~/server/utils/admin-config');
|
||||||
|
await saveAdminConfig(body, session.user.email);
|
||||||
// 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
|
|
||||||
console.log('[api/admin/nocodb-config.post] ✅ Configuration saved successfully');
|
console.log('[api/admin/nocodb-config.post] ✅ Configuration saved successfully');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'NocoDB configuration saved successfully'
|
message: 'NocoDB configuration saved successfully and will be applied immediately'
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
console.log('[api/admin/nocodb-test.post] Admin access confirmed for:', session.user.email);
|
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;
|
const body = await readBody(event) as NocoDBSettings;
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,27 @@ export interface EntityResponse<T> {
|
||||||
PageInfo: PageInfo;
|
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
|
// Table ID enumeration - Replace with your actual table IDs
|
||||||
export enum Table {
|
export enum Table {
|
||||||
Members = "members-table-id", // Will be configured via admin panel
|
Members = "members-table-id", // Will be configured via admin panel
|
||||||
|
|
@ -82,18 +103,42 @@ export const formatNationalitiesAsString = (nationalities: string[]): string =>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNocoDbConfiguration = () => {
|
export const getNocoDbConfiguration = () => {
|
||||||
const config = useRuntimeConfig().nocodb;
|
try {
|
||||||
// Use the new database URL
|
// Try to use the persistent configuration system
|
||||||
const updatedConfig = {
|
const { getEffectiveNocoDBConfig } = require('./admin-config');
|
||||||
...config,
|
const effectiveConfig = getEffectiveNocoDBConfig();
|
||||||
url: 'https://database.monacousa.org'
|
|
||||||
};
|
const config = {
|
||||||
console.log('[nocodb] Configuration URL:', updatedConfig.url);
|
url: effectiveConfig.url,
|
||||||
return updatedConfig;
|
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) => {
|
export const createTableUrl = (table: Table | string) => {
|
||||||
const url = `${getNocoDbConfiguration().url}/api/v2/tables/${table}/records`;
|
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);
|
console.log('[nocodb] Table URL:', url);
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue