'use strict'; /** * Module dependencies */ var mongoose = require('mongoose'), path = require('path'), config = require(path.resolve('./config/config')), Schema = mongoose.Schema, crypto = require('crypto'), validator = require('validator'), generatePassword = require('generate-password'), owasp = require('owasp-password-strength-test'), moment = require('moment'); owasp.config(config.shared.owasp); /** * A Validation function for local strategy properties */ var validateLocalStrategyProperty = function (property) { return ((this.provider !== 'local' && !this.updated) || property.length); }; /** * A Validation function for local strategy email */ var validateLocalStrategyEmail = function (email) { return ((this.provider !== 'local' && !this.updated) || validator.isEmail(email, {require_tld: false})); }; /** * A Validation function for username * - at least 3 characters * - only a-z0-9_-. * - contain at least one alphanumeric character * - not in list of illegal usernames * - no consecutive dots: "." ok, ".." nope * - not begin or end with "." */ var validateUsername = function (username) { var usernameRegex = /^(?=[\w.-]+$)(?!.*[._-]{2})(?!\.)(?!.*\.$).{3,34}$/; return ( this.provider !== 'local' || (username && usernameRegex.test(username) && config.illegalUsernames.indexOf(username) < 0) ); }; /** * User Schema */ var UserSchema = new Schema({ firstName: { type: String, trim: true, default: '', validate: [validateLocalStrategyProperty, 'Please fill in your first name'] }, lastName: { type: String, trim: true, default: '', validate: [validateLocalStrategyProperty, 'Please fill in your last name'] }, displayName: { type: String, trim: true }, email: { type: String, //index: { // unique: true, // sparse: true // For this to work on a previously indexed field, the index must be dropped & the application restarted. //}, unique: 'email already exists', required: 'Please fill in a email address', lowercase: true, trim: true, default: '', validate: [validateLocalStrategyEmail, 'Please fill a valid email address'] }, username: { type: String, unique: 'Username already exists', required: 'Please fill in a username', validate: [validateUsername, 'Please enter a valid username: 3+ characters long, non restricted word, characters "_-.", no consecutive dots, does not begin or end with dots, letters a-z and numbers 0-9.'], lowercase: true, trim: true }, password: { type: String, default: '' }, passkey: { type: String, default: '' }, salt: { type: String }, profileImageURL: { type: String, default: 'modules/users/client/img/profile/default.png' }, provider: { type: String, required: 'Provider is required' }, providerData: {}, additionalProvidersData: {}, roles: { type: [{ type: String, enum: config.meanTorrentConfig.userRoles }], default: ['user'], required: 'Please provide at least one role' }, status: { type: String, default: 'normal' }, vip_start_at: { type: Date, default: '' }, vip_end_at: { type: Date, default: '' }, score: { type: Number, default: 0 }, uploaded: { type: Number, default: 0 }, downloaded: { type: Number, default: 0 }, ratio: { type: Number, default: 0 }, seeded: { type: Number, default: 0 }, leeched: { type: Number, default: 0 }, finished: { type: Number, default: 0 }, updated: { type: Date }, created: { type: Date, default: Date.now }, /* For reset password */ resetPasswordToken: { type: String }, resetPasswordExpires: { type: Date } }); /** * overwrite toJSON */ UserSchema.methods.toJSON = function (options) { var document = this.toObject(options); document.isVip = false; if (!document.vip_start_at || !document.vip_end_at) { document.isVip = false; } else if (moment(Date.now()) > moment(document.vip_end_at)) { document.isVip = false; } else { document.isVip = true; } document.isOper = (document.roles[0] === 'oper' || document.roles[0] === 'admin'); document.isAdmin = (document.roles[0] === 'admin'); return document; }; /** * Hook a pre save method to hash the password */ UserSchema.pre('save', function (next) { if (this.password && this.isModified('password')) { this.salt = crypto.randomBytes(16).toString('base64'); this.password = this.hashPassword(this.password); } next(); }); /** * Hook a post save method to set the ratio */ UserSchema.post('save', function (doc) { if (doc.uploaded === 0 || doc.downloaded === 0) { doc.ratio = 0; } else { doc.ratio = Math.round((doc.uploaded / doc.downloaded) * 100) / 100; } }); /** * Hook a pre validate method to test the local password */ UserSchema.pre('validate', function (next) { if (this.provider === 'local' && this.password && this.isModified('password')) { var result = owasp.test(this.password); if (result.errors.length) { var error = result.errors.join(' '); this.invalidate('password', error); } } next(); }); /** * Create instance method for hashing a password */ UserSchema.methods.hashPassword = function (password) { if (this.salt && password) { return crypto.pbkdf2Sync(password, new Buffer(this.salt, 'base64'), 10000, 64, 'SHA1').toString('base64'); } else { return password; } }; /** * Create instance method for authenticating user */ UserSchema.methods.authenticate = function (password) { return this.password === this.hashPassword(password); }; /** * create randomString * @param length * @param chars * @returns {string} */ UserSchema.methods.randomString = function (length, chars) { if (!chars) { throw new Error('Argument \'chars\' is undefined'); } var charsLength = chars.length; if (charsLength > 256) { throw new Error('Argument \'chars\' should not have more than 256 characters' + ', otherwise unpredictability will be broken'); } var randomBytes = crypto.randomBytes(length); var result = new Array(length); var cursor = 0; for (var i = 0; i < length; i++) { cursor += randomBytes[i]; result[i] = chars[cursor % charsLength]; } return result.join(''); }; /** * create randomAsciiString * @param length * @param chars * @returns {string} */ UserSchema.methods.randomAsciiString = function (length) { return this.randomString(length, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); }; /** * Find possible not used username */ UserSchema.statics.findUniqueUsername = function (username, suffix, callback) { var _this = this; var possibleUsername = username.toLowerCase() + (suffix || ''); _this.findOne({ username: possibleUsername }, function (err, user) { if (!err) { if (!user) { callback(possibleUsername); } else { return _this.findUniqueUsername(username, (suffix || 0) + 1, callback); } } else { callback(null); } }); }; /** * Generates a random passphrase that passes the owasp test * Returns a promise that resolves with the generated passphrase, or rejects with an error if something goes wrong. * NOTE: Passphrases are only tested against the required owasp strength tests, and not the optional tests. */ UserSchema.statics.generateRandomPassphrase = function () { return new Promise(function (resolve, reject) { var password = ''; var repeatingCharacters = new RegExp('(.)\\1{2,}', 'g'); // iterate until the we have a valid passphrase // NOTE: Should rarely iterate more than once, but we need this to ensure no repeating characters are present while (password.length < 20 || repeatingCharacters.test(password)) { // build the random password password = generatePassword.generate({ length: Math.floor(Math.random() * (20)) + 20, // randomize length between 20 and 40 characters numbers: true, symbols: false, uppercase: true, excludeSimilarCharacters: true }); // check if we need to remove any repeating characters password = password.replace(repeatingCharacters, ''); } // Send the rejection back if the passphrase fails to pass the strength test if (owasp.test(password).errors.length) { reject(new Error('An unexpected problem occured while generating the random passphrase')); } else { // resolve with the validated passphrase resolve(password); } }); }; mongoose.model('User', UserSchema);