@nymphjs/tilmeld 1.0.0-beta.11 → 1.0.0-beta.111

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 (83) hide show
  1. package/CHANGELOG.md +504 -0
  2. package/README.md +15 -2
  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 +182 -15
  7. package/dist/Group.js +571 -191
  8. package/dist/Group.js.map +1 -1
  9. package/dist/Group.test.js +86 -13
  10. package/dist/Group.test.js.map +1 -1
  11. package/dist/Tilmeld.d.ts +241 -12
  12. package/dist/Tilmeld.js +657 -257
  13. package/dist/Tilmeld.js.map +1 -1
  14. package/dist/Tilmeld.types.d.ts +5 -5
  15. package/dist/Tilmeld.types.js +1 -2
  16. package/dist/User.d.ts +396 -22
  17. package/dist/User.js +1386 -367
  18. package/dist/User.js.map +1 -1
  19. package/dist/User.test.js +133 -13
  20. package/dist/User.test.js.map +1 -1
  21. package/dist/conf/d.d.ts +306 -7
  22. package/dist/conf/d.js +1 -2
  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 +2 -2
  27. package/dist/conf/index.js +2 -8
  28. package/dist/conf/index.js.map +1 -1
  29. package/dist/enforceTilmeld.d.ts +3 -0
  30. package/dist/enforceTilmeld.js +18 -0
  31. package/dist/enforceTilmeld.js.map +1 -0
  32. package/dist/errors/AccessControlError.d.ts +2 -1
  33. package/dist/errors/AccessControlError.js +3 -6
  34. package/dist/errors/AccessControlError.js.map +1 -1
  35. package/dist/errors/BadDataError.d.ts +2 -1
  36. package/dist/errors/BadDataError.js +3 -6
  37. package/dist/errors/BadDataError.js.map +1 -1
  38. package/dist/errors/BadEmailError.d.ts +2 -1
  39. package/dist/errors/BadEmailError.js +3 -6
  40. package/dist/errors/BadEmailError.js.map +1 -1
  41. package/dist/errors/BadUsernameError.d.ts +2 -1
  42. package/dist/errors/BadUsernameError.js +3 -6
  43. package/dist/errors/BadUsernameError.js.map +1 -1
  44. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
  45. package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
  46. package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
  47. package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
  48. package/dist/errors/index.d.ts +6 -6
  49. package/dist/errors/index.js +6 -22
  50. package/dist/errors/index.js.map +1 -1
  51. package/dist/index.d.ts +12 -10
  52. package/dist/index.js +17 -35
  53. package/dist/index.js.map +1 -1
  54. package/dist/testArtifacts.d.ts +19 -0
  55. package/dist/testArtifacts.js +920 -0
  56. package/dist/testArtifacts.js.map +1 -0
  57. package/dist/tilmeldJoiProps.d.ts +3 -3
  58. package/dist/tilmeldJoiProps.js +13 -18
  59. package/dist/tilmeldJoiProps.js.map +1 -1
  60. package/jest.config.js +11 -2
  61. package/package.json +32 -31
  62. package/src/AbleObject.ts +2 -2
  63. package/src/Group.test.ts +107 -6
  64. package/src/Group.ts +683 -224
  65. package/src/Tilmeld.ts +450 -262
  66. package/src/Tilmeld.types.ts +5 -5
  67. package/src/User.test.ts +163 -7
  68. package/src/User.ts +1628 -454
  69. package/src/conf/d.ts +83 -17
  70. package/src/conf/defaults.ts +172 -109
  71. package/src/conf/index.ts +2 -2
  72. package/src/enforceTilmeld.ts +20 -0
  73. package/src/errors/AccessControlError.ts +4 -2
  74. package/src/errors/BadDataError.ts +4 -2
  75. package/src/errors/BadEmailError.ts +4 -2
  76. package/src/errors/BadUsernameError.ts +4 -2
  77. package/src/errors/index.ts +6 -6
  78. package/src/index.ts +14 -10
  79. package/src/testArtifacts.ts +1485 -0
  80. package/src/tilmeldJoiProps.ts +8 -23
  81. package/src/type/locutus.d.ts +3 -0
  82. package/tsconfig.json +5 -3
  83. package/typedoc.json +4 -0
package/src/conf/d.ts CHANGED
@@ -1,15 +1,17 @@
1
- import { EmailOptions, EmailConfig } from 'email-templates';
1
+ import type { EmailOptions, EmailConfig } from 'email-templates';
2
2
 
3
- import type Tilmeld from '../Tilmeld';
4
- import Group, { GroupData } from '../Group';
5
- import User, { UserData } from '../User';
3
+ import type Tilmeld from '../Tilmeld.js';
4
+ import type Group from '../Group.js';
5
+ import type { GroupData } from '../Group.js';
6
+ import type User from '../User.js';
7
+ import type { UserData } from '../User.js';
6
8
 
7
9
  /**
8
10
  * Tilmeld Config
9
11
  */
10
12
  export interface Config {
11
13
  /**
12
- * The name of the app. Used in emails sent to users.
14
+ * The name of the app. Used in emails sent to users and as the TOTP issuer.
13
15
  */
14
16
  appName: string;
15
17
  /**
@@ -45,8 +47,27 @@ export interface Config {
45
47
  * Instead of a "username", a user logs in and is referred to by their email
46
48
  * address. Enabling this after many users have been created can be messy.
47
49
  * Make sure they all have email addresses first.
50
+ *
51
+ * Email usernames cannot be enabled if domain support is.
48
52
  */
49
53
  emailUsernames: boolean;
54
+ /**
55
+ * Enable domain support.
56
+ *
57
+ * If domain support is enabled, usernames can be either bare, meaning they
58
+ * belong to the root domain, or can include a domain part (username@domain),
59
+ * meaning they belong to that domain.
60
+ *
61
+ * It also enables domain admin abilities, meaning a domain admin has the
62
+ * ability to create and modify users on the domain
63
+ * ("tilmeld/domain/example.com/admin").
64
+ *
65
+ * Once a user is created, whether it has a domain part can't be changed.
66
+ * However, it can be moved to a different domain.
67
+ *
68
+ * Domain support cannot be enabled if email usernames is.
69
+ */
70
+ domainSupport: boolean;
50
71
  /**
51
72
  * Allow users to register.
52
73
  */
@@ -206,7 +227,23 @@ export interface Config {
206
227
  */
207
228
  validEmailRegexNotice: string;
208
229
  /**
209
- * The maximum length for usernames. Infinity for unlimited.
230
+ * Domains must match this regular expression. By default, this uses a regex
231
+ * that should match most domains:
232
+ *
233
+ * https://stackoverflow.com/a/26987741
234
+ */
235
+ validDomainRegex: RegExp;
236
+ /**
237
+ * When a user enters a domain that doesn't match the regex, this message will
238
+ * be displayed.
239
+ */
240
+ validDomainRegexNotice: string;
241
+ /**
242
+ * The minimum length for usernames.
243
+ */
244
+ minUsernameLength: number;
245
+ /**
246
+ * The maximum length for usernames. `Infinity` for unlimited.
210
247
  */
211
248
  maxUsernameLength: number;
212
249
  /**
@@ -217,6 +254,11 @@ export interface Config {
217
254
  * How long from current time, in seconds, the JWT token expires.
218
255
  */
219
256
  jwtExpire: number;
257
+ /**
258
+ * How long from current time, in seconds, the JWT token for a user switch
259
+ * expires.
260
+ */
261
+ jwtSwitchExpire: number;
220
262
  /**
221
263
  * How long, in seconds, before the JWT token expires to give the user a new
222
264
  * one.
@@ -225,21 +267,25 @@ export interface Config {
225
267
  /**
226
268
  * Function to build the JWT for user sessions.
227
269
  */
228
- jwtBuilder: (config: Config, user: User) => string;
270
+ jwtBuilder: (
271
+ config: Config,
272
+ user: User & UserData,
273
+ switchToken?: boolean,
274
+ ) => string;
229
275
  /**
230
- * Function to verify that a JWT was signed with the secret key, vaildate its
231
- * data, validate the XSRF token, and extract the GUID.
276
+ * Function to verify that a JWT was signed with the secret key, validate its
277
+ * data, validate the XSRF token, and extract the GUID and dates.
232
278
  *
233
279
  * If no XSRF token is supplied, ignore it.
234
280
  *
235
- * Return false if the JWT is not valid, or an array of GUID and expire
236
- * timestamp otherwise.
281
+ * Return false if the JWT is not valid, or an object with GUID, issued date,
282
+ * and expire date otherwise.
237
283
  */
238
284
  jwtExtract: (
239
285
  config: Config,
240
286
  token: string,
241
- xsrfToken?: string
242
- ) => { guid: string; expire: Date } | null;
287
+ xsrfToken?: string,
288
+ ) => { guid: string; issued: Date; expire: Date } | null;
243
289
  /**
244
290
  * The absolute path to the email template directory. Used by the default
245
291
  * email sender.
@@ -255,7 +301,7 @@ export interface Config {
255
301
  configEmail: (
256
302
  tilmeld: Tilmeld,
257
303
  options: EmailOptions,
258
- user: User & UserData
304
+ user: User & UserData,
259
305
  ) => Promise<EmailConfig>;
260
306
  /**
261
307
  * Send an email to a user. Uses `email-templates` by default.
@@ -285,19 +331,39 @@ export interface Config {
285
331
  sendEmail: (
286
332
  tilmeld: Tilmeld,
287
333
  options: EmailOptions,
288
- user: User & UserData
334
+ user: User & UserData,
289
335
  ) => Promise<boolean>;
290
336
  /**
291
337
  * The address you'd like to receive a notification of registered users, if
292
338
  * any.
293
339
  */
294
340
  userRegisteredRecipient: string | null;
341
+ /**
342
+ * A function to run on a failed login attempt.
343
+ *
344
+ * If the username the client attempted doesn't exist, user will be null.
345
+ *
346
+ * This defaults to a promise that resolves after 3-5 seconds. This rate
347
+ * limits failed login attempts.
348
+ *
349
+ * Please note that the data comes from the client, so the types have not been
350
+ * verified. For example, username might be an array or a number.
351
+ */
352
+ failedLoginAttempt: (
353
+ user: (User & UserData) | null,
354
+ data: {
355
+ username: string;
356
+ password: string;
357
+ code?: string;
358
+ additionalData?: { [k: string]: any };
359
+ },
360
+ ) => Promise<void>;
295
361
  /**
296
362
  * The validator used to check groups before saving.
297
363
  */
298
- validatorGroup: (group: Group & GroupData) => void;
364
+ validatorGroup: (tilmeld: Tilmeld, group: Group & GroupData) => void;
299
365
  /**
300
366
  * The validator used to check users before saving.
301
367
  */
302
- validatorUser: (user: User & UserData) => void;
368
+ validatorUser: (tilmeld: Tilmeld, user: User & UserData) => void;
303
369
  }
@@ -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 './d.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;
package/src/conf/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Config } from './d';
1
+ export { Config } from './d.js';
2
2
 
3
- import defaults from './defaults';
3
+ import defaults from './defaults.js';
4
4
  export { defaults as ConfigDefaults };
@@ -0,0 +1,20 @@
1
+ import { Entity, EntityConstructor, Nymph } from '@nymphjs/nymph';
2
+
3
+ import Tilmeld from './Tilmeld.js';
4
+
5
+ export function enforceTilmeld(object: Nymph | Entity | EntityConstructor) {
6
+ let tilmeld: Tilmeld | undefined;
7
+ if (object instanceof Entity) {
8
+ tilmeld = object.$nymph.tilmeld as Tilmeld | undefined;
9
+ } else if (object instanceof Nymph) {
10
+ tilmeld = object.tilmeld as Tilmeld | undefined;
11
+ } else {
12
+ tilmeld = object.nymph.tilmeld as Tilmeld | undefined;
13
+ }
14
+
15
+ if (tilmeld == null) {
16
+ throw new Error('Tilmeld was not configured on this Nymph instance!');
17
+ }
18
+
19
+ return tilmeld;
20
+ }
@@ -1,6 +1,8 @@
1
- export class AccessControlError extends Error {
1
+ import { HttpError } from '@nymphjs/nymph';
2
+
3
+ export class AccessControlError extends HttpError {
2
4
  constructor(message: string) {
3
- super(message);
5
+ super(message, 403);
4
6
  this.name = 'AccessControlError';
5
7
  }
6
8
  }
@@ -1,6 +1,8 @@
1
- export class BadDataError extends Error {
1
+ import { HttpError } from '@nymphjs/nymph';
2
+
3
+ export class BadDataError extends HttpError {
2
4
  constructor(message: string) {
3
- super(message);
5
+ super(message, 400);
4
6
  this.name = 'BadDataError';
5
7
  }
6
8
  }
@@ -1,6 +1,8 @@
1
- export class BadEmailError extends Error {
1
+ import { HttpError } from '@nymphjs/nymph';
2
+
3
+ export class BadEmailError extends HttpError {
2
4
  constructor(message: string) {
3
- super(message);
5
+ super(message, 400);
4
6
  this.name = 'BadEmailError';
5
7
  }
6
8
  }
@@ -1,6 +1,8 @@
1
- export class BadUsernameError extends Error {
1
+ import { HttpError } from '@nymphjs/nymph';
2
+
3
+ export class BadUsernameError extends HttpError {
2
4
  constructor(message: string) {
3
- super(message);
5
+ super(message, 400);
4
6
  this.name = 'BadUsernameError';
5
7
  }
6
8
  }
@@ -1,11 +1,11 @@
1
- export * from './AccessControlError';
1
+ export * from './AccessControlError.js';
2
2
 
3
- export * from './BadDataError';
3
+ export * from './BadDataError.js';
4
4
 
5
- export * from './BadEmailError';
5
+ export * from './BadEmailError.js';
6
6
 
7
- export * from './BadUsernameError';
7
+ export * from './BadUsernameError.js';
8
8
 
9
- export * from './CouldNotChangeDefaultPrimaryGroupError';
9
+ export * from './CouldNotChangeDefaultPrimaryGroupError.js';
10
10
 
11
- export * from './EmailChangeRateLimitExceededError';
11
+ export * from './EmailChangeRateLimitExceededError.js';