Feature: Reuse tokens if they haven't expired (#7017)
* Reuse tokens if they haven't expired * Fix failing tests * Update UserController.js * Update tests * Tests for invalid config * restart tests
This commit is contained in:
@@ -510,7 +510,7 @@ describe('Email Verification Token Expiration: ', () => {
|
|||||||
userAfterEmailReset._email_verify_token
|
userAfterEmailReset._email_verify_token
|
||||||
);
|
);
|
||||||
expect(userBeforeEmailReset._email_verify_token_expires_at).not.toEqual(
|
expect(userBeforeEmailReset._email_verify_token_expires_at).not.toEqual(
|
||||||
userAfterEmailReset.__email_verify_token_expires_at
|
userAfterEmailReset._email_verify_token_expires_at
|
||||||
);
|
);
|
||||||
expect(sendEmailOptions).toBeDefined();
|
expect(sendEmailOptions).toBeDefined();
|
||||||
done();
|
done();
|
||||||
@@ -594,7 +594,7 @@ describe('Email Verification Token Expiration: ', () => {
|
|||||||
userAfterRequest._email_verify_token
|
userAfterRequest._email_verify_token
|
||||||
);
|
);
|
||||||
expect(userBeforeRequest._email_verify_token_expires_at).not.toEqual(
|
expect(userBeforeRequest._email_verify_token_expires_at).not.toEqual(
|
||||||
userAfterRequest.__email_verify_token_expires_at
|
userAfterRequest._email_verify_token_expires_at
|
||||||
);
|
);
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
@@ -604,6 +604,110 @@ describe('Email Verification Token Expiration: ', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should throw with invalid emailVerifyTokenReuseIfValid', async done => {
|
||||||
|
const sendEmailOptions = [];
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: () => Promise.resolve(),
|
||||||
|
sendPasswordResetEmail: options => {
|
||||||
|
sendEmailOptions.push(options);
|
||||||
|
},
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'passwordPolicy',
|
||||||
|
verifyUserEmails: true,
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
emailVerifyTokenValidityDuration: 5 * 60, // 5 minutes
|
||||||
|
emailVerifyTokenReuseIfValid: [],
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
fail('should have thrown.');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBe('emailVerifyTokenReuseIfValid must be a boolean value');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'passwordPolicy',
|
||||||
|
verifyUserEmails: true,
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
emailVerifyTokenReuseIfValid: true,
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
fail('should have thrown.');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBe(
|
||||||
|
'You cannot use emailVerifyTokenReuseIfValid without emailVerifyTokenValidityDuration'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match codes with emailVerifyTokenReuseIfValid', async done => {
|
||||||
|
let sendEmailOptions;
|
||||||
|
let sendVerificationEmailCallCount = 0;
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: options => {
|
||||||
|
sendEmailOptions = options;
|
||||||
|
sendVerificationEmailCallCount++;
|
||||||
|
},
|
||||||
|
sendPasswordResetEmail: () => Promise.resolve(),
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'emailVerifyToken',
|
||||||
|
verifyUserEmails: true,
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
emailVerifyTokenValidityDuration: 5 * 60, // 5 minutes
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
emailVerifyTokenReuseIfValid: true,
|
||||||
|
});
|
||||||
|
const user = new Parse.User();
|
||||||
|
user.setUsername('resends_verification_token');
|
||||||
|
user.setPassword('expiringToken');
|
||||||
|
user.set('email', 'user@example.com');
|
||||||
|
await user.signUp();
|
||||||
|
|
||||||
|
const config = Config.get('test');
|
||||||
|
const [userBeforeRequest] = await config.database.find('_User', {
|
||||||
|
username: 'resends_verification_token',
|
||||||
|
});
|
||||||
|
// store this user before we make our email request
|
||||||
|
expect(sendVerificationEmailCallCount).toBe(1);
|
||||||
|
await new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
const response = await request({
|
||||||
|
url: 'http://localhost:8378/1/verificationEmailRequest',
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
email: 'user@example.com',
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': Parse.applicationId,
|
||||||
|
'X-Parse-REST-API-Key': 'rest',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(sendVerificationEmailCallCount).toBe(2);
|
||||||
|
expect(sendEmailOptions).toBeDefined();
|
||||||
|
|
||||||
|
const [userAfterRequest] = await config.database.find('_User', {
|
||||||
|
username: 'resends_verification_token',
|
||||||
|
});
|
||||||
|
|
||||||
|
// verify that our token & expiration has been changed for this new request
|
||||||
|
expect(typeof userAfterRequest).toBe('object');
|
||||||
|
expect(userBeforeRequest._email_verify_token).toEqual(userAfterRequest._email_verify_token);
|
||||||
|
expect(userBeforeRequest._email_verify_token_expires_at).toEqual(
|
||||||
|
userAfterRequest._email_verify_token_expires_at
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
it('should not send a new verification email when a resend is requested and the user is VERIFIED', done => {
|
it('should not send a new verification email when a resend is requested and the user is VERIFIED', done => {
|
||||||
const user = new Parse.User();
|
const user = new Parse.User();
|
||||||
let sendEmailOptions;
|
let sendEmailOptions;
|
||||||
|
|||||||
@@ -122,6 +122,102 @@ describe('Password Policy: ', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not keep reset token by default', async done => {
|
||||||
|
const sendEmailOptions = [];
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: () => Promise.resolve(),
|
||||||
|
sendPasswordResetEmail: options => {
|
||||||
|
sendEmailOptions.push(options);
|
||||||
|
},
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'passwordPolicy',
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
passwordPolicy: {
|
||||||
|
resetTokenValidityDuration: 5 * 60, // 5 minutes
|
||||||
|
},
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
const user = new Parse.User();
|
||||||
|
user.setUsername('testResetTokenValidity');
|
||||||
|
user.setPassword('original');
|
||||||
|
user.set('email', 'user@example.com');
|
||||||
|
await user.signUp();
|
||||||
|
await Parse.User.requestPasswordReset('user@example.com');
|
||||||
|
await Parse.User.requestPasswordReset('user@example.com');
|
||||||
|
expect(sendEmailOptions[0].link).not.toBe(sendEmailOptions[1].link);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep reset token with resetTokenReuseIfValid', async done => {
|
||||||
|
const sendEmailOptions = [];
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: () => Promise.resolve(),
|
||||||
|
sendPasswordResetEmail: options => {
|
||||||
|
sendEmailOptions.push(options);
|
||||||
|
},
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'passwordPolicy',
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
passwordPolicy: {
|
||||||
|
resetTokenValidityDuration: 5 * 60, // 5 minutes
|
||||||
|
resetTokenReuseIfValid: true,
|
||||||
|
},
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
const user = new Parse.User();
|
||||||
|
user.setUsername('testResetTokenValidity');
|
||||||
|
user.setPassword('original');
|
||||||
|
user.set('email', 'user@example.com');
|
||||||
|
await user.signUp();
|
||||||
|
await Parse.User.requestPasswordReset('user@example.com');
|
||||||
|
await Parse.User.requestPasswordReset('user@example.com');
|
||||||
|
expect(sendEmailOptions[0].link).toBe(sendEmailOptions[1].link);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw with invalid resetTokenReuseIfValid', async done => {
|
||||||
|
const sendEmailOptions = [];
|
||||||
|
const emailAdapter = {
|
||||||
|
sendVerificationEmail: () => Promise.resolve(),
|
||||||
|
sendPasswordResetEmail: options => {
|
||||||
|
sendEmailOptions.push(options);
|
||||||
|
},
|
||||||
|
sendMail: () => {},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'passwordPolicy',
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
passwordPolicy: {
|
||||||
|
resetTokenValidityDuration: 5 * 60, // 5 minutes
|
||||||
|
resetTokenReuseIfValid: [],
|
||||||
|
},
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
fail('should have thrown.');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBe('resetTokenReuseIfValid must be a boolean value');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await reconfigureServer({
|
||||||
|
appName: 'passwordPolicy',
|
||||||
|
emailAdapter: emailAdapter,
|
||||||
|
passwordPolicy: {
|
||||||
|
resetTokenReuseIfValid: true,
|
||||||
|
},
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
fail('should have thrown.');
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBe('You cannot use resetTokenReuseIfValid without resetTokenValidityDuration');
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
it('should fail if passwordPolicy.resetTokenValidityDuration is not a number', done => {
|
it('should fail if passwordPolicy.resetTokenValidityDuration is not a number', done => {
|
||||||
reconfigureServer({
|
reconfigureServer({
|
||||||
appName: 'passwordPolicy',
|
appName: 'passwordPolicy',
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export class Config {
|
|||||||
readOnlyMasterKey,
|
readOnlyMasterKey,
|
||||||
allowHeaders,
|
allowHeaders,
|
||||||
idempotencyOptions,
|
idempotencyOptions,
|
||||||
|
emailVerifyTokenReuseIfValid,
|
||||||
}) {
|
}) {
|
||||||
if (masterKey === readOnlyMasterKey) {
|
if (masterKey === readOnlyMasterKey) {
|
||||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||||
@@ -82,6 +83,7 @@ export class Config {
|
|||||||
appName,
|
appName,
|
||||||
publicServerURL,
|
publicServerURL,
|
||||||
emailVerifyTokenValidityDuration,
|
emailVerifyTokenValidityDuration,
|
||||||
|
emailVerifyTokenReuseIfValid,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +192,16 @@ export class Config {
|
|||||||
) {
|
) {
|
||||||
throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20';
|
throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
passwordPolicy.resetTokenReuseIfValid &&
|
||||||
|
typeof passwordPolicy.resetTokenReuseIfValid !== 'boolean'
|
||||||
|
) {
|
||||||
|
throw 'resetTokenReuseIfValid must be a boolean value';
|
||||||
|
}
|
||||||
|
if (passwordPolicy.resetTokenReuseIfValid && !passwordPolicy.resetTokenValidityDuration) {
|
||||||
|
throw 'You cannot use resetTokenReuseIfValid without resetTokenValidityDuration';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +219,7 @@ export class Config {
|
|||||||
appName,
|
appName,
|
||||||
publicServerURL,
|
publicServerURL,
|
||||||
emailVerifyTokenValidityDuration,
|
emailVerifyTokenValidityDuration,
|
||||||
|
emailVerifyTokenReuseIfValid,
|
||||||
}) {
|
}) {
|
||||||
if (!emailAdapter) {
|
if (!emailAdapter) {
|
||||||
throw 'An emailAdapter is required for e-mail verification and password resets.';
|
throw 'An emailAdapter is required for e-mail verification and password resets.';
|
||||||
@@ -224,6 +237,12 @@ export class Config {
|
|||||||
throw 'Email verify token validity duration must be a value greater than 0.';
|
throw 'Email verify token validity duration must be a value greater than 0.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (emailVerifyTokenReuseIfValid && typeof emailVerifyTokenReuseIfValid !== 'boolean') {
|
||||||
|
throw 'emailVerifyTokenReuseIfValid must be a boolean value';
|
||||||
|
}
|
||||||
|
if (emailVerifyTokenReuseIfValid && !emailVerifyTokenValidityDuration) {
|
||||||
|
throw 'You cannot use emailVerifyTokenReuseIfValid without emailVerifyTokenValidityDuration';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static validateMasterKeyIps(masterKeyIps) {
|
static validateMasterKeyIps(masterKeyIps) {
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ export class UserController extends AdaptableController {
|
|||||||
}
|
}
|
||||||
if (expiresDate < new Date()) throw 'The password reset link has expired';
|
if (expiresDate < new Date()) throw 'The password reset link has expired';
|
||||||
}
|
}
|
||||||
|
|
||||||
return results[0];
|
return results[0];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -158,6 +157,19 @@ export class UserController extends AdaptableController {
|
|||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
regenerateEmailVerifyToken(user) {
|
regenerateEmailVerifyToken(user) {
|
||||||
|
const { _email_verify_token } = user;
|
||||||
|
let { _email_verify_token_expires_at } = user;
|
||||||
|
if (_email_verify_token_expires_at && _email_verify_token_expires_at.__type === 'Date') {
|
||||||
|
_email_verify_token_expires_at = _email_verify_token_expires_at.iso;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.config.emailVerifyTokenReuseIfValid &&
|
||||||
|
this.config.emailVerifyTokenValidityDuration &&
|
||||||
|
_email_verify_token &&
|
||||||
|
new Date() < new Date(_email_verify_token_expires_at)
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
this.setEmailVerifyToken(user);
|
this.setEmailVerifyToken(user);
|
||||||
return this.config.database.update('_User', { username: user.username }, user);
|
return this.config.database.update('_User', { username: user.username }, user);
|
||||||
}
|
}
|
||||||
@@ -191,36 +203,57 @@ export class UserController extends AdaptableController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendPasswordResetEmail(email) {
|
async sendPasswordResetEmail(email) {
|
||||||
if (!this.adapter) {
|
if (!this.adapter) {
|
||||||
throw 'Trying to send a reset password but no adapter is set';
|
throw 'Trying to send a reset password but no adapter is set';
|
||||||
// TODO: No adapter?
|
// TODO: No adapter?
|
||||||
}
|
}
|
||||||
|
let user;
|
||||||
return this.setPasswordResetToken(email).then(user => {
|
if (
|
||||||
const token = encodeURIComponent(user._perishable_token);
|
this.config.passwordPolicy &&
|
||||||
const username = encodeURIComponent(user.username);
|
this.config.passwordPolicy.resetTokenReuseIfValid &&
|
||||||
|
this.config.passwordPolicy.resetTokenValidityDuration
|
||||||
const link = buildEmailLink(
|
) {
|
||||||
this.config.requestResetPasswordURL,
|
const results = await this.config.database.find(
|
||||||
username,
|
'_User',
|
||||||
token,
|
{
|
||||||
this.config
|
$or: [
|
||||||
|
{ email, _perishable_token: { $exists: true } },
|
||||||
|
{ username: email, email: { $exists: false }, _perishable_token: { $exists: true } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ limit: 1 }
|
||||||
);
|
);
|
||||||
const options = {
|
if (results.length == 1) {
|
||||||
appName: this.config.appName,
|
let expiresDate = results[0]._perishable_token_expires_at;
|
||||||
link: link,
|
if (expiresDate && expiresDate.__type == 'Date') {
|
||||||
user: inflate('_User', user),
|
expiresDate = new Date(expiresDate.iso);
|
||||||
};
|
}
|
||||||
|
if (expiresDate > new Date()) {
|
||||||
if (this.adapter.sendPasswordResetEmail) {
|
user = results[0];
|
||||||
this.adapter.sendPasswordResetEmail(options);
|
}
|
||||||
} else {
|
|
||||||
this.adapter.sendMail(this.defaultResetPasswordEmail(options));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (!user || !user._perishable_token) {
|
||||||
|
user = await this.setPasswordResetToken(email);
|
||||||
|
}
|
||||||
|
const token = encodeURIComponent(user._perishable_token);
|
||||||
|
const username = encodeURIComponent(user.username);
|
||||||
|
|
||||||
return Promise.resolve(user);
|
const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config);
|
||||||
});
|
const options = {
|
||||||
|
appName: this.config.appName,
|
||||||
|
link: link,
|
||||||
|
user: inflate('_User', user),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.adapter.sendPasswordResetEmail) {
|
||||||
|
this.adapter.sendPasswordResetEmail(options);
|
||||||
|
} else {
|
||||||
|
this.adapter.sendMail(this.defaultResetPasswordEmail(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePassword(username, token, password) {
|
updatePassword(username, token, password) {
|
||||||
|
|||||||
@@ -125,6 +125,12 @@ module.exports.ParseServerOptions = {
|
|||||||
help: 'Adapter module for email sending',
|
help: 'Adapter module for email sending',
|
||||||
action: parsers.moduleOrObjectParser,
|
action: parsers.moduleOrObjectParser,
|
||||||
},
|
},
|
||||||
|
emailVerifyTokenReuseIfValid: {
|
||||||
|
env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_REUSE_IF_VALID',
|
||||||
|
help: 'an existing password reset token should be reused when a password reset is requested',
|
||||||
|
action: parsers.booleanParser,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
emailVerifyTokenValidityDuration: {
|
emailVerifyTokenValidityDuration: {
|
||||||
env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION',
|
env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION',
|
||||||
help: 'Email verification token validity duration, in seconds',
|
help: 'Email verification token validity duration, in seconds',
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
* @property {Boolean} directAccess Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.
|
* @property {Boolean} directAccess Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.
|
||||||
* @property {String} dotNetKey Key for Unity and .Net SDK
|
* @property {String} dotNetKey Key for Unity and .Net SDK
|
||||||
* @property {Adapter<MailAdapter>} emailAdapter Adapter module for email sending
|
* @property {Adapter<MailAdapter>} emailAdapter Adapter module for email sending
|
||||||
|
* @property {Boolean} emailVerifyTokenReuseIfValid an existing password reset token should be reused when a password reset is requested
|
||||||
* @property {Number} emailVerifyTokenValidityDuration Email verification token validity duration, in seconds
|
* @property {Number} emailVerifyTokenValidityDuration Email verification token validity duration, in seconds
|
||||||
* @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
|
* @property {Boolean} enableAnonymousUsers Enable (or disable) anonymous users, defaults to true
|
||||||
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
|
* @property {Boolean} enableExpressErrorHandler Enables the default express error handler for all errors
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ export interface ParseServerOptions {
|
|||||||
preventLoginWithUnverifiedEmail: ?boolean;
|
preventLoginWithUnverifiedEmail: ?boolean;
|
||||||
/* Email verification token validity duration, in seconds */
|
/* Email verification token validity duration, in seconds */
|
||||||
emailVerifyTokenValidityDuration: ?number;
|
emailVerifyTokenValidityDuration: ?number;
|
||||||
|
/* an existing password reset token should be reused when resend verification is requested
|
||||||
|
:DEFAULT: false */
|
||||||
|
emailVerifyTokenReuseIfValid: ?boolean;
|
||||||
/* account lockout policy for failed login attempts */
|
/* account lockout policy for failed login attempts */
|
||||||
accountLockout: ?any;
|
accountLockout: ?any;
|
||||||
/* Password policy for enforcing password related rules */
|
/* Password policy for enforcing password related rules */
|
||||||
|
|||||||
@@ -308,6 +308,7 @@ export class UsersRouter extends ClassesRouter {
|
|||||||
appName: req.config.appName,
|
appName: req.config.appName,
|
||||||
publicServerURL: req.config.publicServerURL,
|
publicServerURL: req.config.publicServerURL,
|
||||||
emailVerifyTokenValidityDuration: req.config.emailVerifyTokenValidityDuration,
|
emailVerifyTokenValidityDuration: req.config.emailVerifyTokenValidityDuration,
|
||||||
|
emailVerifyTokenReuseIfValid: req.config.emailVerifyTokenReuseIfValid,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (typeof e === 'string') {
|
if (typeof e === 'string') {
|
||||||
|
|||||||
Reference in New Issue
Block a user