Skip to content
9 changes: 8 additions & 1 deletion examples/rest-express-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ const accountsPassword = new AccountsPassword({
},
});

const accountsMongo = new Mongo(db);

const accountsServer = new AccountsServer(
{
db: new Mongo(db),
db: accountsMongo,
tokenSecret: 'secret',
},
{
Expand All @@ -58,6 +60,11 @@ accountsServer.on(ServerHooks.ValidateLogin, ({ user }) => {
// If you throw an error here it will be returned to the client.
});

// Set the required mongodb indexes once connection is successful
mongoose.connection.on('connected', async () => {
await accountsMongo.setupIndexes();
});

/**
* Load and expose the accounts-js middleware
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/database-manager/__tests__/database-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ describe('DatabaseManager', () => {
});

it('addResetPasswordToken should be called on sessionStorage', () => {
expect(databaseManager.addResetPasswordToken('userId', 'email', 'token', 'reason')).toBe(
expect(databaseManager.addResetPasswordToken('userId', 'email', 'token', 'reason', 1000)).toBe(
'userStorage'
);
});
Expand Down
38 changes: 30 additions & 8 deletions packages/database-mongo-password/__tests__/mongo-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ describe('MongoServicePassword', () => {

beforeEach(async () => {
await database.collection('users').deleteMany({});
await database.collection('resetPasswordTokens').deleteMany({});
});

afterAll(async () => {
Expand All @@ -49,16 +50,19 @@ describe('MongoServicePassword', () => {
it('should create indexes', async () => {
const mongoServicePassword = new MongoServicePassword({ database });
await mongoServicePassword.setupIndexes();
const ret = await database.collection('users').indexInformation();
expect(ret).toEqual({
expect(await database.collection('users').indexInformation()).toEqual({
_id_: [['_id', 1]],
'emails.address_1': [['emails.address', 1]],
'services.email.verificationTokens.token_1': [
['services.email.verificationTokens.token', 1],
],
'services.password.reset.token_1': [['services.password.reset.token', 1]],
username_1: [['username', 1]],
});
expect(await database.collection('resetPasswordTokens').indexInformation()).toEqual({
_id_: [['_id', 1]],
expireAt_1: [['expireAt', 1]],
token_1: [['token', 1]],
});
});
});

Expand Down Expand Up @@ -270,7 +274,13 @@ describe('MongoServicePassword', () => {
it('should return user', async () => {
const mongoServicePassword = new MongoServicePassword({ database });
const userId = await mongoServicePassword.createUser(user);
await mongoServicePassword.addResetPasswordToken(userId, '[email protected]', 'token', 'test');
await mongoServicePassword.addResetPasswordToken(
userId,
'[email protected]',
'token',
'test',
1000
);
const ret = await mongoServicePassword.findUserByResetPasswordToken('token');
expect(ret).toBeTruthy();
expect((ret as any)._id).toBeTruthy();
Expand Down Expand Up @@ -505,7 +515,13 @@ describe('MongoServicePassword', () => {
const testToken = 'testVerificationToken';
const testReason = 'testReason';
const userId = await mongoServicePassword.createUser(user);
await mongoServicePassword.addResetPasswordToken(userId, user.email, testToken, testReason);
await mongoServicePassword.addResetPasswordToken(
userId,
user.email,
testToken,
testReason,
1000
);
const userWithTokens = await mongoServicePassword.findUserByResetPasswordToken(testToken);
expect(userWithTokens).toBeTruthy();
await mongoServicePassword.removeAllResetPasswordTokens(userId);
Expand Down Expand Up @@ -553,15 +569,21 @@ describe('MongoServicePassword', () => {
});
const userId = await mongoServicePassword.createUser(user);
await expect(
mongoServicePassword.addResetPasswordToken(userId, '[email protected]', 'token', 'reset')
mongoServicePassword.addResetPasswordToken(userId, '[email protected]', 'token', 'reset', 1000)
).resolves.not.toThrowError();
});

it('should add a token', async () => {
const mongoServicePassword = new MongoServicePassword({ database });
const userId = await mongoServicePassword.createUser(user);
await mongoServicePassword.addResetPasswordToken(userId, '[email protected]', 'token', 'reset');
const retUser = await mongoServicePassword.findUserById(userId);
await mongoServicePassword.addResetPasswordToken(
userId,
'[email protected]',
'token',
'reset',
1000
);
const retUser = await mongoServicePassword.findUserByResetPasswordToken('token');
const services: any = retUser!.services;
expect(services.password.reset.length).toEqual(1);
expect(services.password.reset[0].address).toEqual('[email protected]');
Expand Down
94 changes: 61 additions & 33 deletions packages/database-mongo-password/src/mongo-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CreateUserServicePassword, DatabaseInterfaceServicePassword, User } fro
import { toMongoID } from './utils';

export interface MongoUser {
_id?: string | object;
_id: string | object;
username?: string;
services: {
password?: {
Expand All @@ -19,6 +19,16 @@ export interface MongoUser {
[key: string]: any;
}

export interface MongoResetPasswordToken {
_id: string | object;
userId: string;
token: string;
address: string;
when: Date;
reason: string;
expireAt: Date;
}

export interface MongoServicePasswordOptions {
/**
* Mongo database object.
Expand All @@ -29,6 +39,16 @@ export interface MongoServicePasswordOptions {
* Default 'users'.
*/
userCollectionName?: string;
/**
* The password reset token collection name;
* Default 'resetPasswordTokens'.
*/
resetPasswordTokenCollectionName?: string;
/**
* Should automatically delete the user reset password tokens when they expire via mongo TTL.
* Default to 'true'.
*/
resetPasswordTokenTTL?: boolean;
/**
* The timestamps for the users collection.
* Default 'createdAt' and 'updatedAt'.
Expand Down Expand Up @@ -60,6 +80,8 @@ export interface MongoServicePasswordOptions {

const defaultOptions = {
userCollectionName: 'users',
resetPasswordTokenCollectionName: 'resetPasswordTokens',
resetPasswordTokenTTL: true,
timestamps: {
createdAt: 'createdAt',
updatedAt: 'updatedAt',
Expand All @@ -76,6 +98,8 @@ export class MongoServicePassword implements DatabaseInterfaceServicePassword {
private database: Db;
// Mongo user collection
private userCollection: Collection;
// Mongo password reset token collection
private resetPasswordTokenCollection: Collection<MongoResetPasswordToken>;

constructor(options: MongoServicePasswordOptions) {
this.options = {
Expand All @@ -86,6 +110,9 @@ export class MongoServicePassword implements DatabaseInterfaceServicePassword {

this.database = this.options.database;
this.userCollection = this.database.collection(this.options.userCollectionName);
this.resetPasswordTokenCollection = this.database.collection(
this.options.resetPasswordTokenCollectionName
);
}

/**
Expand Down Expand Up @@ -113,10 +140,16 @@ export class MongoServicePassword implements DatabaseInterfaceServicePassword {
sparse: true,
});
// Token index used to verify a password reset request
await this.userCollection.createIndex('services.password.reset.token', {
await this.resetPasswordTokenCollection.createIndex('token', {
...options,
unique: true,
sparse: true,
});
if (this.options.resetPasswordTokenTTL) {
await this.resetPasswordTokenCollection.createIndex('expireAt', {
expireAfterSeconds: 0,
});
}
}

/**
Expand All @@ -129,7 +162,7 @@ export class MongoServicePassword implements DatabaseInterfaceServicePassword {
email,
...cleanUser
}: CreateUserServicePassword): Promise<string> {
const user: MongoUser = {
const user: Omit<MongoUser, '_id'> = {
...cleanUser,
services: {
password: {
Expand Down Expand Up @@ -227,11 +260,20 @@ export class MongoServicePassword implements DatabaseInterfaceServicePassword {
* @param token Reset password token used to query the user.
*/
public async findUserByResetPasswordToken(token: string): Promise<User | null> {
const user = await this.userCollection.findOne({
'services.password.reset.token': token,
});
const resetPasswordToken = await this.resetPasswordTokenCollection.findOne({ token });
if (!resetPasswordToken) {
return null;
}

const userId = this.options.convertUserIdToMongoObjectId
? toMongoID(resetPasswordToken.userId)
: resetPasswordToken.userId;
const user = await this.userCollection.findOne({ _id: userId });
if (user) {
user.id = user._id.toString();
user.services.password.reset = [
{ ...resetPasswordToken, when: resetPasswordToken.when.getTime() },
];
}
return user;
}
Expand Down Expand Up @@ -342,9 +384,6 @@ export class MongoServicePassword implements DatabaseInterfaceServicePassword {
'services.password.bcrypt': newPassword,
[this.options.timestamps.updatedAt]: this.options.dateProvider(),
},
$unset: {
'services.password.reset': '',
},
}
);
if (ret.result.nModified === 0) {
Expand Down Expand Up @@ -389,37 +428,26 @@ export class MongoServicePassword implements DatabaseInterfaceServicePassword {
userId: string,
email: string,
token: string,
reason: string
reason: string,
expireAfterSeconds: number
): Promise<void> {
const _id = this.options.convertUserIdToMongoObjectId ? toMongoID(userId) : userId;
await this.userCollection.updateOne(
{ _id },
{
$push: {
'services.password.reset': {
token,
address: email.toLowerCase(),
when: this.options.dateProvider(),
reason,
},
},
}
);
const now = new Date();
await this.resetPasswordTokenCollection.insertOne({
userId,
address: email.toLowerCase(),
token,
when: now,
reason,
// Set when the object should be removed by mongo TTL
expireAt: new Date(now.getTime() + expireAfterSeconds * 1000),
});
}

/**
* Remove all the reset password tokens for a user.
* @param userId Id used to update the user.
*/
public async removeAllResetPasswordTokens(userId: string): Promise<void> {
const id = this.options.convertUserIdToMongoObjectId ? toMongoID(userId) : userId;
await this.userCollection.updateOne(
{ _id: id },
{
$unset: {
'services.password.reset': '',
},
}
);
await this.resetPasswordTokenCollection.deleteMany({ userId });
}
}
28 changes: 23 additions & 5 deletions packages/database-mongo/__tests__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,13 @@ describe('Mongo', () => {

it('should return user', async () => {
const userId = await databaseTests.database.createUser(user);
await databaseTests.database.addResetPasswordToken(userId, '[email protected]', 'token', 'test');
await databaseTests.database.addResetPasswordToken(
userId,
'[email protected]',
'token',
'test',
1000
);
const ret = await databaseTests.database.findUserByResetPasswordToken('token');
expect(ret).toBeTruthy();
expect((ret as any)._id).toBeTruthy();
Expand Down Expand Up @@ -790,7 +796,13 @@ describe('Mongo', () => {
const testToken = 'testVerificationToken';
const testReason = 'testReason';
const userId = await databaseTests.database.createUser(user);
await databaseTests.database.addResetPasswordToken(userId, user.email, testToken, testReason);
await databaseTests.database.addResetPasswordToken(
userId,
user.email,
testToken,
testReason,
1000
);
const userWithTokens = await databaseTests.database.findUserByResetPasswordToken(testToken);
expect(userWithTokens).toBeTruthy();
await databaseTests.database.removeAllResetPasswordTokens(userId);
Expand Down Expand Up @@ -832,13 +844,19 @@ describe('Mongo', () => {
idProvider: () => new ObjectId().toString(),
});
const userId = await mongoOptions.createUser(user);
await mongoOptions.addResetPasswordToken(userId, '[email protected]', 'token', 'reset');
await mongoOptions.addResetPasswordToken(userId, '[email protected]', 'token', 'reset', 1000);
});

it('should add a token', async () => {
const userId = await databaseTests.database.createUser(user);
await databaseTests.database.addResetPasswordToken(userId, '[email protected]', 'token', 'reset');
const retUser = await databaseTests.database.findUserById(userId);
await databaseTests.database.addResetPasswordToken(
userId,
'[email protected]',
'token',
'reset',
1000
);
const retUser = await databaseTests.database.findUserByResetPasswordToken('token');
const services: any = retUser!.services;
expect(services.password.reset.length).toEqual(1);
expect(services.password.reset[0].address).toEqual('[email protected]');
Expand Down
11 changes: 9 additions & 2 deletions packages/database-mongo/src/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,16 @@ export class Mongo implements DatabaseInterface {
userId: string,
email: string,
token: string,
reason: string
reason: string,
expireAfterSeconds: number
): Promise<void> {
return this.servicePassword.addResetPasswordToken(userId, email, token, reason);
return this.servicePassword.addResetPasswordToken(
userId,
email,
token,
reason,
expireAfterSeconds
);
}

public async createSession(
Expand Down
4 changes: 2 additions & 2 deletions packages/password/__tests__/accounts-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ describe('AccountsPassword', () => {
} as any;
set(password.server, 'options.emailTemplates', {});
await password.sendResetPasswordEmail(email);
expect(addResetPasswordToken.mock.calls[0].length).toBe(4);
expect(addResetPasswordToken.mock.calls[0].length).toBe(5);
expect(prepareMail.mock.calls[0].length).toBe(6);
expect(sendMail.mock.calls[0].length).toBe(1);
});
Expand Down Expand Up @@ -610,7 +610,7 @@ describe('AccountsPassword', () => {
} as any;
set(password.server, 'options.emailTemplates', {});
await password.sendEnrollmentEmail(email);
expect(addResetPasswordToken.mock.calls[0].length).toBe(4);
expect(addResetPasswordToken.mock.calls[0].length).toBe(5);
expect(prepareMail.mock.calls[0].length).toBe(6);
expect(sendMail.mock.calls[0].length).toBe(1);
});
Expand Down
Loading