1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183 | 1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
1×
16×
16×
1×
3×
3×
1×
1×
1×
2×
2×
2×
1×
1×
1×
13×
13×
13×
13×
13×
8×
8×
13×
1×
12×
12×
12×
12×
12×
1×
1×
8×
8×
8×
4×
4×
3×
8×
1×
| var _ = require('lodash');
var bcrypt = require('bcrypt-nodejs');
var FieldType = require('../Type');
var util = require('util');
var utils = require('keystone-utils');
/**
* password FieldType Constructor
* @extends Field
* @api public
*/
function password (list, path, options) {
this._nativeType = String;
this._underscoreMethods = ['format', 'compare'];
this._fixedSize = 'full';
// You can't sort on password fields
options.nosort = true;
options.nofilter = true; // TODO: remove this when 0.4 is merged
this.workFactor = options.workFactor || 10;
password.super_.call(this, list, path, options);
}
util.inherits(password, FieldType);
/**
* Registers the field on the List's Mongoose Schema.
*
* Adds ...
*
* @api public
*/
password.prototype.addToSchema = function () {
var field = this;
var schema = this.list.schema;
var needs_hashing = '__' + field.path + '_needs_hashing';
this.paths = {
confirm: this.options.confirmPath || this._path.append('_confirm'),
hash: this.options.hashPath || this._path.append('_hash'),
};
schema.path(this.path, _.defaults({
type: String,
set: function (newValue) {
this[needs_hashing] = true;
return newValue;
},
}, this.options));
schema.virtual(this.paths.hash).set(function (newValue) {
this.set(field.path, newValue);
this[needs_hashing] = false;
});
schema.pre('save', function (next) {
if (!this.isModified(field.path) || !this[needs_hashing]) {
return next();
}
if (!this.get(field.path)) {
this.set(field.path, undefined);
return next();
}
var item = this;
bcrypt.genSalt(field.workFactor, function (err, salt) {
if (err) {
return next(err);
}
bcrypt.hash(item.get(field.path), salt, function () {}, function (err, hash) {
if (err) {
return next(err);
}
// override the cleartext password with the hashed one
item.set(field.path, hash);
next();
});
});
});
this.bindUnderscoreMethods();
};
/**
* Add filters to a query
*/
password.prototype.addFilterToQuery = function (filter, query) {
query = query || {};
query[this.path] = (filter.exists) ? { $ne: null } : null;
return query;
};
/**
* Formats the field value
*
* Password fields are always formatted as a random no. of asterisks,
* because the saved hash should never be displayed nor the length
* of the actual password hinted at.
*
* @api public
*/
password.prototype.format = function (item) {
if (!item.get(this.path)) return '';
var len = Math.round(Math.random() * 4) + 6;
var stars = '';
for (var i = 0; i < len; i++) stars += '*';
return stars;
};
/**
* Compares
*
* @api public
*/
password.prototype.compare = function (item, candidate, callback) {
if (typeof callback !== 'function') throw new Error('Password.compare() requires a callback function.');
var value = item.get(this.path);
if (!value) return callback(null, false);
bcrypt.compare(candidate, item.get(this.path), callback);
};
/**
* Asynchronously confirms that the provided password is valid
*/
password.prototype.validateInput = function (data, callback) {
var detail;
var result = true;
var confirmValue = this.getValueFromData(data, '_confirm');
var passwordValue = this.getValueFromData(data);
if (confirmValue !== undefined
&& passwordValue !== confirmValue) {
result = false;
detail = 'passwords must match';
}
// TODO: we could support a password complexity option (or regexp) here
utils.defer(callback, result, detail);
};
/**
* Asynchronously confirms that the provided password is valid
*/
password.prototype.validateRequiredInput = function (item, data, callback) {
var hashValue = this.getValueFromData(data, '_hash');
var passwordValue = this.getValueFromData(data);
var result = hashValue || passwordValue ? true : false;
if (!result && passwordValue === undefined && hashValue === undefined && item.get(this.path)) result = true;
utils.defer(callback, result);
};
/**
* If password fields are required, check that either a value has been
* provided or already exists in the field.
*
* Otherwise, input is always considered valid, as providing an empty
* value will not change the password.
*
* Deprecated
*/
password.prototype.inputIsValid = function (data, required, item) {
if (data[this.path] && this.paths.confirm in data) {
return data[this.path] === data[this.paths.confirm] ? true : false;
}
if (data[this.path] || data[this.paths.hash] || (item && item.get(this.path))) return true;
return required ? false : true;
};
/**
* Updates the value for this field in the item from a data object
*
* Will accept either the field path, or paths.hash to bypass bcrypt
*
* @api public
*/
password.prototype.updateItem = function (item, data, callback) {
var hashValue = this.getValueFromData(data, '_hash');
var passwordValue = this.getValueFromData(data);
if (passwordValue !== undefined) {
item.set(this.path, passwordValue);
} else if (hashValue !== undefined) {
item.set(this.paths.hash, hashValue);
}
process.nextTick(callback);
};
/* Export Field Type */
module.exports = password;
|