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'); } } 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) => 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) => { // In a real implementation, you might want to blacklist the JWT // For now, just return success return { success: true, }; } ) ); // Verify Identity Handler this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'verifyIdentity', async (dataArg) => { if (!dataArg.identity?.jwt) { return { valid: false, }; } try { const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt); // Check if expired if (jwtData.expiresAt < Date.now()) { return { valid: false, }; } // Check if logged in if (jwtData.status !== 'loggedIn') { return { valid: false, }; } const user = await this.resolveUser(jwtData.userId); if (!user) { return { valid: false, }; } return { valid: true, identity: { jwt: dataArg.identity.jwt, userId: user.id, name: user.name || user.username, expiresAt: jwtData.expiresAt, role: user.role, type: 'user', }, }; } catch (error) { return { valid: false, }; } } ) ); } /** * Create a guard for valid identity (matching cloudly pattern) */ public validIdentityGuard = new plugins.smartguard.Guard<{ identity: interfaces.data.IIdentity; }>( async (dataArg) => { if (!dataArg.identity?.jwt) { return false; } try { const jwtData = await this.smartjwtInstance.verifyJWTAndGetData(dataArg.identity.jwt); // Check expiration if (jwtData.expiresAt < Date.now()) { return false; } // Check status if (jwtData.status !== 'loggedIn') { return false; } // Verify data hasn't been tampered with if (dataArg.identity.expiresAt !== jwtData.expiresAt) { return false; } if (dataArg.identity.userId !== jwtData.userId) { return false; } const user = await this.resolveUser(jwtData.userId); if (!user) { return false; } if (dataArg.identity.role && dataArg.identity.role !== user.role) { return false; } return true; } catch (error) { return false; } }, { 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) => { // First check if identity is valid const isValid = await this.validIdentityGuard.exec(dataArg); if (!isValid) { return false; } // Check if user has admin role return dataArg.identity.role === 'admin'; }, { failedHint: 'user is not admin', name: 'adminIdentityGuard', } ); 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 (!baseUrl) { return undefined; } if (!this.idpClient) { this.idpClient = new plugins.idpSdkServer.IdpGlobalServerClient({ baseUrl }); this.ownsIdpClient = true; } return this.idpClient; } private isIdpGlobalConfigured(): boolean { return !!( this.opsServerRef.dcRouterRef.options.adminAuth?.idpClient || this.opsServerRef.dcRouterRef.options.adminAuth?.idpGlobalUrl || process.env.DCROUTER_IDP_GLOBAL_URL ); } 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', }; } }