@proteinjs/user-server 1.21.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +5 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/src/authentication/PasswordResetToken.d.ts +45 -0
- package/dist/src/authentication/PasswordResetToken.d.ts.map +1 -0
- package/dist/src/authentication/PasswordResetToken.js +138 -0
- package/dist/src/authentication/PasswordResetToken.js.map +1 -0
- package/dist/src/routes/devMail.d.ts +4 -0
- package/dist/src/routes/devMail.d.ts.map +1 -0
- package/dist/src/routes/devMail.js +119 -0
- package/dist/src/routes/devMail.js.map +1 -0
- package/dist/src/routes/executePasswordReset.d.ts +4 -4
- package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
- package/dist/src/routes/executePasswordReset.js +27 -26
- package/dist/src/routes/executePasswordReset.js.map +1 -1
- package/dist/src/routes/initiatePasswordReset.js +2 -2
- package/dist/src/routes/initiatePasswordReset.js.map +1 -1
- package/dist/src/routes/validateResetPasswordToken.d.ts.map +1 -1
- package/dist/src/routes/validateResetPasswordToken.js +13 -16
- package/dist/src/routes/validateResetPasswordToken.js.map +1 -1
- package/dist/test/DevMail.test.d.ts +2 -0
- package/dist/test/DevMail.test.d.ts.map +1 -0
- package/dist/test/DevMail.test.js +250 -0
- package/dist/test/DevMail.test.js.map +1 -0
- package/dist/test/ExecutePasswordReset.test.d.ts +2 -0
- package/dist/test/ExecutePasswordReset.test.d.ts.map +1 -0
- package/dist/test/ExecutePasswordReset.test.js +394 -0
- package/dist/test/ExecutePasswordReset.test.js.map +1 -0
- package/dist/test/ValidateResetToken.test.js +30 -7
- package/dist/test/ValidateResetToken.test.js.map +1 -1
- package/generated/index.ts +5 -1
- package/package.json +3 -3
- package/src/authentication/PasswordResetToken.ts +96 -0
- package/src/routes/devMail.ts +72 -0
- package/src/routes/executePasswordReset.ts +25 -24
- package/src/routes/initiatePasswordReset.ts +2 -2
- package/src/routes/validateResetPasswordToken.ts +12 -15
- package/test/DevMail.test.ts +151 -0
- package/test/ExecutePasswordReset.test.ts +224 -0
- package/test/ValidateResetToken.test.ts +24 -6
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
var email_server_1 = require("@proteinjs/email-server");
|
|
40
|
+
var devMail_1 = require("../src/routes/devMail");
|
|
41
|
+
/**
|
|
42
|
+
* `GET /dev/mail` and `GET /dev/mail/<id>` — the dev-only door onto the mail sink, gated exactly
|
|
43
|
+
* like `/dev/login`: `DEVELOPMENT` AND `DEV_AUTO_LOGIN_EMAIL`, else 404 as if unregistered. With
|
|
44
|
+
* the gates open, the last invite a development sender recorded is readable — its link off the
|
|
45
|
+
* list, its rendered html off the message — so a lane never needs a real inbox. No database:
|
|
46
|
+
* the sink is the process's own ring.
|
|
47
|
+
*/
|
|
48
|
+
var ENV_EMAIL = 'dev@test.local';
|
|
49
|
+
var invoke = function (route, _a) {
|
|
50
|
+
var _b = _a === void 0 ? {} : _a, query = _b.query, params = _b.params;
|
|
51
|
+
return __awaiter(void 0, void 0, void 0, function () {
|
|
52
|
+
var outcome, response;
|
|
53
|
+
return __generator(this, function (_c) {
|
|
54
|
+
switch (_c.label) {
|
|
55
|
+
case 0:
|
|
56
|
+
outcome = {};
|
|
57
|
+
response = {
|
|
58
|
+
status: function (code) {
|
|
59
|
+
outcome.status = code;
|
|
60
|
+
return this;
|
|
61
|
+
},
|
|
62
|
+
type: function (kind) {
|
|
63
|
+
outcome.type = kind;
|
|
64
|
+
return this;
|
|
65
|
+
},
|
|
66
|
+
send: function (body) {
|
|
67
|
+
outcome.body = body;
|
|
68
|
+
},
|
|
69
|
+
json: function (body) {
|
|
70
|
+
outcome.body = body;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return [4 /*yield*/, route.onRequest({ query: query !== null && query !== void 0 ? query : {}, params: params !== null && params !== void 0 ? params : {} }, response)];
|
|
74
|
+
case 1:
|
|
75
|
+
_c.sent();
|
|
76
|
+
return [2 /*return*/, outcome];
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
var INVITE_LINK = 'http://localhost:7985/auth/signup?token=abc123';
|
|
82
|
+
var INVITE_HTML = "<html><body><p>You are invited.</p><a href=\"".concat(INVITE_LINK, "\">Accept</a></body></html>");
|
|
83
|
+
/** The product's path onto the sink: a development sender with a real-looking SMTP config, no opt-in. */
|
|
84
|
+
var sendInviteThroughSender = function (to) { return __awaiter(void 0, void 0, void 0, function () {
|
|
85
|
+
return __generator(this, function (_a) {
|
|
86
|
+
switch (_a.label) {
|
|
87
|
+
case 0: return [4 /*yield*/, new email_server_1.EmailSender({
|
|
88
|
+
host: 'smtp.test.local',
|
|
89
|
+
port: 465,
|
|
90
|
+
secure: true,
|
|
91
|
+
auth: { user: 'mailbox@test.local', pass: 'unused' },
|
|
92
|
+
from: '"Example" <hi@example.com>',
|
|
93
|
+
}).sendEmail({ to: to, subject: "You're invited", text: "Accept your invite: ".concat(INVITE_LINK), html: INVITE_HTML })];
|
|
94
|
+
case 1:
|
|
95
|
+
_a.sent();
|
|
96
|
+
return [2 /*return*/];
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}); };
|
|
100
|
+
describe('devMail routes', function () {
|
|
101
|
+
var originalEnv = {
|
|
102
|
+
DEVELOPMENT: process.env.DEVELOPMENT,
|
|
103
|
+
DEV_AUTO_LOGIN_EMAIL: process.env.DEV_AUTO_LOGIN_EMAIL,
|
|
104
|
+
EMAIL_TRANSPORT: process.env.EMAIL_TRANSPORT,
|
|
105
|
+
EMAIL_ALLOW_REAL_SEND: process.env.EMAIL_ALLOW_REAL_SEND,
|
|
106
|
+
};
|
|
107
|
+
beforeEach(function () {
|
|
108
|
+
process.env.DEVELOPMENT = 'true';
|
|
109
|
+
process.env.DEV_AUTO_LOGIN_EMAIL = ENV_EMAIL;
|
|
110
|
+
delete process.env.EMAIL_TRANSPORT;
|
|
111
|
+
delete process.env.EMAIL_ALLOW_REAL_SEND;
|
|
112
|
+
email_server_1.MailSink.get().clear();
|
|
113
|
+
});
|
|
114
|
+
afterEach(function () {
|
|
115
|
+
for (var _i = 0, _a = Object.entries(originalEnv); _i < _a.length; _i++) {
|
|
116
|
+
var _b = _a[_i], key = _b[0], value = _b[1];
|
|
117
|
+
if (value === undefined) {
|
|
118
|
+
delete process.env[key];
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
process.env[key] = value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
it("lists the last sent messages newest first — the invite's recipient, subject, first link and timestamp — and the message renders its html", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
126
|
+
var list, messages, message;
|
|
127
|
+
return __generator(this, function (_a) {
|
|
128
|
+
switch (_a.label) {
|
|
129
|
+
case 0: return [4 /*yield*/, sendInviteThroughSender('earlier@test.local')];
|
|
130
|
+
case 1:
|
|
131
|
+
_a.sent();
|
|
132
|
+
return [4 /*yield*/, sendInviteThroughSender('lane-after@test.local')];
|
|
133
|
+
case 2:
|
|
134
|
+
_a.sent();
|
|
135
|
+
return [4 /*yield*/, invoke(devMail_1.devMail)];
|
|
136
|
+
case 3:
|
|
137
|
+
list = _a.sent();
|
|
138
|
+
expect(list.status).toBe(200);
|
|
139
|
+
messages = list.body.messages;
|
|
140
|
+
expect(messages).toHaveLength(2);
|
|
141
|
+
expect(messages[0]).toMatchObject({
|
|
142
|
+
to: ['lane-after@test.local'],
|
|
143
|
+
subject: "You're invited",
|
|
144
|
+
link: INVITE_LINK,
|
|
145
|
+
from: 'hi@example.com',
|
|
146
|
+
refused: true,
|
|
147
|
+
});
|
|
148
|
+
expect(typeof messages[0].id).toBe('string');
|
|
149
|
+
expect(Date.parse(String(messages[0].at))).not.toBeNaN();
|
|
150
|
+
expect(messages[0].url).toBe("/dev/mail/".concat(messages[0].id));
|
|
151
|
+
expect(messages[1].to).toEqual(['earlier@test.local']);
|
|
152
|
+
return [4 /*yield*/, invoke(devMail_1.devMailMessage, { params: { id: String(messages[0].id) } })];
|
|
153
|
+
case 4:
|
|
154
|
+
message = _a.sent();
|
|
155
|
+
expect(message.status).toBe(200);
|
|
156
|
+
expect(message.type).toBe('html');
|
|
157
|
+
expect(message.body).toBe(INVITE_HTML);
|
|
158
|
+
return [2 /*return*/];
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}); });
|
|
162
|
+
it('?n= bounds the list; a text-only message renders as text; an unknown id is 404', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
163
|
+
var one, all, messages, plain, _a;
|
|
164
|
+
return __generator(this, function (_b) {
|
|
165
|
+
switch (_b.label) {
|
|
166
|
+
case 0:
|
|
167
|
+
process.env.EMAIL_TRANSPORT = 'sink';
|
|
168
|
+
return [4 /*yield*/, new email_server_1.EmailSender({ host: 'h', port: 465, secure: true, from: '"Example" <hi@example.com>' }).sendEmail({
|
|
169
|
+
to: 'a@test.local',
|
|
170
|
+
subject: 'Plain',
|
|
171
|
+
text: 'just text',
|
|
172
|
+
})];
|
|
173
|
+
case 1:
|
|
174
|
+
_b.sent();
|
|
175
|
+
return [4 /*yield*/, sendInviteThroughSender('b@test.local')];
|
|
176
|
+
case 2:
|
|
177
|
+
_b.sent();
|
|
178
|
+
return [4 /*yield*/, invoke(devMail_1.devMail, { query: { n: '1' } })];
|
|
179
|
+
case 3:
|
|
180
|
+
one = _b.sent();
|
|
181
|
+
expect(one.body.messages).toHaveLength(1);
|
|
182
|
+
return [4 /*yield*/, invoke(devMail_1.devMail, { query: { n: 'garbage' } })];
|
|
183
|
+
case 4:
|
|
184
|
+
all = _b.sent();
|
|
185
|
+
messages = all.body.messages;
|
|
186
|
+
expect(messages.map(function (m) { return m.subject; })).toEqual(["You're invited", 'Plain']);
|
|
187
|
+
expect(messages[1].refused).toBe(false);
|
|
188
|
+
return [4 /*yield*/, invoke(devMail_1.devMailMessage, { params: { id: messages[1].id } })];
|
|
189
|
+
case 5:
|
|
190
|
+
plain = _b.sent();
|
|
191
|
+
expect(plain.status).toBe(200);
|
|
192
|
+
expect(plain.type).toBe('text');
|
|
193
|
+
expect(plain.body).toBe('just text');
|
|
194
|
+
_a = expect;
|
|
195
|
+
return [4 /*yield*/, invoke(devMail_1.devMailMessage, { params: { id: 'nope' } })];
|
|
196
|
+
case 6:
|
|
197
|
+
_a.apply(void 0, [(_b.sent()).status]).toBe(404);
|
|
198
|
+
return [2 /*return*/];
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}); });
|
|
202
|
+
it('DEVELOPMENT unset → 404 on both paths, even with messages in the sink', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
203
|
+
var record, _a, _b;
|
|
204
|
+
return __generator(this, function (_c) {
|
|
205
|
+
switch (_c.label) {
|
|
206
|
+
case 0: return [4 /*yield*/, sendInviteThroughSender('lane@test.local')];
|
|
207
|
+
case 1:
|
|
208
|
+
_c.sent();
|
|
209
|
+
record = email_server_1.MailSink.get().list()[0];
|
|
210
|
+
delete process.env.DEVELOPMENT;
|
|
211
|
+
_a = expect;
|
|
212
|
+
return [4 /*yield*/, invoke(devMail_1.devMail)];
|
|
213
|
+
case 2:
|
|
214
|
+
_a.apply(void 0, [(_c.sent()).status]).toBe(404);
|
|
215
|
+
_b = expect;
|
|
216
|
+
return [4 /*yield*/, invoke(devMail_1.devMailMessage, { params: { id: record.id } })];
|
|
217
|
+
case 3:
|
|
218
|
+
_b.apply(void 0, [(_c.sent()).status]).toBe(404);
|
|
219
|
+
return [2 /*return*/];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}); });
|
|
223
|
+
it('DEV_AUTO_LOGIN_EMAIL unset (or blank) → 404 on both paths', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
224
|
+
var record, _a, _b, _c;
|
|
225
|
+
return __generator(this, function (_d) {
|
|
226
|
+
switch (_d.label) {
|
|
227
|
+
case 0: return [4 /*yield*/, sendInviteThroughSender('lane@test.local')];
|
|
228
|
+
case 1:
|
|
229
|
+
_d.sent();
|
|
230
|
+
record = email_server_1.MailSink.get().list()[0];
|
|
231
|
+
delete process.env.DEV_AUTO_LOGIN_EMAIL;
|
|
232
|
+
_a = expect;
|
|
233
|
+
return [4 /*yield*/, invoke(devMail_1.devMail)];
|
|
234
|
+
case 2:
|
|
235
|
+
_a.apply(void 0, [(_d.sent()).status]).toBe(404);
|
|
236
|
+
_b = expect;
|
|
237
|
+
return [4 /*yield*/, invoke(devMail_1.devMailMessage, { params: { id: record.id } })];
|
|
238
|
+
case 3:
|
|
239
|
+
_b.apply(void 0, [(_d.sent()).status]).toBe(404);
|
|
240
|
+
process.env.DEV_AUTO_LOGIN_EMAIL = ' ';
|
|
241
|
+
_c = expect;
|
|
242
|
+
return [4 /*yield*/, invoke(devMail_1.devMail)];
|
|
243
|
+
case 4:
|
|
244
|
+
_c.apply(void 0, [(_d.sent()).status]).toBe(404);
|
|
245
|
+
return [2 /*return*/];
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}); });
|
|
249
|
+
});
|
|
250
|
+
//# sourceMappingURL=DevMail.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevMail.test.js","sourceRoot":"","sources":["../../test/DevMail.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExecutePasswordReset.test.d.ts","sourceRoot":"","sources":["../../test/ExecutePasswordReset.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
24
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
var moment_1 = __importDefault(require("moment"));
|
|
54
|
+
var db_1 = require("@proteinjs/db");
|
|
55
|
+
var logger_1 = require("@proteinjs/logger");
|
|
56
|
+
var user_1 = require("@proteinjs/user");
|
|
57
|
+
var executePasswordReset_1 = require("../src/routes/executePasswordReset");
|
|
58
|
+
var PasswordHasher_1 = require("../src/authentication/PasswordHasher");
|
|
59
|
+
var PasswordResetToken_1 = require("../src/authentication/PasswordResetToken");
|
|
60
|
+
var UserServerTestEnvironment_1 = require("./UserServerTestEnvironment");
|
|
61
|
+
var testEnv = new UserServerTestEnvironment_1.UserServerTestEnvironment();
|
|
62
|
+
var invokeExecute = function (body) { return __awaiter(void 0, void 0, void 0, function () {
|
|
63
|
+
var outcome, response;
|
|
64
|
+
return __generator(this, function (_a) {
|
|
65
|
+
switch (_a.label) {
|
|
66
|
+
case 0:
|
|
67
|
+
outcome = { status: 200 };
|
|
68
|
+
response = {
|
|
69
|
+
status: function (code) {
|
|
70
|
+
outcome.status = code;
|
|
71
|
+
return this;
|
|
72
|
+
},
|
|
73
|
+
send: function (body) {
|
|
74
|
+
outcome.body = body;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
return [4 /*yield*/, executePasswordReset_1.executePasswordReset.onRequest({ body: body }, response)];
|
|
78
|
+
case 1:
|
|
79
|
+
_a.sent();
|
|
80
|
+
return [2 /*return*/, outcome];
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}); };
|
|
84
|
+
var mintToken = function () { return new PasswordResetToken_1.PasswordResetToken().mint(); };
|
|
85
|
+
var armResetToken = function (email, token, expiration) { return __awaiter(void 0, void 0, void 0, function () {
|
|
86
|
+
var user;
|
|
87
|
+
return __generator(this, function (_a) {
|
|
88
|
+
switch (_a.label) {
|
|
89
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Reset User', email: email })];
|
|
90
|
+
case 1:
|
|
91
|
+
user = _a.sent();
|
|
92
|
+
return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, {
|
|
93
|
+
id: user.id,
|
|
94
|
+
passwordResetToken: token,
|
|
95
|
+
passwordResetTokenExpiration: expiration,
|
|
96
|
+
})];
|
|
97
|
+
case 2:
|
|
98
|
+
_a.sent();
|
|
99
|
+
return [2 /*return*/, user];
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}); };
|
|
103
|
+
var userRow = function (id) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
104
|
+
switch (_a.label) {
|
|
105
|
+
case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.User, { id: id })];
|
|
106
|
+
case 1: return [2 /*return*/, _a.sent()];
|
|
107
|
+
}
|
|
108
|
+
}); }); };
|
|
109
|
+
/** Every password in the table, by email — the whole-table outcome a refused request must leave untouched. */
|
|
110
|
+
var passwordsByEmail = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
111
|
+
var byEmail, _i, _a, user;
|
|
112
|
+
return __generator(this, function (_b) {
|
|
113
|
+
switch (_b.label) {
|
|
114
|
+
case 0:
|
|
115
|
+
byEmail = {};
|
|
116
|
+
_i = 0;
|
|
117
|
+
return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(user_1.tables.User, {})];
|
|
118
|
+
case 1:
|
|
119
|
+
_a = _b.sent();
|
|
120
|
+
_b.label = 2;
|
|
121
|
+
case 2:
|
|
122
|
+
if (!(_i < _a.length)) return [3 /*break*/, 4];
|
|
123
|
+
user = _a[_i];
|
|
124
|
+
byEmail[user.email] = user.password;
|
|
125
|
+
_b.label = 3;
|
|
126
|
+
case 3:
|
|
127
|
+
_i++;
|
|
128
|
+
return [3 /*break*/, 2];
|
|
129
|
+
case 4: return [2 /*return*/, byEmail];
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}); };
|
|
133
|
+
describe('executePasswordReset route', function () {
|
|
134
|
+
beforeAll(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
135
|
+
return __generator(this, function (_a) {
|
|
136
|
+
switch (_a.label) {
|
|
137
|
+
case 0: return [4 /*yield*/, testEnv.beforeAll()];
|
|
138
|
+
case 1:
|
|
139
|
+
_a.sent();
|
|
140
|
+
// No pending reset — the account a `null` token's `IS NULL` filter would match.
|
|
141
|
+
return [4 /*yield*/, testEnv.createUser({ name: 'Idle User', email: 'reset-idle@test.local' })];
|
|
142
|
+
case 2:
|
|
143
|
+
// No pending reset — the account a `null` token's `IS NULL` filter would match.
|
|
144
|
+
_a.sent();
|
|
145
|
+
// An emptied token column — the account an empty token would match.
|
|
146
|
+
return [4 /*yield*/, armResetToken('reset-emptied@test.local', '', (0, moment_1.default)().add(1, 'hour'))];
|
|
147
|
+
case 3:
|
|
148
|
+
// An emptied token column — the account an empty token would match.
|
|
149
|
+
_a.sent();
|
|
150
|
+
return [2 /*return*/];
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}); });
|
|
154
|
+
afterAll(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
155
|
+
return __generator(this, function (_a) {
|
|
156
|
+
switch (_a.label) {
|
|
157
|
+
case 0: return [4 /*yield*/, testEnv.afterAll()];
|
|
158
|
+
case 1:
|
|
159
|
+
_a.sent();
|
|
160
|
+
return [2 /*return*/];
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}); });
|
|
164
|
+
describe('a request without a well-formed token is refused before any lookup', function () {
|
|
165
|
+
it.each([
|
|
166
|
+
['absent', {}],
|
|
167
|
+
['null', { token: null }],
|
|
168
|
+
['empty', { token: '' }],
|
|
169
|
+
['not a token string', { token: 'tok-valid-1' }],
|
|
170
|
+
['a number', { token: 123 }],
|
|
171
|
+
['an array', { token: ['a', 'b'] }],
|
|
172
|
+
['an object', { token: { passwordResetToken: null } }],
|
|
173
|
+
])('%s: 400, and no password in the table changes', function (_label, tokenField) { return __awaiter(void 0, void 0, void 0, function () {
|
|
174
|
+
var before, outcome, _a;
|
|
175
|
+
return __generator(this, function (_b) {
|
|
176
|
+
switch (_b.label) {
|
|
177
|
+
case 0: return [4 /*yield*/, passwordsByEmail()];
|
|
178
|
+
case 1:
|
|
179
|
+
before = _b.sent();
|
|
180
|
+
return [4 /*yield*/, invokeExecute(__assign(__assign({}, tokenField), { newPassword: 'hijacked' }))];
|
|
181
|
+
case 2:
|
|
182
|
+
outcome = _b.sent();
|
|
183
|
+
expect(outcome.status).toBe(400);
|
|
184
|
+
expect(outcome.body).toEqual({ error: 'Invalid or expired reset token' });
|
|
185
|
+
_a = expect;
|
|
186
|
+
return [4 /*yield*/, passwordsByEmail()];
|
|
187
|
+
case 3:
|
|
188
|
+
_a.apply(void 0, [_b.sent()]).toEqual(before);
|
|
189
|
+
return [2 /*return*/];
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}); });
|
|
193
|
+
});
|
|
194
|
+
it('a live token resets the password once: the row verifies it, the token clears, a re-presentation is refused', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
195
|
+
var token, user, first, afterFirst, second, _a;
|
|
196
|
+
return __generator(this, function (_b) {
|
|
197
|
+
switch (_b.label) {
|
|
198
|
+
case 0:
|
|
199
|
+
token = mintToken();
|
|
200
|
+
return [4 /*yield*/, armResetToken('reset-live@test.local', token, (0, moment_1.default)().add(1, 'hour'))];
|
|
201
|
+
case 1:
|
|
202
|
+
user = _b.sent();
|
|
203
|
+
return [4 /*yield*/, invokeExecute({ token: token, newPassword: 'first new password' })];
|
|
204
|
+
case 2:
|
|
205
|
+
first = _b.sent();
|
|
206
|
+
expect(first.status).toBe(200);
|
|
207
|
+
expect(first.body).toEqual({ message: 'Password has been successfully reset' });
|
|
208
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
209
|
+
case 3:
|
|
210
|
+
afterFirst = _b.sent();
|
|
211
|
+
return [4 /*yield*/, expect(new PasswordHasher_1.PasswordHasher().verify(afterFirst.password, 'first new password')).resolves.toBe(true)];
|
|
212
|
+
case 4:
|
|
213
|
+
_b.sent();
|
|
214
|
+
expect(afterFirst.passwordResetToken).toBeNull();
|
|
215
|
+
expect(afterFirst.passwordResetTokenExpiration).toBeNull();
|
|
216
|
+
return [4 /*yield*/, invokeExecute({ token: token, newPassword: 'second new password' })];
|
|
217
|
+
case 5:
|
|
218
|
+
second = _b.sent();
|
|
219
|
+
expect(second.status).toBe(400);
|
|
220
|
+
_a = expect;
|
|
221
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
222
|
+
case 6:
|
|
223
|
+
_a.apply(void 0, [(_b.sent()).password]).toBe(afterFirst.password);
|
|
224
|
+
return [2 /*return*/];
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}); });
|
|
228
|
+
it('an expired token is refused and changes nothing', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
229
|
+
var token, user, before, outcome, row;
|
|
230
|
+
return __generator(this, function (_a) {
|
|
231
|
+
switch (_a.label) {
|
|
232
|
+
case 0:
|
|
233
|
+
token = mintToken();
|
|
234
|
+
return [4 /*yield*/, armResetToken('reset-expired@test.local', token, (0, moment_1.default)().subtract(1, 'minute'))];
|
|
235
|
+
case 1:
|
|
236
|
+
user = _a.sent();
|
|
237
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
238
|
+
case 2:
|
|
239
|
+
before = (_a.sent()).password;
|
|
240
|
+
return [4 /*yield*/, invokeExecute({ token: token, newPassword: 'hijacked' })];
|
|
241
|
+
case 3:
|
|
242
|
+
outcome = _a.sent();
|
|
243
|
+
expect(outcome.status).toBe(400);
|
|
244
|
+
expect(outcome.body).toEqual({ error: 'Reset token has expired' });
|
|
245
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
246
|
+
case 4:
|
|
247
|
+
row = _a.sent();
|
|
248
|
+
expect(row.password).toBe(before);
|
|
249
|
+
expect(row.passwordResetToken).toBe(token);
|
|
250
|
+
return [2 /*return*/];
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}); });
|
|
254
|
+
it('a token whose row carries no expiry is refused — never live by omission', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
255
|
+
var token, user, before, outcome, _a;
|
|
256
|
+
return __generator(this, function (_b) {
|
|
257
|
+
switch (_b.label) {
|
|
258
|
+
case 0:
|
|
259
|
+
token = mintToken();
|
|
260
|
+
return [4 /*yield*/, armResetToken('reset-no-expiry@test.local', token, null)];
|
|
261
|
+
case 1:
|
|
262
|
+
user = _b.sent();
|
|
263
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
264
|
+
case 2:
|
|
265
|
+
before = (_b.sent()).password;
|
|
266
|
+
return [4 /*yield*/, invokeExecute({ token: token, newPassword: 'hijacked' })];
|
|
267
|
+
case 3:
|
|
268
|
+
outcome = _b.sent();
|
|
269
|
+
expect(outcome.status).toBe(400);
|
|
270
|
+
_a = expect;
|
|
271
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
272
|
+
case 4:
|
|
273
|
+
_a.apply(void 0, [(_b.sent()).password]).toBe(before);
|
|
274
|
+
return [2 /*return*/];
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}); });
|
|
278
|
+
it('a blank new password is refused without consuming the token', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
279
|
+
var token, user, before, _i, _a, newPassword, outcome, row;
|
|
280
|
+
return __generator(this, function (_b) {
|
|
281
|
+
switch (_b.label) {
|
|
282
|
+
case 0:
|
|
283
|
+
token = mintToken();
|
|
284
|
+
return [4 /*yield*/, armResetToken('reset-blank@test.local', token, (0, moment_1.default)().add(1, 'hour'))];
|
|
285
|
+
case 1:
|
|
286
|
+
user = _b.sent();
|
|
287
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
288
|
+
case 2:
|
|
289
|
+
before = (_b.sent()).password;
|
|
290
|
+
_i = 0, _a = ['', undefined, 42];
|
|
291
|
+
_b.label = 3;
|
|
292
|
+
case 3:
|
|
293
|
+
if (!(_i < _a.length)) return [3 /*break*/, 6];
|
|
294
|
+
newPassword = _a[_i];
|
|
295
|
+
return [4 /*yield*/, invokeExecute({ token: token, newPassword: newPassword })];
|
|
296
|
+
case 4:
|
|
297
|
+
outcome = _b.sent();
|
|
298
|
+
expect(outcome.status).toBe(400);
|
|
299
|
+
_b.label = 5;
|
|
300
|
+
case 5:
|
|
301
|
+
_i++;
|
|
302
|
+
return [3 /*break*/, 3];
|
|
303
|
+
case 6: return [4 /*yield*/, userRow(user.id)];
|
|
304
|
+
case 7:
|
|
305
|
+
row = _b.sent();
|
|
306
|
+
expect(row.password).toBe(before);
|
|
307
|
+
expect(row.passwordResetToken).toBe(token);
|
|
308
|
+
return [2 /*return*/];
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}); });
|
|
312
|
+
it('the presented token never reaches the log', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
313
|
+
var token, info, _i, _a, entry;
|
|
314
|
+
return __generator(this, function (_b) {
|
|
315
|
+
switch (_b.label) {
|
|
316
|
+
case 0:
|
|
317
|
+
token = mintToken();
|
|
318
|
+
info = jest.spyOn(logger_1.Logger.prototype, 'info');
|
|
319
|
+
_b.label = 1;
|
|
320
|
+
case 1:
|
|
321
|
+
_b.trys.push([1, , 3, 4]);
|
|
322
|
+
return [4 /*yield*/, invokeExecute({ token: token, newPassword: 'hijacked' })];
|
|
323
|
+
case 2:
|
|
324
|
+
_b.sent();
|
|
325
|
+
expect(info).toHaveBeenCalled();
|
|
326
|
+
for (_i = 0, _a = info.mock.calls; _i < _a.length; _i++) {
|
|
327
|
+
entry = _a[_i][0];
|
|
328
|
+
expect(JSON.stringify(entry)).not.toContain(token);
|
|
329
|
+
}
|
|
330
|
+
return [3 /*break*/, 4];
|
|
331
|
+
case 3:
|
|
332
|
+
info.mockRestore();
|
|
333
|
+
return [7 /*endfinally*/];
|
|
334
|
+
case 4: return [2 /*return*/];
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}); });
|
|
338
|
+
describe('PasswordResetToken', function () {
|
|
339
|
+
var internals = new PasswordResetToken_1.PasswordResetToken();
|
|
340
|
+
it('mints 64 lowercase hex characters, fresh each time', function () {
|
|
341
|
+
var token = mintToken();
|
|
342
|
+
expect(token).toMatch(/^[0-9a-f]{64}$/);
|
|
343
|
+
expect(mintToken()).not.toBe(token);
|
|
344
|
+
});
|
|
345
|
+
it('a stored token matches only the same string — never an absent or emptied one', function () {
|
|
346
|
+
var token = mintToken();
|
|
347
|
+
expect(internals.matches(token, token)).toBe(true);
|
|
348
|
+
expect(internals.matches(mintToken(), token)).toBe(false);
|
|
349
|
+
expect(internals.matches(null, token)).toBe(false);
|
|
350
|
+
expect(internals.matches(undefined, token)).toBe(false);
|
|
351
|
+
expect(internals.matches('', token)).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
it('an expiry is live only as a valid timestamp in the future', function () {
|
|
354
|
+
expect(internals.isLive((0, moment_1.default)().add(1, 'minute'))).toBe(true);
|
|
355
|
+
expect(internals.isLive((0, moment_1.default)().subtract(1, 'minute'))).toBe(false);
|
|
356
|
+
expect(internals.isLive(null)).toBe(false);
|
|
357
|
+
expect(internals.isLive(undefined)).toBe(false);
|
|
358
|
+
expect(internals.isLive(moment_1.default.invalid())).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
it('redeem writes only while the row still carries the token', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
361
|
+
var live, user, before, _a, row;
|
|
362
|
+
return __generator(this, function (_b) {
|
|
363
|
+
switch (_b.label) {
|
|
364
|
+
case 0:
|
|
365
|
+
live = mintToken();
|
|
366
|
+
return [4 /*yield*/, armResetToken('reset-redeem@test.local', live, (0, moment_1.default)().add(1, 'hour'))];
|
|
367
|
+
case 1:
|
|
368
|
+
user = _b.sent();
|
|
369
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
370
|
+
case 2:
|
|
371
|
+
before = (_b.sent()).password;
|
|
372
|
+
return [4 /*yield*/, expect(new PasswordResetToken_1.PasswordResetToken().redeem(user, mintToken(), 'other-hash')).resolves.toBe(false)];
|
|
373
|
+
case 3:
|
|
374
|
+
_b.sent();
|
|
375
|
+
_a = expect;
|
|
376
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
377
|
+
case 4:
|
|
378
|
+
_a.apply(void 0, [(_b.sent()).password]).toBe(before);
|
|
379
|
+
return [4 /*yield*/, expect(new PasswordResetToken_1.PasswordResetToken().redeem(user, live, 'new-hash')).resolves.toBe(true)];
|
|
380
|
+
case 5:
|
|
381
|
+
_b.sent();
|
|
382
|
+
return [4 /*yield*/, userRow(user.id)];
|
|
383
|
+
case 6:
|
|
384
|
+
row = _b.sent();
|
|
385
|
+
expect(row.password).toBe('new-hash');
|
|
386
|
+
expect(row.passwordResetToken).toBeNull();
|
|
387
|
+
expect(row.passwordResetTokenExpiration).toBeNull();
|
|
388
|
+
return [2 /*return*/];
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}); });
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
//# sourceMappingURL=ExecutePasswordReset.test.js.map
|