@nymphjs/tilmeld 1.0.0-beta.14 → 1.0.0-beta.140

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.
Files changed (105) hide show
  1. package/CHANGELOG.md +668 -0
  2. package/README.md +21 -5
  3. package/dist/AbleObject.d.ts +20 -2
  4. package/dist/AbleObject.js +25 -13
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +190 -27
  7. package/dist/Group.js +827 -440
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.testArtifacts.d.ts +3 -0
  10. package/dist/Group.testArtifacts.js +120 -0
  11. package/dist/Group.testArtifacts.js.map +1 -0
  12. package/dist/Tilmeld.d.ts +275 -14
  13. package/dist/Tilmeld.js +789 -310
  14. package/dist/Tilmeld.js.map +1 -1
  15. package/dist/Tilmeld.types.d.ts +9 -9
  16. package/dist/Tilmeld.types.js +1 -2
  17. package/dist/User.d.ts +410 -41
  18. package/dist/User.js +2042 -1057
  19. package/dist/User.js.map +1 -1
  20. package/dist/User.testArtifacts.d.ts +3 -0
  21. package/dist/User.testArtifacts.js +175 -0
  22. package/dist/User.testArtifacts.js.map +1 -0
  23. package/dist/conf/defaults.d.ts +1 -1
  24. package/dist/conf/defaults.js +215 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +361 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/decorators/index.d.ts +2 -0
  30. package/dist/decorators/index.js +3 -0
  31. package/dist/decorators/index.js.map +1 -0
  32. package/dist/decorators/requireAbility.d.ts +10 -0
  33. package/dist/decorators/requireAbility.js +21 -0
  34. package/dist/decorators/requireAbility.js.map +1 -0
  35. package/dist/decorators/requireAuthentication.d.ts +8 -0
  36. package/dist/decorators/requireAuthentication.js +16 -0
  37. package/dist/decorators/requireAuthentication.js.map +1 -0
  38. package/dist/enforceTilmeld.d.ts +3 -0
  39. package/dist/enforceTilmeld.js +18 -0
  40. package/dist/enforceTilmeld.js.map +1 -0
  41. package/dist/errors/AccessControlError.d.ts +2 -1
  42. package/dist/errors/AccessControlError.js +3 -6
  43. package/dist/errors/AccessControlError.js.map +1 -1
  44. package/dist/errors/BadDataError.d.ts +2 -1
  45. package/dist/errors/BadDataError.js +3 -6
  46. package/dist/errors/BadDataError.js.map +1 -1
  47. package/dist/errors/BadEmailError.d.ts +2 -1
  48. package/dist/errors/BadEmailError.js +3 -6
  49. package/dist/errors/BadEmailError.js.map +1 -1
  50. package/dist/errors/BadUsernameError.d.ts +2 -1
  51. package/dist/errors/BadUsernameError.js +3 -6
  52. package/dist/errors/BadUsernameError.js.map +1 -1
  53. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  54. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  55. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  56. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  57. package/dist/errors/index.d.ts +6 -6
  58. package/dist/errors/index.js +6 -22
  59. package/dist/errors/index.js.map +1 -1
  60. package/dist/index.d.ts +10 -14
  61. package/dist/index.js +11 -35
  62. package/dist/index.js.map +1 -1
  63. package/dist/testArtifacts.d.ts +26 -0
  64. package/dist/testArtifacts.js +1346 -0
  65. package/dist/testArtifacts.js.map +1 -0
  66. package/dist/tilmeldJoiProps.d.ts +4 -5
  67. package/dist/tilmeldJoiProps.js +13 -18
  68. package/dist/tilmeldJoiProps.js.map +1 -1
  69. package/jest.config.js +31 -5
  70. package/package.json +49 -36
  71. package/src/AbleObject.ts +17 -11
  72. package/src/Group.testArtifacts.ts +159 -0
  73. package/src/Group.ts +712 -299
  74. package/src/Tilmeld.ts +666 -344
  75. package/src/Tilmeld.types.ts +9 -9
  76. package/src/User.testArtifacts.ts +226 -0
  77. package/src/User.ts +1984 -879
  78. package/src/conf/defaults.ts +205 -109
  79. package/src/conf/index.ts +381 -2
  80. package/src/decorators/index.ts +2 -0
  81. package/src/decorators/requireAbility.ts +26 -0
  82. package/src/decorators/requireAuthentication.ts +19 -0
  83. package/src/enforceTilmeld.ts +20 -0
  84. package/src/errors/AccessControlError.ts +4 -2
  85. package/src/errors/BadDataError.ts +4 -2
  86. package/src/errors/BadEmailError.ts +4 -2
  87. package/src/errors/BadUsernameError.ts +4 -2
  88. package/src/errors/index.ts +6 -6
  89. package/src/index.ts +11 -14
  90. package/src/testArtifacts.ts +2148 -0
  91. package/src/tilmeldJoiProps.ts +9 -24
  92. package/tsconfig.json +6 -3
  93. package/typedoc.json +4 -0
  94. package/dist/Group.test.d.ts +0 -1
  95. package/dist/Group.test.js +0 -48
  96. package/dist/Group.test.js.map +0 -1
  97. package/dist/User.test.d.ts +0 -1
  98. package/dist/User.test.js +0 -51
  99. package/dist/User.test.js.map +0 -1
  100. package/dist/conf/d.d.ts +0 -55
  101. package/dist/conf/d.js +0 -3
  102. package/dist/conf/d.js.map +0 -1
  103. package/src/Group.test.ts +0 -58
  104. package/src/User.test.ts +0 -61
  105. package/src/conf/d.ts +0 -303
@@ -1,14 +1,16 @@
1
- import path from 'path';
2
- import { URL } from 'url';
1
+ import path from 'node:path';
2
+ import { URL, fileURLToPath } from 'node:url';
3
3
  import jwt from 'jsonwebtoken';
4
4
  import Email from 'email-templates';
5
5
  import Joi from 'joi';
6
6
  import { nanoid } from '@nymphjs/guid';
7
7
  import { nymphJoiProps } from '@nymphjs/nymph';
8
8
 
9
- import { Config } from './d';
10
- import User from '../User';
11
- import Group from '../Group';
9
+ import type { Config } from './index.js';
10
+ import { User } from '../User.js';
11
+ import { Group } from '../Group.js';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
14
 
13
15
  export default {
14
16
  appName: 'My Nymph App',
@@ -18,6 +20,7 @@ export default {
18
20
  setupPath: '/tilmeld',
19
21
  createAdmin: true,
20
22
  emailUsernames: true,
23
+ domainSupport: false,
21
24
  allowRegistration: true,
22
25
  allowUsernameChange: true,
23
26
  enableUserSearch: false,
@@ -49,25 +52,43 @@ export default {
49
52
  validEmailRegex:
50
53
  /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
51
54
  validEmailRegexNotice: 'Email must be a correctly formatted address.',
55
+ validDomainRegex:
56
+ /^(((?!-))(xn--|_)?[a-z0-9\-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9\-]{1,30}\.[a-z]{2,})$/,
57
+ validDomainRegexNotice: 'Domain must be a correctly formatted domain name.',
58
+ minUsernameLength: 1,
52
59
  maxUsernameLength: 128,
53
60
  jwtSecret: '',
54
- jwtExpire: 60 * 60 * 24 * 7 * 8, // 8 weeks(ish)
55
- jwtRenew: 60 * 60 * 24 * 7 * 2, // 2 weeks(ish)
56
- jwtBuilder: (config, user) => {
61
+ jwtExpire: 60 * 60 * 24 * 7 * 24, // 24 weeks(ish) (6 months)
62
+ jwtSwitchExpire: 60 * 60 * 6, // 6 hours
63
+ jwtRenew: 60 * 60 * 24 * 7 * 12, // 12 weeks(ish) (3 months)
64
+ jwtBuilder: (config, user, switchToken, switchForUser) => {
57
65
  const secret = config.jwtSecret;
58
66
  if (secret === '') {
59
67
  throw new Error('JWT secret is not configured.');
60
68
  }
61
69
 
70
+ if (switchToken && (!switchForUser || switchForUser.guid == null)) {
71
+ throw new Error(
72
+ "Issuing a switch token requires providing the user it's for.",
73
+ );
74
+ }
75
+
62
76
  return jwt.sign(
63
77
  {
64
78
  iss: config.appUrl,
65
- nbf: Date.now() / 1000,
79
+ sub: switchForUser ? switchForUser.guid : user.guid,
80
+ nbf: Math.floor(Date.now() / 1000),
81
+ exp:
82
+ Math.floor(Date.now() / 1000) +
83
+ (switchToken ? config.jwtSwitchExpire : config.jwtExpire),
84
+ // TODO: remove this guid claim in favor of sub claim
85
+ // this needs to be done long enough after Nov 1 2026, when sub added
86
+ // also don't forget to change how switchForUser guid is stored.
66
87
  guid: user.guid,
67
88
  xsrfToken: 'TILMELDXSRF-' + nanoid(),
89
+ ...(switchToken ? { switch: true } : {}),
68
90
  },
69
91
  secret,
70
- { expiresIn: config.jwtExpire }
71
92
  );
72
93
  },
73
94
  jwtExtract: (config, token, xsrfToken?) => {
@@ -79,14 +100,21 @@ export default {
79
100
  try {
80
101
  const payload = jwt.verify(token, secret, {
81
102
  issuer: config.appUrl,
82
- clockTolerance: 10,
103
+ clockTolerance: 60,
83
104
  });
84
105
 
85
106
  if (typeof payload === 'string') {
86
107
  return null;
87
108
  }
88
109
 
89
- const { guid, xsrfToken: jwtXsrfToken, exp } = payload;
110
+ const {
111
+ sub,
112
+ guid,
113
+ xsrfToken: jwtXsrfToken,
114
+ switch: switchBool,
115
+ exp,
116
+ nbf,
117
+ } = payload;
90
118
 
91
119
  if (xsrfToken != null && xsrfToken !== jwtXsrfToken) {
92
120
  return null;
@@ -96,13 +124,19 @@ export default {
96
124
  return null;
97
125
  }
98
126
 
127
+ if (nbf == null) {
128
+ return null;
129
+ }
130
+
131
+ const issued = new Date(nbf * 1000);
132
+
99
133
  if (exp == null) {
100
134
  return null;
101
135
  }
102
136
 
103
137
  const expire = new Date(exp * 1000);
104
138
 
105
- return { guid, expire };
139
+ return { sub, guid, issued, expire, switch: switchBool };
106
140
  } catch (e: any) {
107
141
  return null;
108
142
  }
@@ -117,18 +151,21 @@ export default {
117
151
  address: `noreply@${appUrl.hostname}`,
118
152
  },
119
153
  },
154
+ preview: {
155
+ openSimulator: false,
156
+ },
120
157
  };
121
158
  },
122
159
  sendEmail: async (tilmeld, options, user) => {
123
160
  const email = new Email(
124
- await tilmeld.config.configEmail(tilmeld, options, user)
161
+ await tilmeld.config.configEmail(tilmeld, options, user),
125
162
  );
126
163
  try {
127
164
  const result = await email.send({
128
165
  ...options,
129
166
  template: path.join(
130
167
  tilmeld.config.emailTemplateDir,
131
- options.template ?? '.'
168
+ options.template ?? '.',
132
169
  ),
133
170
  locals: {
134
171
  // System
@@ -156,29 +193,70 @@ export default {
156
193
  });
157
194
  return !!result;
158
195
  } catch (e: any) {
196
+ tilmeld.nymph.config.debugError('tilmeld', `Failed to send email: ${e}`);
159
197
  return false;
160
198
  }
161
199
  },
162
200
  userRegisteredRecipient: null,
163
- validatorGroup: (group) => {
201
+ failedLoginAttempt: (_user, _data) => {
202
+ // Wait 3-5 seconds to return a failure. This rate limits failed login
203
+ // attempts.
204
+ return new Promise((resolve) =>
205
+ setTimeout(resolve, Math.random() * 2000 + 3000),
206
+ );
207
+ },
208
+ validatorGroup: (tilmeld, group) => {
209
+ const nameFields = tilmeld.config.userFields.includes('name')
210
+ ? {
211
+ name: Joi.string()
212
+ .trim(false)
213
+ .pattern(/[\x01-\x1F\x7F]/, {
214
+ name: 'control characters',
215
+ invert: true,
216
+ })
217
+ .max(512)
218
+ .required(),
219
+ }
220
+ : {};
221
+ const emailFields = tilmeld.config.userFields.includes('email')
222
+ ? {
223
+ email: Joi.string()
224
+ .trim(false)
225
+ .email({ minDomainSegments: 1, tlds: false })
226
+ .required(),
227
+ }
228
+ : {};
229
+ const phoneFields = tilmeld.config.userFields.includes('phone')
230
+ ? {
231
+ phone: Joi.string()
232
+ .trim(false)
233
+ .pattern(/^[0-9]+$/, 'numbers'),
234
+ }
235
+ : {};
236
+ for (let ability of group.abilities ?? []) {
237
+ if (ability.startsWith('system/')) {
238
+ throw new Error('Invalid Group: Groups cannot have system abilities.');
239
+ }
240
+ if (ability === 'tilmeld/admin') {
241
+ throw new Error(
242
+ 'Invalid Group: Groups cannot have Tilmeld admin ability.',
243
+ );
244
+ }
245
+ if (ability === 'tilmeld/switch') {
246
+ throw new Error(
247
+ 'Invalid Group: Groups cannot have Tilmeld switch ability.',
248
+ );
249
+ }
250
+ }
164
251
  Joi.attempt(
165
252
  group.$getValidatable(),
166
253
  Joi.object().keys({
167
254
  ...nymphJoiProps,
255
+ ...nameFields,
256
+ ...emailFields,
257
+ ...phoneFields,
168
258
  groupname: Joi.string().trim(false).required(),
169
259
  enabled: Joi.boolean().required(),
170
- email: Joi.string()
171
- .trim(false)
172
- .email({ minDomainSegments: 1, tlds: false })
173
- .required(),
174
- name: Joi.string()
175
- .trim(false)
176
- .pattern(/[\x01-\x1F\x7F]/, {
177
- name: 'control characters',
178
- invert: true,
179
- })
180
- .max(512)
181
- .required(),
182
260
  avatar: Joi.string()
183
261
  .trim(false)
184
262
  .uri()
@@ -187,9 +265,6 @@ export default {
187
265
  invert: true,
188
266
  })
189
267
  .max(1024),
190
- phone: Joi.string()
191
- .trim(false)
192
- .pattern(/^[0-9]+$/, 'numbers'),
193
268
  parent: Joi.object().instance(Group),
194
269
  user: Joi.object().instance(User),
195
270
  abilities: Joi.array().items(
@@ -198,56 +273,110 @@ export default {
198
273
  name: 'control characters',
199
274
  invert: true,
200
275
  })
201
- .max(256)
276
+ .max(256),
202
277
  ),
203
278
  defaultPrimary: Joi.boolean(),
204
279
  defaultSecondary: Joi.boolean(),
205
280
  unverifiedSecondary: Joi.boolean(),
206
281
  }),
207
- 'Invalid Group: '
282
+ 'Invalid Group: ',
208
283
  );
209
284
  },
210
- validatorUser: (user) => {
285
+ validatorUser: (tilmeld, user) => {
286
+ const nameFields = tilmeld.config.userFields.includes('name')
287
+ ? {
288
+ nameFirst: Joi.string()
289
+ .trim(false)
290
+ .pattern(/[\x01-\x1F\x7F]/, {
291
+ name: 'control characters',
292
+ invert: true,
293
+ })
294
+ .max(512)
295
+ .required(),
296
+ nameMiddle: Joi.string()
297
+ .trim(false)
298
+ .pattern(/[\x01-\x1F\x7F]/, {
299
+ name: 'control characters',
300
+ invert: true,
301
+ })
302
+ .max(512),
303
+ nameLast: Joi.string()
304
+ .trim(false)
305
+ .pattern(/[\x01-\x1F\x7F]/, {
306
+ name: 'control characters',
307
+ invert: true,
308
+ })
309
+ .max(512),
310
+ name: Joi.string()
311
+ .trim(false)
312
+ .pattern(/[\x01-\x1F\x7F]/, {
313
+ name: 'control characters',
314
+ invert: true,
315
+ })
316
+ .max(512)
317
+ .required(),
318
+ }
319
+ : {};
320
+ const emailFields = tilmeld.config.userFields.includes('email')
321
+ ? {
322
+ email: Joi.string()
323
+ .trim(false)
324
+ .email({ minDomainSegments: 1, tlds: false })
325
+ .required(),
326
+ secret: Joi.string()
327
+ .trim(false)
328
+ .pattern(/[\x01-\x1F\x7F]/, {
329
+ name: 'control characters',
330
+ invert: true,
331
+ })
332
+ .length(21),
333
+ emailChangeDate: Joi.number(),
334
+ newEmailSecret: Joi.string()
335
+ .trim(false)
336
+ .pattern(/[\x01-\x1F\x7F]/, {
337
+ name: 'control characters',
338
+ invert: true,
339
+ })
340
+ .length(21),
341
+ newEmailAddress: Joi.string()
342
+ .trim(false)
343
+ .email({ minDomainSegments: 1, tlds: false }),
344
+ cancelEmailSecret: Joi.string()
345
+ .trim(false)
346
+ .pattern(/[\x01-\x1F\x7F]/, {
347
+ name: 'control characters',
348
+ invert: true,
349
+ })
350
+ .length(21),
351
+ cancelEmailAddress: Joi.string()
352
+ .trim(false)
353
+ .email({ minDomainSegments: 1, tlds: false }),
354
+ recoverSecret: Joi.string()
355
+ .trim(false)
356
+ .pattern(/[\x01-\x1F\x7F]/, {
357
+ name: 'control characters',
358
+ invert: true,
359
+ })
360
+ .length(10),
361
+ recoverSecretDate: Joi.number(),
362
+ }
363
+ : {};
364
+ const phoneFields = tilmeld.config.userFields.includes('phone')
365
+ ? {
366
+ phone: Joi.string()
367
+ .trim(false)
368
+ .pattern(/^[0-9]+$/, 'numbers'),
369
+ }
370
+ : {};
211
371
  Joi.attempt(
212
372
  user.$getValidatable(),
213
373
  Joi.object().keys({
214
374
  ...nymphJoiProps,
375
+ ...nameFields,
376
+ ...emailFields,
377
+ ...phoneFields,
215
378
  username: Joi.string().trim(false).required(),
216
379
  enabled: Joi.boolean().required(),
217
- email: Joi.string()
218
- .trim(false)
219
- .email({ minDomainSegments: 1, tlds: false })
220
- .required(),
221
- nameFirst: Joi.string()
222
- .trim(false)
223
- .pattern(/[\x01-\x1F\x7F]/, {
224
- name: 'control characters',
225
- invert: true,
226
- })
227
- .max(512)
228
- .required(),
229
- nameMiddle: Joi.string()
230
- .trim(false)
231
- .pattern(/[\x01-\x1F\x7F]/, {
232
- name: 'control characters',
233
- invert: true,
234
- })
235
- .max(512),
236
- nameLast: Joi.string()
237
- .trim(false)
238
- .pattern(/[\x01-\x1F\x7F]/, {
239
- name: 'control characters',
240
- invert: true,
241
- })
242
- .max(512),
243
- name: Joi.string()
244
- .trim(false)
245
- .pattern(/[\x01-\x1F\x7F]/, {
246
- name: 'control characters',
247
- invert: true,
248
- })
249
- .max(512)
250
- .required(),
251
380
  avatar: Joi.string()
252
381
  .trim(false)
253
382
  .uri()
@@ -256,9 +385,6 @@ export default {
256
385
  invert: true,
257
386
  })
258
387
  .max(1024),
259
- phone: Joi.string()
260
- .trim(false)
261
- .pattern(/^[0-9]+$/, 'numbers'),
262
388
  group: Joi.object().instance(Group),
263
389
  groups: Joi.array().items(Joi.object().instance(Group)).required(),
264
390
  abilities: Joi.array().items(
@@ -267,49 +393,19 @@ export default {
267
393
  name: 'control characters',
268
394
  invert: true,
269
395
  })
270
- .max(256)
396
+ .max(256),
271
397
  ),
272
398
  inheritAbilities: Joi.boolean(),
273
- secret: Joi.string()
274
- .trim(false)
275
- .pattern(/[\x01-\x1F\x7F]/, {
276
- name: 'control characters',
277
- invert: true,
278
- })
279
- .length(21),
280
- emailChangeDate: Joi.number(),
281
- newEmailSecret: Joi.string()
282
- .trim(false)
283
- .pattern(/[\x01-\x1F\x7F]/, {
284
- name: 'control characters',
285
- invert: true,
286
- })
287
- .length(21),
288
- newEmailAddress: Joi.string()
289
- .trim(false)
290
- .email({ minDomainSegments: 1, tlds: false }),
291
- cancelEmailSecret: Joi.string()
292
- .trim(false)
293
- .pattern(/[\x01-\x1F\x7F]/, {
294
- name: 'control characters',
295
- invert: true,
296
- })
297
- .length(21),
298
- cancelEmailAddress: Joi.string()
299
- .trim(false)
300
- .email({ minDomainSegments: 1, tlds: false }),
301
- recoverSecret: Joi.string()
302
- .trim(false)
303
- .pattern(/[\x01-\x1F\x7F]/, {
304
- name: 'control characters',
305
- invert: true,
306
- })
307
- .length(10),
308
- recoverSecretDate: Joi.number(),
309
399
  salt: Joi.string().trim(false),
310
400
  password: Joi.string().trim(false).required(),
401
+ revokeTokenDate: Joi.number(),
402
+ totpSecret: Joi.string()
403
+ .trim(false)
404
+ .min(24)
405
+ .max(40)
406
+ .pattern(/^[A-Z234567]+$/, 'base32 encoded unpadded'),
311
407
  }),
312
- 'Invalid User: '
408
+ 'Invalid User: ',
313
409
  );
314
410
  },
315
411
  } as Config;