Files
Eventment-API/models/user.js
2019-08-20 08:39:10 -04:00

456 lines
10 KiB
JavaScript

const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const timestamps = require('mongoose-timestamp');
const config = require('../config.js');
const UserEmails = require('../emails/user');
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,
},
tokenCheckBit: {
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.confirmRegistration = function (callback = () => {}) {
this.isVerified = true;
this.tokenCheckBit = undefined;
this.save(callback);
};
UserSchema.methods.generateAccountToken = function (callback = () => {}) {
const tokenCheckBit = crypto.randomBytes(16).toString('hex');
const token = jwt.sign({
sub: this.id,
key: tokenCheckBit,
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.tokenCheckBit = tokenCheckBit;
this.save();
return token;
};
UserSchema.methods.generateLoginToken = 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.isEventManager = function () {
return this.isOrganizationEmployee || false;
};
UserSchema.methods.isNomAvailable = function (nom) {
return !!!this.model('User').findOne({ nomDeBid });
};
UserSchema.methods.isRegistrationVerified = function () {
return this.isVerified || false;
};
UserSchema.methods.sendPasswordReset = function () {
const resetToken = this.generateAccountToken();
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.sendConfirmationEmail = function (callback) {
const user = {
email: this.email,
firstName: this.firstName,
token: this.generateAccountToken(),
};
callback = typeof callback === 'function' ? callback :
(err, info) => console.log('[UserSchema.methods.sendConfirmationEmail]', { err, info });
UserEmails.confirmation(user, callback);
};
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 }, $unset: { tokenCheckBit: '' } },
{ upsert: true },
callback,
);
}
if (!hasLocalStrategy) {
this.credentials.push(strategy);
this.tokenCheckBit = undefined;
this.save(callback);
}
};
UserSchema.methods.toAuthJSON = function () {
const hasNomDeBid = !!this.nomDeBid;
const nomDeBid = this.getNomDeBid();
return {
email: this.email,
token: this.generateLoginToken(),
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.register = function (user, callback = () => {}) {
this.create(user, (err, user) => {
if (err) {
return callback(err, null);
}
if (user) {
user.sendConfirmationEmail((err, info) => {
if (err) {
return callback(err, null);
}
callback(null, user);
});
}
});
};
UserSchema.statics.verifyAccountToken = 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, tokenCheckBit: 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.verifyAccountToken(token, (err, user, info) => {
if (err) {
return callback(err);
}
if (!user) {
return callback(err, false, info);
}
user.setPassword(password, callback);
});
};
UserSchema.statics.verifyTokenAndConfirmRegistration = function (token, callback) {
this.verifyAccountToken(token, (err, user, info) => {
if (err) {
return callback(err);
}
if (!user) {
return callback(err, false, info);
}
user.confirmRegistration(callback);
});
};
/**
* PATH OPERATIONS
*/
UserSchema.path('avatar').get(v => (v ? `${config.assetStoreUrl}${v}` : null));
/**
* Export
*/
const User = mongoose.model('User', UserSchema);
module.exports = User;