@nymphjs/tilmeld 1.0.0-beta.11 → 1.0.0-beta.110
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/CHANGELOG.md +498 -0
- package/README.md +15 -2
- package/dist/AbleObject.d.ts +15 -0
- package/dist/AbleObject.js +18 -6
- package/dist/AbleObject.js.map +1 -1
- package/dist/Group.d.ts +182 -15
- package/dist/Group.js +564 -191
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +86 -13
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +241 -12
- package/dist/Tilmeld.js +657 -257
- package/dist/Tilmeld.js.map +1 -1
- package/dist/Tilmeld.types.d.ts +5 -5
- package/dist/Tilmeld.types.js +1 -2
- package/dist/User.d.ts +396 -22
- package/dist/User.js +1379 -367
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +133 -13
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +306 -7
- package/dist/conf/d.js +1 -2
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +196 -141
- package/dist/conf/defaults.js.map +1 -1
- package/dist/conf/index.d.ts +2 -2
- package/dist/conf/index.js +2 -8
- package/dist/conf/index.js.map +1 -1
- package/dist/enforceTilmeld.d.ts +3 -0
- package/dist/enforceTilmeld.js +18 -0
- package/dist/enforceTilmeld.js.map +1 -0
- package/dist/errors/AccessControlError.d.ts +2 -1
- package/dist/errors/AccessControlError.js +3 -6
- package/dist/errors/AccessControlError.js.map +1 -1
- package/dist/errors/BadDataError.d.ts +2 -1
- package/dist/errors/BadDataError.js +3 -6
- package/dist/errors/BadDataError.js.map +1 -1
- package/dist/errors/BadEmailError.d.ts +2 -1
- package/dist/errors/BadEmailError.js +3 -6
- package/dist/errors/BadEmailError.js.map +1 -1
- package/dist/errors/BadUsernameError.d.ts +2 -1
- package/dist/errors/BadUsernameError.js +3 -6
- package/dist/errors/BadUsernameError.js.map +1 -1
- package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
- package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
- package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
- package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
- package/dist/errors/index.d.ts +6 -6
- package/dist/errors/index.js +6 -22
- package/dist/errors/index.js.map +1 -1
- package/dist/index.d.ts +12 -10
- package/dist/index.js +17 -35
- package/dist/index.js.map +1 -1
- package/dist/testArtifacts.d.ts +19 -0
- package/dist/testArtifacts.js +920 -0
- package/dist/testArtifacts.js.map +1 -0
- package/dist/tilmeldJoiProps.d.ts +3 -3
- package/dist/tilmeldJoiProps.js +13 -18
- package/dist/tilmeldJoiProps.js.map +1 -1
- package/jest.config.js +11 -2
- package/package.json +32 -31
- package/src/AbleObject.ts +2 -2
- package/src/Group.test.ts +107 -6
- package/src/Group.ts +676 -224
- package/src/Tilmeld.ts +450 -262
- package/src/Tilmeld.types.ts +5 -5
- package/src/User.test.ts +163 -7
- package/src/User.ts +1621 -454
- package/src/conf/d.ts +83 -17
- package/src/conf/defaults.ts +172 -109
- package/src/conf/index.ts +2 -2
- package/src/enforceTilmeld.ts +20 -0
- package/src/errors/AccessControlError.ts +4 -2
- package/src/errors/BadDataError.ts +4 -2
- package/src/errors/BadEmailError.ts +4 -2
- package/src/errors/BadUsernameError.ts +4 -2
- package/src/errors/index.ts +6 -6
- package/src/index.ts +14 -10
- package/src/testArtifacts.ts +1485 -0
- package/src/tilmeldJoiProps.ts +8 -23
- package/src/type/locutus.d.ts +3 -0
- package/tsconfig.json +5 -3
- 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
|
|
5
|
-
import
|
|
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
|
-
*
|
|
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: (
|
|
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,
|
|
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
|
|
236
|
-
*
|
|
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
|
}
|
package/src/conf/defaults.ts
CHANGED
|
@@ -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 *
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
}
|
package/src/errors/index.ts
CHANGED
|
@@ -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';
|