import * as plugins from '../../plugins.js'; import type { OpsServer } from '../classes.opsserver.js'; import * as interfaces from '../../../ts_interfaces/index.js'; export interface IJwtData { userId: string; status: 'loggedIn' | 'loggedOut'; expiresAt: number; } type TAdminUser = { id: string; username: string; email?: string; name?: string; role: string; status?: 'active' | 'disabled'; authSources?: Array<'local' | 'idp.global'>; }; export class AdminHandler { public typedrouter = new plugins.typedrequest.TypedRouter(); // JWT instance public smartjwtInstance!: plugins.smartjwt.SmartJwt; // Ephemeral bootstrap users. Persisted accounts take over once an active admin exists. private users = new Map(); private accountStore?: plugins.idpSdkServer.SmartdataAccountStore; private idpClient?: plugins.idpSdkServer.IdpGlobalServerClient; private ownsIdpClient = false; constructor(private opsServerRef: OpsServer) { // Add this handler's router to the parent this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); } public async initialize(): Promise { await this.initializeJwt(); this.initializeDefaultUsers(); this.registerHandlers(); } public async stop(): Promise { if (this.ownsIdpClient) { await this.idpClient?.stop(); } this.idpClient = undefined; this.ownsIdpClient = false; } private async initializeJwt(): Promise { this.smartjwtInstance = new plugins.smartjwt.SmartJwt(); await this.smartjwtInstance.init(); // For development, create new keypair each time // In production, load from storage like cloudly does await this.smartjwtInstance.createNewKeyPair(); } private initializeDefaultUsers(): void { const username = process.env.DCROUTER_ADMIN_USERNAME || 'admin'; const configuredPassword = process.env.DCROUTER_ADMIN_PASSWORD; const password = configuredPassword || plugins.crypto.randomBytes(24).toString('base64url'); const adminId = plugins.uuid.v4(); this.users.set(adminId, { id: adminId, username, password, role: 'admin', }); if (!configuredPassword) { console.warn(`DCRouter generated one-time admin password for ${username}: ${password}`); } } /** * Return a safe projection of the active user source — excludes password fields. * Used by UsersHandler to serve the admin-only listUsers endpoint. */ public async listUsers(): Promise { if (await this.hasPersistentAdminAccount()) { const store = this.getAccountStore(); const accounts = await store!.listAccounts(); return accounts.map((accountArg) => this.accountToUser(accountArg)); } return Array.from(this.users.values()).map((user) => ({ id: user.id, username: user.username, role: user.role, })); } public async getBootstrapStatus(): Promise { const dbEnabled = this.opsServerRef.dcRouterRef.options.dbConfig?.enabled !== false; const store = this.getAccountStore(); const dbReady = !!store; const hasPersistentAdmin = dbReady ? await store.hasActiveAdminAccount() : false; return { dbEnabled, dbReady, hasPersistentAdmin, needsBootstrap: dbEnabled && dbReady && !hasPersistentAdmin, ephemeralAdminAvailable: !hasPersistentAdmin, idpGlobalConfigured: this.isIdpGlobalConfigured(), }; } public async createInitialAdminUser(optionsArg: { email: string; name?: string; password: string; enableIdpGlobalAuth?: boolean; }): Promise { const store = this.getAccountStore(); if (!store) { throw new plugins.typedrequest.TypedResponseError('database is not ready'); } if (await store.hasActiveAdminAccount()) { throw new plugins.typedrequest.TypedResponseError('initial admin already exists'); } const password = String(optionsArg.password || ''); if (!password) { throw new plugins.typedrequest.TypedResponseError('password is required'); } const email = String(optionsArg.email || '').trim(); const authSources: Array<'local' | 'idp.global'> = ['local']; if (optionsArg.enableIdpGlobalAuth) { authSources.push('idp.global'); } try { const account = await store.createAccount({ email, name: String(optionsArg.name || '').trim() || email, role: 'admin', authSources, password, }); const user = this.accountToUser(account); return { success: true, identity: await this.createIdentityForUser(user), user, }; } catch (error) { throw new plugins.typedrequest.TypedResponseError((error as Error).message || 'failed to create initial admin'); } } public async createUser(optionsArg: { email: string; name?: string; role: interfaces.requests.TUserManagementRole; password: string; enableIdpGlobalAuth?: boolean; }): Promise { const store = this.getAccountStore(); if (!store) { return { success: false, message: 'database is not ready' }; } if (!(await store.hasActiveAdminAccount())) { return { success: false, message: 'initial admin bootstrap is required before creating users' }; } const role = optionsArg.role; if (role !== 'admin' && role !== 'user') { return { success: false, message: 'role must be admin or user' }; } const password = String(optionsArg.password || ''); if (!password) { return { success: false, message: 'password is required' }; } const authSources: Array<'local' | 'idp.global'> = ['local']; if (optionsArg.enableIdpGlobalAuth) { authSources.push('idp.global'); } try { const email = String(optionsArg.email || '').trim(); const account = await store.createAccount({ email, name: String(optionsArg.name || '').trim() || email, role, authSources, password, }); return { success: true, user: this.accountToUser(account) }; } catch (error) { return { success: false, message: (error as Error).message || 'failed to create user' }; } } public async deleteUser(optionsArg: { id: string; requestingUserId: string; }): Promise { const store = this.getAccountStore(); if (!store) { return { success: false, message: 'database is not ready' }; } if (!(await store.hasActiveAdminAccount())) { return { success: false, message: 'initial admin bootstrap is required before deleting users' }; } const id = String(optionsArg.id || '').trim(); if (!id) { return { success: false, message: 'user id is required' }; } if (id === optionsArg.requestingUserId) { return { success: false, message: 'cannot delete the current user' }; } const account = await store.getAccountById(id); if (!account) { return { success: false, message: 'user not found' }; } if (account.role === 'admin' && account.status === 'active') { const activeAdmins = (await store.listAccounts()).filter( (accountArg) => accountArg.role === 'admin' && accountArg.status === 'active', ); if (activeAdmins.length <= 1) { return { success: false, message: 'cannot delete the last active admin' }; } } const doc = await plugins.idpSdkServer.IdpSdkAccountDoc.findById(id); if (!doc) { return { success: false, message: 'user not found' }; } await doc.delete(); return { success: true }; } private registerHandlers(): void { this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getAdminBootstrapStatus', async (_dataArg) => this.getBootstrapStatus() ) ); this.opsServerRef.adminRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createInitialAdminUser', async (dataArg) => { const isAdmin = await this.adminIdentityGuard.exec({ identity: dataArg.identity }); if (!isAdmin) { throw new plugins.typedrequest.TypedResponseError('admin identity required'); } return this.createInitialAdminUser({ email: dataArg.email, name: dataArg.name, password: dataArg.password, enableIdpGlobalAuth: dataArg.enableIdpGlobalAuth, }); } ) ); // Admin Login Handler this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'adminLoginWithUsernameAndPassword', async (dataArg) => { try { const user = await this.authenticateUser({ username: dataArg.username, password: dataArg.password, authSource: dataArg.authSource, }); if (!user) { throw new plugins.typedrequest.TypedResponseError('login failed'); } return { identity: await this.createIdentityForUser(user), }; } catch (error) { if (error instanceof plugins.typedrequest.TypedResponseError) { throw error; } throw new plugins.typedrequest.TypedResponseError('login failed'); } } ) ); // Admin Logout Handler this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'adminLogout', async (dataArg) => { const identity = await this.validateIdentity(dataArg.identity); if (!identity) { throw new plugins.typedrequest.TypedResponseError('identity is not valid'); } return { success: true, }; } ) ); // Verify Identity Handler this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'verifyIdentity', async (dataArg) => { const identity = await this.validateIdentity(dataArg.identity); return identity ? { valid: true, identity } : { valid: false }; } ) ); } /** * Create a guard for valid identity (matching cloudly pattern) */ public validIdentityGuard = new plugins.smartguard.Guard<{ identity: interfaces.data.IIdentity; }>( async (dataArg) => { return Boolean(await this.validateIdentity(dataArg.identity)); }, { failedHint: 'identity is not valid', name: 'validIdentityGuard', } ); /** * Create a guard for admin identity (matching cloudly pattern) */ public adminIdentityGuard = new plugins.smartguard.Guard<{ identity: interfaces.data.IIdentity; }>( async (dataArg) => { const identity = await this.validateIdentity(dataArg.identity); return identity?.role === 'admin'; }, { failedHint: 'user is not admin', name: 'adminIdentityGuard', } ); public async validateIdentity( identityArg?: interfaces.data.IIdentity, ): Promise { if (!identityArg?.jwt) { return null; } try { const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(identityArg.jwt); if (jwtData.expiresAt < Date.now()) { return null; } if (jwtData.status !== 'loggedIn') { return null; } if (identityArg.expiresAt !== jwtData.expiresAt) { return null; } if (identityArg.userId !== jwtData.userId) { return null; } const user = await this.resolveUser(jwtData.userId); if (!user) { return null; } if (identityArg.role && identityArg.role !== user.role) { return null; } return { jwt: identityArg.jwt, userId: user.id, name: user.name || user.username, expiresAt: jwtData.expiresAt, role: user.role, type: 'user', }; } catch { return null; } } private async authenticateUser(optionsArg: { username: string; password: string; authSource?: interfaces.requests.TAdminLoginAuthSource; }): Promise { if (await this.hasPersistentAdminAccount()) { const store = this.getAccountStore(); const authService = new plugins.idpSdkServer.AccountAuthService({ store: store!, idpClient: this.getIdpClient() as plugins.idpSdkServer.IdpGlobalServerClient | undefined, }); const result = await authService.authenticate({ email: optionsArg.username, password: optionsArg.password, authSource: optionsArg.authSource || 'auto', }); return result ? this.accountToUser(result.account) : null; } for (const [_, userData] of this.users) { if (userData.username === optionsArg.username && userData.password === optionsArg.password) { return userData; } } return null; } private async resolveUser(userIdArg: string): Promise { if (await this.hasPersistentAdminAccount()) { const account = await this.getAccountStore()!.getAccountById(userIdArg); if (!account || account.status !== 'active') { return null; } return this.accountToUser(account); } return this.users.get(userIdArg) || null; } private async hasPersistentAdminAccount(): Promise { const store = this.getAccountStore(); return store ? store.hasActiveAdminAccount() : false; } private getAccountStore(): plugins.idpSdkServer.SmartdataAccountStore | null { if (this.opsServerRef.dcRouterRef.options.dbConfig?.enabled === false) { return null; } const dcRouterDb = this.opsServerRef.dcRouterRef.dcRouterDb; if (!dcRouterDb?.isReady()) { return null; } if (!this.accountStore) { this.accountStore = new plugins.idpSdkServer.SmartdataAccountStore({ smartdataDb: dcRouterDb.getDb(), }); } return this.accountStore; } private getIdpClient(): Pick | undefined { const configuredClient = this.opsServerRef.dcRouterRef.options.adminAuth?.idpClient; if (configuredClient) { return configuredClient; } const baseUrl = this.opsServerRef.dcRouterRef.options.adminAuth?.idpGlobalUrl || process.env.DCROUTER_IDP_GLOBAL_URL; if (!this.idpClient) { this.idpClient = baseUrl ? new plugins.idpSdkServer.IdpGlobalServerClient({ baseUrl }) : new plugins.idpSdkServer.IdpGlobalServerClient({} as plugins.idpSdkServer.IIdpGlobalServerClientOptions); this.ownsIdpClient = true; } return this.idpClient; } private isIdpGlobalConfigured(): boolean { return true; } private accountToUser(accountArg: plugins.idpSdkServer.IIdpSdkAccount): TAdminUser { return { id: accountArg.id, username: accountArg.email, email: accountArg.email, name: accountArg.name, role: accountArg.role, status: accountArg.status, authSources: accountArg.authSources, }; } private async createIdentityForUser(userArg: TAdminUser): Promise { const expiresAtTimestamp = Date.now() + 3600 * 1000 * 24; // 24 hours const jwt = await this.smartjwtInstance.createJWT({ userId: userArg.id, status: 'loggedIn', expiresAt: expiresAtTimestamp, }); return { jwt, userId: userArg.id, name: userArg.name || userArg.username, expiresAt: expiresAtTimestamp, role: userArg.role, type: 'user', }; } }