@accounts/mongo
Version:
MongoDB adaptor for accounts
257 lines (223 loc) • 8.38 kB
text/typescript
import { type Collection, type Db, ObjectId, type CreateIndexesOptions } from 'mongodb';
import {
type ConnectionInformations,
type CreateUserServicePassword,
type DatabaseInterface,
type User,
type Session,
} from '@accounts/types';
import { MongoSessions } from '@accounts/mongo-sessions';
import { MongoServicePassword } from '@accounts/mongo-password';
import { MongoServiceMagicLink } from '@accounts/mongo-magic-link';
import { AccountsMongoConfigToken, type AccountsMongoOptions } from './types';
import { MongoConnectionToken } from './types/MongoConnection.symbol';
import { Inject, Injectable } from 'graphql-modules';
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
const toMongoID = (objectId: string | ObjectId) => {
if (typeof objectId === 'string') {
return new ObjectId(objectId);
}
return objectId;
};
const defaultOptions = {
collectionName: 'users',
timestamps: {
createdAt: 'createdAt',
updatedAt: 'updatedAt',
},
convertUserIdToMongoObjectId: true,
convertSessionIdToMongoObjectId: true,
caseSensitiveUserName: true,
dateProvider: (date?: Date) => (date ? date.getTime() : Date.now()),
};
export class Mongo implements DatabaseInterface {
// Options of Mongo class
private options: AccountsMongoOptions & typeof defaultOptions;
// Database connection
private dbConn: Db;
// Account collection
private collection: Collection<PartialBy<User & { _id?: string | object }, 'id' | 'deactivated'>>;
// Session adaptor
private sessions: MongoSessions;
// Password service
private servicePassword: MongoServicePassword;
// Magic link service
private serviceMagicLink: MongoServiceMagicLink;
constructor(
dbConn: any,
options: AccountsMongoOptions = {}
) {
this.options = {
...defaultOptions,
...options,
timestamps: { ...defaultOptions.timestamps, ...options.timestamps },
};
if (!dbConn) {
throw new Error('A database connection is required');
}
this.dbConn = dbConn;
this.collection = this.dbConn.collection(this.options.collectionName);
this.sessions = new MongoSessions({ ...this.options, database: this.dbConn });
this.servicePassword = new MongoServicePassword({
...this.options,
userCollectionName: this.options.collectionName,
database: this.dbConn,
});
this.serviceMagicLink = new MongoServiceMagicLink({
...this.options,
userCollectionName: this.options.collectionName,
database: this.dbConn,
});
}
/**
* Setup the mongo indexes needed.
* @param options Options passed to the mongo native `createIndex` method.
*/
public async setupIndexes(
options: Omit<CreateIndexesOptions, 'unique' | 'sparse'> = {}
): Promise<void> {
await this.sessions.setupIndexes(options);
await this.servicePassword.setupIndexes(options);
await this.serviceMagicLink.setupIndexes(options);
}
public async findUserById(userId: string): Promise<User | null> {
return this.servicePassword.findUserById(userId);
}
public async createUser(user: CreateUserServicePassword): Promise<string> {
return this.servicePassword.createUser(user);
}
public async findUserByEmail(email: string): Promise<User | null> {
return this.servicePassword.findUserByEmail(email);
}
public async findUserByUsername(username: string): Promise<User | null> {
return this.servicePassword.findUserByUsername(username);
}
public async findPasswordHash(userId: string): Promise<string | null> {
return this.servicePassword.findPasswordHash(userId);
}
public async findUserByEmailVerificationToken(token: string): Promise<User | null> {
return this.servicePassword.findUserByEmailVerificationToken(token);
}
public async findUserByResetPasswordToken(token: string): Promise<User | null> {
return this.servicePassword.findUserByResetPasswordToken(token);
}
public async addEmail(userId: string, newEmail: string, verified: boolean): Promise<void> {
return this.servicePassword.addEmail(userId, newEmail, verified);
}
public async removeEmail(userId: string, email: string): Promise<void> {
return this.servicePassword.removeEmail(userId, email);
}
public async verifyEmail(userId: string, email: string): Promise<void> {
return this.servicePassword.verifyEmail(userId, email);
}
public async setUsername(userId: string, newUsername: string): Promise<void> {
return this.servicePassword.setUsername(userId, newUsername);
}
public async setPassword(userId: string, newPassword: string): Promise<void> {
return this.servicePassword.setPassword(userId, newPassword);
}
public async removeAllResetPasswordTokens(userId: string): Promise<void> {
return this.servicePassword.removeAllResetPasswordTokens(userId);
}
public async addEmailVerificationToken(
userId: string,
email: string,
token: string
): Promise<void> {
return this.servicePassword.addEmailVerificationToken(userId, email, token);
}
public async addResetPasswordToken(
userId: string,
email: string,
token: string,
reason: string
): Promise<void> {
return this.servicePassword.addResetPasswordToken(userId, email, token, reason);
}
public async addLoginToken(userId: string, email: string, token: string): Promise<void> {
return this.serviceMagicLink.addLoginToken(userId, email, token);
}
public async findUserByLoginToken(token: string): Promise<User | null> {
return this.serviceMagicLink.findUserByLoginToken(token);
}
public async removeAllLoginTokens(userId: string): Promise<void> {
return this.serviceMagicLink.removeAllLoginTokens(userId);
}
public async createSession(
userId: string,
token: string,
connection: ConnectionInformations = {},
extraData?: object
): Promise<string> {
return this.sessions.createSession(userId, token, connection, extraData);
}
public async findSessionById(sessionId: string): Promise<Session | null> {
return this.sessions.findSessionById(sessionId);
}
public async findSessionByToken(token: string): Promise<Session | null> {
return this.sessions.findSessionByToken(token);
}
public async updateSession(
sessionId: string,
connection: ConnectionInformations,
newToken?: string
): Promise<void> {
return this.sessions.updateSession(sessionId, connection, newToken);
}
public async invalidateSession(sessionId: string): Promise<void> {
return this.sessions.invalidateSession(sessionId);
}
public async invalidateAllSessions(userId: string, excludedSessionIds?: string[]): Promise<void> {
return this.sessions.invalidateAllSessions(userId, excludedSessionIds);
}
public async findUserByServiceId(serviceName: string, serviceId: string): Promise<User | null> {
const user = await this.collection.findOne({
[`services.${serviceName}.id`]: serviceId,
});
if (user) {
user.id = user._id.toString();
}
return user as User;
}
public async setService(userId: string, serviceName: string, service: object): Promise<void> {
const id = this.options.convertUserIdToMongoObjectId ? toMongoID(userId) : userId;
await this.collection.updateOne(
{ _id: id },
{
$set: {
[`services.${serviceName}`]: service,
[this.options.timestamps.updatedAt]: this.options.dateProvider(),
},
}
);
}
public async unsetService(userId: string, serviceName: string): Promise<void> {
const id = this.options.convertUserIdToMongoObjectId ? toMongoID(userId) : userId;
await this.collection.updateOne(
{ _id: id },
{
$set: {
[this.options.timestamps.updatedAt]: this.options.dateProvider(),
},
$unset: {
[`services.${serviceName}`]: '',
},
}
);
}
public async setUserDeactivated(userId: string, deactivated: boolean): Promise<void> {
const id = this.options.convertUserIdToMongoObjectId ? toMongoID(userId) : userId;
await this.collection.updateOne(
{ _id: id },
{
$set: {
deactivated,
[this.options.timestamps.updatedAt]: this.options.dateProvider(),
},
}
);
}
}