@accounts/mongo-sessions
Version:
MongoDB sessions adaptor for accounts
239 lines (216 loc) • 7.66 kB
text/typescript
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(),
},
});
}
}