"use strict";
/**
* @module account
*/
var debug = require('debug')('models:account');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var extend = require('mongoose-schema-extend');
var bcrypt = require('bcrypt');
var parentModel = require('./thing');
/** salt length for password */
var SALT_WORK_FACTOR = 10;
/**
* Максимальное количество попыток
* неудачных авторизации пользователя
* до блокировки
* @type {number}
* @default 10
*/
var MAX_LOGIN_ATTEMPTS = 10;
/**
* Время блокировки в милисекундах
* @type {number}
* @default 2 * 60 * 60 * 1000
*/
var LOCK_TIME = 2 * 60 * 60 * 1000;
var Model = function(){
var Parent = mongoose.model('Thing').schema;
/**
*
* Account
* Модель аккаунта в системе
* @version 0.0.1
* @class Account
* @extends Thing
*/
var _Schema = Parent.extend(
/** @lends Account.prototype */
{
/**
* имя пользователя === email
* @todo - match email
* @public
*/
username: {
type: String,
required: true,
index: { unique: true },
trim: true
},
/**
* Пароль / Хеш пароля
*/
password: {
type: String,
required: true
},
/**
* Уникальное имя в системе
*/
nickname: {
type: String,
index: { unique: true, sparse: true },
trim: true
},
/**
* общее количество авторизаций
* пользователя
* @type {number}
*/
totalLogins: {
type: Number
},
/**
* количество попыток авторизации
* с ошибками
* @type {number}
* @default 0
*/
loginAttempts: {
type: Number,
required: true,
default: 0
},
/**
* Время до которого
* пользователь не может авторизоватся в
* системе
* @type {number}
*/
lockUntil: {
type: Number
},
/**
* Администратор
*/
isAdmin: {
type: Boolean,
default: false
},
/**
* Супер-админ
* создается при инициализации системы
* может быть только "1" суперадмин.
* Если в системе два суперадмина, блокируем всё )
*/
isSuper: {
type: Boolean,
default: false
},
/**
* Подробные профили
* пользователя для хранения контактов
* баллинга и т.д
* @todo: separate emails
* @todo: separate phones
*
* @type {Array}
*/
profiles: [{
type: Schema.Types.ObjectId,
ref: 'Profile'
}],
/**
*
* login services
* based on http://docs.meteor.com/#accounts_api
*
* @example
*
* services: {
* facebook: {
* id: "709050", // facebook id
* accessToken: "AAACCgdX7G2...AbV9AZDZD"
* },
* resume: {
* loginTokens: [
* { token: "97e8c205-c7e4-47c9-9bea-8e2ccc0694cd",
* when: 1349761684048 }
* ]
* }
* }
*
*/
services: [],
/**
* Электронная почта
* @type {array}
* @example
*
* { address: 'tech77@diera.ru', verified: true }
*
*/
emails: [{
address: { type: String },
verified: { type: Boolean, default: true }
}],
/**
* Ключи API выданные пользователю
* создавать сразу в коллекиции с expires (типа кеша)
* @deprecated move to services
*/
apiKeys: [{
type: Schema.Types.ObjectId,
ref: 'Key'
}],
/**
* связь с социальными сетями
* профили социальных сетей
* @type {array}
*/
socialProfiles: [],
/**
* Группы пользователя
* @type {Array.<Group>}
*/
groups: [{
type: Schema.Types.ObjectId,
ref: 'Group'
}],
/**
* Роли пользователя
* @ignore
* @todo использовать mongoose-rbac
*/
// roles: [{ type: Schema.Types.ObjectId, ref: 'Role' }],
/**
* Учетная запись подтверждена
* устанавливается после подтверждения
* основного email адреса
* @type {boolean}
*/
confirmed: {
type: Boolean,
default: false
}
},
{
/**
* Имя коллекции MongoDB
* @default accounts
*/
collection: 'accounts',
/**
* Ключ по которому будут различатся записи
* обычно соответствует имени Схемы
* @default _type
*/
discriminatorKey: '_type'
}
);
/**
* Виртуальное поле. Проверка времени блокировки.
*
* @name isLocked
* @type {boolean}
* @memberOf Account
* @instance
*
* @example
*
* var account = new Account();
* if(account.isLocked) return false;
*
*/
_Schema.virtual('isLocked').get(function(){
return !!(this.lockUntil && this.lockUntil > Date.now());
});
_Schema.pre('save', function(next){
var user = this;
/**
* преобразование пароля
* только если было изменение (или новый)
*/
if(!user.isModified('password')) return next();
/** генерация salt */
bcrypt.genSalt(SALT_WORK_FACTOR, function(err,salt){
if(err) return next(err);
/**
* хеширование пароля
*/
bcrypt.hash(user.password,salt, function(err,hash){
if(err) return next(err);
// замена строки на хеш
user.password = hash;
next();
})
});
// custom validate
});
/**
* Сравнивает строку с паролем пользователя
*
* @param {string} candidate Строка пароля для проверки
* @param {Function} cb callback function
*
* @function comparePassword
* @memberOf Account
* @instance
*
*/
_Schema.methods.comparePassword = function(candidate,cb){
bcrypt.compare(candidate, this.password, function(err,isMatch){
if(err) return cb(err);
cb(null,isMatch);
});
};
/**
*
* @param {Function} cb callback
* @function incLoginAttempts
* @memberOf Account
* @instance
*
*/
_Schema.methods.incLoginAttempts = function(cb){
// если время блокировки истекло
if(this.lockUntil && this.lockUntil < Date.now()){
return this.update({
$set: { loginAttempts: 1 },
$unset: { lockUntil: 1 }
}, cb);
}
// иначе прибавляем счетчик попыток
var updates = { $inc : { loginAttempts: 1 } };
// блокировка если количество попыток превысило лимит
// и запись еще не заблокирована
if(this.loginAttempts +1 >= MAX_LOGIN_ATTEMPTS && !this.isLocked) {
updates.$set = { lockUntil: Date.now() + LOCK_TIME };
}
return this.update(updates,cb);
};
/**
*
* @memberOf Account
* @name failedLogin
* @type {object}
*
*/
var reasons = _Schema.statics.failedLogin = {
NOT_FOUND: 0,
PASSWORD_INCORRECT: 1,
MAX_ATTEMPTS: 2
};
/**
*
* Аутентификация пользователя
*
* @function getAuthenticated
* @type {function}
*
* @memberOf Account
*
* @param {string} username имя пользователя
* @param {string} password пароль
* @param {function} cb callback
*
*/
_Schema.statics.getAuthenticated = function(username, password, cb){
debug('statics',username,password);
//var self = this;
// @todo: local strategy broken this in statics
// var self = mongoose.model('User');
this.findOne({ username: username }, function(err, user){
if(err) cb(err);
if(!user) {
debug('user NOT_FOUND');
return cb(null,null,reasons.NOT_FOUND);
}
// проверка блокировки
if(user.isLocked){
debug('user locked',user);
return user.incLoginAttempts(function(err){
if(err) cb(err);
return cb(null,null, reasons.MAX_ATTEMPTS);
});
}
// проверка пароля
user.comparePassword(password, function(err,isMatch){
if(err) cb(err);
if(isMatch) {
debug('user success');
if(!user.loginAttempts && !user.lockUntil) return cb(null,user);
var updates = {
$set: { loginAttempts: 0 },
$unset: { lockUntil: 1 }
};
return user.update(updates, function(err){
if(err) return cd(err);
return cb(null,user);
})
}
// пароль не верный
debug('password fail', user.name);
user.incLoginAttempts(function(err){
if(err) return cb(err);
return cb(null,null,reasons.PASSWORD_INCORRECT);
});
});
});
};
return mongoose.model('Account',_Schema);
};
module.exports = new Model();