Puesto que vamos a guardar los detalles de usuario en Mongo, vamos a crear un modelo de usuario en Mongoose vamos a guardar este modelo dentro de la carpeta /api/user puesto que también estos registros serán expuestos atraves de una API.
Primero vamos a instalar las dependencias que vamos a necesitar para esta parte.
Los esquemas virtuales son campos adicionales para un modelo dado. Sus valores se pueden configurar de forma manual o automática con la funcionalidad definida. Una propiedad virtual común es el nombre completo de una persona, compuesto por el nombre y apellido del usuario.
Vamos agregar a nuestro archivo api/user/user.model.js el siguiente código:
api/user/user.model.js
/**
* Virtuals
*/
// Public profile information
UserSchema
.virtual('profile')
.get(function () {
return {
name: this.name,
role: this.role,
};
});
// Non-sensitive info we'll be putting in the token
UserSchema
.virtual('token')
.get(function () {
return {
_id: this._id,
role: this.role,
};
});
Schema Validation
La validación de esquema de JSON extiende la validación de documentos de muchas maneras diferentes, incluida la capacidad de imponer esquemas dentro de arrays y evitar que se agreguen atributos no aprobados.
Para nuestro caso de uso vamos adicionar validaciones para el campo email y password con el siguiente código:
api/user/user.model.js
/**
* Validations
*/
// Validate empty email
UserSchema
.path('email')
.validate(function (email) {
if (authTypes.indexOf(this.provider) !== -1) {
return true;
}
return email.length;
}, 'Email cannot be blank');
// Validate empty password
UserSchema
.path('password')
.validate(function (password) {
if (authTypes.indexOf(this.provider) !== -1) {
return true;
}
return password.length;
}, 'Password cannot be blank');
// Validate email is not taken
UserSchema
.path('email')
.validate(function (value) {
if (authTypes.indexOf(this.provider) !== -1) {
return true;
}
return this.constructor.findOne({ email: value }).exec()
.then((user) => {
if (user) {
if (this.id === user.id) {
return true;
}
return false;
}
return true;
})
.catch((err) => {
throw err;
});
}, 'The specified email address is already in use.');
Schema Hook
api/user/user.model.js
const validatePresenceOf = function (value) {
return value && value.length;
};
/**
* Pre-save hook
*/
UserSchema
.pre('save', function (next) {
// Handle new/update passwords
if (!this.isModified('password')) {
return next();
}
if (!validatePresenceOf(this.password)) {
if (authTypes.indexOf(this.provider) === -1) {
return next(new Error('Invalid password'));
}
return next();
}
// Make salt with a callback
return this.makeSalt((saltErr, salt) => {
if (saltErr) {
return next(saltErr);
}
this.salt = salt;
this.encryptPassword(this.password, (encryptErr, hashedPassword) => {
if (encryptErr) {
return next(encryptErr);
}
this.password = hashedPassword;
return next();
});
});
});
Schema Methods
api/user/user.model.js
/**
* Methods
*/
UserSchema.methods = {
/**
* Authenticate - check if the passwords are the same
*
* @param {String} password
* @param {Function} callback
* @return {Boolean}
* @api public
*/
authenticate(password, callback) {
if (!callback) {
return this.password === this.encryptPassword(password);
}
return this.encryptPassword(password, (err, pwdGen) => {
if (err) {
return callback(err);
}
if (this.password === pwdGen) {
return callback(null, true);
}
return callback(null, false);
});
},
/**
* Make salt
*
* @param {Number} [byteSize] - Optional salt byte size, default to 16
* @param {Function} callback
* @return {String}
* @api public
*/
makeSalt(...args) {
const defaultByteSize = 16;
let byteSize;
let callback;
if (typeof args[0] === 'function') {
callback = args[0];
byteSize = defaultByteSize;
} else if (typeof args[1] === 'function') {
callback = args[1];
} else {
throw new Error('Missing Callback');
}
if (!byteSize) {
byteSize = defaultByteSize;
}
return crypto.randomBytes(byteSize, (err, salt) => {
if (err) {
return callback(err);
}
return callback(null, salt.toString('base64'));
});
},
/**
* Encrypt password
*
* @param {String} password
* @param {Function} callback
* @return {String}
* @api public
*/
encryptPassword(password, callback) {
if (!password || !this.salt) {
if (!callback) {
return null;
}
return callback('Missing password or salt');
}
const defaultIterations = 10000;
const defaultKeyLength = 64;
const salt = Buffer.from(this.salt, 'base64');
if (!callback) {
return crypto.pbkdf2Sync(
password,
salt,
defaultIterations,
defaultKeyLength,
'sha256',
).toString('base64');
}
return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, 'sha256', (err, key) => {
if (err) {
return callback(err);
}
return callback(null, key.toString('base64'));
});
},
};