@accounts/mongo-magic-link
Version:
MongoDB magic link adaptor for accounts
121 lines (110 loc) • 3.57 kB
text/typescript
import { type Collection, type Db, type CreateIndexesOptions } from 'mongodb';
import { type DatabaseInterfaceServiceMagicLink, type User } from '@accounts/types';
import { toMongoID } from './utils';
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export interface MongoServiceMagicLinkOptions {
/**
* Mongo database object.
*/
database: Db;
/**
* The users collection name.
* Default 'users'.
*/
userCollectionName?: string;
/**
* Should the user collection use _id as string or ObjectId.
* Default 'true'.
*/
convertUserIdToMongoObjectId?: boolean;
/**
* Function that generate the date for the timestamps.
* Default to `(date?: Date) => (date ? date.getTime() : Date.now())`.
*/
dateProvider?: (date?: Date) => any;
}
const defaultOptions = {
userCollectionName: 'users',
convertUserIdToMongoObjectId: true,
dateProvider: (date?: Date) => (date ? date.getTime() : Date.now()),
};
export class MongoServiceMagicLink implements DatabaseInterfaceServiceMagicLink {
// Merged options that can be used
private options: MongoServiceMagicLinkOptions & typeof defaultOptions;
// Mongo database object
private database: Db;
// Mongo user collection
private userCollection: Collection<
PartialBy<User & { _id?: string | object }, 'id' | 'deactivated'>
>;
constructor(options: MongoServiceMagicLinkOptions) {
this.options = {
...defaultOptions,
...options,
};
this.database = this.options.database;
this.userCollection = this.database.collection(this.options.userCollectionName);
}
/**
* Setup the mongo indexes needed for the token service.
* @param options Options passed to the mongo native `createIndex` method.
*/
public async setupIndexes(
options: Omit<CreateIndexesOptions, 'unique' | 'sparse'> = {}
): Promise<void> {
// Token index used to verify the email address of a user
await this.userCollection.createIndex('services.magicLink.loginTokens.token', {
...options,
sparse: true,
});
}
/**
* Find a user from a login token.
* @param token Random token used to allow user to login.
*/
public async findUserByLoginToken(token: string): Promise<User | null> {
const user = await this.userCollection.findOne({
'services.magicLink.loginTokens.token': token,
});
if (user) {
user.id = user._id.toString();
}
return user as User;
}
/**
* Add a login token to a user.
* @param userId Id used to update the user.
* @param email Which address of the user's to link the token to.
* @param token Random token used to allow user to login.
*/
public async addLoginToken(userId: string, email: string, token: string): Promise<void> {
const _id = this.options.convertUserIdToMongoObjectId ? toMongoID(userId) : userId;
await this.userCollection.updateOne(
{ _id },
{
$push: {
'services.magicLink.loginTokens': {
token,
address: email.toLowerCase(),
when: this.options.dateProvider(),
},
},
}
);
}
/**
* Remove all the login tokens for a user.
* @param userId Id used to update the user.
*/
public async removeAllLoginTokens(userId: string): Promise<void> {
const id = this.options.convertUserIdToMongoObjectId ? toMongoID(userId) : userId;
await this.userCollection.updateOne(
{ _id: id },
{
$unset: {
'services.magicLink.loginTokens': '',
},
}
);
}
}