From 333cbeb2219669c549e53bf7bceed3c756a48e61 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Tue, 28 Apr 2026 15:07:08 +0000 Subject: [PATCH] fix: make startup bootstrap production-safe --- test/helpers/cloudlyfactory.ts | 1 + test/test.apiclient.ts | 4 +-- ts/index.ts | 9 ++++-- ts/manager.auth/classes.authmanager.ts | 38 ++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/test/helpers/cloudlyfactory.ts b/test/helpers/cloudlyfactory.ts index bf1f704..c1e7439 100644 --- a/test/helpers/cloudlyfactory.ts +++ b/test/helpers/cloudlyfactory.ts @@ -41,6 +41,7 @@ export const testCloudlyConfig: cloudly.ICloudlyConfig = { bucketName: 'cloudly_test_bucket' }), sslMode: 'none', + servezoneAdminaccount: 'testadmin:testpassword', ...(() => { if (process.env.NPMCI_SECRET01) { return { diff --git a/test/test.apiclient.ts b/test/test.apiclient.ts index ff57ed5..dd05797 100644 --- a/test/test.apiclient.ts +++ b/test/test.apiclient.ts @@ -56,8 +56,8 @@ tap.test('DEBUG: Check existing users', async () => { console.log(` - User: ${user.data.username} (ID: ${user.id})`); console.log(` - Type: ${user.data.type}`); console.log(` - Role: ${user.data.role}`); - console.log(` - Tokens: ${user.data.tokens.length}`); - for (const token of user.data.tokens) { + console.log(` - Tokens: ${user.data.tokens?.length ?? 0}`); + for (const token of user.data.tokens ?? []) { console.log(` - Token: '${token.token}' | Roles: ${token.assignedRoles?.join(', ')}`); } } diff --git a/ts/index.ts b/ts/index.ts index 15d802c..c05a315 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -11,7 +11,7 @@ early.stop(); * starts the cloudly instance */ const runCli = async () => { - logger.log('info', process.env.SERVEZONE_ENVIRONMENT); + logger.log('info', process.env.SERVEZONE_ENVIRONMENT || ''); const cloudlyInstance = new Cloudly(); logger.log( @@ -20,8 +20,11 @@ const runCli = async () => { ); await cloudlyInstance.start(); - const demoMod = await import('./00demo/index.js'); - demoMod.installDemoData(cloudlyInstance); + if (process.env.SERVEZONE_INSTALL_DEMO_DATA === 'true') { + logger.log('warn', 'SERVEZONE_INSTALL_DEMO_DATA=true: installing destructive demo data'); + const demoMod = await import('./00demo/index.js'); + await demoMod.installDemoData(cloudlyInstance); + } }; export { runCli, Cloudly }; diff --git a/ts/manager.auth/classes.authmanager.ts b/ts/manager.auth/classes.authmanager.ts index 911e8a6..00097a2 100644 --- a/ts/manager.auth/classes.authmanager.ts +++ b/ts/manager.auth/classes.authmanager.ts @@ -49,6 +49,8 @@ export class CloudlyAuthManager { this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys); } + await this.bootstrapInitialAdmin(); + this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'adminLoginWithUsernameAndPassword', @@ -82,6 +84,42 @@ export class CloudlyAuthManager { ); } + private async bootstrapInitialAdmin() { + const users = await this.CUser.getInstances({}); + const hasHumanUser = users.some((userArg) => userArg.data?.type === 'human'); + if (hasHumanUser) { + return; + } + + const adminAccount = this.cloudlyRef.config.data.servezoneAdminaccount; + if (!adminAccount) { + throw new Error('SERVEZONE_ADMINACCOUNT is required for first-run Cloudly bootstrap'); + } + + const separatorIndex = adminAccount.indexOf(':'); + if (separatorIndex <= 0 || separatorIndex === adminAccount.length - 1) { + throw new Error('SERVEZONE_ADMINACCOUNT must use username:password format'); + } + + const username = adminAccount.slice(0, separatorIndex).trim(); + const password = adminAccount.slice(separatorIndex + 1); + if (!username || !password) { + throw new Error('SERVEZONE_ADMINACCOUNT must include a non-empty username and password'); + } + + const user = new this.CUser({ + id: await this.CUser.getNewId(), + data: { + type: 'human', + username, + password, + role: 'admin', + }, + }); + await user.save(); + logger.log('success', `created initial admin user ${username}`); + } + public async stop() {} public validIdentityGuard = new plugins.smartguard.Guard<{