377 lines
8.3 KiB
JavaScript
377 lines
8.3 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 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,
|
|
},
|
|
|
|
address: [ AddressSchema ],
|
|
phone: [ 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: {
|
|
nomDeBid: nomDeBid,
|
|
email: this.email,
|
|
firstName: this.firstName,
|
|
lastName: this.lastName,
|
|
avatar: this.avatar,
|
|
isAllowedToBid: this.isAllowedToBid,
|
|
isOrganizationEmployee: this.isOrganizationEmployee,
|
|
generatedNomDeBid: !hasNomDeBid,
|
|
},
|
|
};
|
|
};
|
|
|
|
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;
|