@adminforth/open-signup 2.0.1 → 2.0.3
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/.woodpecker/buildRelease.sh +2 -3
- package/build.log +39 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -9
- package/index.ts +19 -13
- package/package.json +3 -2
- package/tests/signupEmailCase.test.mjs +113 -0
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
#!/bin/bash
|
|
3
3
|
|
|
4
4
|
# write npm run output both to console and to build.log
|
|
5
|
-
npm
|
|
5
|
+
npm test 2>&1 | tee build.log
|
|
6
6
|
build_status=${PIPESTATUS[0]}
|
|
7
7
|
|
|
8
|
-
# if
|
|
9
|
-
# then exit with the status code from the npm run build
|
|
8
|
+
# if exit status is not 0, exit with the status code from the build+test run
|
|
10
9
|
if [ $build_status -ne 0 ]; then
|
|
11
10
|
echo "Build failed. Exiting with status code $build_status"
|
|
12
11
|
exit $build_status
|
package/build.log
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
|
|
2
|
+
> @adminforth/open-signup@1.0.9 test
|
|
3
|
+
> npm run build && node --test tests/*.test.mjs
|
|
4
|
+
|
|
5
|
+
|
|
2
6
|
> @adminforth/open-signup@1.0.9 build
|
|
3
7
|
> tsc && rsync -av --exclude 'node_modules' custom dist/
|
|
4
8
|
|
|
@@ -10,3 +14,38 @@ custom/tsconfig.json
|
|
|
10
14
|
|
|
11
15
|
sent 14,167 bytes received 77 bytes 28,488.00 bytes/sec
|
|
12
16
|
total size is 13,884 speedup is 0.97
|
|
17
|
+
TAP version 13
|
|
18
|
+
# Subtest: a case-variant signup is rejected instead of creating a second account
|
|
19
|
+
ok 1 - a case-variant signup is rejected instead of creating a second account
|
|
20
|
+
---
|
|
21
|
+
duration_ms: 45.208471
|
|
22
|
+
...
|
|
23
|
+
# Subtest: case folding does not depend on which column is the auth username field
|
|
24
|
+
ok 2 - case folding does not depend on which column is the auth username field
|
|
25
|
+
---
|
|
26
|
+
duration_ms: 43.686471
|
|
27
|
+
...
|
|
28
|
+
# Subtest: a configured column normalize runs on the folded value, and the stored value is the one probed
|
|
29
|
+
ok 3 - a configured column normalize runs on the folded value, and the stored value is the one probed
|
|
30
|
+
---
|
|
31
|
+
duration_ms: 48.158384
|
|
32
|
+
...
|
|
33
|
+
# Subtest: the probed value survives the column normalize core re-applies on write
|
|
34
|
+
ok 4 - the probed value survives the column normalize core re-applies on write
|
|
35
|
+
---
|
|
36
|
+
duration_ms: 41.121963
|
|
37
|
+
...
|
|
38
|
+
# Subtest: a login whose account lookup finds nothing returns an error instead of crashing
|
|
39
|
+
ok 5 - a login whose account lookup finds nothing returns an error instead of crashing
|
|
40
|
+
---
|
|
41
|
+
duration_ms: 0.192068
|
|
42
|
+
...
|
|
43
|
+
1..5
|
|
44
|
+
# tests 5
|
|
45
|
+
# suites 0
|
|
46
|
+
# pass 5
|
|
47
|
+
# fail 0
|
|
48
|
+
# cancelled 0
|
|
49
|
+
# skipped 0
|
|
50
|
+
# todo 0
|
|
51
|
+
# duration_ms 345.451587
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
11
11
|
constructor(options: PluginOptions);
|
|
12
12
|
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource): Promise<void>;
|
|
13
13
|
getAllowedOrigins(): string[];
|
|
14
|
+
normalizeUsername(email: string): string;
|
|
14
15
|
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource): void;
|
|
15
16
|
instanceUniqueRepresentation(pluginOptions: any): string;
|
|
16
17
|
doLogin(email: string, response: any, extra: HttpExtra): Promise<{
|
package/dist/index.js
CHANGED
|
@@ -115,6 +115,12 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
115
115
|
const originList = Array.isArray(rawOrigins) ? rawOrigins : [rawOrigins];
|
|
116
116
|
return originList.map(o => new URL(o).origin);
|
|
117
117
|
}
|
|
118
|
+
// Unconditional fold: this value is both the lookup filter and the value stored.
|
|
119
|
+
normalizeUsername(email) {
|
|
120
|
+
const folded = typeof email === 'string' ? email.toLowerCase() : email;
|
|
121
|
+
const normalize = this.emailField.normalize;
|
|
122
|
+
return normalize ? normalize(folded) : folded;
|
|
123
|
+
}
|
|
118
124
|
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
119
125
|
if (this.options.confirmEmails) {
|
|
120
126
|
this.options.confirmEmails.adapter.validate();
|
|
@@ -133,8 +139,11 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
133
139
|
}
|
|
134
140
|
doLogin(email, response, extra) {
|
|
135
141
|
return __awaiter(this, void 0, void 0, function* () {
|
|
136
|
-
const username = email;
|
|
137
|
-
const userRecord = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name,
|
|
142
|
+
const username = this.normalizeUsername(email);
|
|
143
|
+
const userRecord = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, username));
|
|
144
|
+
if (!userRecord) {
|
|
145
|
+
return { allowedLogin: false, error: 'User not found' };
|
|
146
|
+
}
|
|
138
147
|
const adminUser = {
|
|
139
148
|
dbUser: userRecord,
|
|
140
149
|
pk: userRecord[this.authResource.columns.find((col) => col.primaryKey).name],
|
|
@@ -189,7 +198,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
189
198
|
if (!decoded || !decoded.email) {
|
|
190
199
|
return { error: yield tr('Invalid token', 'opensignup'), ok: false };
|
|
191
200
|
}
|
|
192
|
-
const
|
|
201
|
+
const email = this.normalizeUsername(decoded.email);
|
|
193
202
|
if (!password) {
|
|
194
203
|
return { error: yield tr('Password is required', 'opensignup'), ok: false };
|
|
195
204
|
}
|
|
@@ -219,6 +228,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
219
228
|
if (!email || typeof email !== 'string') {
|
|
220
229
|
return { error: yield tr('Email is required', 'opensignup'), ok: false };
|
|
221
230
|
}
|
|
231
|
+
const normalizedEmail = this.normalizeUsername(email);
|
|
222
232
|
if (!this.options.confirmEmails && !password) {
|
|
223
233
|
return { error: yield tr('Password is required', 'opensignup'), ok: false };
|
|
224
234
|
}
|
|
@@ -238,7 +248,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
238
248
|
// validate email
|
|
239
249
|
if (this.emailField.validation) {
|
|
240
250
|
for (const { regExp, message } of this.emailField.validation) {
|
|
241
|
-
if (!new RegExp(regExp).test(
|
|
251
|
+
if (!new RegExp(regExp).test(normalizedEmail)) {
|
|
242
252
|
return { error: yield tr(message, 'opensignup'), ok: false };
|
|
243
253
|
}
|
|
244
254
|
}
|
|
@@ -265,9 +275,6 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
265
275
|
}
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
|
-
// This is not needed when right email validator is set on email field because
|
|
269
|
-
// it will not allow to create such email, but if user forgot to set it it might save situation
|
|
270
|
-
const normalizedEmail = email.toLowerCase(); // normalize email
|
|
271
278
|
// first check again if email already exists
|
|
272
279
|
const existingUser = yield this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, normalizedEmail));
|
|
273
280
|
if ((!this.options.confirmEmails && existingUser) || (this.options.confirmEmails && (existingUser === null || existingUser === void 0 ? void 0 : existingUser[this.emailConfirmedField.name]))) {
|
|
@@ -315,7 +322,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
315
322
|
}
|
|
316
323
|
// send confirmation email
|
|
317
324
|
const brandName = this.adminforth.config.customization.brandName;
|
|
318
|
-
const verifyToken = this.adminforth.auth.issueJWT({ email, issuer: brandName }, 'tempVerifyEmailToken', '2h');
|
|
325
|
+
const verifyToken = this.adminforth.auth.issueJWT({ email: normalizedEmail, issuer: brandName }, 'tempVerifyEmailToken', '2h');
|
|
319
326
|
process.env.HEAVY_DEBUG && console.log('🐛Sending reset tok to', verifyToken);
|
|
320
327
|
const emailText = yield tr(`
|
|
321
328
|
Dear user,
|
|
@@ -359,7 +366,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
359
366
|
`;
|
|
360
367
|
const emailSubject = yield tr(`Signup request at {brandName}`, 'opensignup', { brandName });
|
|
361
368
|
// send email with AWS SES this.options.providerOptions.AWS_SES
|
|
362
|
-
this.options.confirmEmails.adapter.sendEmail(this.options.confirmEmails.sendFrom,
|
|
369
|
+
this.options.confirmEmails.adapter.sendEmail(this.options.confirmEmails.sendFrom, normalizedEmail, emailText, emailHtml, emailSubject);
|
|
363
370
|
return { ok: true };
|
|
364
371
|
})
|
|
365
372
|
});
|
package/index.ts
CHANGED
|
@@ -129,6 +129,13 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
129
129
|
const originList = Array.isArray(rawOrigins) ? rawOrigins : [rawOrigins];
|
|
130
130
|
return originList.map(o => new URL(o).origin);
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
// Unconditional fold: this value is both the lookup filter and the value stored.
|
|
134
|
+
normalizeUsername(email: string): string {
|
|
135
|
+
const folded = typeof email === 'string' ? email.toLowerCase() : email;
|
|
136
|
+
const normalize = (this.emailField as AdminForthResourceColumn & { normalize?: (value: any) => any }).normalize;
|
|
137
|
+
return normalize ? normalize(folded) : folded;
|
|
138
|
+
}
|
|
132
139
|
|
|
133
140
|
validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
|
|
134
141
|
if (this.options.confirmEmails) {
|
|
@@ -151,10 +158,12 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
async doLogin(email: string, response: any, extra: HttpExtra): Promise<{ error?: string; allowedLogin: boolean; redirectTo?: string; }> {
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
const username = this.normalizeUsername(email);
|
|
162
|
+
const userRecord = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, username));
|
|
163
|
+
if (!userRecord) {
|
|
164
|
+
return { allowedLogin: false, error: 'User not found' };
|
|
165
|
+
}
|
|
166
|
+
const adminUser = {
|
|
158
167
|
dbUser: userRecord,
|
|
159
168
|
pk: userRecord[this.authResource.columns.find((col) => col.primaryKey).name],
|
|
160
169
|
username,
|
|
@@ -218,7 +227,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
218
227
|
if (!decoded || !decoded.email) {
|
|
219
228
|
return { error: await tr('Invalid token', 'opensignup'), ok: false };
|
|
220
229
|
}
|
|
221
|
-
const
|
|
230
|
+
const email = this.normalizeUsername(decoded.email);
|
|
222
231
|
|
|
223
232
|
if(!password) {
|
|
224
233
|
return { error: await tr('Password is required', 'opensignup'), ok: false };
|
|
@@ -251,6 +260,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
251
260
|
if (!email || typeof email !== 'string') {
|
|
252
261
|
return { error: await tr('Email is required', 'opensignup'), ok: false };
|
|
253
262
|
}
|
|
263
|
+
const normalizedEmail = this.normalizeUsername(email);
|
|
254
264
|
if (!this.options.confirmEmails && !password) {
|
|
255
265
|
return { error: await tr('Password is required', 'opensignup'), ok: false };
|
|
256
266
|
}
|
|
@@ -271,7 +281,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
271
281
|
// validate email
|
|
272
282
|
if (this.emailField.validation) {
|
|
273
283
|
for (const { regExp, message } of this.emailField.validation) {
|
|
274
|
-
if (!new RegExp(regExp).test(
|
|
284
|
+
if (!new RegExp(regExp).test(normalizedEmail)) {
|
|
275
285
|
return { error: await tr(message, 'opensignup'), ok: false };
|
|
276
286
|
}
|
|
277
287
|
}
|
|
@@ -299,10 +309,6 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
299
309
|
}
|
|
300
310
|
}
|
|
301
311
|
|
|
302
|
-
// This is not needed when right email validator is set on email field because
|
|
303
|
-
// it will not allow to create such email, but if user forgot to set it it might save situation
|
|
304
|
-
const normalizedEmail = email.toLowerCase(); // normalize email
|
|
305
|
-
|
|
306
312
|
// first check again if email already exists
|
|
307
313
|
const existingUser = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, normalizedEmail));
|
|
308
314
|
if ((!this.options.confirmEmails && existingUser) || (this.options.confirmEmails && existingUser?.[this.emailConfirmedField.name])) {
|
|
@@ -365,7 +371,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
365
371
|
|
|
366
372
|
const brandName = this.adminforth.config.customization.brandName;
|
|
367
373
|
|
|
368
|
-
const verifyToken = this.adminforth.auth.issueJWT({email, issuer: brandName }, 'tempVerifyEmailToken', '2h');
|
|
374
|
+
const verifyToken = this.adminforth.auth.issueJWT({ email: normalizedEmail, issuer: brandName }, 'tempVerifyEmailToken', '2h');
|
|
369
375
|
process.env.HEAVY_DEBUG && console.log('🐛Sending reset tok to', verifyToken);
|
|
370
376
|
const emailText = await tr(`
|
|
371
377
|
Dear user,
|
|
@@ -413,7 +419,7 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
413
419
|
const emailSubject = await tr(`Signup request at {brandName}`, 'opensignup', { brandName });
|
|
414
420
|
|
|
415
421
|
// send email with AWS SES this.options.providerOptions.AWS_SES
|
|
416
|
-
this.options.confirmEmails.adapter.sendEmail(this.options.confirmEmails.sendFrom,
|
|
422
|
+
this.options.confirmEmails.adapter.sendEmail(this.options.confirmEmails.sendFrom, normalizedEmail, emailText, emailHtml, emailSubject);
|
|
417
423
|
|
|
418
424
|
return { ok: true };
|
|
419
425
|
}
|
|
@@ -423,4 +429,4 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
|
|
|
423
429
|
|
|
424
430
|
}
|
|
425
431
|
|
|
426
|
-
}
|
|
432
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adminforth/open-signup",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://adminforth.dev/docs/tutorial/Plugins/open-signup/",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/"
|
|
12
|
+
"build": "tsc && rsync -av --exclude 'node_modules' custom dist/",
|
|
13
|
+
"test": "npm run build && node --test tests/*.test.mjs"
|
|
13
14
|
},
|
|
14
15
|
"repository": {
|
|
15
16
|
"type": "git",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import OpenSignupPlugin from '../dist/index.js';
|
|
4
|
+
|
|
5
|
+
function makePlugin({ usernameField = 'email', normalize } = {}) {
|
|
6
|
+
const records = [];
|
|
7
|
+
const cookies = [];
|
|
8
|
+
const plugin = Object.create(OpenSignupPlugin.prototype);
|
|
9
|
+
|
|
10
|
+
plugin.options = {
|
|
11
|
+
emailField: 'email',
|
|
12
|
+
passwordHashField: 'password_hash',
|
|
13
|
+
expectedOrigin: 'https://app.example.com',
|
|
14
|
+
};
|
|
15
|
+
plugin.pluginInstanceId = 'open-signup-test';
|
|
16
|
+
plugin.emailField = { name: 'email', ...(normalize ? { normalize } : {}) };
|
|
17
|
+
plugin.passwordField = { minLength: 8, maxLength: 64 };
|
|
18
|
+
plugin.authResource = {
|
|
19
|
+
resourceId: 'users',
|
|
20
|
+
columns: [{ name: 'id', primaryKey: true }, { name: 'email' }],
|
|
21
|
+
};
|
|
22
|
+
plugin.adminforth = {
|
|
23
|
+
config: { auth: { usernameField }, customization: { brandName: 'Test' } },
|
|
24
|
+
resource: () => ({
|
|
25
|
+
get: async (filter) => records.find((record) => record.email === filter.value) ?? null,
|
|
26
|
+
create: async (record) => {
|
|
27
|
+
// core applies the column's normalize again on write, so the harness has to as well
|
|
28
|
+
const email = normalize ? normalize(record.email) : record.email;
|
|
29
|
+
const created = { id: `user-${records.length + 1}`, ...record, email };
|
|
30
|
+
records.push(created);
|
|
31
|
+
return created;
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
restApi: { processLoginCallbacks: async () => {} },
|
|
35
|
+
auth: { setAuthCookie: (args) => cookies.push(args) },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let handler;
|
|
39
|
+
plugin.setupEndpoints({
|
|
40
|
+
endpoint: (options) => {
|
|
41
|
+
if (options.path.endsWith('/signup')) {
|
|
42
|
+
handler = options.handler;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const signup = (email) => handler({
|
|
48
|
+
body: { email, url: 'https://app.example.com/signup', password: 'S3cret-pass' },
|
|
49
|
+
response: {},
|
|
50
|
+
headers: {},
|
|
51
|
+
query: {},
|
|
52
|
+
cookies: [],
|
|
53
|
+
requestUrl: '/plugin/open-signup-test/signup',
|
|
54
|
+
tr: async (message) => message,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { plugin, records, cookies, signup };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test('a case-variant signup is rejected instead of creating a second account', async () => {
|
|
61
|
+
const { records, signup, cookies } = makePlugin();
|
|
62
|
+
|
|
63
|
+
// the first address is deliberately mixed-case: it is what pins the stored value to the folded one
|
|
64
|
+
const first = await signup('Bob@Example.COM');
|
|
65
|
+
assert.equal(first.allowedLogin, true);
|
|
66
|
+
assert.equal(records.length, 1);
|
|
67
|
+
assert.equal(records[0].email, 'bob@example.com');
|
|
68
|
+
assert.equal(cookies.at(-1).username, 'bob@example.com');
|
|
69
|
+
|
|
70
|
+
const second = await signup('bob@example.com');
|
|
71
|
+
assert.match(second.error, /already exists/i);
|
|
72
|
+
assert.equal(records.length, 1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('case folding does not depend on which column is the auth username field', async () => {
|
|
76
|
+
const { records, signup } = makePlugin({ usernameField: 'login' });
|
|
77
|
+
|
|
78
|
+
await signup('bob@example.com');
|
|
79
|
+
const second = await signup('BOB@example.com');
|
|
80
|
+
|
|
81
|
+
assert.match(second.error, /already exists/i);
|
|
82
|
+
assert.equal(records.length, 1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('a configured column normalize runs on the folded value, and the stored value is the one probed', async () => {
|
|
86
|
+
const { records, signup } = makePlugin({ normalize: (value) => value.trim() });
|
|
87
|
+
|
|
88
|
+
await signup(' bob@example.com ');
|
|
89
|
+
const second = await signup('Bob@Example.com');
|
|
90
|
+
|
|
91
|
+
assert.match(second.error, /already exists/i);
|
|
92
|
+
assert.equal(records.length, 1);
|
|
93
|
+
assert.equal(records[0].email, 'bob@example.com');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('the probed value survives the column normalize core re-applies on write', async () => {
|
|
97
|
+
const { records, signup } = makePlugin({ normalize: (value) => value.toUpperCase() });
|
|
98
|
+
|
|
99
|
+
await signup('bob@example.com');
|
|
100
|
+
const second = await signup('Bob@Example.com');
|
|
101
|
+
|
|
102
|
+
assert.match(second.error, /already exists/i);
|
|
103
|
+
assert.equal(records.length, 1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('a login whose account lookup finds nothing returns an error instead of crashing', async () => {
|
|
107
|
+
const { plugin } = makePlugin();
|
|
108
|
+
|
|
109
|
+
const result = await plugin.doLogin('nobody@example.com', {}, {});
|
|
110
|
+
|
|
111
|
+
assert.equal(result.allowedLogin, false);
|
|
112
|
+
assert.match(result.error, /not found/i);
|
|
113
|
+
});
|