'use strict'; /** * Module dependencies */ var mongoose = require('mongoose'), path = require('path'), config = require(path.resolve('./config/config')), common = require(path.resolve('./config/lib/common')), CommonSchema = require(path.resolve('./modules/core/server/models/common.server.model')), Schema = mongoose.Schema, crypto = require('crypto'), validator = require('validator'), generatePassword = require('generate-password'), owasp = require('owasp-password-strength-test'), moment = require('moment'), Peer = mongoose.model('Peer'), chalk = require('chalk'); owasp.config(config.shared.owasp); var announceConfig = config.meanTorrentConfig.announce; var examinationConfig = config.meanTorrentConfig.examination; var mtDebug = require(path.resolve('./config/lib/debug')); const PEERSTATE_SEEDER = 'seeder'; const PEERSTATE_LEECHER = 'leecher'; /** * 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 display name'] }, lastName: { type: String, trim: true, default: '' //validate: [validateLocalStrategyProperty, 'Please fill in your last name'] }, displayName: { type: String, trim: true, maxlength: 16 }, 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, unique: true, 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' }, hideMoreDetail: { type: Boolean, default: false }, isOper: { type: Boolean, default: false }, isAdmin: { type: Boolean, default: false }, upload_access: { type: String, default: 'review' }, status: { type: String, default: 'inactive' }, banReason: { reason: { type: String, default: '' }, params: {} }, vip_start_at: { type: Date, default: '' }, vip_end_at: { type: Date, default: '' }, isVip: { type: Boolean, default: false }, score: { type: Schema.Types.Decimal128, get: function (v) { return parseFloat(parseFloat(v).toFixed(2)) || 0; }, default: 0 }, uploaded: { type: Number, default: 0 }, downloaded: { type: Number, default: 0 }, true_uploaded: { type: Number, default: 0 }, true_downloaded: { type: Number, default: 0 }, ratio: { type: Number, default: 0 }, uptotal: { type: Number, default: 0 }, seeded: { type: Number, default: 0 }, leeched: { type: Number, default: 0 }, finished: { type: Number, default: 0 }, hnr_warning: { type: Number, default: 0 }, topics: { type: Number, default: 0 }, replies: { type: Number, default: 0 }, followers: [{ type: Schema.Types.ObjectId, ref: 'User' }], following: [{ type: Schema.Types.ObjectId, ref: 'User' }], updated: { type: Date }, last_signed: { type: Date }, last_idled: { type: Date }, medals: [String], signed_ip: [String], leeched_ip: [String], curr_signed_ip: String, curr_leeched_ip: String, client_agent: [String], invited_by: { type: Schema.Types.ObjectId, ref: 'User' }, makers: [{ type: Schema.Types.ObjectId, ref: 'Maker' }], /* examination data */ examinationData: { type: Object, default: undefined }, signature: { type: String, default: '' }, created: { type: Date, default: Date.now }, refreshat: { type: Date, default: Date.now }, /* for operate history */ history: [CommonSchema.HistorySchema], remarks: { type: String, default: '' }, /* for sing up active */ signUpActiveToken: { type: String }, signUpActiveExpires: { type: Date }, /* For reset password */ resetPasswordToken: { type: String }, resetPasswordExpires: { type: Date }, nextResetPasswordTime: { type: Date } }, {usePushEach: true}); /** * Hook a pre save method to hash the password */ UserSchema.pre('save', function (next) { var user = this; if (this.password && this.isModified('password')) { this.salt = crypto.randomBytes(16).toString('base64'); this.password = this.hashPassword(this.password); } countRatio(this); updateVipFlag(this); updateOperAdminFlag(this); updateExaminationData(this); this.constructor.count(function (err, count) { if (err) { return next(err); } if (count === 0) { user.roles = ['admin']; updateOperAdminFlag(user); } next(); }); }); /** * countRatio * @param user */ function countRatio(user) { if (user.uploaded > 0 && user.downloaded === 0) { user.ratio = -1; } else if (user.uploaded === 0 || user.downloaded === 0) { user.ratio = 0; } else { user.ratio = Math.round((user.uploaded / user.downloaded) * 100) / 100; } } /** * updateVipFlag * @param user */ function updateVipFlag(user) { user.isVip = false; if (!user.vip_start_at || !user.vip_end_at) { user.isVip = false; } else if (moment(Date.now()) > moment(user.vip_end_at)) { user.isVip = false; } else { user.isVip = true; } } /** * updateOperAdminFlag * @param user */ function updateOperAdminFlag(user) { user.isOper = false; user.isAdmin = false; if (user.roles) { user.isOper = (user.roles[0] === 'oper' || user.roles[0] === 'admin'); user.isAdmin = (user.roles[0] === 'admin'); } if (user.isOper || user.isAdmin) { user.upload_access = 'pass'; } } /** * updateExaminationData * @param user */ function updateExaminationData(user) { if (common.examinationIsValid(user)) { var uploadFinished = user.examinationData.uploaded >= examinationConfig.incrementData.upload; var downloadFinished = user.examinationData.downloaded >= examinationConfig.incrementData.download; var scoreFinished = user.examinationData.score >= examinationConfig.incrementData.score; user.examinationData.isFinished = uploadFinished && downloadFinished && scoreFinished; user.examinationData.finishedTime = user.examinationData.isFinished ? Date.now() : null; user.markModified('examinationData'); } } /** * 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; } }; /** * update user last signed time */ UserSchema.methods.updateSignedTime = function () { this.update({ $set: {last_signed: Date.now()} }).exec(); }; /** * update user last signed ip * @param ip */ UserSchema.methods.addSignedIp = function (ip) { this.update({ curr_signed_ip: ip, $addToSet: {signed_ip: ip} }).exec(); }; /** * update user last leeched ip * @param ip */ UserSchema.methods.addLeechedIp = function (ip) { this.update({ curr_leeched_ip: ip, $addToSet: {leeched_ip: ip} }).exec(); }; /** * globalUpdateMethod */ UserSchema.methods.globalUpdateMethod = function (findThenUpdate, cb) { if (typeof findThenUpdate === 'function') { cb = findThenUpdate; findThenUpdate = false; } if (findThenUpdate) { this.model('User').findById(this._id, function (err, u) { if (u) { u.refreshat = Date.now(); u.save(function (err, nu) { if (cb) cb(nu || this); }); } else { if (cb) cb(this); } }); } else { this.refreshat = Date.now(); this.save(function (err, u) { if (cb) cb(u || this); }); } }; /** * updateSeedLeechNumbers */ UserSchema.methods.updateSeedLeechNumbers = function (callback) { var user = this; Peer.aggregate([{ $match: { user: user._id, last_announce_at: {$gt: new Date(Date.now() - announceConfig.announceInterval - announceConfig.announceIdleTime)} } }, { $group: { _id: '$peer_status', count: {$sum: 1} } }]).exec(function (err, counts) { if (!err) { var sc = 0; var lc = 0; counts.forEach(function (c) { switch (c._id) { case PEERSTATE_SEEDER: sc = c.count; break; case PEERSTATE_LEECHER: lc = c.count; break; } }); user.update({ $set: { seeded: sc, leeched: lc } }, function (err) { if (callback) { callback({seedCount: sc, leechCount: lc}); } }); } else { if (callback) { callback(null); } } }); }; /** * update user last client_agent * @param ip */ UserSchema.methods.addClientAgent = function (ca) { this.update({ $addToSet: {client_agent: ca} }).exec(); }; /** * 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); } }); }; /** * Seeds the User collection with document (User) * and provided options. */ function seed(doc, options) { var User = mongoose.model('User'); return new Promise(function (resolve, reject) { skipDocument() .then(add) .then(function (response) { return resolve(response); }) .catch(function (err) { return reject(err); }); function skipDocument() { return new Promise(function (resolve, reject) { User .findOne({ username: doc.username }) .exec(function (err, existing) { if (err) { return reject(err); } if (!existing) { return resolve(false); } if (existing && !options.overwrite) { return resolve(true); } // Remove User (overwrite) existing.remove(function (err) { if (err) { return reject(err); } return resolve(false); }); }); }); } function add(skip) { return new Promise(function (resolve, reject) { if (skip) { return resolve({ message: chalk.yellow('Database Seeding: User\t\t' + doc.username + ' skipped') }); } User.generateRandomPassphrase() .then(function (passphrase) { var user = new User(doc); user.provider = 'local'; user.displayName = user.firstName + ' ' + user.lastName; user.password = passphrase; user.save(function (err) { if (err) { return reject(err); } return resolve({ message: 'Database Seeding: User\t\t' + user.username + ' added with password set to ' + passphrase }); }); }) .catch(function (err) { return reject(err); }); }); } }); } UserSchema.statics.seed = seed; UserSchema.index({passkey: 1}); UserSchema.index({email: 1}); UserSchema.index({uptotal: 1}); //for rank list UserSchema.index({uploaded: -1, downloaded: -1, ratio: -1, score: -1, status: 1}); UserSchema.index({created: -1, status: 1}); UserSchema.index({created: -1, status: 1, roles: 1, isVip: 1}); UserSchema.index({isVip: 1, username: 1, displayName: 1, email: 1, created: -1}); UserSchema.index({roles: 1, username: 1, displayName: 1, email: 1, created: -1}); UserSchema.index({status: 1, username: 1, displayName: 1, email: 1, created: -1}); UserSchema.index({username: 1, email: 1}); UserSchema.index({resetPasswordToken: 1, resetPasswordExpires: 1}); UserSchema.index({signUpActiveToken: 1, status: 1, signUpActiveExpires: 1}); mongoose.model('User', UserSchema);