@qwickapps/server 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/dist/src/core/control-panel.js +5 -5
- package/dist/src/core/control-panel.js.map +1 -1
- package/dist/src/core/gateway.d.ts.map +1 -1
- package/dist/src/core/gateway.js +117 -15
- package/dist/src/core/gateway.js.map +1 -1
- package/dist/src/core/plugin-registry.d.ts +70 -0
- package/dist/src/core/plugin-registry.d.ts.map +1 -1
- package/dist/src/core/plugin-registry.js +94 -0
- package/dist/src/core/plugin-registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js +53 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
- package/dist/src/plugins/api-keys/index.d.ts +1 -1
- package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/index.js.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.js +83 -65
- package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
- package/dist/src/plugins/api-keys/types.d.ts +13 -1
- package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/types.js.map +1 -1
- package/dist/src/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/src/plugins/diagnostics-plugin.js +73 -0
- package/dist/src/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/src/plugins/index.d.ts +1 -1
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedExecutor.d.ts +2 -0
- package/dist/src/plugins/maintenance/SeedExecutor.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedExecutor.js +6 -2
- package/dist/src/plugins/maintenance/SeedExecutor.js.map +1 -1
- package/dist/src/plugins/maintenance/SeedList.d.ts +2 -2
- package/dist/src/plugins/maintenance/SeedList.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedList.js +39 -14
- package/dist/src/plugins/maintenance/SeedList.js.map +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.d.ts +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.js +9 -5
- package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
- package/dist/src/plugins/maintenance/seed-executor.d.ts +6 -4
- package/dist/src/plugins/maintenance/seed-executor.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/seed-executor.js +53 -17
- package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
- package/dist/src/plugins/maintenance-plugin.d.ts +24 -0
- package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
- package/dist/src/plugins/maintenance-plugin.js +222 -34
- package/dist/src/plugins/maintenance-plugin.js.map +1 -1
- package/dist/src/plugins/postgres-plugin.d.ts +12 -0
- package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/src/plugins/postgres-plugin.js +319 -5
- package/dist/src/plugins/postgres-plugin.js.map +1 -1
- package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
- package/dist/ui/src/components/ControlPanelApp.js +4 -3
- package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
- package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +17 -4
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +5 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -2
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +12 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +174 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -3
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +256 -16
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
- package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.js +1 -0
- package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
- package/dist-ui/assets/index-BkGp7ZKd.js +529 -0
- package/dist-ui/assets/index-BkGp7ZKd.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/index.js +3735 -3187
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +11 -0
- package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
- package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/core/control-panel.ts +5 -5
- package/src/core/gateway.ts +135 -15
- package/src/core/plugin-registry.ts +171 -0
- package/src/index.ts +2 -0
- package/src/plugins/api-keys/api-keys-plugin.ts +58 -1
- package/src/plugins/api-keys/index.ts +1 -0
- package/src/plugins/api-keys/stores/postgres-store.ts +90 -67
- package/src/plugins/api-keys/types.ts +14 -1
- package/src/plugins/diagnostics-plugin.ts +77 -0
- package/src/plugins/index.ts +1 -1
- package/src/plugins/maintenance/SeedExecutor.tsx +9 -1
- package/src/plugins/maintenance/SeedList.tsx +85 -38
- package/src/plugins/maintenance/SeedManagementPage.tsx +10 -4
- package/src/plugins/maintenance/seed-executor.ts +56 -17
- package/src/plugins/maintenance-plugin.ts +267 -36
- package/src/plugins/postgres-plugin.ts +410 -5
- package/ui/src/App.tsx +3 -3
- package/ui/src/components/ControlPanelApp.tsx +4 -3
- package/ui/src/dashboard/builtInWidgets.tsx +3 -0
- package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +17 -4
- package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +5 -1
- package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -2
- package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +410 -0
- package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -3
- package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +533 -49
- package/ui/src/dashboard/widgets/index.ts +1 -0
- package/dist-ui/assets/index-0gzisPdy.js +0 -528
- package/dist-ui/assets/index-0gzisPdy.js.map +0 -1
|
@@ -90,6 +90,26 @@ export interface PostgresPluginConfig {
|
|
|
90
90
|
|
|
91
91
|
/** Called on pool errors */
|
|
92
92
|
onError?: (error: Error) => void;
|
|
93
|
+
|
|
94
|
+
// Database initialization options
|
|
95
|
+
|
|
96
|
+
/** Admin user for database operations (e.g., 'postgres') */
|
|
97
|
+
adminUser?: string;
|
|
98
|
+
|
|
99
|
+
/** Admin password for database operations */
|
|
100
|
+
adminPassword?: string;
|
|
101
|
+
|
|
102
|
+
/** Admin database to connect to for operations (default: 'postgres') */
|
|
103
|
+
adminDatabase?: string;
|
|
104
|
+
|
|
105
|
+
/** Database name to create/ensure exists (parsed from url if not provided) */
|
|
106
|
+
databaseName?: string;
|
|
107
|
+
|
|
108
|
+
/** User who should own the database (parsed from url if not provided) */
|
|
109
|
+
databaseOwner?: string;
|
|
110
|
+
|
|
111
|
+
/** Automatically initialize database if connection fails (default: true if admin credentials provided) */
|
|
112
|
+
autoInitialize?: boolean;
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
/**
|
|
@@ -140,6 +160,127 @@ export interface PostgresInstance {
|
|
|
140
160
|
// Global registry of PostgreSQL instances by name
|
|
141
161
|
const instances = new Map<string, PostgresInstance>();
|
|
142
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Parse database connection URL to extract components
|
|
165
|
+
*/
|
|
166
|
+
function parseConnectionUrl(url: string): {
|
|
167
|
+
user: string;
|
|
168
|
+
password: string;
|
|
169
|
+
host: string;
|
|
170
|
+
port: number;
|
|
171
|
+
database: string;
|
|
172
|
+
} {
|
|
173
|
+
const match = url.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
174
|
+
if (!match) {
|
|
175
|
+
throw new Error('Invalid PostgreSQL connection URL format');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
user: match[1],
|
|
179
|
+
password: match[2],
|
|
180
|
+
host: match[3],
|
|
181
|
+
port: parseInt(match[4], 10),
|
|
182
|
+
database: match[5],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Helper to create an admin pool for database operations
|
|
188
|
+
*/
|
|
189
|
+
function createAdminPool(config: {
|
|
190
|
+
adminUser: string;
|
|
191
|
+
adminPassword: string;
|
|
192
|
+
host: string;
|
|
193
|
+
port: number;
|
|
194
|
+
adminDatabase?: string;
|
|
195
|
+
}): pg.Pool {
|
|
196
|
+
return new Pool({
|
|
197
|
+
user: config.adminUser,
|
|
198
|
+
password: config.adminPassword,
|
|
199
|
+
host: config.host,
|
|
200
|
+
port: config.port,
|
|
201
|
+
database: config.adminDatabase || 'postgres',
|
|
202
|
+
max: 1,
|
|
203
|
+
idleTimeoutMillis: 5000,
|
|
204
|
+
connectionTimeoutMillis: 5000,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Ensure database user exists with password
|
|
210
|
+
*/
|
|
211
|
+
async function ensureUserExists(
|
|
212
|
+
adminPool: pg.Pool,
|
|
213
|
+
user: string,
|
|
214
|
+
password: string
|
|
215
|
+
): Promise<void> {
|
|
216
|
+
const result = await adminPool.query(
|
|
217
|
+
`SELECT 1 FROM pg_roles WHERE rolname = $1`,
|
|
218
|
+
[user]
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (result.rows.length === 0) {
|
|
222
|
+
await adminPool.query(
|
|
223
|
+
`CREATE USER ${user} WITH PASSWORD '${password}'`
|
|
224
|
+
);
|
|
225
|
+
} else {
|
|
226
|
+
await adminPool.query(
|
|
227
|
+
`ALTER USER ${user} WITH PASSWORD '${password}'`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Ensure database exists with correct owner
|
|
234
|
+
*/
|
|
235
|
+
async function ensureDatabaseExists(
|
|
236
|
+
adminPool: pg.Pool,
|
|
237
|
+
database: string,
|
|
238
|
+
owner: string
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
const result = await adminPool.query(
|
|
241
|
+
`SELECT 1 FROM pg_database WHERE datname = $1`,
|
|
242
|
+
[database]
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (result.rows.length === 0) {
|
|
246
|
+
await adminPool.query(
|
|
247
|
+
`CREATE DATABASE ${database} OWNER ${owner}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Grant all permissions to user on database
|
|
254
|
+
*/
|
|
255
|
+
async function grantPermissions(
|
|
256
|
+
adminPool: pg.Pool,
|
|
257
|
+
database: string,
|
|
258
|
+
user: string
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
const tempPool = new Pool({
|
|
261
|
+
user: adminPool.options.user as string,
|
|
262
|
+
password: adminPool.options.password as string,
|
|
263
|
+
host: adminPool.options.host as string,
|
|
264
|
+
port: adminPool.options.port as number,
|
|
265
|
+
database,
|
|
266
|
+
max: 1,
|
|
267
|
+
idleTimeoutMillis: 5000,
|
|
268
|
+
connectionTimeoutMillis: 5000,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
await tempPool.query(`
|
|
273
|
+
GRANT ALL ON SCHEMA public TO ${user};
|
|
274
|
+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${user};
|
|
275
|
+
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${user};
|
|
276
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${user};
|
|
277
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${user};
|
|
278
|
+
`);
|
|
279
|
+
} finally {
|
|
280
|
+
await tempPool.end();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
143
284
|
/**
|
|
144
285
|
* Get a PostgreSQL instance by name
|
|
145
286
|
*
|
|
@@ -305,15 +446,279 @@ export function createPostgresPlugin(
|
|
|
305
446
|
const instance = createInstance();
|
|
306
447
|
instances.set(instanceName, instance);
|
|
307
448
|
|
|
308
|
-
//
|
|
449
|
+
// Register maintenance widget FIRST (before connection attempt)
|
|
450
|
+
// This ensures the widget is available even if database connection fails
|
|
451
|
+
registry.addWidget({
|
|
452
|
+
id: `postgres-operations-${instanceName}`,
|
|
453
|
+
title: `Database Operations (${instanceName})`,
|
|
454
|
+
component: 'DatabaseOperationsWidget',
|
|
455
|
+
type: 'maintenance',
|
|
456
|
+
priority: 50,
|
|
457
|
+
showByDefault: true,
|
|
458
|
+
pluginId: pluginId,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Three-phase initialization: connect → auto-repair → error state
|
|
462
|
+
|
|
463
|
+
// PHASE 1: Try to connect with DATABASE_URI
|
|
309
464
|
try {
|
|
310
465
|
await instance.query('SELECT 1');
|
|
311
|
-
logger.
|
|
312
|
-
} catch (
|
|
313
|
-
|
|
314
|
-
|
|
466
|
+
logger.info(`PostgreSQL "${instanceName}" connected successfully`);
|
|
467
|
+
} catch (connectionError) {
|
|
468
|
+
const errorMsg = connectionError instanceof Error ? connectionError.message : String(connectionError);
|
|
469
|
+
logger.warn(`PostgreSQL "${instanceName}" connection failed: ${errorMsg}`);
|
|
470
|
+
|
|
471
|
+
// PHASE 2: Auto-repair if admin credentials provided
|
|
472
|
+
const shouldAutoRepair =
|
|
473
|
+
config.adminUser &&
|
|
474
|
+
config.adminPassword &&
|
|
475
|
+
config.autoInitialize !== false &&
|
|
476
|
+
config.url;
|
|
477
|
+
|
|
478
|
+
if (shouldAutoRepair) {
|
|
479
|
+
logger.info(`Attempting auto-repair for "${instanceName}"...`);
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const connParams = parseConnectionUrl(config.url!);
|
|
483
|
+
const adminPool = createAdminPool({
|
|
484
|
+
adminUser: config.adminUser!,
|
|
485
|
+
adminPassword: config.adminPassword!,
|
|
486
|
+
host: connParams.host,
|
|
487
|
+
port: connParams.port,
|
|
488
|
+
adminDatabase: config.adminDatabase,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
// Ensure user exists
|
|
493
|
+
logger.debug(`Ensuring user "${connParams.user}" exists...`);
|
|
494
|
+
await ensureUserExists(adminPool, connParams.user, connParams.password);
|
|
495
|
+
|
|
496
|
+
// Ensure database exists
|
|
497
|
+
logger.debug(`Ensuring database "${connParams.database}" exists...`);
|
|
498
|
+
await ensureDatabaseExists(adminPool, connParams.database, connParams.user);
|
|
499
|
+
|
|
500
|
+
// Grant permissions
|
|
501
|
+
logger.debug(`Granting permissions to "${connParams.user}" on "${connParams.database}"...`);
|
|
502
|
+
await grantPermissions(adminPool, connParams.database, connParams.user);
|
|
503
|
+
|
|
504
|
+
logger.info(`Auto-repair completed successfully for "${instanceName}"`);
|
|
505
|
+
|
|
506
|
+
// Try connection again after repair
|
|
507
|
+
await instance.query('SELECT 1');
|
|
508
|
+
logger.info(`PostgreSQL "${instanceName}" connected after auto-repair`);
|
|
509
|
+
} finally {
|
|
510
|
+
await adminPool.end();
|
|
511
|
+
}
|
|
512
|
+
} catch (repairError) {
|
|
513
|
+
const repairMsg = repairError instanceof Error ? repairError.message : String(repairError);
|
|
514
|
+
// Log error but don't throw - allow plugin to continue starting
|
|
515
|
+
// This ensures the widget and API routes are available for manual database initialization
|
|
516
|
+
logger.error(
|
|
517
|
+
`PostgreSQL connection failed and auto-repair unsuccessful. ` +
|
|
518
|
+
`Original error: ${errorMsg}. Repair error: ${repairMsg}. ` +
|
|
519
|
+
`Use the maintenance UI to manually initialize the database.`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
// PHASE 3: No auto-repair available, remain in error state
|
|
524
|
+
const missingConfig = [];
|
|
525
|
+
if (!config.adminUser) missingConfig.push('adminUser');
|
|
526
|
+
if (!config.adminPassword) missingConfig.push('adminPassword');
|
|
527
|
+
|
|
528
|
+
const hint = missingConfig.length > 0
|
|
529
|
+
? ` Provide ${missingConfig.join(', ')} in config to enable auto-repair.`
|
|
530
|
+
: ' Set autoInitialize=true to enable auto-repair.';
|
|
531
|
+
|
|
532
|
+
// Log error but don't throw - allow plugin to continue starting
|
|
533
|
+
// This ensures the widget and API routes are available for manual database initialization
|
|
534
|
+
logger.error(
|
|
535
|
+
`PostgreSQL connection failed: ${errorMsg}.${hint} ` +
|
|
536
|
+
`Use the maintenance UI to manually initialize the database.`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
315
539
|
}
|
|
316
540
|
|
|
541
|
+
// Register API routes for database operations
|
|
542
|
+
registry.addRoute({
|
|
543
|
+
method: 'get',
|
|
544
|
+
path: '/status',
|
|
545
|
+
pluginId: pluginId,
|
|
546
|
+
handler: async (req: import('express').Request, res: import('express').Response) => {
|
|
547
|
+
try {
|
|
548
|
+
const requestedInstance = (req.query.instance as string) || 'default';
|
|
549
|
+
const targetInstance = instances.get(requestedInstance);
|
|
550
|
+
|
|
551
|
+
if (!targetInstance) {
|
|
552
|
+
return res.status(404).json({
|
|
553
|
+
status: 'error',
|
|
554
|
+
connected: false,
|
|
555
|
+
errorMessage: `PostgreSQL instance "${requestedInstance}" not found`,
|
|
556
|
+
autoInitializeEnabled: false,
|
|
557
|
+
adminCredentialsProvided: false,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let connParams: ReturnType<typeof parseConnectionUrl> | null = null;
|
|
562
|
+
if (config.url) {
|
|
563
|
+
try {
|
|
564
|
+
connParams = parseConnectionUrl(config.url);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
// URL parsing failed, ignore
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
await targetInstance.query('SELECT 1');
|
|
572
|
+
res.json({
|
|
573
|
+
status: 'healthy',
|
|
574
|
+
connected: true,
|
|
575
|
+
database: connParams?.database,
|
|
576
|
+
user: connParams?.user,
|
|
577
|
+
host: connParams?.host,
|
|
578
|
+
port: connParams?.port,
|
|
579
|
+
autoInitializeEnabled: config.autoInitialize !== false,
|
|
580
|
+
adminCredentialsProvided: !!(config.adminUser && config.adminPassword),
|
|
581
|
+
});
|
|
582
|
+
} catch (err) {
|
|
583
|
+
res.json({
|
|
584
|
+
status: 'error',
|
|
585
|
+
connected: false,
|
|
586
|
+
database: connParams?.database,
|
|
587
|
+
user: connParams?.user,
|
|
588
|
+
host: connParams?.host,
|
|
589
|
+
port: connParams?.port,
|
|
590
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
591
|
+
autoInitializeEnabled: config.autoInitialize !== false,
|
|
592
|
+
adminCredentialsProvided: !!(config.adminUser && config.adminPassword),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
} catch (err) {
|
|
596
|
+
res.status(500).json({
|
|
597
|
+
status: 'error',
|
|
598
|
+
connected: false,
|
|
599
|
+
errorMessage: err instanceof Error ? err.message : 'Unknown error',
|
|
600
|
+
autoInitializeEnabled: false,
|
|
601
|
+
adminCredentialsProvided: false,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
registry.addRoute({
|
|
608
|
+
method: 'post',
|
|
609
|
+
path: '/initialize',
|
|
610
|
+
pluginId: pluginId,
|
|
611
|
+
handler: async (req: import('express').Request, res: import('express').Response) => {
|
|
612
|
+
try {
|
|
613
|
+
const { instance: requestedInstance, adminUser, adminPassword } = req.body;
|
|
614
|
+
const targetInstance = requestedInstance || 'default';
|
|
615
|
+
|
|
616
|
+
if (!instances.has(targetInstance)) {
|
|
617
|
+
return res.status(404).json({ message: `Instance "${targetInstance}" not found` });
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (!config.url) {
|
|
621
|
+
return res.status(400).json({ message: 'No database URL configured' });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const connParams = parseConnectionUrl(config.url);
|
|
625
|
+
const effectiveAdminUser = adminUser || config.adminUser;
|
|
626
|
+
const effectiveAdminPassword = adminPassword || config.adminPassword;
|
|
627
|
+
|
|
628
|
+
if (!effectiveAdminUser || !effectiveAdminPassword) {
|
|
629
|
+
return res.status(400).json({
|
|
630
|
+
message: 'Admin credentials required. Provide adminUser and adminPassword.',
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const adminPool = createAdminPool({
|
|
635
|
+
adminUser: effectiveAdminUser,
|
|
636
|
+
adminPassword: effectiveAdminPassword,
|
|
637
|
+
host: connParams.host,
|
|
638
|
+
port: connParams.port,
|
|
639
|
+
adminDatabase: config.adminDatabase,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
await ensureUserExists(adminPool, connParams.user, connParams.password);
|
|
644
|
+
await ensureDatabaseExists(adminPool, connParams.database, connParams.user);
|
|
645
|
+
await grantPermissions(adminPool, connParams.database, connParams.user);
|
|
646
|
+
|
|
647
|
+
logger.info(`Database "${connParams.database}" initialized successfully`);
|
|
648
|
+
res.json({ message: `Database "${connParams.database}" initialized successfully` });
|
|
649
|
+
} finally {
|
|
650
|
+
await adminPool.end();
|
|
651
|
+
}
|
|
652
|
+
} catch (err) {
|
|
653
|
+
logger.error('Database initialization failed', { error: err });
|
|
654
|
+
res.status(500).json({
|
|
655
|
+
message: err instanceof Error ? err.message : 'Unknown error',
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
registry.addRoute({
|
|
662
|
+
method: 'post',
|
|
663
|
+
path: '/recreate',
|
|
664
|
+
pluginId: pluginId,
|
|
665
|
+
handler: async (req: import('express').Request, res: import('express').Response) => {
|
|
666
|
+
try {
|
|
667
|
+
const { instance: requestedInstance, adminUser, adminPassword } = req.body;
|
|
668
|
+
const targetInstance = requestedInstance || 'default';
|
|
669
|
+
|
|
670
|
+
if (!instances.has(targetInstance)) {
|
|
671
|
+
return res.status(404).json({ message: `Instance "${targetInstance}" not found` });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (!config.url) {
|
|
675
|
+
return res.status(400).json({ message: 'No database URL configured' });
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const connParams = parseConnectionUrl(config.url);
|
|
679
|
+
const effectiveAdminUser = adminUser || config.adminUser;
|
|
680
|
+
const effectiveAdminPassword = adminPassword || config.adminPassword;
|
|
681
|
+
|
|
682
|
+
if (!effectiveAdminUser || !effectiveAdminPassword) {
|
|
683
|
+
return res.status(400).json({
|
|
684
|
+
message: 'Admin credentials required. Provide adminUser and adminPassword.',
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const adminPool = createAdminPool({
|
|
689
|
+
adminUser: effectiveAdminUser,
|
|
690
|
+
adminPassword: effectiveAdminPassword,
|
|
691
|
+
host: connParams.host,
|
|
692
|
+
port: connParams.port,
|
|
693
|
+
adminDatabase: config.adminDatabase,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
// Drop database if exists
|
|
698
|
+
await adminPool.query(`DROP DATABASE IF EXISTS ${connParams.database}`);
|
|
699
|
+
|
|
700
|
+
// Recreate database
|
|
701
|
+
await adminPool.query(
|
|
702
|
+
`CREATE DATABASE ${connParams.database} OWNER ${connParams.user}`
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
// Grant permissions
|
|
706
|
+
await grantPermissions(adminPool, connParams.database, connParams.user);
|
|
707
|
+
|
|
708
|
+
logger.info(`Database "${connParams.database}" recreated successfully`);
|
|
709
|
+
res.json({ message: `Database "${connParams.database}" recreated successfully` });
|
|
710
|
+
} finally {
|
|
711
|
+
await adminPool.end();
|
|
712
|
+
}
|
|
713
|
+
} catch (err) {
|
|
714
|
+
logger.error('Database recreation failed', { error: err });
|
|
715
|
+
res.status(500).json({
|
|
716
|
+
message: err instanceof Error ? err.message : 'Unknown error',
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
|
|
317
722
|
// Register health check if enabled
|
|
318
723
|
if (config.healthCheck !== false) {
|
|
319
724
|
registry.registerHealthCheck({
|
package/ui/src/App.tsx
CHANGED
|
@@ -71,9 +71,9 @@ declare global {
|
|
|
71
71
|
*/
|
|
72
72
|
const basePath = window.__APP_BASE_PATH__ ?? '';
|
|
73
73
|
|
|
74
|
-
// API
|
|
75
|
-
//
|
|
76
|
-
api.setBaseUrl(
|
|
74
|
+
// Set API base URL to match the control panel mount path
|
|
75
|
+
// When proxied through a gateway at /cpanel, API calls need to go to /cpanel/api
|
|
76
|
+
api.setBaseUrl(basePath);
|
|
77
77
|
|
|
78
78
|
// Footer content with QwickApps Server branding
|
|
79
79
|
const footerContent = (
|
|
@@ -147,9 +147,10 @@ export function ControlPanelApp({
|
|
|
147
147
|
// Combine built-in widget components with custom ones
|
|
148
148
|
const allWidgetComponents = [...getBuiltInWidgetComponents(), ...widgetComponents];
|
|
149
149
|
|
|
150
|
-
// Configure API base URL -
|
|
151
|
-
//
|
|
152
|
-
|
|
150
|
+
// Configure API base URL - read from injected __APP_BASE_PATH__ if available
|
|
151
|
+
// Server injects window.__APP_BASE_PATH__ when control panel is mounted at a base path
|
|
152
|
+
// Example: mounted at /cpanel → API calls go to /cpanel/api (not /api)
|
|
153
|
+
const apiBasePath = (window as any).__APP_BASE_PATH__ || '';
|
|
153
154
|
api.setBaseUrl(apiBasePath);
|
|
154
155
|
|
|
155
156
|
// Fetch version from API
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
ServiceControlWidget,
|
|
22
22
|
EnvironmentConfigWidget,
|
|
23
23
|
DatabaseOpsWidget,
|
|
24
|
+
DatabaseOperationsWidget,
|
|
24
25
|
LogsMaintenanceWidget,
|
|
25
26
|
CacheMaintenanceWidget,
|
|
26
27
|
} from './widgets';
|
|
@@ -42,6 +43,7 @@ export const builtInWidgetComponents: Record<string, React.ComponentType> = {
|
|
|
42
43
|
ServiceControlWidget: ServiceControlWidget,
|
|
43
44
|
EnvironmentConfigWidget: EnvironmentConfigWidget,
|
|
44
45
|
DatabaseOpsWidget: DatabaseOpsWidget,
|
|
46
|
+
DatabaseOperationsWidget: DatabaseOperationsWidget,
|
|
45
47
|
LogsMaintenanceWidget: LogsMaintenanceWidget,
|
|
46
48
|
CacheMaintenanceWidget: CacheMaintenanceWidget,
|
|
47
49
|
PreferencesPage: PreferencesPage,
|
|
@@ -66,6 +68,7 @@ export function getBuiltInWidgetComponents(): WidgetComponent[] {
|
|
|
66
68
|
{ name: 'ServiceControlWidget', component: ServiceControlWidget },
|
|
67
69
|
{ name: 'EnvironmentConfigWidget', component: EnvironmentConfigWidget },
|
|
68
70
|
{ name: 'DatabaseOpsWidget', component: DatabaseOpsWidget },
|
|
71
|
+
{ name: 'DatabaseOperationsWidget', component: DatabaseOperationsWidget },
|
|
69
72
|
{ name: 'LogsMaintenanceWidget', component: LogsMaintenanceWidget },
|
|
70
73
|
{ name: 'CacheMaintenanceWidget', component: CacheMaintenanceWidget },
|
|
71
74
|
{ name: 'PreferencesPage', component: PreferencesPage },
|
|
@@ -49,7 +49,11 @@ export function CMSMaintenanceWidget() {
|
|
|
49
49
|
|
|
50
50
|
const fetchStatus = async () => {
|
|
51
51
|
try {
|
|
52
|
-
const
|
|
52
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
53
|
+
const response = await fetch(`${basePath}/api/cms/status`);
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
56
|
+
}
|
|
53
57
|
const data = await response.json();
|
|
54
58
|
setStatus(data);
|
|
55
59
|
} catch (err) {
|
|
@@ -59,7 +63,11 @@ export function CMSMaintenanceWidget() {
|
|
|
59
63
|
|
|
60
64
|
const fetchSeeds = async () => {
|
|
61
65
|
try {
|
|
62
|
-
const
|
|
66
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
67
|
+
const response = await fetch(`${basePath}/api/cms/seeds`);
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
70
|
+
}
|
|
63
71
|
const data = await response.json();
|
|
64
72
|
setSeeds(data.seeds || []);
|
|
65
73
|
} catch (err) {
|
|
@@ -80,7 +88,8 @@ export function CMSMaintenanceWidget() {
|
|
|
80
88
|
setError(null);
|
|
81
89
|
setSuccess(null);
|
|
82
90
|
try {
|
|
83
|
-
const
|
|
91
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
92
|
+
const response = await fetch(`${basePath}/api/cms/restart`, { method: 'POST' });
|
|
84
93
|
const data = await response.json();
|
|
85
94
|
|
|
86
95
|
if (response.ok) {
|
|
@@ -100,9 +109,13 @@ export function CMSMaintenanceWidget() {
|
|
|
100
109
|
setSuccess(null);
|
|
101
110
|
|
|
102
111
|
try {
|
|
103
|
-
const
|
|
112
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
113
|
+
const response = await fetch(`${basePath}/api/cms/seeds/${seedName}/execute`, {
|
|
104
114
|
method: 'POST',
|
|
105
115
|
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
118
|
+
}
|
|
106
119
|
const data = await response.json();
|
|
107
120
|
|
|
108
121
|
if (data.success) {
|
|
@@ -35,7 +35,11 @@ export function CMSStatusWidget() {
|
|
|
35
35
|
|
|
36
36
|
const fetchStatus = async () => {
|
|
37
37
|
try {
|
|
38
|
-
const
|
|
38
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
39
|
+
const response = await fetch(`${basePath}/api/cms/status`);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
42
|
+
}
|
|
39
43
|
const data = await response.json();
|
|
40
44
|
setStatus(data);
|
|
41
45
|
setError(null);
|
|
@@ -47,7 +47,8 @@ export function CacheMaintenanceWidget() {
|
|
|
47
47
|
setLoading(true);
|
|
48
48
|
setError(null);
|
|
49
49
|
try {
|
|
50
|
-
const
|
|
50
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
51
|
+
const response = await fetch(`${basePath}/api/cache:default/stats`);
|
|
51
52
|
if (!response.ok) {
|
|
52
53
|
if (response.status === 404) {
|
|
53
54
|
throw new Error('Cache plugin not configured');
|
|
@@ -75,7 +76,8 @@ export function CacheMaintenanceWidget() {
|
|
|
75
76
|
setSuccess(null);
|
|
76
77
|
|
|
77
78
|
try {
|
|
78
|
-
const
|
|
79
|
+
const basePath = (window as any).__APP_BASE_PATH__ || '';
|
|
80
|
+
const response = await fetch(`${basePath}/api/cache:default/flush`, {
|
|
79
81
|
method: 'POST',
|
|
80
82
|
headers: { 'Content-Type': 'application/json' },
|
|
81
83
|
});
|