UNPKG

5.58 kBJavaScriptView Raw
1/**
2 * Password reset module
3 *
4 * @author James Brumond
5 */
6
7var util = require('util');
8var uuid = require('uuid-v4');
9
10/**
11 * Stored requests that have not yet been fulfilled
12 */
13var storage = exports._storage = (function() {
14 var tokens = { };
15 return {
16 create: function(id) {
17 var token = uuid();
18 tokens[token] = {
19 id: id,
20 timer: setTimeout(
21 function() {
22 storage.destroy(token);
23 },
24 expireTimeout
25 )
26 };
27 return token;
28 },
29 lookup: function(token) {
30 if (tokens[token]) {
31 return tokens[token].id;
32 }
33 },
34 destroy: function(token) {
35 if (tokens[token]) {
36 clearTimeout(tokens[token].timer);
37 delete tokens[token];
38 }
39 }
40 };
41}());
42
43/**
44 * Set the expire timeout
45 *
46 * Defaults to 43200000, or 12 hours
47 */
48var timeoutUnits = {
49 sec: 1000,
50 secs: 1000,
51 min: 1000 * 60,
52 mins: 1000 * 60,
53 hour: 1000 * 60 * 60,
54 hours: 1000 * 60 * 60,
55 day: 1000 * 60 * 60 * 24,
56 days: 1000 * 60 * 60 * 24,
57 week: 1000 * 60 * 60 * 24 * 7,
58 weeks: 1000 * 60 * 60 * 24 * 7
59};
60var expireTimeout = 43200000;
61exports.expireTimeout = function(num, unit) {
62 if (typeof num === 'number' && ! isNaN(num)) {
63 var multiplier = 1;
64 if (unit && timeoutUnits.hasOwnProperty(unit)) {
65 multiplier = timeoutUnits[unit];
66 }
67 expireTimeout = num * multiplier;
68 }
69 return expireTimeout;
70};
71
72/**
73 * User lookup routine
74 *
75 * Should result in an array of objects containing a unique id and email.
76 */
77var lookupUsers = function(login, callback) {
78 callback(null, null);
79};
80exports.lookupUsers = function(func) {
81 if (typeof func === 'function') {
82 lookupUsers = func;
83 }
84};
85
86/**
87 * Password setting routine
88 *
89 * Should take a unique id (as returned from the user lookup
90 * routine) and a new password.
91 */
92var setPassword = function(id, password, callback) {
93 callback(null, false, null);
94};
95exports.setPassword = function(func) {
96 if (typeof func === 'function') {
97 setPassword = func;
98 }
99};
100
101/**
102 * Email sending routine
103 *
104 * Should take an email address and tokens.
105 */
106var sendEmail = function(email, resets, callback) {
107 callback(null, false);
108};
109exports.sendEmail = function(func) {
110 if (typeof func === 'function') {
111 sendEmail = func;
112 }
113};
114
115/**
116 * The route that takes reset requests
117 *
118 * eg. POST /password/reset
119 */
120exports.requestResetToken = function(opts) {
121 opts = merge({
122 next: null,
123 loginParam: 'login',
124 callbackURL: '/password/reset/{token}'
125 }, opts);
126 var func = function(req, res, next) {
127 var login = req.body ? req.body[opts.loginParam] : null;
128 if (! login) {
129 return res.json(jsonError('No login given'), 400);
130 }
131 lookupUsers(login, function(err, users) {
132 if (err) {
133 return res.json(jsonError(err), 500);
134 }
135 if (! users) {
136 return res.json(jsonError('No such user'), 404);
137 }
138 users.users = users.users.map(function(user) {
139 var token = storage.create(user.id);
140 return {
141 token: token,
142 name: user.name,
143 url: opts.callbackURL.replace('{token}', token)
144 };
145 });
146 sendEmail(users.email, users.users, function(err, sent) {
147 if (err) {
148 return res.json(jsonError(err), 500);
149 }
150 if (! opts.next) {
151 return res.send(200);
152 }
153 if (typeof opts.next === 'string') {
154 res.redirect(opts.next);
155 } else if (typeof opts.next === 'function') {
156 opts.next(req, res, next);
157 } else {
158 next();
159 }
160 });
161 });
162 };
163 func._opts = opts;
164 return func;
165};
166
167/**
168 * The route that actually does resets
169 *
170 * eg. PUT /password/reset
171 */
172exports.resetPassword = function(opts) {
173 opts = merge({
174 next: null,
175 tokenParam: 'token',
176 passwordParam: 'password',
177 confirmParam: 'confirm'
178 }, opts);
179 var func = function(req, res, next) {
180 var params = req.body ? {
181 token: req.body[opts.tokenParam],
182 password: req.body[opts.passwordParam],
183 confirm: req.body[opts.confirmParam]
184 } : { };
185 if (! params.token || ! params.password || ! params.confirm) {
186 return res.json(jsonError('Cannot attempt reset with missing params'), 400);
187 }
188 var id = storage.lookup(params.token);
189 if (! id) {
190 return res.json(jsonError('Request token is invalid'), 401);
191 }
192 if (params.password !== params.confirm) {
193 return res.json(jsonError('Password and confirmation do not match'), 400);
194 }
195 setPassword(id, params.password,
196 function(err, success, validationError) {
197 if (err) {
198 return res.json(jsonError(err), 500);
199 }
200 if (! success) {
201 return res.json(jsonError(validationError), 400);
202 }
203 storage.destroy(params.token);
204 if (! opts.next) {
205 return res.send(200);
206 }
207 if (typeof opts.next === 'string') {
208 res.redirect(opts.next);
209 } else if (typeof opts.next === 'function') {
210 opts.next(req, res, next);
211 } else {
212 next();
213 }
214 }
215 );
216 };
217 func._opts = opts;
218 return func;
219};
220
221// ------------------------------------------------------------------
222// Utilities
223
224function jsonError(msg) {
225 if (msg instanceof Error) {
226 msg = {
227 type: msg.type,
228 message: msg.message,
229 stack: msg.stack,
230 stackArray: msg.stack.split('\n').slice(1).map(function(str) {
231 return str.trim();
232 })
233 };
234 } else {
235 msg = {message: msg};
236 }
237 return {error: msg};
238}
239
240function merge(host) {
241 host = isMutable(host) ? host : { };
242 Array.prototype.slice.call(arguments, 1).forEach(function(arg) {
243 if (isMutable(arg)) {
244 Object.keys(arg).forEach(function(prop) {
245 host[prop] = arg[prop];
246 });
247 }
248 });
249 return host;
250}
251
252function isMutable(value) {
253 return (typeof value === 'object' && value) || typeof value === 'function';
254}
255
256/* End of file index.js */