const crypto = require('crypto'); const jwt = require('jsonwebtoken'); const mongoose = require('mongoose'); const timestamps = require('mongoose-timestamp'); const config = require('../config.js'); const AddressSchema = require('./common/address.js'); const PhoneSchema = require('./common/phone.js'); const LoginSchema = new mongoose.Schema( { method: { type: String, required: true, enum: [ 'apple', 'facebook', 'google', 'local' ], }, userId: { type: String, trim: true, }, accessToken: { type: String, required: true, trim: true, }, associatedEmail: { type: String, trim: true, }, secret: { type: String, trim: true, }, profile: {}, }, { minimize: false }, ); const UserSchema = new mongoose.Schema( { nomDeBid: { type: String, trim: true, unique: true, }, firstName: { type: String, required: true, trim: true, }, lastName: { type: String, required: true, trim: true, }, email: { type: String, required: true, unique: true, }, avatar: { type: String, trim: true, }, addresses: [ AddressSchema ], phones: [ PhoneSchema ], credentials: [ LoginSchema ], tokenCheckBit: { type: String, trim: true, }, organizationIdentifier: { type: String, trim: true, }, paymentToken: { type: String, trim: true, }, isVerified: { type: Boolean, default: false, }, isAllowedToBid: { type: Boolean, default: false, }, isOrganizationEmployee: { type: Boolean, default: false, }, resetCheckBit: { type: String, default: null, }, }, { minimize: false }, ); /** * PLUGINS */ UserSchema.plugin(timestamps); /** * METHODS */ UserSchema.methods.authenticate = function (username, password) { const user = this.model('User').findOne({ email: username }); const strategy = user ? user.getAuthStrategy('local') : null; if (strategy) { let hash = crypto.pbkdf2Sync(password, strategy.get('secret'), 10000, 512, 'sha512').toString('hex') return strategy.get('accessToken') === hash; } return false; }; UserSchema.methods.isNomAvailable = function (nom) { return !!!this.model('User').findOne({ nomDeBid }); }; UserSchema.methods.generateJWT = function (props = {}) { const { exp, iss } = props; const today = new Date(); let expirationDate = exp; if (!expirationDate) { expirationDate = new Date(today); expirationDate.setDate(today.getDate() + config.security.jwt.daysValid); } return jwt.sign({ sub: this._id, iss: iss || config.security.jwt.issuer, aud: config.security.jwt.audience, iat: parseInt(today.getTime()), exp: parseInt(expirationDate.getTime() / 1000, 10), }, config.security.jwt.secret); } UserSchema.methods.getAuthStrategy = function (method = 'local') { return this.credentials.filter((strategy) => { return strategy.method === method; }).pop() || false; }; UserSchema.methods.getNomDeBid = function () { return this.nomDeBid || `${this.firstName} ${this.lastName.charAt(0)}`; }; UserSchema.methods.generateResetToken = function (callback = () => {}) { const resetCheckBit = crypto.randomBytes(16).toString('hex'); const token = jwt.sign({ sub: this.id, key: resetCheckBit, iss:config.security.jwt.issuer, aud: config.security.jwt.audience, iat: Date.now(), exp: (Date.now() + (24*60*60*1000)), }, config.security.jwt.secret); this.resetCheckBit = resetCheckBit; this.save(); return token; }; UserSchema.methods.isEventManager = function () { return this.isOrganizationEmployee || false; }; UserSchema.methods.isRegistrationVerified = function () { return this.isVerified || false; }; UserSchema.methods.sendPasswordReset = function () { const resetToken = this.generateResetToken(); let resetRoute = config.security.resetRoute; resetRoute = resetRoute.replace(':user_id', this.id); resetRoute = resetRoute.replace(':reset_token?', resetToken); const resetUrl = `${config.api.url}${resetRoute}`; console.log('[sendPasswordReset] resetUrl:', resetUrl); }; UserSchema.methods.setNomDeBid = function (nomDeBid, callback = () => {}) { const alreadyExists = this.isNomAvailable(nomDeBid); if (this.isNomAvailable(nomDeBid)) { this.nomDeBid = nomDeBid; return this.save(callback); } callback({ success: false, info: 'Nom de Bid already exists!' }, false); }; UserSchema.methods.setPassword = function (password, callback = () => {}) { const hasLocalStrategy = !!this.credentials.length && !!this.credentials.filter(strategy => strategy.method === 'local').length; const salt = crypto.randomBytes(16).toString('hex'); const accessToken = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex'); const strategy = { accessToken, method: 'local', secret: salt, }; if (hasLocalStrategy) { this.model('User').findOneAndUpdate( { _id: this._id, 'credentials.method': 'local' }, { $set: { 'credentials.$': strategy, resetCheckBit: null } }, { upsert: true }, callback, ); } if (!hasLocalStrategy) { this.credentials.push(strategy); this.resetCheckBit = null; this.save(callback); } }; UserSchema.methods.toAuthJSON = function () { const hasNomDeBid = !!this.nomDeBid; const nomDeBid = this.getNomDeBid(); return { email: this.email, token: this.generateJWT(), user: { id: this._id, nomDeBid: nomDeBid, email: this.email, firstName: this.firstName, lastName: this.lastName, avatar: this.avatar, isAllowedToBid: this.isAllowedToBid, isOrganizationEmployee: this.isOrganizationEmployee, generatedNomDeBid: !hasNomDeBid, }, }; }; UserSchema.methods.toProfileJSON = function () { const hasNomDeBid = !!this.nomDeBid; const nomDeBid = this.getNomDeBid(); return { addresses: this.addresses, avatar: this.avatar, email: this.email, firstName: this.firstName, generatedNomDeBid: !hasNomDeBid, hasLinkedApple: !!this.getAuthStrategy('apple'), hasLinkedFacebook: !!this.getAuthStrategy('facebook'), hasLinkedGoogle: !!this.getAuthStrategy('google'), hasLocalAccount: !!this.getAuthStrategy('local'), _id: this.id, isAllowedToBid: this.isAllowedToBid, isOrganizationEmployee: this.isOrganizationEmployee, isVerified: this.isVerified, lastName: this.lastName, nomDeBid: nomDeBid, organizationIdentifier: this.organizationIdentifier, paymentToken: this.paymentToken, phones: this.phones, }; }; UserSchema.methods.validatePassword = function (password) { const strategy = this.getAuthStrategy('local'); if (strategy) { let hash = crypto.pbkdf2Sync(password, strategy.secret, 10000, 512, 'sha512').toString('hex'); return strategy.accessToken === hash; } return false; }; /** * STATICS */ UserSchema.statics.findOrCreate = function (filter = {}, profile = {}, callback = () => {}) { const self = this; this.findOne(filter, function(err,result) { if (err) { callback(err, null); } if (!result) { self.create(profile, (err, result) => callback(err, result)); }else{ callback(err, result); } }); }; UserSchema.statics.findOneAndUpdateOrCreate = function ( filter = {}, strategy = {}, profile = {}, callback = () => {}, ) { const self = this; this.findOne(filter, function(err, result) { if (err) { callback(err, null); } if (!result) { self.create( { strategy: [ strategy ], ...profile }, (err, result) => callback(err, result), ); } else { const hasStrategy = !!result.credentials.length && !!result.credentials.filter(auth => auth.method === strategy.method).length; if (hasStrategy) { self.model('User').findOneAndUpdate( { _id: result._id, 'credentials.method': strategy.method }, { $set: { 'credentials.$': strategy } }, { upsert: true }, callback, ); } else { result.credentials.push(strategy); result.save(callback); } } }); }; UserSchema.statics.verifyResetToken = function (token, callback) { jwt.verify(token, config.security.jwt.secret, (err, decoded) => { if (err) { return callback(err); } const { sub, key } = decoded; this.findOne({ _id: sub, resetCheckBit: key }, (err, user) => { if (err) { return callback(err); } if (!user) { return callback(err, false, 'The reset token was not valid.'); } callback(err, user); }); }); }; UserSchema.statics.verifyTokenAndResetPassword = function (token, password, callback) { this.verifyResetToken(token, (err, user, info) => { if (err) { return callback(err); } if (!user) { return callback(err, false, info); } user.setPassword(password, callback); }); }; /** * PATH OPERATIONS */ UserSchema.path('avatar').get(v => (v ? `${config.assetStoreUrl}${v}` : null)); /** * Export */ const User = mongoose.model('User', UserSchema); module.exports = User;