feat(reception): persist email action tokens and registration sessions for authentication and signup flows
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-20 - 1.18.0 - feat(reception)
|
||||||
|
persist email action tokens and registration sessions for authentication and signup flows
|
||||||
|
|
||||||
|
- add persisted email action tokens for email login and password reset with one-time consumption and expiry cleanup
|
||||||
|
- store registration sessions in the database so signup state, email validation, and SMS verification survive restarts
|
||||||
|
- enforce password changes through either a valid reset token or the current password
|
||||||
|
- add housekeeping jobs and tests for token/session expiry and state persistence
|
||||||
|
|
||||||
## 2026-04-20 - 1.17.1 - fix(docs)
|
## 2026-04-20 - 1.17.1 - fix(docs)
|
||||||
refresh module readmes and add repository license file
|
refresh module readmes and add repository license file
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
import { EmailActionToken } from '../ts/reception/classes.emailactiontoken.js';
|
||||||
import { LoginSession } from '../ts/reception/classes.loginsession.js';
|
import { LoginSession } from '../ts/reception/classes.loginsession.js';
|
||||||
|
import { RegistrationSession } from '../ts/reception/classes.registrationsession.js';
|
||||||
import { User } from '../ts/reception/classes.user.js';
|
import { User } from '../ts/reception/classes.user.js';
|
||||||
import * as plugins from '../ts/plugins.js';
|
import * as plugins from '../ts/plugins.js';
|
||||||
|
|
||||||
@@ -12,6 +14,42 @@ const createTestLoginSession = () => {
|
|||||||
return loginSession;
|
return loginSession;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createTestEmailActionToken = () => {
|
||||||
|
const emailActionToken = new EmailActionToken();
|
||||||
|
emailActionToken.id = 'email-action-token';
|
||||||
|
emailActionToken.data.email = 'user@example.com';
|
||||||
|
emailActionToken.data.action = 'emailLogin';
|
||||||
|
emailActionToken.data.validUntil = Date.now() + 60_000;
|
||||||
|
|
||||||
|
let deleted = false;
|
||||||
|
(emailActionToken as EmailActionToken & { delete: () => Promise<void> }).delete = async () => {
|
||||||
|
deleted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
emailActionToken,
|
||||||
|
wasDeleted: () => deleted,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTestRegistrationSession = () => {
|
||||||
|
const registrationSession = new RegistrationSession();
|
||||||
|
registrationSession.id = 'registration-session';
|
||||||
|
registrationSession.data.emailAddress = 'user@example.com';
|
||||||
|
registrationSession.data.validUntil = Date.now() + 60_000;
|
||||||
|
|
||||||
|
let deleted = false;
|
||||||
|
(registrationSession as RegistrationSession & { save: () => Promise<void> }).save = async () => undefined;
|
||||||
|
(registrationSession as RegistrationSession & { delete: () => Promise<void> }).delete = async () => {
|
||||||
|
deleted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
registrationSession,
|
||||||
|
wasDeleted: () => deleted,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
tap.test('hashes passwords with argon2 and verifies them', async () => {
|
tap.test('hashes passwords with argon2 and verifies them', async () => {
|
||||||
const passwordHash = await User.hashPassword('correct horse battery staple');
|
const passwordHash = await User.hashPassword('correct horse battery staple');
|
||||||
|
|
||||||
@@ -58,4 +96,45 @@ tap.test('persists transfer tokens as one-time hashes', async () => {
|
|||||||
expect(await loginSession.validateTransferToken(transferToken)).toBeFalse();
|
expect(await loginSession.validateTransferToken(transferToken)).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('consumes email action tokens exactly once', async () => {
|
||||||
|
const { emailActionToken, wasDeleted } = createTestEmailActionToken();
|
||||||
|
const plainToken = EmailActionToken.createOpaqueToken('emailLogin');
|
||||||
|
emailActionToken.data.tokenHash = EmailActionToken.hashToken(plainToken);
|
||||||
|
|
||||||
|
expect(await emailActionToken.consume(plainToken)).toBeTrue();
|
||||||
|
expect(wasDeleted()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('invalidates expired email action tokens', async () => {
|
||||||
|
const { emailActionToken, wasDeleted } = createTestEmailActionToken();
|
||||||
|
emailActionToken.data.tokenHash = EmailActionToken.hashToken('expired-token');
|
||||||
|
emailActionToken.data.validUntil = Date.now() - 1;
|
||||||
|
|
||||||
|
expect(await emailActionToken.consume('expired-token')).toBeFalse();
|
||||||
|
expect(wasDeleted()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('persists registration token validation and sms verification state', async () => {
|
||||||
|
const { registrationSession } = createTestRegistrationSession();
|
||||||
|
const emailToken = 'registration-token';
|
||||||
|
registrationSession.data.hashedEmailToken = RegistrationSession.hashToken(emailToken);
|
||||||
|
|
||||||
|
expect(await registrationSession.validateEmailToken(emailToken)).toBeTrue();
|
||||||
|
expect(registrationSession.data.status).toEqual('emailValidated');
|
||||||
|
expect(registrationSession.data.collectedData.userData.email).toEqual('user@example.com');
|
||||||
|
|
||||||
|
registrationSession.data.smsCodeHash = RegistrationSession.hashToken('123456');
|
||||||
|
expect(await registrationSession.validateSmsCode('123456')).toBeTrue();
|
||||||
|
expect(registrationSession.data.status).toEqual('mobileVerified');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('removes expired registration sessions on token validation', async () => {
|
||||||
|
const { registrationSession, wasDeleted } = createTestRegistrationSession();
|
||||||
|
registrationSession.data.hashedEmailToken = RegistrationSession.hashToken('expired-registration');
|
||||||
|
registrationSession.data.validUntil = Date.now() - 1;
|
||||||
|
|
||||||
|
expect(await registrationSession.validateEmailToken('expired-registration')).toBeFalse();
|
||||||
|
expect(wasDeleted()).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@idp.global/idp.global',
|
name: '@idp.global/idp.global',
|
||||||
version: '1.17.1',
|
version: '1.18.0',
|
||||||
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { LoginSessionManager } from './classes.loginsessionmanager.js';
|
||||||
|
|
||||||
|
@plugins.smartdata.Manager()
|
||||||
|
export class EmailActionToken extends plugins.smartdata.SmartDataDbDoc<
|
||||||
|
EmailActionToken,
|
||||||
|
plugins.idpInterfaces.data.IEmailActionToken,
|
||||||
|
LoginSessionManager
|
||||||
|
> {
|
||||||
|
public static hashToken(tokenArg: string) {
|
||||||
|
return plugins.smarthash.sha256FromStringSync(tokenArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static createOpaqueToken(actionArg: plugins.idpInterfaces.data.TEmailActionTokenAction) {
|
||||||
|
return `${actionArg}_${plugins.crypto.randomBytes(32).toString('base64url')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.idpInterfaces.data.IEmailActionToken['data'] = {
|
||||||
|
email: '',
|
||||||
|
action: 'emailLogin',
|
||||||
|
tokenHash: '',
|
||||||
|
validUntil: 0,
|
||||||
|
createdAt: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
public isExpired() {
|
||||||
|
return this.data.validUntil < Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public matchesToken(tokenArg: string) {
|
||||||
|
return this.data.tokenHash === EmailActionToken.hashToken(tokenArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async consume(tokenArg: string) {
|
||||||
|
if (this.isExpired() || !this.matchesToken(tokenArg)) {
|
||||||
|
if (this.isExpired()) {
|
||||||
|
await this.delete();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.delete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,46 @@ export class ReceptionHousekeeping {
|
|||||||
'2 * * * * *'
|
'2 * * * * *'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.taskmanager.addAndScheduleTask(
|
||||||
|
new plugins.taskbuffer.Task({
|
||||||
|
name: 'expiredEmailActionTokens',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const expiredEmailActionTokens =
|
||||||
|
await this.receptionRef.loginSessionManager.CEmailActionToken.getInstances({
|
||||||
|
data: {
|
||||||
|
validUntil: {
|
||||||
|
$lt: Date.now(),
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const emailActionToken of expiredEmailActionTokens) {
|
||||||
|
await emailActionToken.delete();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'2 * * * * *'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.taskmanager.addAndScheduleTask(
|
||||||
|
new plugins.taskbuffer.Task({
|
||||||
|
name: 'expiredRegistrationSessions',
|
||||||
|
taskFunction: async () => {
|
||||||
|
const expiredRegistrationSessions =
|
||||||
|
await this.receptionRef.registrationSessionManager.CRegistrationSession.getInstances({
|
||||||
|
data: {
|
||||||
|
validUntil: {
|
||||||
|
$lt: Date.now(),
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const registrationSession of expiredRegistrationSessions) {
|
||||||
|
await registrationSession.delete();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
'2 * * * * *'
|
||||||
|
);
|
||||||
|
|
||||||
this.taskmanager.start();
|
this.taskmanager.start();
|
||||||
logger.log('info', 'housekeeping started');
|
logger.log('info', 'housekeeping started');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
|
import { EmailActionToken } from './classes.emailactiontoken.js';
|
||||||
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
|
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
|
||||||
import { Reception } from './classes.reception.js';
|
import { Reception } from './classes.reception.js';
|
||||||
import { logger } from './logging.js';
|
import { logger } from './logging.js';
|
||||||
@@ -10,18 +11,11 @@ export class LoginSessionManager {
|
|||||||
return this.receptionRef.db.smartdataDb;
|
return this.receptionRef.db.smartdataDb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CEmailActionToken = plugins.smartdata.setDefaultManagerForDoc(this, EmailActionToken);
|
||||||
public CLoginSession = plugins.smartdata.setDefaultManagerForDoc(this, LoginSession);
|
public CLoginSession = plugins.smartdata.setDefaultManagerForDoc(this, LoginSession);
|
||||||
|
|
||||||
public loginSessions = new plugins.lik.ObjectMap<LoginSession>();
|
|
||||||
|
|
||||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
public emailTokenMap = new plugins.lik.ObjectMap<{
|
|
||||||
email: string;
|
|
||||||
token: string;
|
|
||||||
action: 'emailLogin' | 'passwordReset';
|
|
||||||
}>();
|
|
||||||
|
|
||||||
constructor(receptionRefArg: Reception) {
|
constructor(receptionRefArg: Reception) {
|
||||||
this.receptionRef = receptionRefArg;
|
this.receptionRef = receptionRefArg;
|
||||||
this.receptionRef.typedrouter.addTypedRouter(this.typedRouter);
|
this.receptionRef.typedrouter.addTypedRouter(this.typedRouter);
|
||||||
@@ -55,7 +49,6 @@ export class LoginSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||||
this.loginSessions.add(loginSession);
|
|
||||||
const refreshToken = await loginSession.getRefreshToken();
|
const refreshToken = await loginSession.getRefreshToken();
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||||
@@ -84,33 +77,21 @@ export class LoginSessionManager {
|
|||||||
});
|
});
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
logger.log('info', `loginWithEmail found user: ${existingUser.data.email}`);
|
logger.log('info', `loginWithEmail found user: ${existingUser.data.email}`);
|
||||||
this.emailTokenMap.findOneAndRemoveSync(
|
const loginEmailToken = await this.createEmailActionToken(
|
||||||
(itemArg) => itemArg.email === existingUser.data.email
|
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);
|
this.receptionRef.receptionMailer.sendLoginWithEMailMail(existingUser, loginEmailToken);
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
testOnlyToken: process.env.TEST_MODE ? loginEmailToken : undefined,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
|
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 {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
testOnlyToken,
|
testOnlyToken: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -120,9 +101,11 @@ export class LoginSessionManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmailAfterEmailTokenAquired>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_LoginWithEmailAfterEmailTokenAquired>(
|
||||||
'loginWithEmailAfterEmailTokenAquired',
|
'loginWithEmailAfterEmailTokenAquired',
|
||||||
async (requestArg) => {
|
async (requestArg) => {
|
||||||
const tokenObject = this.emailTokenMap.findSync((itemArg) => {
|
const tokenObject = await this.consumeEmailActionToken(
|
||||||
return itemArg.email === requestArg.email && itemArg.token === requestArg.token;
|
requestArg.email,
|
||||||
});
|
requestArg.token,
|
||||||
|
'emailLogin'
|
||||||
|
);
|
||||||
if (tokenObject) {
|
if (tokenObject) {
|
||||||
const user = await this.receptionRef.userManager.CUser.getInstance({
|
const user = await this.receptionRef.userManager.CUser.getInstance({
|
||||||
data: {
|
data: {
|
||||||
@@ -133,7 +116,6 @@ export class LoginSessionManager {
|
|||||||
throw new plugins.typedrequest.TypedResponseError('User not found');
|
throw new plugins.typedrequest.TypedResponseError('User not found');
|
||||||
}
|
}
|
||||||
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
const loginSession = await LoginSession.createLoginSessionForUser(user);
|
||||||
this.loginSessions.add(loginSession);
|
|
||||||
const refreshToken = await loginSession.getRefreshToken();
|
const refreshToken = await loginSession.getRefreshToken();
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
|
||||||
@@ -213,23 +195,13 @@ export class LoginSessionManager {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
this.emailTokenMap.findOneAndRemoveSync(
|
const resetToken = await this.createEmailActionToken(
|
||||||
(itemArg) => itemArg.email === existingUser.data.email
|
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(
|
this.receptionRef.receptionMailer.sendPasswordResetMail(
|
||||||
existingUser,
|
existingUser,
|
||||||
this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
|
resetToken
|
||||||
.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.
|
// 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>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_SetNewPassword>(
|
||||||
'setNewPassword',
|
'setNewPassword',
|
||||||
async (requestData) => {
|
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 {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
};
|
};
|
||||||
@@ -393,4 +402,50 @@ export class LoginSessionManager {
|
|||||||
const isValid = await loginSession.validateTransferToken(transferTokenArg);
|
const isValid = await loginSession.validateTransferToken(transferTokenArg);
|
||||||
return isValid ? loginSession : null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,191 +5,187 @@ import { logger } from './logging.js';
|
|||||||
import { User } from './classes.user.js';
|
import { User } from './classes.user.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* a RegistrationSession is a in memory session for signing up
|
* a RegistrationSession persists a sign up flow across restarts
|
||||||
*/
|
*/
|
||||||
export class RegistrationSession {
|
@plugins.smartdata.Manager()
|
||||||
// ======
|
export class RegistrationSession extends plugins.smartdata.SmartDataDbDoc<
|
||||||
// STATIC
|
RegistrationSession,
|
||||||
// ======
|
plugins.idpInterfaces.data.IRegistrationSession,
|
||||||
|
RegistrationSessionManager
|
||||||
|
> {
|
||||||
|
public static hashToken(tokenArg: string) {
|
||||||
|
return plugins.smarthash.sha256FromStringSync(tokenArg);
|
||||||
|
}
|
||||||
|
|
||||||
public static async createRegistrationSessionForEmail(
|
public static async createRegistrationSessionForEmail(
|
||||||
registrationSessionManageremailArg: RegistrationSessionManager,
|
|
||||||
emailArg: string
|
emailArg: string
|
||||||
) {
|
) {
|
||||||
const newRegistrationSession = new RegistrationSession(
|
const newRegistrationSession = new RegistrationSession();
|
||||||
registrationSessionManageremailArg,
|
newRegistrationSession.id = plugins.smartunique.shortId();
|
||||||
emailArg
|
newRegistrationSession.data.emailAddress = emailArg;
|
||||||
);
|
newRegistrationSession.data.validUntil =
|
||||||
const emailValidationResult = await newRegistrationSession
|
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ minutes: 10 });
|
||||||
.validateEMailAddress()
|
newRegistrationSession.data.createdAt = Date.now();
|
||||||
.catch((error) => {
|
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
const emailValidationResult = await newRegistrationSession.validateEMailAddress().catch(() => {
|
||||||
'Error occured during email provider & dns validation'
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
);
|
'Error occured during email provider & dns validation'
|
||||||
});
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (!emailValidationResult?.valid) {
|
if (!emailValidationResult?.valid) {
|
||||||
newRegistrationSession.destroy();
|
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'Email Address is not valid. Please use a correctly formated email address'
|
'Email Address is not valid. Please use a correctly formated email address'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (emailValidationResult.disposable) {
|
if (emailValidationResult.disposable) {
|
||||||
newRegistrationSession.destroy();
|
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'Email is disposable. Please use a non disposable email address.'
|
'Email is disposable. Please use a non disposable email address.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
console.log(
|
|
||||||
`${newRegistrationSession.emailAddress} is valid. Continuing registration process!`
|
const validationToken = await newRegistrationSession.sendTokenValidationEmail();
|
||||||
);
|
newRegistrationSession.unhashedEmailToken = validationToken;
|
||||||
await newRegistrationSession.sendTokenValidationEmail();
|
|
||||||
console.log(`Successfully sent email validation email`);
|
|
||||||
return newRegistrationSession;
|
return newRegistrationSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========
|
@plugins.smartdata.unI()
|
||||||
// INSTANCE
|
public id: string;
|
||||||
// ========
|
|
||||||
public registrationSessionManagerRef: RegistrationSessionManager;
|
|
||||||
|
|
||||||
public emailAddress: string;
|
@plugins.smartdata.svDb()
|
||||||
|
public data: plugins.idpInterfaces.data.IRegistrationSession['data'] = {
|
||||||
|
emailAddress: '',
|
||||||
|
hashedEmailToken: '',
|
||||||
|
smsCodeHash: null,
|
||||||
|
smsvalidationCounter: 0,
|
||||||
|
status: 'announced',
|
||||||
|
validUntil: 0,
|
||||||
|
createdAt: 0,
|
||||||
|
collectedData: {
|
||||||
|
userData: {
|
||||||
|
username: null,
|
||||||
|
connectedOrgs: [],
|
||||||
|
email: null,
|
||||||
|
name: null,
|
||||||
|
status: null,
|
||||||
|
mobileNumber: null,
|
||||||
|
password: null,
|
||||||
|
passwordHash: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* only used during testing
|
* only used during testing
|
||||||
*/
|
*/
|
||||||
public unhashedEmailToken?: string;
|
public unhashedEmailToken?: string;
|
||||||
public hashedEmailToken: string;
|
|
||||||
private smsvalidationCounter = 0;
|
|
||||||
public smsCode: string;
|
|
||||||
|
|
||||||
/**
|
public get emailAddress() {
|
||||||
* the status of the registration. should progress in a linear fashion.
|
return this.data.emailAddress;
|
||||||
*/
|
}
|
||||||
public status: 'announced' | 'emailValidated' | 'mobileVerified' | 'registered' | 'failed' =
|
|
||||||
'announced';
|
|
||||||
|
|
||||||
public collectedData: {
|
public get status() {
|
||||||
userData: plugins.idpInterfaces.data.IUser['data'];
|
return this.data.status;
|
||||||
} = {
|
}
|
||||||
userData: {
|
|
||||||
username: null,
|
|
||||||
connectedOrgs: [],
|
|
||||||
email: null,
|
|
||||||
name: null,
|
|
||||||
status: null,
|
|
||||||
mobileNumber: null,
|
|
||||||
password: null,
|
|
||||||
passwordHash: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
public set status(statusArg: plugins.idpInterfaces.data.TRegistrationSessionStatus) {
|
||||||
registrationSessionManagerRefArg: RegistrationSessionManager,
|
this.data.status = statusArg;
|
||||||
emailAddressArg: string
|
}
|
||||||
) {
|
|
||||||
this.registrationSessionManagerRef = registrationSessionManagerRefArg;
|
|
||||||
this.emailAddress = emailAddressArg;
|
|
||||||
this.registrationSessionManagerRef.registrationSessions.addToMap(this.emailAddress, this);
|
|
||||||
|
|
||||||
// lets destroy this after 10 minutes,
|
public get collectedData() {
|
||||||
// works in unrefed mode so not blocking node exiting.
|
return this.data.collectedData;
|
||||||
plugins.smartdelay.delayFor(600000, null, true).then(() => this.destroy());
|
}
|
||||||
|
|
||||||
|
public isExpired() {
|
||||||
|
return this.data.validUntil < Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* validates a token by comparing its hash against the stored hashed token
|
* validates a token by comparing its hash against the stored hashed token
|
||||||
* @param tokenArg
|
|
||||||
*/
|
*/
|
||||||
public validateEmailToken(tokenArg: string): boolean {
|
public async validateEmailToken(tokenArg: string): Promise<boolean> {
|
||||||
const result = this.hashedEmailToken === plugins.smarthash.sha256FromStringSync(tokenArg);
|
if (this.isExpired()) {
|
||||||
if (result && this.status === 'announced') {
|
await this.destroy();
|
||||||
this.status = 'emailValidated';
|
return false;
|
||||||
this.collectedData.userData.email = this.emailAddress;
|
|
||||||
}
|
}
|
||||||
if (!result && this.status === 'announced') {
|
|
||||||
this.status = 'failed';
|
const result = this.data.hashedEmailToken === RegistrationSession.hashToken(tokenArg);
|
||||||
|
if (result && this.data.status === 'announced') {
|
||||||
|
this.data.status = 'emailValidated';
|
||||||
|
this.data.collectedData.userData.email = this.data.emailAddress;
|
||||||
|
await this.save();
|
||||||
|
}
|
||||||
|
if (!result && this.data.status === 'announced') {
|
||||||
|
this.data.status = 'failed';
|
||||||
|
await this.save();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** validates the sms code */
|
/** validates the sms code */
|
||||||
public validateSmsCode(smsCodeArg: string) {
|
public async validateSmsCode(smsCodeArg: string) {
|
||||||
this.smsvalidationCounter++;
|
this.data.smsvalidationCounter++;
|
||||||
const result = this.smsCode === smsCodeArg;
|
const result = this.data.smsCodeHash === RegistrationSession.hashToken(smsCodeArg);
|
||||||
if (this.status === 'emailValidated' && result) {
|
if (this.data.status === 'emailValidated' && result) {
|
||||||
this.status = 'mobileVerified';
|
this.data.status = 'mobileVerified';
|
||||||
|
await this.save();
|
||||||
return result;
|
return result;
|
||||||
} else {
|
|
||||||
if (this.smsvalidationCounter === 5) {
|
|
||||||
this.destroy();
|
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
|
||||||
'Registration cancelled due to repeated wrong verification code submission'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.data.smsvalidationCounter >= 5) {
|
||||||
|
await this.destroy();
|
||||||
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
|
'Registration cancelled due to repeated wrong verification code submission'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.save();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* validate the email address with provider and dns sanity checks
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
public async validateEMailAddress(): Promise<plugins.smartmail.IEmailValidationResult> {
|
public async validateEMailAddress(): Promise<plugins.smartmail.IEmailValidationResult> {
|
||||||
console.log(`validating email ${this.emailAddress}`);
|
const result = await new plugins.smartmail.EmailAddressValidator().validate(this.data.emailAddress);
|
||||||
const result = await new plugins.smartmail.EmailAddressValidator().validate(this.emailAddress);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* send the validation email
|
|
||||||
*/
|
|
||||||
public async sendTokenValidationEmail() {
|
public async sendTokenValidationEmail() {
|
||||||
const uuidToSend = plugins.smartunique.uuid4();
|
const uuidToSend = plugins.smartunique.uuid4();
|
||||||
this.unhashedEmailToken = uuidToSend;
|
this.data.hashedEmailToken = RegistrationSession.hashToken(uuidToSend);
|
||||||
this.hashedEmailToken = plugins.smarthash.sha256FromStringSync(uuidToSend);
|
await this.save();
|
||||||
this.registrationSessionManagerRef.receptionRef.receptionMailer.sendRegistrationEmail(
|
this.manager.receptionRef.receptionMailer.sendRegistrationEmail(this, uuidToSend);
|
||||||
this,
|
logger.log('info', `sent a validation email with a verification code to ${this.data.emailAddress}`);
|
||||||
uuidToSend
|
return uuidToSend;
|
||||||
);
|
|
||||||
logger.log('info', `sent a validation email with a verification code to ${this.emailAddress}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* validate the mobile number of someone
|
|
||||||
*/
|
|
||||||
public async sendValidationSms() {
|
public async sendValidationSms() {
|
||||||
this.smsCode =
|
const smsCode =
|
||||||
await this.registrationSessionManagerRef.receptionRef.szPlatformClient.smsConnector.sendSmsVerifcation(
|
await this.manager.receptionRef.szPlatformClient.smsConnector.sendSmsVerifcation({
|
||||||
{
|
fromName: this.manager.receptionRef.options.name,
|
||||||
fromName: this.registrationSessionManagerRef.receptionRef.options.name,
|
toNumber: parseInt(this.data.collectedData.userData.mobileNumber),
|
||||||
toNumber: parseInt(this.collectedData.userData.mobileNumber),
|
});
|
||||||
}
|
this.data.smsCodeHash = RegistrationSession.hashToken(smsCode);
|
||||||
);
|
await this.save();
|
||||||
|
return smsCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* this method can be called when this registrationsession is validated
|
|
||||||
* and all data has been set
|
|
||||||
*/
|
|
||||||
public async manifestUserWithAccountData(): Promise<User> {
|
public async manifestUserWithAccountData(): Promise<User> {
|
||||||
if (this.status !== 'mobileVerified') {
|
if (this.data.status !== 'mobileVerified') {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'You can only manifest user that have a validated email Address and Mobile Number'
|
'You can only manifest user that have a validated email Address and Mobile Number'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!this.collectedData) {
|
if (!this.data.collectedData) {
|
||||||
throw new Error('You have to set the accountdata first');
|
throw new Error('You have to set the accountdata first');
|
||||||
}
|
}
|
||||||
const manifestedUser =
|
const manifestedUser = await this.manager.receptionRef.userManager.CUser.createNewUserForUserData(
|
||||||
await this.registrationSessionManagerRef.receptionRef.userManager.CUser.createNewUserForUserData(
|
this.data.collectedData.userData as plugins.idpInterfaces.data.IUser['data']
|
||||||
this.collectedData.userData
|
);
|
||||||
);
|
this.data.status = 'registered';
|
||||||
|
await this.save();
|
||||||
return manifestedUser;
|
return manifestedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public async destroy() {
|
||||||
* destroys the registrationsession
|
await this.delete();
|
||||||
*/
|
|
||||||
public destroy() {
|
|
||||||
this.registrationSessionManagerRef.registrationSessions.removeFromMap(this.emailAddress);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import { logger } from './logging.js';
|
|||||||
|
|
||||||
export class RegistrationSessionManager {
|
export class RegistrationSessionManager {
|
||||||
public receptionRef: Reception;
|
public receptionRef: Reception;
|
||||||
|
|
||||||
public registrationSessions = new plugins.lik.FastMap<RegistrationSession>();
|
|
||||||
public typedRouter = new plugins.typedrequest.TypedRouter();
|
public typedRouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
public get db() {
|
||||||
|
return this.receptionRef.db.smartdataDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CRegistrationSession = plugins.smartdata.setDefaultManagerForDoc(this, RegistrationSession);
|
||||||
|
|
||||||
constructor(receptionRefArg: Reception) {
|
constructor(receptionRefArg: Reception) {
|
||||||
this.receptionRef = receptionRefArg;
|
this.receptionRef = receptionRefArg;
|
||||||
this.receptionRef.typedrouter.addTypedRouter(this.typedRouter);
|
this.receptionRef.typedrouter.addTypedRouter(this.typedRouter);
|
||||||
@@ -29,17 +33,16 @@ export class RegistrationSessionManager {
|
|||||||
`We sent you an Email with more information.`
|
`We sent you an Email with more information.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// check for exiting SignupSession
|
|
||||||
const existingSession = this.registrationSessions.getByKey(requestData.email);
|
const existingSessions = await this.CRegistrationSession.getInstances({
|
||||||
if (existingSession) {
|
'data.emailAddress': requestData.email,
|
||||||
|
});
|
||||||
|
for (const existingSession of existingSessions) {
|
||||||
logger.log('warn', `destroyed old signupSession for ${requestData.email}`);
|
logger.log('warn', `destroyed old signupSession for ${requestData.email}`);
|
||||||
existingSession.destroy();
|
await existingSession.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// lets check the email before we create a signup session
|
|
||||||
|
|
||||||
const newSignupSession = await RegistrationSession.createRegistrationSessionForEmail(
|
const newSignupSession = await RegistrationSession.createRegistrationSessionForEmail(
|
||||||
this,
|
|
||||||
requestData.email
|
requestData.email
|
||||||
).catch((e: plugins.typedrequest.TypedResponseError) => {
|
).catch((e: plugins.typedrequest.TypedResponseError) => {
|
||||||
console.log(e.errorText);
|
console.log(e.errorText);
|
||||||
@@ -63,10 +66,7 @@ export class RegistrationSessionManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_AfterRegistrationEmailClicked>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_AfterRegistrationEmailClicked>(
|
||||||
'afterRegistrationEmailClicked',
|
'afterRegistrationEmailClicked',
|
||||||
async (requestData) => {
|
async (requestData) => {
|
||||||
console.log(requestData);
|
const signupSession = await this.findRegistrationSessionByToken(requestData.token);
|
||||||
const signupSession = await this.registrationSessions.find(async (itemArg) =>
|
|
||||||
itemArg.validateEmailToken(requestData.token)
|
|
||||||
);
|
|
||||||
if (signupSession) {
|
if (signupSession) {
|
||||||
return {
|
return {
|
||||||
email: signupSession.emailAddress,
|
email: signupSession.emailAddress,
|
||||||
@@ -86,9 +86,7 @@ export class RegistrationSessionManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_SetDataForRegistration>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_SetDataForRegistration>(
|
||||||
'setDataForRegistration',
|
'setDataForRegistration',
|
||||||
async (requestData) => {
|
async (requestData) => {
|
||||||
const registrationSession = await this.registrationSessions.find(async (itemArg) =>
|
const registrationSession = await this.findRegistrationSessionByToken(requestData.token);
|
||||||
itemArg.validateEmailToken(requestData.token)
|
|
||||||
);
|
|
||||||
if (!registrationSession) {
|
if (!registrationSession) {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'could not find a matching signupsession'
|
'could not find a matching signupsession'
|
||||||
@@ -114,9 +112,7 @@ export class RegistrationSessionManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_MobileVerificationForRegistration>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_MobileVerificationForRegistration>(
|
||||||
'mobileVerificationForRegistration',
|
'mobileVerificationForRegistration',
|
||||||
async (requestData) => {
|
async (requestData) => {
|
||||||
const registrationSession = await this.registrationSessions.find(async (itemArg) =>
|
const registrationSession = await this.findRegistrationSessionByToken(requestData.token);
|
||||||
itemArg.validateEmailToken(requestData.token)
|
|
||||||
);
|
|
||||||
if (!registrationSession) {
|
if (!registrationSession) {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'could not find a matching signupsession'
|
'could not find a matching signupsession'
|
||||||
@@ -131,17 +127,16 @@ export class RegistrationSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (requestData.mobileNumber) {
|
if (requestData.mobileNumber) {
|
||||||
registrationSession.status = 'emailValidated';
|
|
||||||
registrationSession.collectedData.userData.mobileNumber = requestData.mobileNumber;
|
registrationSession.collectedData.userData.mobileNumber = requestData.mobileNumber;
|
||||||
await registrationSession.sendValidationSms();
|
const smsCode = await registrationSession.sendValidationSms();
|
||||||
return {
|
return {
|
||||||
messageSent: true,
|
messageSent: true,
|
||||||
testOnlySmsCode: process.env.TEST_MODE ? registrationSession.smsCode : null,
|
testOnlySmsCode: process.env.TEST_MODE ? smsCode : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestData.verificationCode) {
|
if (requestData.verificationCode) {
|
||||||
const validationResult = registrationSession.validateSmsCode(
|
const validationResult = await registrationSession.validateSmsCode(
|
||||||
requestData.verificationCode
|
requestData.verificationCode
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -160,9 +155,7 @@ export class RegistrationSessionManager {
|
|||||||
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_FinishRegistration>(
|
new plugins.typedrequest.TypedHandler<plugins.idpInterfaces.request.IReq_FinishRegistration>(
|
||||||
'finishRegistration',
|
'finishRegistration',
|
||||||
async (requestData) => {
|
async (requestData) => {
|
||||||
const registrationSession = await this.registrationSessions.find(async (itemArg) =>
|
const registrationSession = await this.findRegistrationSessionByToken(requestData.token);
|
||||||
itemArg.validateEmailToken(requestData.token)
|
|
||||||
);
|
|
||||||
if (!registrationSession) {
|
if (!registrationSession) {
|
||||||
throw new plugins.typedrequest.TypedResponseError(
|
throw new plugins.typedrequest.TypedResponseError(
|
||||||
'could not find a matching signupsession'
|
'could not find a matching signupsession'
|
||||||
@@ -170,7 +163,7 @@ export class RegistrationSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resultingUser = await registrationSession.manifestUserWithAccountData();
|
const resultingUser = await registrationSession.manifestUserWithAccountData();
|
||||||
registrationSession.destroy();
|
await registrationSession.destroy();
|
||||||
this.receptionRef.receptionMailer.sendWelcomeEMail(resultingUser);
|
this.receptionRef.receptionMailer.sendWelcomeEMail(resultingUser);
|
||||||
return {
|
return {
|
||||||
accountData: {
|
accountData: {
|
||||||
@@ -187,4 +180,17 @@ export class RegistrationSessionManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findRegistrationSessionByToken(tokenArg: string) {
|
||||||
|
const registrationSession = await this.CRegistrationSession.getInstance({
|
||||||
|
'data.hashedEmailToken': RegistrationSession.hashToken(tokenArg),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!registrationSession) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await registrationSession.validateEmailToken(tokenArg);
|
||||||
|
return isValid ? registrationSession : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './loint-reception.activity.js';
|
export * from './loint-reception.activity.js';
|
||||||
export * from './loint-reception.app.js';
|
export * from './loint-reception.app.js';
|
||||||
|
export * from './loint-reception.emailactiontoken.js';
|
||||||
export * from './loint-reception.oidc.js';
|
export * from './loint-reception.oidc.js';
|
||||||
export * from './loint-reception.appconnection.js';
|
export * from './loint-reception.appconnection.js';
|
||||||
export * from './loint-reception.billingplan.js';
|
export * from './loint-reception.billingplan.js';
|
||||||
@@ -8,6 +9,7 @@ export * from './loint-reception.jwt.js';
|
|||||||
export * from './loint-reception.loginsession.js';
|
export * from './loint-reception.loginsession.js';
|
||||||
export * from './loint-reception.organization.js';
|
export * from './loint-reception.organization.js';
|
||||||
export * from './loint-reception.paddlecheckoutdata.js';
|
export * from './loint-reception.paddlecheckoutdata.js';
|
||||||
|
export * from './loint-reception.registrationsession.js';
|
||||||
export * from './loint-reception.role.js';
|
export * from './loint-reception.role.js';
|
||||||
export * from './loint-reception.user.js';
|
export * from './loint-reception.user.js';
|
||||||
export * from './loint-reception.userinvitation.js';
|
export * from './loint-reception.userinvitation.js';
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export type TEmailActionTokenAction = 'emailLogin' | 'passwordReset';
|
||||||
|
|
||||||
|
export interface IEmailActionToken {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
email: string;
|
||||||
|
action: TEmailActionTokenAction;
|
||||||
|
tokenHash: string;
|
||||||
|
validUntil: number;
|
||||||
|
createdAt: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export type TRegistrationSessionStatus =
|
||||||
|
| 'announced'
|
||||||
|
| 'emailValidated'
|
||||||
|
| 'mobileVerified'
|
||||||
|
| 'registered'
|
||||||
|
| 'failed';
|
||||||
|
|
||||||
|
export interface IRegistrationSession {
|
||||||
|
id: string;
|
||||||
|
data: {
|
||||||
|
emailAddress: string;
|
||||||
|
hashedEmailToken: string;
|
||||||
|
smsCodeHash?: string | null;
|
||||||
|
smsvalidationCounter: number;
|
||||||
|
status: TRegistrationSessionStatus;
|
||||||
|
validUntil: number;
|
||||||
|
createdAt: number;
|
||||||
|
collectedData: {
|
||||||
|
userData: {
|
||||||
|
username?: string | null;
|
||||||
|
connectedOrgs: string[];
|
||||||
|
email?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
status?: 'new' | 'active' | 'deleted' | 'suspended' | null;
|
||||||
|
mobileNumber?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
passwordHash?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@idp.global/idp.global',
|
name: '@idp.global/idp.global',
|
||||||
version: '1.17.1',
|
version: '1.18.0',
|
||||||
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
description: 'An identity provider software managing user authentications, registrations, and sessions.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user