Files
app/ts/reception/classes.loginsessionmanager.ts
T

452 lines
16 KiB
TypeScript
Raw Normal View History

import * as plugins from '../plugins.js';
import { EmailActionToken } from './classes.emailactiontoken.js';
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
2024-09-29 13:56:38 +02:00
import { Reception } from './classes.reception.js';
import { logger } from './logging.js';
2024-09-29 13:56:38 +02:00
export class LoginSessionManager {
// refs
public receptionRef: Reception;
public get db() {
return this.receptionRef.db.smartdataDb;
}
public CEmailActionToken = plugins.smartdata.setDefaultManagerForDoc(this, EmailActionToken);
2024-09-29 13:56:38 +02:00
public CLoginSession = plugins.smartdata.setDefaultManagerForDoc(this, LoginSession);
public typedRouter = new plugins.typedrequest.TypedRouter();
constructor(receptionRefArg: Reception) {
this.receptionRef = receptionRefArg;
this.receptionRef.typedrouter.addTypedRouter(this.typedRouter);
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmailOrUsernameAndPassword>(
2024-09-29 13:56:38 +02:00
'loginWithEmailOrUsernameAndPassword',
async (requestData) => {
let user = await this.receptionRef.userManager.CUser.getInstance({
data: {
username: requestData.username,
},
});
if (!user && requestData.username.includes('@')) {
user = await this.receptionRef.userManager.CUser.getInstance({
data: {
email: requestData.username,
},
});
}
if (user && (await this.receptionRef.userManager.CUser.verifyPassword(
requestData.password,
user.data.passwordHash
))) {
if (this.receptionRef.userManager.CUser.shouldUpgradePasswordHash(user.data.passwordHash)) {
user.data.passwordHash = await this.receptionRef.userManager.CUser.hashPassword(
requestData.password
2024-09-29 13:56:38 +02:00
);
await user.save();
2024-09-29 13:56:38 +02:00
}
const loginSession = await LoginSession.createLoginSessionForUser(user);
const refreshToken = await loginSession.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
2024-09-29 13:56:38 +02:00
return {
refreshToken,
2024-09-29 13:56:38 +02:00
twoFaNeeded: false,
};
} else {
throw new plugins.typedrequest.TypedResponseError('User not found!');
}
}
)
);
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmail>(
2024-09-29 13:56:38 +02:00
'loginWithEmail',
async (requestDataArg) => {
logger.log('info', `loginWithEmail requested for: ${requestDataArg.email}`);
2024-09-29 13:56:38 +02:00
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
data: {
email: requestDataArg.email,
},
});
if (existingUser) {
logger.log('info', `loginWithEmail found user: ${existingUser.data.email}`);
const loginEmailToken = await this.createEmailActionToken(
existingUser.data.email,
'emailLogin'
2024-09-29 13:56:38 +02:00
);
this.receptionRef.receptionMailer.sendLoginWithEMailMail(existingUser, loginEmailToken);
return {
status: 'ok',
testOnlyToken: process.env.TEST_MODE ? loginEmailToken : undefined,
};
} else {
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
2024-09-29 13:56:38 +02:00
}
return {
status: 'ok',
testOnlyToken: undefined,
2024-09-29 13:56:38 +02:00
};
}
)
);
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmailAfterEmailTokenAquired>(
2024-09-29 13:56:38 +02:00
'loginWithEmailAfterEmailTokenAquired',
async (requestArg) => {
const tokenObject = await this.consumeEmailActionToken(
requestArg.email,
requestArg.token,
'emailLogin'
);
2024-09-29 13:56:38 +02:00
if (tokenObject) {
const user = await this.receptionRef.userManager.CUser.getInstance({
data: {
email: requestArg.email,
},
});
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
2024-09-29 13:56:38 +02:00
const loginSession = await LoginSession.createLoginSessionForUser(user);
const refreshToken = await loginSession.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
2024-09-29 13:56:38 +02:00
return {
refreshToken,
2024-09-29 13:56:38 +02:00
};
} else {
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
}
}
)
);
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
2024-09-29 13:56:38 +02:00
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
const sessionLookup = await this.findLoginSessionByRefreshToken(requestDataArg.refreshToken);
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
throw new plugins.typedrequest.TypedResponseError('Invalid refresh token');
}
await sessionLookup.loginSession.invalidate();
2024-09-29 13:56:38 +02:00
return {}
})
);
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>(
2024-09-29 13:56:38 +02:00
new plugins.typedrequest.TypedHandler(
'exchangeRefreshTokenAndTransferToken',
async (requestDataArg) => {
switch (true) {
case !!requestDataArg.refreshToken: {
const sessionLookup = await this.findLoginSessionByRefreshToken(
requestDataArg.refreshToken
);
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
if (sessionLookup?.validationStatus === 'reused') {
await sessionLookup.loginSession.invalidate();
}
2024-09-29 13:56:38 +02:00
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
}
return {
transferToken: await sessionLookup.loginSession.getTransferToken(),
2024-09-29 13:56:38 +02:00
};
}
case !!requestDataArg.transferToken: {
const loginSession2 = await this.findLoginSessionByTransferToken(
requestDataArg.transferToken
);
2024-09-29 13:56:38 +02:00
if (!loginSession2) {
throw new plugins.typedrequest.TypedResponseError(
'Your transfer token is not valid.'
);
}
const refreshToken = await loginSession2.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
2024-09-29 13:56:38 +02:00
return {
refreshToken,
2024-09-29 13:56:38 +02:00
};
}
default:
throw new plugins.typedrequest.TypedResponseError('Invalid token exchange request');
2024-09-29 13:56:38 +02:00
}
}
)
);
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_ResetPassword>(
2024-09-29 13:56:38 +02:00
'resetPassword',
async (requestDataArg) => {
const emailOfPasswordToReset = requestDataArg.email;
const existingUser = await this.receptionRef.userManager.CUser.getInstance({
data: {
email: emailOfPasswordToReset,
},
});
if (existingUser) {
const resetToken = await this.createEmailActionToken(
existingUser.data.email,
'passwordReset'
2024-09-29 13:56:38 +02:00
);
this.receptionRef.receptionMailer.sendPasswordResetMail(
existingUser,
resetToken
2024-09-29 13:56:38 +02:00
);
}
// 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<plugins.idpInterfaces.request.IReq_SetNewPassword>(
2024-09-29 13:56:38 +02:00
'setNewPassword',
async (requestData) => {
const user = await this.receptionRef.userManager.CUser.getInstance({
data: {
email: requestData.email,
},
});
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
if (requestData.tokenArg) {
const tokenObject = await this.consumeEmailActionToken(
requestData.email,
requestData.tokenArg,
'passwordReset'
);
if (!tokenObject) {
throw new plugins.typedrequest.TypedResponseError('Password reset token invalid');
}
} else if (requestData.oldPassword) {
const passwordOk = await this.receptionRef.userManager.CUser.verifyPassword(
requestData.oldPassword,
user.data.passwordHash
);
if (!passwordOk) {
throw new plugins.typedrequest.TypedResponseError('Old password invalid');
}
} else {
throw new plugins.typedrequest.TypedResponseError(
'Either a reset token or the old password is required'
);
}
user.data.passwordHash = await this.receptionRef.userManager.CUser.hashPassword(
requestData.newPassword
);
await user.save();
2024-09-29 13:56:38 +02:00
return {
status: 'ok',
};
}
)
);
/**
* returns a device id by simply returning a uuid4
*/
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_ObtainDeviceId>('obtainDeviceId', async (reqData) => {
2024-09-29 13:56:38 +02:00
reqData;
return {
deviceId: {
id: plugins.smartunique.uuid4()
}
}
})
)
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_AttachDeviceId>('attachDeviceId', async (reqData) => {
2024-09-29 13:56:38 +02:00
// 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<plugins.idpInterfaces.request.IReq_GetUserSessions>(
'getUserSessions',
async (requestArg) => {
const jwt = await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt);
if (!jwt) {
throw new plugins.typedrequest.TypedResponseError('Invalid JWT');
}
const currentLoginSession = await jwt.getLoginSession();
// 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.id === currentLoginSession?.id,
})),
};
}
)
);
// Revoke a specific session
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_RevokeSession>(
'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');
}
const currentLoginSession = await jwt.getLoginSession();
// Don't allow revoking the current session via this method
if (sessionToRevoke.id === currentLoginSession?.id) {
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 };
}
)
);
2024-09-29 13:56:38 +02:00
}
public async findLoginSessionByRefreshToken(refreshTokenArg: string): Promise<{
loginSession: LoginSession;
validationStatus: TRefreshTokenValidationResult;
} | null> {
const directMatch = await this.CLoginSession.getLoginSessionByRefreshToken(refreshTokenArg);
if (directMatch) {
return {
loginSession: directMatch,
validationStatus: await directMatch.validateRefreshToken(refreshTokenArg),
};
}
const loginSessions = await this.CLoginSession.getInstances({});
for (const loginSession of loginSessions) {
const validationStatus = await loginSession.validateRefreshToken(refreshTokenArg);
if (validationStatus !== 'invalid') {
return {
loginSession,
validationStatus,
};
}
}
return null;
}
public async findLoginSessionByTransferToken(transferTokenArg: string) {
const transferTokenHash = await LoginSession.hashSessionToken(transferTokenArg);
const loginSession = await this.CLoginSession.getInstance({
'data.transferTokenHash': transferTokenHash,
});
if (!loginSession) {
return null;
}
const isValid = await loginSession.validateTransferToken(transferTokenArg);
return isValid ? loginSession : null;
}
public async createEmailActionToken(
emailArg: string,
actionArg: plugins.idpInterfaces.data.TEmailActionTokenAction
) {
const existingTokens = await this.CEmailActionToken.getInstances({
'data.email': emailArg,
'data.action': actionArg,
});
for (const existingToken of existingTokens) {
await existingToken.delete();
}
const plainToken = EmailActionToken.createOpaqueToken(actionArg);
const emailActionToken = new EmailActionToken();
emailActionToken.id = plugins.smartunique.shortId();
emailActionToken.data = {
email: emailArg,
action: actionArg,
tokenHash: EmailActionToken.hashToken(plainToken),
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ minutes: 10 }),
createdAt: Date.now(),
};
await emailActionToken.save();
return plainToken;
}
public async consumeEmailActionToken(
emailArg: string,
tokenArg: string,
actionArg: plugins.idpInterfaces.data.TEmailActionTokenAction
) {
const emailActionToken = await this.CEmailActionToken.getInstance({
'data.email': emailArg,
'data.action': actionArg,
'data.tokenHash': EmailActionToken.hashToken(tokenArg),
});
if (!emailActionToken) {
return null;
}
const consumed = await emailActionToken.consume(tokenArg);
return consumed ? emailActionToken : null;
}
2024-09-29 13:56:38 +02:00
}