feat(reception): persist email action tokens and registration sessions for authentication and signup flows
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { EmailActionToken } from './classes.emailactiontoken.js';
|
||||
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
|
||||
import { Reception } from './classes.reception.js';
|
||||
import { logger } from './logging.js';
|
||||
@@ -10,18 +11,11 @@ export class LoginSessionManager {
|
||||
return this.receptionRef.db.smartdataDb;
|
||||
}
|
||||
|
||||
public CEmailActionToken = plugins.smartdata.setDefaultManagerForDoc(this, EmailActionToken);
|
||||
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);
|
||||
@@ -55,7 +49,6 @@ export class LoginSessionManager {
|
||||
}
|
||||
|
||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||
this.loginSessions.add(loginSession);
|
||||
const refreshToken = await loginSession.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||
@@ -84,33 +77,21 @@ export class LoginSessionManager {
|
||||
});
|
||||
if (existingUser) {
|
||||
logger.log('info', `loginWithEmail found user: ${existingUser.data.email}`);
|
||||
this.emailTokenMap.findOneAndRemoveSync(
|
||||
(itemArg) => itemArg.email === existingUser.data.email
|
||||
const loginEmailToken = await this.createEmailActionToken(
|
||||
existingUser.data.email,
|
||||
'emailLogin'
|
||||
);
|
||||
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);
|
||||
return {
|
||||
status: 'ok',
|
||||
testOnlyToken: process.env.TEST_MODE ? loginEmailToken : undefined,
|
||||
};
|
||||
} else {
|
||||
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
|
||||
}
|
||||
const testOnlyToken =
|
||||
process.env.TEST_MODE && existingUser
|
||||
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
||||
?.token
|
||||
: undefined;
|
||||
return {
|
||||
status: 'ok',
|
||||
testOnlyToken,
|
||||
testOnlyToken: undefined,
|
||||
};
|
||||
}
|
||||
)
|
||||
@@ -120,9 +101,11 @@ export class LoginSessionManager {
|
||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmailAfterEmailTokenAquired>(
|
||||
'loginWithEmailAfterEmailTokenAquired',
|
||||
async (requestArg) => {
|
||||
const tokenObject = this.emailTokenMap.findSync((itemArg) => {
|
||||
return itemArg.email === requestArg.email && itemArg.token === requestArg.token;
|
||||
});
|
||||
const tokenObject = await this.consumeEmailActionToken(
|
||||
requestArg.email,
|
||||
requestArg.token,
|
||||
'emailLogin'
|
||||
);
|
||||
if (tokenObject) {
|
||||
const user = await this.receptionRef.userManager.CUser.getInstance({
|
||||
data: {
|
||||
@@ -133,7 +116,6 @@ export class LoginSessionManager {
|
||||
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||
}
|
||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||
this.loginSessions.add(loginSession);
|
||||
const refreshToken = await loginSession.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||
@@ -213,23 +195,13 @@ export class LoginSessionManager {
|
||||
},
|
||||
});
|
||||
if (existingUser) {
|
||||
this.emailTokenMap.findOneAndRemoveSync(
|
||||
(itemArg) => itemArg.email === existingUser.data.email
|
||||
const resetToken = await this.createEmailActionToken(
|
||||
existingUser.data.email,
|
||||
'passwordReset'
|
||||
);
|
||||
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
|
||||
resetToken
|
||||
);
|
||||
}
|
||||
// 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.
|
||||
@@ -244,6 +216,43 @@ export class LoginSessionManager {
|
||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_SetNewPassword>(
|
||||
'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();
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
@@ -393,4 +402,50 @@ export class LoginSessionManager {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user