import * as plugins from '../plugins.js'; import { LoginSession } from './classes.loginsession.js'; import { Reception } from './classes.reception.js'; import { logger } from './logging.js'; export class LoginSessionManager { // refs public receptionRef: Reception; public get db() { return this.receptionRef.db.smartdataDb; } public CLoginSession = plugins.smartdata.setDefaultManagerForDoc(this, LoginSession); public loginSessions = new plugins.lik.ObjectMap(); public typedRouter = new plugins.typedrequest.TypedRouter(); public emailTokenMap = new plugins.lik.ObjectMap<{ email: string; token: string; action: 'emailLogin' | 'passwordReset'; }>(); constructor(receptionRefArg: Reception) { this.receptionRef = receptionRefArg; this.receptionRef.typedrouter.addTypedRouter(this.typedRouter); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'loginWithEmailOrUsernameAndPassword', async (requestData) => { let user = await this.receptionRef.userManager.CUser.getInstance({ data: { username: requestData.username, passwordHash: await this.receptionRef.userManager.CUser.hashPassword( requestData.password ), }, }); if (!user && requestData.username.includes('@')) { user = await this.receptionRef.userManager.CUser.getInstance({ data: { email: requestData.username, passwordHash: await this.receptionRef.userManager.CUser.hashPassword( requestData.password ), }, }); } if (user) { // lets recheck if ( (user.data.username !== requestData.username && user.data.email !== requestData.username) || user.data.passwordHash !== (await this.receptionRef.userManager.CUser.hashPassword(requestData.password)) ) { throw new Error( 'database returned a user that does not match wanted criterea. CRITICAL!' ); } const loginSession = await LoginSession.createLoginSessionForUser(user); this.loginSessions.add(loginSession); const refreshToken = await loginSession.getRefreshToken(); return { status: 'ok', refreshToken: refreshToken, twoFaNeeded: false, }; } else { throw new plugins.typedrequest.TypedResponseError('User not found!'); } } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'loginWithEmail', async (requestDataArg) => { logger.log('info', `loginWithEmail requested for: ${requestDataArg.email}`); const existingUser = await this.receptionRef.userManager.CUser.getInstance({ data: { email: requestDataArg.email, }, }); if (existingUser) { logger.log('info', `loginWithEmail found user: ${existingUser.data.email}`); this.emailTokenMap.findOneAndRemoveSync( (itemArg) => itemArg.email === existingUser.data.email ); const loginEmailToken = plugins.smartunique.uuid4(); this.emailTokenMap.add({ email: existingUser.data.email, token: loginEmailToken, action: 'emailLogin', }); // lets make sure its only valid for 10 minutes plugins.smartdelay.delayFor(600000, null, true).then(() => { this.emailTokenMap.findOneAndRemoveSync( (itemArg) => itemArg.token === loginEmailToken ); }); this.receptionRef.receptionMailer.sendLoginWithEMailMail(existingUser, loginEmailToken); } else { logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`); } return { status: 'ok', testOnlyToken: process.env.TEST_MODE ? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email) .token : null, }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'loginWithEmailAfterEmailTokenAquired', async (requestArg) => { const tokenObject = this.emailTokenMap.findSync((itemArg) => { return itemArg.email === requestArg.email && itemArg.token === requestArg.token; }); if (tokenObject) { const user = await this.receptionRef.userManager.CUser.getInstance({ data: { email: requestArg.email, }, }); const loginSession = await LoginSession.createLoginSessionForUser(user); this.loginSessions.add(loginSession); return { refreshToken: await loginSession.getRefreshToken(), }; } else { throw new plugins.typedrequest.TypedResponseError('Validation Token not found'); } } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => { const loginSession = await this.CLoginSession.getLoginSessionByRefreshToken(requestDataArg.refreshToken); await loginSession.invalidate(); return {} }) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'exchangeRefreshTokenAndTransferToken', async (requestDataArg) => { switch (true) { case !!requestDataArg.refreshToken: const loginSession = await this.loginSessions.find(async (loginSessionArg) => { return loginSessionArg.validateRefreshToken(requestDataArg.refreshToken); }); if (!loginSession) { throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid'); } return { transferToken: await loginSession.getTransferToken(), }; break; case !!requestDataArg.transferToken: let transferToken: string; const loginSession2 = await this.loginSessions.find(async (loginSessionArg) => { return loginSessionArg.validateTransferToken(requestDataArg.transferToken); }); if (!loginSession2) { throw new plugins.typedrequest.TypedResponseError( 'Your transfer token is not valid.' ); } return { refreshToken: await loginSession2.getRefreshToken(), }; break; } } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'resetPassword', async (requestDataArg) => { const emailOfPasswordToReset = requestDataArg.email; const existingUser = await this.receptionRef.userManager.CUser.getInstance({ data: { email: emailOfPasswordToReset, }, }); if (existingUser) { this.emailTokenMap.findOneAndRemoveSync( (itemArg) => itemArg.email === existingUser.data.email ); this.emailTokenMap.add({ email: existingUser.data.email, token: plugins.smartunique.shortId(), action: 'passwordReset', }); plugins.smartdelay.delayFor(600000, null, true).then(() => { this.emailTokenMap.findOneAndRemoveSync( (itemArg) => itemArg.email === existingUser.data.email ); }); this.receptionRef.receptionMailer.sendPasswordResetMail( existingUser, this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email) .token ); } // note: we always return ok here, since we don't want to give any indication as to wether a user is already registered with us. return { status: 'ok', }; } ) ); this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'setNewPassword', async (requestData) => { return { status: 'ok', }; } ) ); /** * returns a device id by simply returning a uuid4 */ this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler('obtainDeviceId', async (reqData) => { reqData; return { deviceId: { id: plugins.smartunique.uuid4() } } }) ) this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler('attachDeviceId', async (reqData) => { // TODO: Blocked by proper JWT handling reqData.jwt; return { ok: false } }) ); // Get all sessions for the current user this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getUserSessions', async (requestArg) => { const jwt = await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt); if (!jwt) { throw new plugins.typedrequest.TypedResponseError('Invalid JWT'); } // Get the current session's refresh token to identify the current session const currentRefreshToken = jwt.data.refreshToken; // Get all sessions for this user const sessions = await this.CLoginSession.getInstances({ 'data.userId': jwt.data.userId, 'data.invalidated': false, }); return { sessions: sessions.map((session) => ({ id: session.id, deviceId: session.data.deviceId || 'unknown', deviceName: session.data.deviceInfo?.deviceName || 'Unknown Device', browser: session.data.deviceInfo?.browser || 'Unknown Browser', os: session.data.deviceInfo?.os || 'Unknown OS', ip: session.data.deviceInfo?.ip || 'Unknown', lastActive: session.data.lastActive || session.data.createdAt || Date.now(), createdAt: session.data.createdAt || Date.now(), isCurrent: session.data.refreshToken === currentRefreshToken, })), }; } ) ); // Revoke a specific session this.typedRouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'revokeSession', async (requestArg) => { const jwt = await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt); if (!jwt) { throw new plugins.typedrequest.TypedResponseError('Invalid JWT'); } // Get the session to revoke const sessionToRevoke = await this.CLoginSession.getInstance({ id: requestArg.sessionId, 'data.userId': jwt.data.userId, // Ensure user can only revoke their own sessions }); if (!sessionToRevoke) { throw new plugins.typedrequest.TypedResponseError('Session not found'); } // Don't allow revoking the current session via this method if (sessionToRevoke.data.refreshToken === jwt.data.refreshToken) { throw new plugins.typedrequest.TypedResponseError( 'Cannot revoke current session. Use logout instead.' ); } await sessionToRevoke.invalidate(); // Log the activity await this.receptionRef.activityLogManager.logActivity( jwt.data.userId, 'session_revoked', `Revoked session on ${sessionToRevoke.data.deviceInfo?.deviceName || 'unknown device'}` ); return { success: true }; } ) ); } }