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

342 lines
12 KiB
TypeScript
Raw Normal View History

import * as plugins from '../plugins.js';
2024-09-29 13:56:38 +02:00
import { LoginSession } from './classes.loginsession.js';
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 CLoginSession = plugins.smartdata.setDefaultManagerForDoc(this, LoginSession);
public loginSessions = new plugins.lik.ObjectMap<LoginSession>();
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<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,
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<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}`);
2024-09-29 13:56:38 +02:00
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}`);
2024-09-29 13:56:38 +02:00
}
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<plugins.idpInterfaces.request.IReq_LoginWithEmailAfterEmailTokenAquired>(
2024-09-29 13:56:38 +02:00
'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<plugins.idpInterfaces.request.ILogoutRequest>(
2024-09-29 13:56:38 +02:00
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
const loginSession = await this.CLoginSession.getLoginSessionByRefreshToken(requestDataArg.refreshToken);
await loginSession.invalidate();
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 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<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) {
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<plugins.idpInterfaces.request.IReq_SetNewPassword>(
2024-09-29 13:56:38 +02:00
'setNewPassword',
async (requestData) => {
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');
}
// 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<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');
}
// 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 };
}
)
);
2024-09-29 13:56:38 +02:00
}
}