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); } 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, }, }; }, ), ); } 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', }, ); }