@nymphjs/tilmeld 1.0.0-beta.13 → 1.0.0-beta.130

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 +598 -0
  2. package/README.md +21 -5
  3. package/dist/AbleObject.d.ts +15 -0
  4. package/dist/AbleObject.js +18 -6
  5. package/dist/AbleObject.js.map +1 -1
  6. package/dist/Group.d.ts +186 -19
  7. package/dist/Group.js +817 -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 +241 -12
  13. package/dist/Tilmeld.js +725 -286
  14. package/dist/Tilmeld.js.map +1 -1
  15. package/dist/Tilmeld.types.d.ts +5 -5
  16. package/dist/Tilmeld.types.js +1 -2
  17. package/dist/User.d.ts +399 -26
  18. package/dist/User.js +1955 -1055
  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 +196 -141
  25. package/dist/conf/defaults.js.map +1 -1
  26. package/dist/conf/index.d.ts +355 -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 +12 -10
  61. package/dist/index.js +17 -35
  62. package/dist/index.js.map +1 -1
  63. package/dist/testArtifacts.d.ts +23 -0
  64. package/dist/testArtifacts.js +1317 -0
  65. package/dist/testArtifacts.js.map +1 -0
  66. package/dist/tilmeldJoiProps.d.ts +3 -3
  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 +2 -2
  72. package/src/Group.testArtifacts.ts +159 -0
  73. package/src/Group.ts +647 -238
  74. package/src/Tilmeld.ts +550 -315
  75. package/src/Tilmeld.types.ts +5 -5
  76. package/src/User.testArtifacts.ts +226 -0
  77. package/src/User.ts +1657 -634
  78. package/src/conf/defaults.ts +172 -109
  79. package/src/conf/index.ts +370 -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 +13 -10
  90. package/src/testArtifacts.ts +2098 -0
  91. package/src/tilmeldJoiProps.ts +8 -23
  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,11 +52,16 @@ 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) => {
57
65
  const secret = config.jwtSecret;
58
66
  if (secret === '') {
59
67
  throw new Error('JWT secret is not configured.');
@@ -62,12 +70,14 @@ export default {
62
70
  return jwt.sign(
63
71
  {
64
72
  iss: config.appUrl,
65
- nbf: Date.now() / 1000,
73
+ nbf: Math.floor(Date.now() / 1000),
74
+ exp:
75
+ Math.floor(Date.now() / 1000) +
76
+ (switchToken ? config.jwtSwitchExpire : config.jwtExpire),
66
77
  guid: user.guid,
67
78
  xsrfToken: 'TILMELDXSRF-' + nanoid(),
68
79
  },
69
80
  secret,
70
- { expiresIn: config.jwtExpire }
71
81
  );
72
82
  },
73
83
  jwtExtract: (config, token, xsrfToken?) => {
@@ -79,14 +89,14 @@ export default {
79
89
  try {
80
90
  const payload = jwt.verify(token, secret, {
81
91
  issuer: config.appUrl,
82
- clockTolerance: 10,
92
+ clockTolerance: 60,
83
93
  });
84
94
 
85
95
  if (typeof payload === 'string') {
86
96
  return null;
87
97
  }
88
98
 
89
- const { guid, xsrfToken: jwtXsrfToken, exp } = payload;
99
+ const { guid, xsrfToken: jwtXsrfToken, exp, nbf } = payload;
90
100
 
91
101
  if (xsrfToken != null && xsrfToken !== jwtXsrfToken) {
92
102
  return null;
@@ -96,13 +106,19 @@ export default {
96
106
  return null;
97
107
  }
98
108
 
109
+ if (nbf == null) {
110
+ return null;
111
+ }
112
+
113
+ const issued = new Date(nbf * 1000);
114
+
99
115
  if (exp == null) {
100
116
  return null;
101
117
  }
102
118
 
103
119
  const expire = new Date(exp * 1000);
104
120
 
105
- return { guid, expire };
121
+ return { guid, issued, expire };
106
122
  } catch (e: any) {
107
123
  return null;
108
124
  }
@@ -117,18 +133,21 @@ export default {
117
133
  address: `noreply@${appUrl.hostname}`,
118
134
  },
119
135
  },
136
+ preview: {
137
+ openSimulator: false,
138
+ },
120
139
  };
121
140
  },
122
141
  sendEmail: async (tilmeld, options, user) => {
123
142
  const email = new Email(
124
- await tilmeld.config.configEmail(tilmeld, options, user)
143
+ await tilmeld.config.configEmail(tilmeld, options, user),
125
144
  );
126
145
  try {
127
146
  const result = await email.send({
128
147
  ...options,
129
148
  template: path.join(
130
149
  tilmeld.config.emailTemplateDir,
131
- options.template ?? '.'
150
+ options.template ?? '.',
132
151
  ),
133
152
  locals: {
134
153
  // System
@@ -156,29 +175,55 @@ export default {
156
175
  });
157
176
  return !!result;
158
177
  } catch (e: any) {
178
+ tilmeld.nymph.config.debugError('tilmeld', `Failed to send email: ${e}`);
159
179
  return false;
160
180
  }
161
181
  },
162
182
  userRegisteredRecipient: null,
163
- validatorGroup: (group) => {
183
+ failedLoginAttempt: (_user, _data) => {
184
+ // Wait 3-5 seconds to return a failure. This rate limits failed login
185
+ // attempts.
186
+ return new Promise((resolve) =>
187
+ setTimeout(resolve, Math.random() * 2000 + 3000),
188
+ );
189
+ },
190
+ validatorGroup: (tilmeld, group) => {
191
+ const nameFields = tilmeld.config.userFields.includes('name')
192
+ ? {
193
+ name: Joi.string()
194
+ .trim(false)
195
+ .pattern(/[\x01-\x1F\x7F]/, {
196
+ name: 'control characters',
197
+ invert: true,
198
+ })
199
+ .max(512)
200
+ .required(),
201
+ }
202
+ : {};
203
+ const emailFields = tilmeld.config.userFields.includes('email')
204
+ ? {
205
+ email: Joi.string()
206
+ .trim(false)
207
+ .email({ minDomainSegments: 1, tlds: false })
208
+ .required(),
209
+ }
210
+ : {};
211
+ const phoneFields = tilmeld.config.userFields.includes('phone')
212
+ ? {
213
+ phone: Joi.string()
214
+ .trim(false)
215
+ .pattern(/^[0-9]+$/, 'numbers'),
216
+ }
217
+ : {};
164
218
  Joi.attempt(
165
219
  group.$getValidatable(),
166
220
  Joi.object().keys({
167
221
  ...nymphJoiProps,
222
+ ...nameFields,
223
+ ...emailFields,
224
+ ...phoneFields,
168
225
  groupname: Joi.string().trim(false).required(),
169
226
  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
227
  avatar: Joi.string()
183
228
  .trim(false)
184
229
  .uri()
@@ -187,9 +232,6 @@ export default {
187
232
  invert: true,
188
233
  })
189
234
  .max(1024),
190
- phone: Joi.string()
191
- .trim(false)
192
- .pattern(/^[0-9]+$/, 'numbers'),
193
235
  parent: Joi.object().instance(Group),
194
236
  user: Joi.object().instance(User),
195
237
  abilities: Joi.array().items(
@@ -198,56 +240,110 @@ export default {
198
240
  name: 'control characters',
199
241
  invert: true,
200
242
  })
201
- .max(256)
243
+ .max(256),
202
244
  ),
203
245
  defaultPrimary: Joi.boolean(),
204
246
  defaultSecondary: Joi.boolean(),
205
247
  unverifiedSecondary: Joi.boolean(),
206
248
  }),
207
- 'Invalid Group: '
249
+ 'Invalid Group: ',
208
250
  );
209
251
  },
210
- validatorUser: (user) => {
252
+ validatorUser: (tilmeld, user) => {
253
+ const nameFields = tilmeld.config.userFields.includes('name')
254
+ ? {
255
+ nameFirst: Joi.string()
256
+ .trim(false)
257
+ .pattern(/[\x01-\x1F\x7F]/, {
258
+ name: 'control characters',
259
+ invert: true,
260
+ })
261
+ .max(512)
262
+ .required(),
263
+ nameMiddle: Joi.string()
264
+ .trim(false)
265
+ .pattern(/[\x01-\x1F\x7F]/, {
266
+ name: 'control characters',
267
+ invert: true,
268
+ })
269
+ .max(512),
270
+ nameLast: Joi.string()
271
+ .trim(false)
272
+ .pattern(/[\x01-\x1F\x7F]/, {
273
+ name: 'control characters',
274
+ invert: true,
275
+ })
276
+ .max(512),
277
+ name: Joi.string()
278
+ .trim(false)
279
+ .pattern(/[\x01-\x1F\x7F]/, {
280
+ name: 'control characters',
281
+ invert: true,
282
+ })
283
+ .max(512)
284
+ .required(),
285
+ }
286
+ : {};
287
+ const emailFields = tilmeld.config.userFields.includes('email')
288
+ ? {
289
+ email: Joi.string()
290
+ .trim(false)
291
+ .email({ minDomainSegments: 1, tlds: false })
292
+ .required(),
293
+ secret: Joi.string()
294
+ .trim(false)
295
+ .pattern(/[\x01-\x1F\x7F]/, {
296
+ name: 'control characters',
297
+ invert: true,
298
+ })
299
+ .length(21),
300
+ emailChangeDate: Joi.number(),
301
+ newEmailSecret: Joi.string()
302
+ .trim(false)
303
+ .pattern(/[\x01-\x1F\x7F]/, {
304
+ name: 'control characters',
305
+ invert: true,
306
+ })
307
+ .length(21),
308
+ newEmailAddress: Joi.string()
309
+ .trim(false)
310
+ .email({ minDomainSegments: 1, tlds: false }),
311
+ cancelEmailSecret: Joi.string()
312
+ .trim(false)
313
+ .pattern(/[\x01-\x1F\x7F]/, {
314
+ name: 'control characters',
315
+ invert: true,
316
+ })
317
+ .length(21),
318
+ cancelEmailAddress: Joi.string()
319
+ .trim(false)
320
+ .email({ minDomainSegments: 1, tlds: false }),
321
+ recoverSecret: Joi.string()
322
+ .trim(false)
323
+ .pattern(/[\x01-\x1F\x7F]/, {
324
+ name: 'control characters',
325
+ invert: true,
326
+ })
327
+ .length(10),
328
+ recoverSecretDate: Joi.number(),
329
+ }
330
+ : {};
331
+ const phoneFields = tilmeld.config.userFields.includes('phone')
332
+ ? {
333
+ phone: Joi.string()
334
+ .trim(false)
335
+ .pattern(/^[0-9]+$/, 'numbers'),
336
+ }
337
+ : {};
211
338
  Joi.attempt(
212
339
  user.$getValidatable(),
213
340
  Joi.object().keys({
214
341
  ...nymphJoiProps,
342
+ ...nameFields,
343
+ ...emailFields,
344
+ ...phoneFields,
215
345
  username: Joi.string().trim(false).required(),
216
346
  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
347
  avatar: Joi.string()
252
348
  .trim(false)
253
349
  .uri()
@@ -256,9 +352,6 @@ export default {
256
352
  invert: true,
257
353
  })
258
354
  .max(1024),
259
- phone: Joi.string()
260
- .trim(false)
261
- .pattern(/^[0-9]+$/, 'numbers'),
262
355
  group: Joi.object().instance(Group),
263
356
  groups: Joi.array().items(Joi.object().instance(Group)).required(),
264
357
  abilities: Joi.array().items(
@@ -267,49 +360,19 @@ export default {
267
360
  name: 'control characters',
268
361
  invert: true,
269
362
  })
270
- .max(256)
363
+ .max(256),
271
364
  ),
272
365
  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
366
  salt: Joi.string().trim(false),
310
367
  password: Joi.string().trim(false).required(),
368
+ revokeTokenDate: Joi.number(),
369
+ totpSecret: Joi.string()
370
+ .trim(false)
371
+ .min(24)
372
+ .max(40)
373
+ .pattern(/^[A-Z234567]+$/, 'base32 encoded unpadded'),
311
374
  }),
312
- 'Invalid User: '
375
+ 'Invalid User: ',
313
376
  );
314
377
  },
315
378
  } as Config;