UNPKG

@accounts/mongo-sessions

Version:
239 lines (216 loc) 7.66 kB
import { type Collection, type Db, type ObjectId, type CreateIndexesOptions } from 'mongodb'; import { type ConnectionInformations, type DatabaseInterfaceSessions, type Session, } from '@accounts/types'; import { toMongoID } from './utils'; type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; export interface MongoSessionsOptions { /** * Mongo database object. */ database: Db; /** * The sessions collection name. * Default 'sessions'. */ sessionCollectionName?: string; /** * The timestamps for the sessions collection. * Default 'createdAt' and 'updatedAt'. */ timestamps?: { createdAt: string; updatedAt: string; }; /** * Should the session collection use _id as string or ObjectId. * If 'false' must include an 'idSessionProvider'. * Default 'true'. */ convertSessionIdToMongoObjectId?: boolean; /** * Function that generates the _id for new Session objects. * If 'undefined' then 'convertSessionIdToMongoObjectId' must be 'true'. * Default 'undefined' */ idSessionProvider?: () => string | object; /** * Function that generate the id for new User objects. */ idProvider?: () => string | object; /** * Function that generate the date for the timestamps. * Default to `(date?: Date) => (date ? date.getTime() : Date.now())`. */ dateProvider?: (date?: Date) => any; } const defaultOptions = { sessionCollectionName: 'sessions', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt', }, convertSessionIdToMongoObjectId: true, dateProvider: (date?: Date) => (date ? date.getTime() : Date.now()), }; export class MongoSessions implements DatabaseInterfaceSessions { // Merged options that can be used private options: MongoSessionsOptions & typeof defaultOptions; // Mongo database object private database: Db; // Mongo session collection private sessionCollection: Collection< PartialBy<Session & { _id?: string | object }, 'createdAt' | 'updatedAt' | 'id'> >; constructor(options: MongoSessionsOptions) { this.options = { ...defaultOptions, ...options, timestamps: { ...defaultOptions.timestamps, ...options.timestamps }, }; if ( typeof this.options.idSessionProvider === 'function' && this.options.convertSessionIdToMongoObjectId ) { console.warn(`You have set both "options.idSessionProvider" and "options.convertSessionIdToMongoObjectId = true" which will cause your "options.idSessionProvider" to be ignored. In order to fix this warning change "options.convertSessionIdToMongoObjectId" to "false" or remove your "options.idSessionProvider" from the configuration. `); } this.database = this.options.database; this.sessionCollection = this.database.collection(this.options.sessionCollectionName); } /** * Setup the mongo indexes needed for the sessions. * @param options Options passed to the mongo native `createIndex` method. */ public async setupIndexes( options: Omit<CreateIndexesOptions, 'unique' | 'sparse'> = {} ): Promise<void> { // Token index used to query a session await this.sessionCollection.createIndex('token', { ...options, unique: true, sparse: true, }); } /** * Create a new session attached to a user. * @param userId User id of the session. * @param token Random token used to identify the session. * @param connection Connection informations related to the session (such as user agent, ip etc..). * @param extraData Any extra data you would like to add. The data will be added to the root of the object. */ public async createSession( userId: string, token: string, connection: ConnectionInformations = {}, extraData?: object ): Promise<string> { const session = { userId, token, userAgent: connection.userAgent, ip: connection.ip, extraData, valid: true, [this.options.timestamps.createdAt]: this.options.dateProvider(), [this.options.timestamps.updatedAt]: this.options.dateProvider(), }; if (this.options.idSessionProvider && !this.options.convertSessionIdToMongoObjectId) { session._id = this.options.idSessionProvider(); } const ret = await this.sessionCollection.insertOne(session); // keep ret.ops for compatibility with MongoDB 3.X, version 4.X uses insertedId return ((ret.insertedId ? ret.insertedId : (ret as any).ops[0]._id) as ObjectId).toString(); } /** * Get a session by his id. * @param sessionId Id used to query the session. */ public async findSessionById(sessionId: string): Promise<Session | null> { const _id = this.options.convertSessionIdToMongoObjectId ? toMongoID(sessionId) : sessionId; const session = await this.sessionCollection.findOne({ _id }); if (session) { session.id = session._id.toString(); } return session as Session; } /** * Get a session by his token. * @param token Token used to query the session. */ public async findSessionByToken(token: string): Promise<Session | null> { const session = await this.sessionCollection.findOne({ token }); if (session) { session.id = session._id.toString(); } return session as Session; } /** * Update the session informations, token and connection informations. * @param sessionId Id used to update the session. * @param connection Connection informations related to the session (such as user agent, ip etc..). * @param newToken New token that will replace the existing token for the session. */ public async updateSession( sessionId: string, connection: ConnectionInformations, newToken?: string ): Promise<void> { const updateClause = { $set: { userAgent: connection.userAgent, ip: connection.ip, [this.options.timestamps.updatedAt]: this.options.dateProvider(), }, }; if (newToken) { updateClause.$set.token = newToken; } const _id = this.options.convertSessionIdToMongoObjectId ? toMongoID(sessionId) : sessionId; await this.sessionCollection.updateOne({ _id }, updateClause); } /** * Invalidate a session. * @param sessionId Id of the session to invalidate. */ public async invalidateSession(sessionId: string): Promise<void> { const _id = this.options.convertSessionIdToMongoObjectId ? toMongoID(sessionId) : sessionId; await this.sessionCollection.updateOne( { _id }, { $set: { valid: false, [this.options.timestamps.updatedAt]: this.options.dateProvider(), }, } ); } /** * Invalidate all the session of a user. * @param userId User id of the sessions. * @param excludedSessionIds Can be used to whitelist some sessions. Eg: close all the sessions except these ones. */ public async invalidateAllSessions(userId: string, excludedSessionIds?: string[]): Promise<void> { const selector: { userId: string; _id?: object } = { userId }; if (excludedSessionIds && excludedSessionIds.length > 0) { let excludedObjectIds: string[] | ObjectId[] = excludedSessionIds; if (this.options.convertSessionIdToMongoObjectId) { excludedObjectIds = excludedSessionIds.map((sessionId) => { return toMongoID(sessionId); }); } selector._id = { $nin: excludedObjectIds, }; } await this.sessionCollection.updateMany(selector, { $set: { valid: false, [this.options.timestamps.updatedAt]: this.options.dateProvider(), }, }); } }