UNPKG

@qelos/auth

Version:

Express Passport authentication service

311 lines (273 loc) 7.53 kB
import mongoose, { ObjectId, Document, Model, Schema } from 'mongoose'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import { getSignedToken, getUniqueId } from '../services/tokens'; import { cacheManager } from '../services/cache-manager'; import { cookieTokenExpiration, defaultAuthType, defaultRole, refreshTokenExpiration, refreshTokenSecret } from '../../config'; export interface IUser { _id?: any; tenant: string; username: string; email?: string; phone?: string; password: string; fullName: string; firstName: string; lastName: string; birthDate: string; profileImage: string; salt: string; roles: string[]; tokens: any[]; metadata: any; emailVerified?: boolean; socialLogins?: string[], lastLogin: { created: Date, workspace: ObjectId | string } created: Date; } export interface UserDocument extends Omit<IUser, '_id'>, Document { } export interface UserModel extends Model<UserDocument> { comparePassword( password: string, callback: (err: Error, success: boolean) => void ): void; getToken(payload: { authType: string, expiresIn?: string, workspace? }): any; getRefreshToken(relatedToken: string): any; updateToken( authType: string, currentPayload: { tokenIdentifier, workspace? }, newIdentifier: string, relatedToken?: string ): Promise<UserDocument>; deleteToken(authType: string, tokenIdentifier: string): Promise<UserDocument>; getTokenByRelatedTokens(authType: string, tokenIdentifier: string): string; getUsersList(tenant: string, usersIds: ObjectId[], privilegedUserFields?: string): Promise<string>; } const TokenSchema = new mongoose.Schema({ kind: { type: String, enum: ['cookie', 'oauth'], default: () => defaultAuthType, }, metadata: { type: mongoose.Schema.Types.Mixed, default: () => ({}), }, tokenIdentifier: String, expiresAt: Date, }) // define the User model schema const UserSchema = new mongoose.Schema<UserDocument, UserModel>({ tenant: { type: String, index: true, default: '0', }, username: { type: String, required: true, }, email: { type: String, validate(email = '') { return !email || email.includes('@') } }, emailVerified: { type: Boolean, default: () => false, }, socialLogins: [String], phone: String, password: String, fullName: String, firstName: String, lastName: String, birthDate: String, profileImage: String, salt: String, roles: { type: [String], }, metadata: mongoose.Schema.Types.Mixed, tokens: [TokenSchema], lastLogin: { created: Date, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace' } }, created: { type: Date, default: Date.now, }, }); UserSchema.index({ tenant: 1, username: 1 }, { unique: true }); /** * Compare the passed password with the value in the database. A model method. * * @param {string} password * @param {function} callback * @returns {object} callback */ UserSchema.methods.comparePassword = function comparePassword( this: UserDocument, password, callback ) { bcrypt.compare(password, this.password, callback); }; UserSchema.methods.getToken = function getToken({ authType, expiresIn, workspace }: { authType: 'cookie' | 'oauth', expiresIn?, workspace? }) { let tokenIdentifier; if (authType === 'cookie') { tokenIdentifier = getUniqueId(); this.tokens.push({ expiresAt: new Date(Date.now() + cookieTokenExpiration), kind: authType, tokenIdentifier, }); this.markModified('tokens'); } this.lastLogin = { created: new Date(), workspace: workspace?._id } return getSignedToken(this, workspace, tokenIdentifier, expiresIn).token; }; UserSchema.methods.getRefreshToken = function getRefreshToken(relatedToken, workspace?: any) { const tokenIdentifier = getUniqueId(); this.tokens.push({ kind: 'oauth', tokenIdentifier, metadata: { relatedToken, workspace: workspace?._id }, expiresAt: new Date(Date.now() + cookieTokenExpiration), }); this.markModified('tokens'); return jwt.sign( { sub: this._id, tenant: this.tenant, workspace: workspace?._id, tokenIdentifier, }, refreshTokenSecret, { expiresIn: refreshTokenExpiration } ); }; UserSchema.methods.updateToken = function updateToken( authType, currentPayload: { tokenIdentifier, workspace? }, newIdentifier, relatedToken ) { this.tokens = this.tokens.filter( (token) => !(token.kind === authType && token.tokenIdentifier === currentPayload.tokenIdentifier) ); const token = { kind: authType, tokenIdentifier: newIdentifier }; if (relatedToken || currentPayload.workspace) { (token as any).metadata = { relatedToken, workspace: currentPayload.workspace?._id }; } this.tokens.push(token); this.lastLogin = { created: new Date(), workspace: currentPayload.workspace?._id } return this.save(); }; UserSchema.methods.deleteToken = function deleteToken( authType, tokenIdentifier ) { this.tokens = this.tokens.filter( (token) => !(token.kind === authType && token.tokenIdentifier === tokenIdentifier) ); return this.save(); }; UserSchema.methods.getTokenByRelatedTokens = function getTokenByRelatedTokens( authType, tokenIdentifier ) { const token = this.tokens.find( (token) => token.kind === authType && token.metadata?.relatedToken === tokenIdentifier ); return token ? token.tokenIdentifier : tokenIdentifier; }; UserSchema.statics.getUsersList = function getUsersList(tenant: string, usersIds: ObjectId[], privilegedUserFields?: Array<string>) { if (!usersIds.length) { return this.find({ tenant }) .select(privilegedUserFields) .lean() .exec() .then(users => JSON.stringify(users)); } return cacheManager.wrap(`usersList.${tenant}.${usersIds.map(id => id.toString()).join(',')}`, async () => { const query: Record<string, any> = { _id: { $in: usersIds } } query.tenant = tenant; try { const users = await this.find(query) .select(privilegedUserFields) .lean() .exec(); return JSON.stringify(users); } catch { return '[]'; } }); } /** * The pre-save hook method. */ UserSchema.pre('save', function saveHook(next) { const user = this; if (user.email && !user.username) { user.username = user.email; } if (!user.email) { user.email = user.username.includes('@') ? user.username : (user.username + '@null') } // define role for new user if (!user.roles || user.roles.length === 0) { user.roles = [defaultRole]; } if (user.tokens.length > 10) { const now = Date.now(); user.tokens = user.tokens.filter(token => token.expiresAt && token.expiresAt - now > 0); } if (!this.salt) { this.salt = bcrypt.genSaltSync(); } // proceed further only if the password is modified or the user is new if (!user.isModified('password')) return next(); return bcrypt.genSalt((saltError, salt) => { if (saltError) { return next(saltError); } return bcrypt.hash(user.password, salt, (hashError, hash) => { if (hashError) { return next(hashError); } // replace a password string with hash value user.password = hash; return next(); }); }); }); const User = mongoose.model<UserDocument, UserModel>('User', UserSchema); export default User