@adminforth/open-signup 2.0.2 → 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.
@@ -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 run build 2>&1 | tee build.log
5
+ npm test 2>&1 | tee build.log
6
6
  build_status=${PIPESTATUS[0]}
7
7
 
8
- # if exist status from the npm run build is not 0
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
 
@@ -8,5 +12,40 @@ custom/SignupPage.vue
8
12
  custom/SignupUnderLogin.vue
9
13
  custom/tsconfig.json
10
14
 
11
- sent 14,163 bytes received 77 bytes 28,480.00 bytes/sec
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.js CHANGED
@@ -115,12 +115,11 @@ 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.
118
119
  normalizeUsername(email) {
119
- if (this.adminforth.config.auth.usernameField !== this.emailField.name) {
120
- return email;
121
- }
120
+ const folded = typeof email === 'string' ? email.toLowerCase() : email;
122
121
  const normalize = this.emailField.normalize;
123
- return normalize ? normalize(email) : email;
122
+ return normalize ? normalize(folded) : folded;
124
123
  }
125
124
  validateConfigAfterDiscover(adminforth, resourceConfig) {
126
125
  if (this.options.confirmEmails) {
@@ -142,6 +141,9 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
142
141
  return __awaiter(this, void 0, void 0, function* () {
143
142
  const username = this.normalizeUsername(email);
144
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
+ }
145
147
  const adminUser = {
146
148
  dbUser: userRecord,
147
149
  pk: userRecord[this.authResource.columns.find((col) => col.primaryKey).name],
package/index.ts CHANGED
@@ -130,13 +130,11 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
130
130
  return originList.map(o => new URL(o).origin);
131
131
  }
132
132
 
133
+ // Unconditional fold: this value is both the lookup filter and the value stored.
133
134
  normalizeUsername(email: string): string {
134
- if (this.adminforth.config.auth.usernameField !== this.emailField.name) {
135
- return email;
136
- }
137
-
135
+ const folded = typeof email === 'string' ? email.toLowerCase() : email;
138
136
  const normalize = (this.emailField as AdminForthResourceColumn & { normalize?: (value: any) => any }).normalize;
139
- return normalize ? normalize(email) : email;
137
+ return normalize ? normalize(folded) : folded;
140
138
  }
141
139
 
142
140
  validateConfigAfterDiscover(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
@@ -162,7 +160,10 @@ export default class OpenSignupPlugin extends AdminForthPlugin {
162
160
  async doLogin(email: string, response: any, extra: HttpExtra): Promise<{ error?: string; allowedLogin: boolean; redirectTo?: string; }> {
163
161
  const username = this.normalizeUsername(email);
164
162
  const userRecord = await this.adminforth.resource(this.authResource.resourceId).get(Filters.EQ(this.emailField.name, username));
165
- const adminUser = {
163
+ if (!userRecord) {
164
+ return { allowedLogin: false, error: 'User not found' };
165
+ }
166
+ const adminUser = {
166
167
  dbUser: userRecord,
167
168
  pk: userRecord[this.authResource.columns.find((col) => col.primaryKey).name],
168
169
  username,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/open-signup",
3
- "version": "2.0.2",
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
+ });