2024-10-01 13:49:18 +02:00
|
|
|
import * as plugins from '../plugins.js';
|
2026-04-20 08:27:35 +00:00
|
|
|
import { EmailActionToken } from './classes.emailactiontoken.js';
|
2026-04-20 08:12:07 +00:00
|
|
|
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
|
2024-09-29 13:56:38 +02:00
|
|
|
import { Reception } from './classes.reception.js';
|
2024-10-04 02:18:47 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 08:27:35 +00:00
|
|
|
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(
|
2024-10-07 10:26:21 +02:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 08:12:07 +00:00
|
|
|
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
|
|
|
);
|
2026-04-20 08:12:07 +00:00
|
|
|
await user.save();
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
|
|
|
|
const refreshToken = await loginSession.getRefreshToken();
|
2026-04-20 08:12:07 +00:00
|
|
|
if (!refreshToken) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
|
|
|
|
}
|
2024-09-29 13:56:38 +02:00
|
|
|
|
|
|
|
|
return {
|
2026-04-20 08:12:07 +00:00
|
|
|
refreshToken,
|
2024-09-29 13:56:38 +02:00
|
|
|
twoFaNeeded: false,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('User not found!');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.typedRouter.addTypedHandler(
|
2024-10-07 10:26:21 +02:00
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmail>(
|
2024-09-29 13:56:38 +02:00
|
|
|
'loginWithEmail',
|
|
|
|
|
async (requestDataArg) => {
|
2024-10-04 02:18:47 +02:00
|
|
|
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) {
|
2024-10-04 02:18:47 +02:00
|
|
|
logger.log('info', `loginWithEmail found user: ${existingUser.data.email}`);
|
2026-04-20 08:27:35 +00:00
|
|
|
const loginEmailToken = await this.createEmailActionToken(
|
|
|
|
|
existingUser.data.email,
|
|
|
|
|
'emailLogin'
|
2024-09-29 13:56:38 +02:00
|
|
|
);
|
|
|
|
|
this.receptionRef.receptionMailer.sendLoginWithEMailMail(existingUser, loginEmailToken);
|
2026-04-20 08:27:35 +00:00
|
|
|
return {
|
|
|
|
|
status: 'ok',
|
|
|
|
|
testOnlyToken: process.env.TEST_MODE ? loginEmailToken : undefined,
|
|
|
|
|
};
|
2024-10-04 02:18:47 +02:00
|
|
|
} else {
|
|
|
|
|
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
status: 'ok',
|
2026-04-20 08:27:35 +00:00
|
|
|
testOnlyToken: undefined,
|
2024-09-29 13:56:38 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.typedRouter.addTypedHandler(
|
2024-10-07 10:26:21 +02:00
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmailAfterEmailTokenAquired>(
|
2024-09-29 13:56:38 +02:00
|
|
|
'loginWithEmailAfterEmailTokenAquired',
|
|
|
|
|
async (requestArg) => {
|
2026-04-20 08:27:35 +00:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-04-20 08:12:07 +00:00
|
|
|
if (!user) {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('User not found');
|
|
|
|
|
}
|
2024-09-29 13:56:38 +02:00
|
|
|
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
2026-04-20 08:12:07 +00:00
|
|
|
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 {
|
2026-04-20 08:12:07 +00:00
|
|
|
refreshToken,
|
2024-09-29 13:56:38 +02:00
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
2024-10-07 10:26:21 +02:00
|
|
|
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
|
2024-09-29 13:56:38 +02:00
|
|
|
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
|
2026-04-20 08:12:07 +00:00
|
|
|
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 {}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2024-10-07 10:26:21 +02:00
|
|
|
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) {
|
2026-04-20 08:12:07 +00:00
|
|
|
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 {
|
2026-04-20 08:12:07 +00:00
|
|
|
transferToken: await sessionLookup.loginSession.getTransferToken(),
|
2024-09-29 13:56:38 +02:00
|
|
|
};
|
2026-04-20 08:12:07 +00: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.'
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-20 08:12:07 +00:00
|
|
|
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 {
|
2026-04-20 08:12:07 +00:00
|
|
|
refreshToken,
|
2024-09-29 13:56:38 +02:00
|
|
|
};
|
2026-04-20 08:12:07 +00:00
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
throw new plugins.typedrequest.TypedResponseError('Invalid token exchange request');
|
2024-09-29 13:56:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.typedRouter.addTypedHandler(
|
2024-10-07 10:26:21 +02:00
|
|
|
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) {
|
2026-04-20 08:27:35 +00:00
|
|
|
const resetToken = await this.createEmailActionToken(
|
|
|
|
|
existingUser.data.email,
|
|
|
|
|
'passwordReset'
|
2024-09-29 13:56:38 +02:00
|
|
|
);
|
|
|
|
|
this.receptionRef.receptionMailer.sendPasswordResetMail(
|
|
|
|
|
existingUser,
|
2026-04-20 08:27:35 +00:00
|
|
|
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(
|
2024-10-07 10:26:21 +02:00
|
|
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_SetNewPassword>(
|
2024-09-29 13:56:38 +02:00
|
|
|
'setNewPassword',
|
|
|
|
|
async (requestData) => {
|
2026-04-20 08:27:35 +00:00
|
|
|
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(
|
2024-10-07 10:26:21 +02:00
|
|
|
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(
|
2024-10-07 10:26:21 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
})
|
2025-12-01 18:56:16 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 08:12:07 +00:00
|
|
|
const currentLoginSession = await jwt.getLoginSession();
|
2025-12-01 18:56:16 +00:00
|
|
|
|
|
|
|
|
// 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(),
|
2026-04-20 08:12:07 +00:00
|
|
|
isCurrent: session.id === currentLoginSession?.id,
|
2025-12-01 18:56:16 +00:00
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 08:12:07 +00:00
|
|
|
const currentLoginSession = await jwt.getLoginSession();
|
|
|
|
|
|
2025-12-01 18:56:16 +00:00
|
|
|
// Don't allow revoking the current session via this method
|
2026-04-20 08:12:07 +00:00
|
|
|
if (sessionToRevoke.id === currentLoginSession?.id) {
|
2025-12-01 18:56:16 +00:00
|
|
|
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
|
|
|
}
|
2026-04-20 08:12:07 +00: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;
|
|
|
|
|
}
|
2026-04-20 08:27:35 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
}
|