import * as plugins from '../plugins.js'; import type { Cloudly } from '../classes.cloudly.js'; import { logger } from '../logger.js'; import { Authorization } from './classes.authorization.js'; import { User } from './classes.user.js'; export interface IJwtData { userId: string; status: 'loggedIn' | 'loggedOut'; expiresAt: number; } export class CloudlyAuthManager { cloudlyRef: Cloudly; public get db() { return this.cloudlyRef.mongodbConnector.smartdataDb; } public CUser = plugins.smartdata.setDefaultManagerForDoc(this, User); public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization); public typedrouter = new plugins.typedrequest.TypedRouter(); public smartjwtInstance: plugins.smartjwt.SmartJwt; constructor(cloudlyRef: Cloudly) { this.cloudlyRef = cloudlyRef; this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter); } public async createNewSecureToken() { return plugins.smartunique.uniSimple('secureToken', 64); } public async start() { // lets setup the smartjwtInstance this.smartjwtInstance = new plugins.smartjwt.SmartJwt(); await this.smartjwtInstance.init(); const kvStore = await this.cloudlyRef.config.appData.getKvStore(); const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = (await kvStore.readKey( 'jwtKeypair', )) as plugins.tsclass.network.IJwtKeypair; if (!existingJwtKeys) { await this.smartjwtInstance.createNewKeyPair(); const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson(); await kvStore.writeKey('jwtKeypair', newJwtKeys); } else { this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys); } await this.bootstrapInitialAdmin(); this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'adminLoginWithUsernameAndPassword', async (dataArg) => { let jwt: string; let expiresAtTimestamp: number = Date.now() + 3600 * 1000 * 24 * 7; const user = await User.findUserByUsernameAndPassword(dataArg.username, dataArg.password); if (!user) { logger.log('warn', 'login failed'); throw new plugins.typedrequest.TypedResponseError('login failed'); } else { jwt = await this.smartjwtInstance.createJWT({ userId: user.id, status: 'loggedIn', expiresAt: expiresAtTimestamp, }); logger.log('success', 'login successful'); } return { identity: { jwt, userId: user.id, name: user.data.username, expiresAt: expiresAtTimestamp, role: user.data.role, type: user.data.type, }, }; }, ), ); } 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<{ identity: plugins.servezoneInterfaces.data.IIdentity; }>( async (dataArg) => { const jwt = dataArg.identity.jwt; const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt); const expired = jwtData.expiresAt < Date.now(); plugins.smartexpect .expect(jwtData.status) .setFailMessage('user not logged in') .toEqual('loggedIn'); plugins.smartexpect.expect(expired).setFailMessage(`jwt expired`).toBeFalse(); plugins.smartexpect .expect(dataArg.identity.expiresAt) .setFailMessage( `expiresAt >>identity valid until:${dataArg.identity.expiresAt}, but jwt says: ${jwtData.expiresAt}<< has been tampered with`, ) .toEqual(jwtData.expiresAt); plugins.smartexpect .expect(dataArg.identity.userId) .setFailMessage('userId has been tampered with') .toEqual(jwtData.userId); if (expired) { throw new Error('identity is expired'); } return true; }, { failedHint: 'identity is not valid.', name: 'validIdentityGuard', }, ); public adminIdentityGuard = new plugins.smartguard.Guard<{ identity: plugins.servezoneInterfaces.data.IIdentity; }>( async (dataArg) => { await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]); const jwt = dataArg.identity.jwt; const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt); const user = await this.CUser.getInstance({ id: jwtData.userId }); const isAdminBool = user.data.role === 'admin'; console.log(`user is admin: ${isAdminBool}`); return isAdminBool; }, { failedHint: 'user is not admin.', name: 'adminIdentityGuard', }, ); }