@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.
- package/CHANGELOG.md +598 -0
- package/README.md +21 -5
- 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 +186 -19
- package/dist/Group.js +817 -440
- package/dist/Group.js.map +1 -1
- package/dist/Group.testArtifacts.d.ts +3 -0
- package/dist/Group.testArtifacts.js +120 -0
- package/dist/Group.testArtifacts.js.map +1 -0
- package/dist/Tilmeld.d.ts +241 -12
- package/dist/Tilmeld.js +725 -286
- 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 +399 -26
- package/dist/User.js +1955 -1055
- package/dist/User.js.map +1 -1
- package/dist/User.testArtifacts.d.ts +3 -0
- package/dist/User.testArtifacts.js +175 -0
- package/dist/User.testArtifacts.js.map +1 -0
- 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 +355 -2
- package/dist/conf/index.js +2 -8
- package/dist/conf/index.js.map +1 -1
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +3 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/requireAbility.d.ts +10 -0
- package/dist/decorators/requireAbility.js +21 -0
- package/dist/decorators/requireAbility.js.map +1 -0
- package/dist/decorators/requireAuthentication.d.ts +8 -0
- package/dist/decorators/requireAuthentication.js +16 -0
- package/dist/decorators/requireAuthentication.js.map +1 -0
- 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 +23 -0
- package/dist/testArtifacts.js +1317 -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 +31 -5
- package/package.json +49 -36
- package/src/AbleObject.ts +2 -2
- package/src/Group.testArtifacts.ts +159 -0
- package/src/Group.ts +647 -238
- package/src/Tilmeld.ts +550 -315
- package/src/Tilmeld.types.ts +5 -5
- package/src/User.testArtifacts.ts +226 -0
- package/src/User.ts +1657 -634
- package/src/conf/defaults.ts +172 -109
- package/src/conf/index.ts +370 -2
- package/src/decorators/index.ts +2 -0
- package/src/decorators/requireAbility.ts +26 -0
- package/src/decorators/requireAuthentication.ts +19 -0
- 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 +13 -10
- package/src/testArtifacts.ts +2098 -0
- package/src/tilmeldJoiProps.ts +8 -23
- package/tsconfig.json +6 -3
- package/typedoc.json +4 -0
- package/dist/Group.test.d.ts +0 -1
- package/dist/Group.test.js +0 -48
- package/dist/Group.test.js.map +0 -1
- package/dist/User.test.d.ts +0 -1
- package/dist/User.test.js +0 -51
- package/dist/User.test.js.map +0 -1
- package/dist/conf/d.d.ts +0 -55
- package/dist/conf/d.js +0 -3
- package/dist/conf/d.js.map +0 -1
- package/src/Group.test.ts +0 -58
- package/src/User.test.ts +0 -61
- package/src/conf/d.ts +0 -303
package/src/User.ts
CHANGED
|
@@ -1,49 +1,88 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import {
|
|
2
|
-
EntityData,
|
|
3
|
-
EntityJson,
|
|
4
|
-
EntityPatch,
|
|
5
|
-
Selector,
|
|
6
|
-
SerializedEntityData,
|
|
3
|
+
type EntityData,
|
|
4
|
+
type EntityJson,
|
|
5
|
+
type EntityPatch,
|
|
6
|
+
type Selector,
|
|
7
|
+
type SerializedEntityData,
|
|
8
|
+
transaction,
|
|
9
|
+
transactional,
|
|
7
10
|
} from '@nymphjs/nymph';
|
|
8
|
-
import { EmailOptions } from 'email-templates';
|
|
9
|
-
import strtotime from 'locutus/php/datetime/strtotime';
|
|
10
|
-
import Base64 from 'crypto-js/enc-base64';
|
|
11
|
-
import sha256 from 'crypto-js/sha256';
|
|
12
|
-
import md5 from 'crypto-js/md5';
|
|
13
|
-
import { difference } from 'lodash';
|
|
14
11
|
import { humanSecret, nanoid } from '@nymphjs/guid';
|
|
15
|
-
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
12
|
+
import type { EmailOptions } from 'email-templates';
|
|
13
|
+
import { strtotime } from 'locutus/php/datetime/index';
|
|
14
|
+
import { difference, xor } from 'lodash-es';
|
|
15
|
+
import { TOTP, Secret } from 'otpauth';
|
|
16
|
+
import { toDataURL } from 'qrcode';
|
|
17
|
+
import { splitn } from '@sciactive/splitn';
|
|
18
|
+
|
|
19
|
+
import { enforceTilmeld } from './enforceTilmeld.js';
|
|
20
|
+
import AbleObject from './AbleObject.js';
|
|
21
|
+
import GroupClass from './Group.js';
|
|
22
|
+
import type { GroupData } from './Group.js';
|
|
19
23
|
import {
|
|
24
|
+
AccessControlError,
|
|
20
25
|
BadDataError,
|
|
21
26
|
BadEmailError,
|
|
22
27
|
BadUsernameError,
|
|
23
28
|
EmailChangeRateLimitExceededError,
|
|
24
|
-
} from './errors';
|
|
29
|
+
} from './errors/index.js';
|
|
25
30
|
|
|
26
31
|
export type EventType =
|
|
32
|
+
| 'checkUsername'
|
|
27
33
|
| 'beforeRegister'
|
|
28
34
|
| 'afterRegister'
|
|
35
|
+
| 'beforeSave'
|
|
36
|
+
| 'afterSave'
|
|
29
37
|
| 'beforeLogin'
|
|
30
38
|
| 'afterLogin'
|
|
31
39
|
| 'beforeLogout'
|
|
32
|
-
| 'afterLogout'
|
|
40
|
+
| 'afterLogout'
|
|
41
|
+
| 'beforeDelete'
|
|
42
|
+
| 'afterDelete';
|
|
43
|
+
/**
|
|
44
|
+
* This is run when the user has entered an otherwise valid username into the
|
|
45
|
+
* signup form. It should return a result, which when false will stop the
|
|
46
|
+
* process and return the included message, disallowing the username.
|
|
47
|
+
*/
|
|
48
|
+
export type TilmeldCheckUsernameCallback = (
|
|
49
|
+
user: User & UserData,
|
|
50
|
+
data: { username: string },
|
|
51
|
+
) => Promise<{ result: boolean; message?: string }>;
|
|
33
52
|
/**
|
|
34
|
-
*
|
|
53
|
+
* This is run before the user data checks, so the only checks before are
|
|
35
54
|
* whether registration is allowed and whether the user is already registered.
|
|
36
55
|
*/
|
|
37
56
|
export type TilmeldBeforeRegisterCallback = (
|
|
38
57
|
user: User & UserData,
|
|
39
|
-
data: { password: string; additionalData?: { [k: string]: any } }
|
|
58
|
+
data: { password: string; additionalData?: { [k: string]: any } },
|
|
40
59
|
) => Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* This is run before the transaction is committed, and you can perform
|
|
62
|
+
* additional functions on the transaction, which is available in `user.$nymph`.
|
|
63
|
+
*/
|
|
41
64
|
export type TilmeldAfterRegisterCallback = (
|
|
42
65
|
user: User & UserData,
|
|
43
|
-
result: { loggedin: boolean; message: string }
|
|
66
|
+
result: { loggedin: boolean; message: string },
|
|
44
67
|
) => Promise<void>;
|
|
45
68
|
/**
|
|
46
|
-
*
|
|
69
|
+
* This is run just before the user is saved, so all checks have passed.
|
|
70
|
+
*
|
|
71
|
+
* The transaction is available in `user.$nymph`.
|
|
72
|
+
*/
|
|
73
|
+
export type TilmeldBeforeSaveCallback = (
|
|
74
|
+
user: User & UserData,
|
|
75
|
+
) => Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* This is run before the transaction is committed, and you can perform
|
|
78
|
+
* additional functions on the transaction, which is available in `user.$nymph`.
|
|
79
|
+
*
|
|
80
|
+
* This is run after a new email verification email has been sent, so throwing
|
|
81
|
+
* an error will cause confusion for the user if they've changed their email.
|
|
82
|
+
*/
|
|
83
|
+
export type TilmeldAfterSaveCallback = (user: User & UserData) => Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* This is run after the authentication checks, but before the login action.
|
|
47
86
|
*/
|
|
48
87
|
export type TilmeldBeforeLoginCallback = (
|
|
49
88
|
user: User & UserData,
|
|
@@ -51,20 +90,32 @@ export type TilmeldBeforeLoginCallback = (
|
|
|
51
90
|
username: string;
|
|
52
91
|
password: string;
|
|
53
92
|
additionalData?: { [k: string]: any };
|
|
54
|
-
}
|
|
93
|
+
},
|
|
55
94
|
) => Promise<void>;
|
|
56
|
-
/**
|
|
57
|
-
* This is run before the transaction is committed, and you can perform
|
|
58
|
-
* additional functions on the transaction, which is available in `user.$nymph`.
|
|
59
|
-
*/
|
|
60
95
|
export type TilmeldAfterLoginCallback = (
|
|
61
|
-
user: User & UserData
|
|
96
|
+
user: User & UserData,
|
|
62
97
|
) => Promise<void>;
|
|
63
98
|
export type TilmeldBeforeLogoutCallback = (
|
|
64
|
-
user: User & UserData
|
|
99
|
+
user: User & UserData,
|
|
65
100
|
) => Promise<void>;
|
|
66
101
|
export type TilmeldAfterLogoutCallback = (
|
|
67
|
-
user: User & UserData
|
|
102
|
+
user: User & UserData,
|
|
103
|
+
) => Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* This is run before the user is logged out (if the current user is being
|
|
106
|
+
* deleted).
|
|
107
|
+
*
|
|
108
|
+
* The transaction is available in `user.$nymph`.
|
|
109
|
+
*/
|
|
110
|
+
export type TilmeldBeforeDeleteCallback = (
|
|
111
|
+
user: User & UserData,
|
|
112
|
+
) => Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* This is run before the transaction is committed, and you can perform
|
|
115
|
+
* additional functions on the transaction, which is available in `user.$nymph`.
|
|
116
|
+
*/
|
|
117
|
+
export type TilmeldAfterDeleteCallback = (
|
|
118
|
+
user: User & UserData,
|
|
68
119
|
) => Promise<void>;
|
|
69
120
|
|
|
70
121
|
export type UserData = {
|
|
@@ -108,11 +159,11 @@ export type UserData = {
|
|
|
108
159
|
/**
|
|
109
160
|
* The user's primary group.
|
|
110
161
|
*/
|
|
111
|
-
group?:
|
|
162
|
+
group?: GroupClass & GroupData;
|
|
112
163
|
/**
|
|
113
164
|
* The user's secondary groups.
|
|
114
165
|
*/
|
|
115
|
-
groups?: (
|
|
166
|
+
groups?: (GroupClass & GroupData)[];
|
|
116
167
|
/**
|
|
117
168
|
* Whether the user should inherit the abilities of his groups.
|
|
118
169
|
*/
|
|
@@ -135,7 +186,8 @@ export type UserData = {
|
|
|
135
186
|
*/
|
|
136
187
|
newEmailSecret?: string;
|
|
137
188
|
/**
|
|
138
|
-
*
|
|
189
|
+
* If the user has changed their email address, this is the new one, awaiting
|
|
190
|
+
* verification.
|
|
139
191
|
*/
|
|
140
192
|
newEmailAddress?: string;
|
|
141
193
|
/**
|
|
@@ -167,6 +219,15 @@ export type UserData = {
|
|
|
167
219
|
* database.
|
|
168
220
|
*/
|
|
169
221
|
passwordTemp?: string;
|
|
222
|
+
/**
|
|
223
|
+
* If set, this timestamp is the cutoff point for JWT issue dates. Any token
|
|
224
|
+
* issued before this date will not authenticate the user.
|
|
225
|
+
*/
|
|
226
|
+
revokeTokenDate?: number;
|
|
227
|
+
/**
|
|
228
|
+
* Two factor auth secret.
|
|
229
|
+
*/
|
|
230
|
+
totpSecret?: string;
|
|
170
231
|
};
|
|
171
232
|
|
|
172
233
|
/**
|
|
@@ -174,7 +235,7 @@ export type UserData = {
|
|
|
174
235
|
*
|
|
175
236
|
* Written by Hunter Perrin for SciActive.
|
|
176
237
|
*
|
|
177
|
-
* @author Hunter Perrin <hperrin@
|
|
238
|
+
* @author Hunter Perrin <hperrin@port87.com>
|
|
178
239
|
* @copyright SciActive Inc
|
|
179
240
|
* @see http://nymph.io/
|
|
180
241
|
*/
|
|
@@ -182,15 +243,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
182
243
|
/**
|
|
183
244
|
* The instance of Tilmeld to use for queries.
|
|
184
245
|
*/
|
|
185
|
-
static tilmeld: Tilmeld;
|
|
186
246
|
static ETYPE = 'tilmeld_user';
|
|
187
247
|
static class = 'User';
|
|
248
|
+
private static checkUsernameCallbacks: TilmeldCheckUsernameCallback[] = [];
|
|
188
249
|
private static beforeRegisterCallbacks: TilmeldBeforeRegisterCallback[] = [];
|
|
189
250
|
private static afterRegisterCallbacks: TilmeldAfterRegisterCallback[] = [];
|
|
251
|
+
private static beforeSaveCallbacks: TilmeldBeforeSaveCallback[] = [];
|
|
252
|
+
private static afterSaveCallbacks: TilmeldAfterSaveCallback[] = [];
|
|
190
253
|
private static beforeLoginCallbacks: TilmeldBeforeLoginCallback[] = [];
|
|
191
254
|
private static afterLoginCallbacks: TilmeldAfterLoginCallback[] = [];
|
|
192
255
|
private static beforeLogoutCallbacks: TilmeldBeforeLogoutCallback[] = [];
|
|
193
256
|
private static afterLogoutCallbacks: TilmeldAfterLogoutCallback[] = [];
|
|
257
|
+
private static beforeDeleteCallbacks: TilmeldBeforeDeleteCallback[] = [];
|
|
258
|
+
private static afterDeleteCallbacks: TilmeldAfterDeleteCallback[] = [];
|
|
194
259
|
|
|
195
260
|
private static DEFAULT_CLIENT_ENABLED_METHODS = [
|
|
196
261
|
'$checkUsername',
|
|
@@ -200,23 +265,33 @@ export default class User extends AbleObject<UserData> {
|
|
|
200
265
|
'$register',
|
|
201
266
|
];
|
|
202
267
|
private static DEFAULT_PRIVATE_DATA = [
|
|
268
|
+
// The things that are commented are there to show what is _not_ private.
|
|
269
|
+
'abilities',
|
|
270
|
+
// 'username',
|
|
271
|
+
// 'nameFirst',
|
|
272
|
+
// 'nameMiddle',
|
|
273
|
+
// 'nameLast',
|
|
274
|
+
// 'name',
|
|
203
275
|
'email',
|
|
204
|
-
'
|
|
276
|
+
// 'avatar',
|
|
205
277
|
'phone',
|
|
206
278
|
'group',
|
|
207
279
|
'groups',
|
|
208
|
-
'abilities',
|
|
209
280
|
'inheritAbilities',
|
|
210
|
-
'
|
|
211
|
-
'recoverSecretDate',
|
|
212
|
-
'password',
|
|
213
|
-
'salt',
|
|
281
|
+
'enabled',
|
|
214
282
|
'secret',
|
|
215
|
-
'
|
|
283
|
+
'emailChangeDate',
|
|
216
284
|
'newEmailSecret',
|
|
217
|
-
'
|
|
285
|
+
'newEmailAddress',
|
|
218
286
|
'cancelEmailSecret',
|
|
219
|
-
'
|
|
287
|
+
'cancelEmailAddress',
|
|
288
|
+
'recoverSecret',
|
|
289
|
+
'recoverSecretDate',
|
|
290
|
+
'salt',
|
|
291
|
+
'password',
|
|
292
|
+
'passwordTemp',
|
|
293
|
+
'revokeTokenDate',
|
|
294
|
+
'totpSecret',
|
|
220
295
|
];
|
|
221
296
|
private static DEFAULT_ALLOWLIST_DATA: string[] = [];
|
|
222
297
|
|
|
@@ -227,6 +302,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
227
302
|
'sendRecovery',
|
|
228
303
|
'recover',
|
|
229
304
|
'getClientConfig',
|
|
305
|
+
'getDomainUsers',
|
|
230
306
|
];
|
|
231
307
|
protected $privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
232
308
|
public static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
@@ -241,13 +317,17 @@ export default class User extends AbleObject<UserData> {
|
|
|
241
317
|
*/
|
|
242
318
|
private $gatekeeperCache?: { [k: string]: true };
|
|
243
319
|
/**
|
|
244
|
-
* This
|
|
320
|
+
* This should only be used by the backend.
|
|
245
321
|
*/
|
|
246
322
|
private $skipAcWhenSaving = false;
|
|
323
|
+
/**
|
|
324
|
+
* This should only be used by the backend.
|
|
325
|
+
*/
|
|
326
|
+
private $skipAcWhenDeleting = false;
|
|
247
327
|
/**
|
|
248
328
|
* The user's group descendants.
|
|
249
329
|
*/
|
|
250
|
-
private $descendantGroups?: (
|
|
330
|
+
private $descendantGroups?: (GroupClass & GroupData)[];
|
|
251
331
|
/**
|
|
252
332
|
* Used to save the current email address to send verification if it changes
|
|
253
333
|
* from the frontend.
|
|
@@ -256,22 +336,48 @@ export default class User extends AbleObject<UserData> {
|
|
|
256
336
|
* verification, don't set this.
|
|
257
337
|
*/
|
|
258
338
|
public $originalEmail?: string;
|
|
339
|
+
/**
|
|
340
|
+
* Used to save the current username for domain admin permissions.
|
|
341
|
+
*/
|
|
342
|
+
public $originalUsername?: string;
|
|
259
343
|
|
|
260
|
-
static
|
|
261
|
-
|
|
344
|
+
public static getFTSText(name: string, value: any) {
|
|
345
|
+
if (
|
|
346
|
+
[
|
|
347
|
+
'abilities',
|
|
348
|
+
'group',
|
|
349
|
+
'groups',
|
|
350
|
+
'inheritAbilities',
|
|
351
|
+
'enabled',
|
|
352
|
+
'secret',
|
|
353
|
+
'emailChangeDate',
|
|
354
|
+
'newEmailSecret',
|
|
355
|
+
'newEmailAddress',
|
|
356
|
+
'cancelEmailSecret',
|
|
357
|
+
'cancelEmailAddress',
|
|
358
|
+
'recoverSecret',
|
|
359
|
+
'recoverSecretDate',
|
|
360
|
+
'salt',
|
|
361
|
+
'password',
|
|
362
|
+
'passwordTemp',
|
|
363
|
+
'revokeTokenDate',
|
|
364
|
+
'totpSecret',
|
|
365
|
+
].indexOf(name) !== -1
|
|
366
|
+
) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return super.getFTSText(name, value);
|
|
262
370
|
}
|
|
263
371
|
|
|
264
372
|
static async factoryUsername(username?: string): Promise<User & UserData> {
|
|
265
373
|
const entity = new this();
|
|
266
374
|
if (username != null) {
|
|
267
375
|
const entity = await this.nymph.getEntity(
|
|
268
|
-
{
|
|
269
|
-
class: this,
|
|
270
|
-
},
|
|
376
|
+
{ class: this },
|
|
271
377
|
{
|
|
272
378
|
type: '&',
|
|
273
379
|
ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
274
|
-
}
|
|
380
|
+
},
|
|
275
381
|
);
|
|
276
382
|
if (entity != null) {
|
|
277
383
|
return entity;
|
|
@@ -280,34 +386,18 @@ export default class User extends AbleObject<UserData> {
|
|
|
280
386
|
return entity;
|
|
281
387
|
}
|
|
282
388
|
|
|
283
|
-
static factorySync(guid?: string): User & UserData {
|
|
284
|
-
return super.factorySync(guid) as User & UserData;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
constructor(guid?: string) {
|
|
288
|
-
super(guid);
|
|
289
|
-
|
|
290
|
-
if (this.guid == null) {
|
|
291
|
-
// Defaults.
|
|
292
|
-
this.$data.enabled = true;
|
|
293
|
-
this.$data.abilities = [];
|
|
294
|
-
this.$data.groups = [];
|
|
295
|
-
this.$data.inheritAbilities = true;
|
|
296
|
-
this.$updateDataProtection();
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
389
|
public static current(returnObjectIfNotExist: true): User & UserData;
|
|
301
390
|
public static current(
|
|
302
|
-
returnObjectIfNotExist?: false
|
|
391
|
+
returnObjectIfNotExist?: false,
|
|
303
392
|
): (User & UserData) | null;
|
|
304
393
|
public static current(
|
|
305
|
-
returnObjectIfNotExist?: boolean
|
|
394
|
+
returnObjectIfNotExist?: boolean,
|
|
306
395
|
): (User & UserData) | null {
|
|
307
|
-
|
|
396
|
+
const tilmeld = enforceTilmeld(this);
|
|
397
|
+
if (tilmeld.currentUser == null) {
|
|
308
398
|
return returnObjectIfNotExist ? this.factorySync() : null;
|
|
309
399
|
}
|
|
310
|
-
return
|
|
400
|
+
return tilmeld.currentUser;
|
|
311
401
|
}
|
|
312
402
|
|
|
313
403
|
/**
|
|
@@ -320,28 +410,23 @@ export default class User extends AbleObject<UserData> {
|
|
|
320
410
|
recoveryType: 'username' | 'password';
|
|
321
411
|
account: string;
|
|
322
412
|
}): Promise<{ result: boolean; message: string }> {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
message: 'Account recovery is not allowed.',
|
|
327
|
-
};
|
|
413
|
+
const tilmeld = enforceTilmeld(this);
|
|
414
|
+
if (!tilmeld.config.pwRecovery) {
|
|
415
|
+
return { result: false, message: 'Account recovery is not allowed.' };
|
|
328
416
|
}
|
|
329
417
|
|
|
330
418
|
let user: User & UserData;
|
|
331
419
|
const options: EmailOptions = {};
|
|
332
420
|
|
|
333
|
-
if (
|
|
334
|
-
!this.tilmeld.config.emailUsernames &&
|
|
335
|
-
data.recoveryType === 'username'
|
|
336
|
-
) {
|
|
421
|
+
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
337
422
|
// Create a username recovery email.
|
|
338
423
|
|
|
339
424
|
const getUser = await this.nymph.getEntity(
|
|
340
|
-
{ class:
|
|
425
|
+
{ class: tilmeld.User, skipAc: true },
|
|
341
426
|
{
|
|
342
427
|
type: '&',
|
|
343
428
|
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
344
|
-
}
|
|
429
|
+
},
|
|
345
430
|
);
|
|
346
431
|
|
|
347
432
|
if (getUser == null) {
|
|
@@ -355,16 +440,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
355
440
|
user = getUser;
|
|
356
441
|
options.template = 'RecoverUsername';
|
|
357
442
|
options.message = {
|
|
358
|
-
to: {
|
|
359
|
-
name: user.name ?? '',
|
|
360
|
-
address: user.email ?? '',
|
|
361
|
-
},
|
|
443
|
+
to: { name: user.name ?? '', address: user.email ?? '' },
|
|
362
444
|
};
|
|
363
445
|
options.locals = {};
|
|
364
446
|
} else if (data.recoveryType === 'password') {
|
|
365
447
|
// Create a password recovery email.
|
|
366
448
|
|
|
367
|
-
const getUser = await
|
|
449
|
+
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
368
450
|
|
|
369
451
|
if (getUser.guid == null) {
|
|
370
452
|
return {
|
|
@@ -376,7 +458,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
376
458
|
// Create a unique secret.
|
|
377
459
|
getUser.recoverSecret = humanSecret();
|
|
378
460
|
getUser.recoverSecretDate = Date.now();
|
|
379
|
-
|
|
461
|
+
try {
|
|
462
|
+
await getUser.$saveSkipAC();
|
|
463
|
+
} catch (e: any) {
|
|
380
464
|
return { result: false, message: "Couldn't save recovery secret." };
|
|
381
465
|
}
|
|
382
466
|
|
|
@@ -384,21 +468,23 @@ export default class User extends AbleObject<UserData> {
|
|
|
384
468
|
user = getUser;
|
|
385
469
|
options.template = 'RecoverPassword';
|
|
386
470
|
options.message = {
|
|
387
|
-
to: {
|
|
388
|
-
name: user.name ?? '',
|
|
389
|
-
address: user.email ?? '',
|
|
390
|
-
},
|
|
471
|
+
to: { name: user.name ?? '', address: user.email ?? '' },
|
|
391
472
|
};
|
|
392
473
|
options.locals = {
|
|
393
474
|
recoverCode: getUser.recoverSecret,
|
|
394
|
-
timeLimit:
|
|
475
|
+
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
395
476
|
};
|
|
396
477
|
} else {
|
|
397
478
|
return { result: false, message: 'Invalid recovery type.' };
|
|
398
479
|
}
|
|
399
480
|
|
|
481
|
+
this.nymph.config.debugLog(
|
|
482
|
+
'tilmeld',
|
|
483
|
+
`User account recovery (${options.template}) request for "${user.username}".`,
|
|
484
|
+
);
|
|
485
|
+
|
|
400
486
|
// Send the email.
|
|
401
|
-
if (await
|
|
487
|
+
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
402
488
|
return {
|
|
403
489
|
result: true,
|
|
404
490
|
message:
|
|
@@ -421,72 +507,77 @@ export default class User extends AbleObject<UserData> {
|
|
|
421
507
|
secret: string;
|
|
422
508
|
password: string;
|
|
423
509
|
}): Promise<{ result: boolean; message: string }> {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
message: 'Account recovery is not allowed.',
|
|
428
|
-
};
|
|
510
|
+
const tilmeld = enforceTilmeld(this);
|
|
511
|
+
if (!tilmeld.config.pwRecovery) {
|
|
512
|
+
return { result: false, message: 'Account recovery is not allowed.' };
|
|
429
513
|
}
|
|
430
514
|
|
|
431
|
-
const user = await
|
|
515
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
432
516
|
|
|
433
517
|
if (
|
|
434
518
|
user.guid == null ||
|
|
435
519
|
user.recoverSecret == null ||
|
|
436
520
|
data.secret !== user.recoverSecret ||
|
|
437
|
-
strtotime(
|
|
438
|
-
'+' +
|
|
439
|
-
Math.floor((user.recoverSecretDate ?? 0) / 1000)
|
|
440
|
-
) *
|
|
521
|
+
(strtotime(
|
|
522
|
+
'+' + tilmeld.config.pwRecoveryTimeLimit,
|
|
523
|
+
Math.floor((user.recoverSecretDate ?? 0) / 1000),
|
|
524
|
+
) || 0) *
|
|
441
525
|
1000 <
|
|
442
526
|
Date.now()
|
|
443
527
|
) {
|
|
444
|
-
return {
|
|
445
|
-
result: false,
|
|
446
|
-
message: 'The secret code does not match.',
|
|
447
|
-
};
|
|
528
|
+
return { result: false, message: 'The secret code does not match.' };
|
|
448
529
|
}
|
|
449
530
|
|
|
450
531
|
if (!('password' in data) || !data.password.length) {
|
|
451
532
|
return { result: false, message: 'Password cannot be empty.' };
|
|
452
533
|
}
|
|
453
534
|
|
|
535
|
+
this.nymph.config.debugLog(
|
|
536
|
+
'tilmeld',
|
|
537
|
+
`User account recovery for "${user.username}".`,
|
|
538
|
+
);
|
|
539
|
+
|
|
454
540
|
user.$password(data.password);
|
|
455
541
|
delete user.recoverSecret;
|
|
456
542
|
delete user.recoverSecretDate;
|
|
457
|
-
|
|
543
|
+
try {
|
|
544
|
+
await user.$saveSkipAC();
|
|
458
545
|
return {
|
|
459
546
|
result: true,
|
|
460
547
|
message:
|
|
461
548
|
'Your password has been reset. You can now log in using your new password.',
|
|
462
549
|
};
|
|
463
|
-
}
|
|
550
|
+
} catch (e: any) {
|
|
464
551
|
return { result: false, message: 'Error saving new password.' };
|
|
465
552
|
}
|
|
466
553
|
}
|
|
467
554
|
|
|
468
555
|
public static getClientConfig() {
|
|
556
|
+
const tilmeld = enforceTilmeld(this);
|
|
469
557
|
return {
|
|
470
|
-
regFields:
|
|
471
|
-
userFields:
|
|
472
|
-
emailUsernames:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
558
|
+
regFields: tilmeld.config.regFields,
|
|
559
|
+
userFields: tilmeld.config.userFields,
|
|
560
|
+
emailUsernames: tilmeld.config.emailUsernames,
|
|
561
|
+
domainSupport: tilmeld.config.domainSupport,
|
|
562
|
+
allowRegistration: tilmeld.config.allowRegistration,
|
|
563
|
+
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
564
|
+
pwRecovery: tilmeld.config.pwRecovery,
|
|
565
|
+
verifyEmail: tilmeld.config.verifyEmail,
|
|
566
|
+
unverifiedAccess: tilmeld.config.unverifiedAccess,
|
|
478
567
|
};
|
|
479
568
|
}
|
|
480
569
|
|
|
481
570
|
public static async loginUser(data: {
|
|
482
571
|
username: string;
|
|
483
572
|
password: string;
|
|
573
|
+
code?: string;
|
|
484
574
|
additionalData?: { [k: string]: any };
|
|
485
575
|
}) {
|
|
576
|
+
const tilmeld = enforceTilmeld(this);
|
|
486
577
|
if (!('username' in data) || !data.username.length) {
|
|
487
578
|
return { result: false, message: 'Incorrect login/password.' };
|
|
488
579
|
}
|
|
489
|
-
const user = await
|
|
580
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
490
581
|
const result: { result: boolean; message: string; user?: User & UserData } =
|
|
491
582
|
await user.$login(data);
|
|
492
583
|
if (result.result) {
|
|
@@ -496,23 +587,120 @@ export default class User extends AbleObject<UserData> {
|
|
|
496
587
|
return result;
|
|
497
588
|
}
|
|
498
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Get the users in a domain. This is only accessible to the domain's admins.
|
|
592
|
+
*/
|
|
593
|
+
public static async getDomainUsers(
|
|
594
|
+
domain: string,
|
|
595
|
+
options?: {
|
|
596
|
+
limit?: number;
|
|
597
|
+
offset?: number;
|
|
598
|
+
sort?: string;
|
|
599
|
+
reverse?: boolean;
|
|
600
|
+
},
|
|
601
|
+
) {
|
|
602
|
+
const tilmeld = enforceTilmeld(this);
|
|
603
|
+
|
|
604
|
+
if (!tilmeld.config.domainSupport) {
|
|
605
|
+
throw new AccessControlError('Domain support is not enabled.');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (!domain) {
|
|
609
|
+
throw new BadDataError('Domain is required.');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
domain = domain.toLowerCase();
|
|
613
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
614
|
+
throw new BadDataError(tilmeld.config.validDomainRegexNotice);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
618
|
+
throw new AccessControlError(
|
|
619
|
+
'You do not have the authority to manage this domain.',
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return await this.nymph.getEntities(
|
|
624
|
+
{
|
|
625
|
+
class: this.nymph.getEntityClass(User),
|
|
626
|
+
...(options?.limit != null ? { limit: options.limit } : {}),
|
|
627
|
+
...(options?.offset != null ? { offset: options.offset } : {}),
|
|
628
|
+
...(options?.sort != null ? { sort: options.sort } : {}),
|
|
629
|
+
...(options?.reverse != null ? { reverse: options.reverse } : {}),
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
type: '&',
|
|
633
|
+
like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
constructor() {
|
|
639
|
+
super();
|
|
640
|
+
|
|
641
|
+
// Defaults.
|
|
642
|
+
this.$data.enabled = true;
|
|
643
|
+
this.$data.abilities = [];
|
|
644
|
+
this.$data.groups = [];
|
|
645
|
+
this.$data.inheritAbilities = true;
|
|
646
|
+
this.$updateDataProtection();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async $getUniques(): Promise<string[]> {
|
|
650
|
+
const tilmeld = enforceTilmeld(this);
|
|
651
|
+
const uniques = [`u:${this.$data.username}`];
|
|
652
|
+
if (
|
|
653
|
+
!tilmeld.config.emailUsernames &&
|
|
654
|
+
tilmeld.config.userFields.includes('email')
|
|
655
|
+
) {
|
|
656
|
+
uniques.push(`e:${this.$data.email}`);
|
|
657
|
+
|
|
658
|
+
if (this.$data.newEmailAddress != null) {
|
|
659
|
+
uniques.push(`e:${this.$data.newEmailAddress}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return uniques;
|
|
663
|
+
}
|
|
664
|
+
|
|
499
665
|
public async $login(data: {
|
|
500
666
|
username: string;
|
|
501
667
|
password: string;
|
|
668
|
+
code?: string;
|
|
502
669
|
additionalData?: { [k: string]: any };
|
|
503
670
|
}) {
|
|
504
|
-
const tilmeld = this
|
|
671
|
+
const tilmeld = enforceTilmeld(this);
|
|
505
672
|
if (this.guid == null) {
|
|
673
|
+
await tilmeld.config.failedLoginAttempt(null, data);
|
|
674
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
675
|
+
}
|
|
676
|
+
if (!this.$checkPassword(data.password)) {
|
|
677
|
+
await tilmeld.config.failedLoginAttempt(this, data);
|
|
506
678
|
return { result: false, message: 'Incorrect login/password.' };
|
|
507
679
|
}
|
|
508
680
|
if (!this.$data.enabled) {
|
|
509
|
-
return { result: false, message: '
|
|
681
|
+
return { result: false, message: 'Your account is disabled.' };
|
|
510
682
|
}
|
|
511
|
-
if (this.$gatekeeper()) {
|
|
683
|
+
if (await this.$gatekeeper()) {
|
|
512
684
|
return { result: true, message: 'You are already logged in.' };
|
|
513
685
|
}
|
|
514
|
-
|
|
515
|
-
|
|
686
|
+
|
|
687
|
+
if (this.$data.totpSecret != null) {
|
|
688
|
+
if (
|
|
689
|
+
data.code == null ||
|
|
690
|
+
typeof data.code !== 'string' ||
|
|
691
|
+
data.code.length !== 6
|
|
692
|
+
) {
|
|
693
|
+
return {
|
|
694
|
+
result: false,
|
|
695
|
+
needTOTP: true,
|
|
696
|
+
message: 'You need to provide a 2FA code.',
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (!this.$checkTOTPCode(data.code)) {
|
|
701
|
+
await tilmeld.config.failedLoginAttempt(this, data);
|
|
702
|
+
return { result: false, message: 'Incorrect 2FA code.' };
|
|
703
|
+
}
|
|
516
704
|
}
|
|
517
705
|
|
|
518
706
|
try {
|
|
@@ -523,14 +711,92 @@ export default class User extends AbleObject<UserData> {
|
|
|
523
711
|
}
|
|
524
712
|
}
|
|
525
713
|
} catch (e: any) {
|
|
714
|
+
return { result: false, message: e.message };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
this.$nymph.config.debugLog(
|
|
718
|
+
'tilmeld',
|
|
719
|
+
`User login for "${this.$data.username}".`,
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
// Authentication was successful, attempt to login.
|
|
723
|
+
if (!(await tilmeld.login(this, true))) {
|
|
724
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for (let callback of (this.constructor as typeof User)
|
|
728
|
+
.afterLoginCallbacks) {
|
|
729
|
+
if (callback) {
|
|
730
|
+
await callback(this);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Login was successful.
|
|
735
|
+
return { result: true, message: 'You are logged in.' };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
public async $switchUser(data?: {
|
|
739
|
+
additionalData?: { [k: string]: any };
|
|
740
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
741
|
+
const tilmeld = enforceTilmeld(this);
|
|
742
|
+
if (this.guid == null) {
|
|
743
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let user = tilmeld.User.current();
|
|
747
|
+
|
|
748
|
+
if (!user || user.guid == null) {
|
|
749
|
+
return { result: false, message: "You're not logged in." };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
753
|
+
|
|
754
|
+
if (isCurrentUser) {
|
|
755
|
+
return { result: false, message: "You can't switch to yourself." };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const toAbilities = this.$data.abilities || [];
|
|
759
|
+
const fromAbilities = user.abilities || [];
|
|
760
|
+
|
|
761
|
+
if (
|
|
762
|
+
!fromAbilities.includes('tilmeld/switch') &&
|
|
763
|
+
!fromAbilities.includes('system/admin')
|
|
764
|
+
) {
|
|
526
765
|
return {
|
|
527
766
|
result: false,
|
|
528
|
-
message:
|
|
767
|
+
message: "You don't have permission to switch users.",
|
|
529
768
|
};
|
|
530
769
|
}
|
|
531
770
|
|
|
771
|
+
if (
|
|
772
|
+
toAbilities.includes('system/admin') ||
|
|
773
|
+
toAbilities.includes('tilmeld/admin')
|
|
774
|
+
) {
|
|
775
|
+
return { result: false, message: "You can't switch to an admin." };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
for (let callback of (this.constructor as typeof User)
|
|
780
|
+
.beforeLoginCallbacks) {
|
|
781
|
+
if (callback) {
|
|
782
|
+
await callback(this, {
|
|
783
|
+
username: this.$data.username || '',
|
|
784
|
+
password: '',
|
|
785
|
+
...(data || {}),
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
} catch (e: any) {
|
|
790
|
+
return { result: false, message: e.message };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
this.$nymph.config.debugLog(
|
|
794
|
+
'tilmeld',
|
|
795
|
+
`User login switch for "${user.username}" to "${this.$data.username}".`,
|
|
796
|
+
);
|
|
797
|
+
|
|
532
798
|
// Authentication was successful, attempt to login.
|
|
533
|
-
if (!tilmeld.
|
|
799
|
+
if (!(await tilmeld.loginSwitch(this, true))) {
|
|
534
800
|
return { result: false, message: 'Incorrect login/password.' };
|
|
535
801
|
}
|
|
536
802
|
|
|
@@ -549,8 +815,8 @@ export default class User extends AbleObject<UserData> {
|
|
|
549
815
|
* Log a user out of the system.
|
|
550
816
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
551
817
|
*/
|
|
552
|
-
public async $logout() {
|
|
553
|
-
const tilmeld = this
|
|
818
|
+
public async $logout(): Promise<{ result: boolean; message: string }> {
|
|
819
|
+
const tilmeld = enforceTilmeld(this);
|
|
554
820
|
|
|
555
821
|
try {
|
|
556
822
|
for (let callback of (this.constructor as typeof User)
|
|
@@ -560,13 +826,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
560
826
|
}
|
|
561
827
|
}
|
|
562
828
|
} catch (e: any) {
|
|
563
|
-
return {
|
|
564
|
-
result: false,
|
|
565
|
-
message: e.message,
|
|
566
|
-
};
|
|
829
|
+
return { result: false, message: e.message };
|
|
567
830
|
}
|
|
568
831
|
|
|
569
|
-
|
|
832
|
+
this.$nymph.config.debugLog(
|
|
833
|
+
'tilmeld',
|
|
834
|
+
`User logout for "${this.$data.username}".`,
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
await tilmeld.logout();
|
|
570
838
|
|
|
571
839
|
for (let callback of (this.constructor as typeof User)
|
|
572
840
|
.afterLogoutCallbacks) {
|
|
@@ -578,7 +846,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
578
846
|
return { result: true, message: 'You have been logged out.' };
|
|
579
847
|
}
|
|
580
848
|
|
|
581
|
-
public $getAvatar() {
|
|
849
|
+
public $getAvatar(): string {
|
|
582
850
|
if (this.$data.avatar != null && this.$data.avatar !== '') {
|
|
583
851
|
return this.$data.avatar;
|
|
584
852
|
}
|
|
@@ -587,89 +855,183 @@ export default class User extends AbleObject<UserData> {
|
|
|
587
855
|
}
|
|
588
856
|
return (
|
|
589
857
|
'https://secure.gravatar.com/avatar/' +
|
|
590
|
-
|
|
858
|
+
crypto
|
|
859
|
+
.createHash('sha256')
|
|
860
|
+
.update(this.$data.email.trim().toLowerCase())
|
|
861
|
+
.digest('hex')
|
|
862
|
+
.toLowerCase() +
|
|
591
863
|
'?d=identicon&s=40'
|
|
592
864
|
);
|
|
593
865
|
}
|
|
594
866
|
|
|
867
|
+
public $getGid() {
|
|
868
|
+
if (this.$data.group) {
|
|
869
|
+
return this.$data.group.guid;
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
public $getGids() {
|
|
875
|
+
if (this.$data.groups) {
|
|
876
|
+
return this.$data.groups
|
|
877
|
+
.map((group) => group.guid)
|
|
878
|
+
.filter((guid) => guid != null) as string[];
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
|
|
595
883
|
/**
|
|
596
884
|
* Get the user's group descendants.
|
|
597
885
|
*/
|
|
598
|
-
public async $getDescendantGroups(): Promise<(
|
|
886
|
+
public async $getDescendantGroups(): Promise<(GroupClass & GroupData)[]> {
|
|
599
887
|
if (this.$descendantGroups == null) {
|
|
600
888
|
this.$descendantGroups = [];
|
|
601
889
|
if (this.$data.group != null) {
|
|
890
|
+
await this.$data.group.$wake();
|
|
602
891
|
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
603
892
|
}
|
|
893
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
604
894
|
for (let curGroup of this.$data.groups ?? []) {
|
|
605
895
|
this.$descendantGroups = this.$descendantGroups?.concat(
|
|
606
|
-
await curGroup.$getDescendants()
|
|
896
|
+
await curGroup.$getDescendants(),
|
|
607
897
|
);
|
|
608
898
|
}
|
|
609
899
|
}
|
|
610
900
|
return this.$descendantGroups;
|
|
611
901
|
}
|
|
612
902
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (
|
|
618
|
-
this.$
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
903
|
+
public $jsonAcceptData(input: EntityJson, allowConflict = false) {
|
|
904
|
+
const tilmeld = enforceTilmeld(this);
|
|
905
|
+
this.$check();
|
|
906
|
+
|
|
907
|
+
if ('abilities' in input.data) {
|
|
908
|
+
this.$checkIncomingAbilities(input.data.abilities);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// If the user is a new domain user, the data protection needs to be updated
|
|
912
|
+
// before the data is accepted, or else some things won't be accepted
|
|
913
|
+
// correctly (enabled, passwordTemp).
|
|
914
|
+
let reloadDataProtection = false;
|
|
915
|
+
if (tilmeld.config.domainSupport) {
|
|
916
|
+
let [_username, domain] = splitn(input.data.username ?? '', '@', 2);
|
|
917
|
+
// Lowercase the domain part.
|
|
918
|
+
if (domain) {
|
|
919
|
+
domain = domain.toLowerCase();
|
|
626
920
|
}
|
|
921
|
+
const user = tilmeld.User.current();
|
|
922
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
923
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
924
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
925
|
+
const abilities =
|
|
926
|
+
(isCurrentUser || user == null
|
|
927
|
+
? this.$data.abilities
|
|
928
|
+
: user?.abilities) ?? [];
|
|
929
|
+
const isNewUser = input.guid == null;
|
|
930
|
+
const isDomainUser =
|
|
931
|
+
!isCurrentUser &&
|
|
932
|
+
user != null &&
|
|
933
|
+
domain &&
|
|
934
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
935
|
+
|
|
936
|
+
reloadDataProtection = !!(isNewUser && isDomainUser);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
940
|
+
|
|
941
|
+
if (reloadDataProtection) {
|
|
942
|
+
// Load it again after data protection is run.
|
|
943
|
+
this.$updateDataProtection();
|
|
944
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
627
945
|
}
|
|
628
|
-
return this.$descendantGroups;
|
|
629
946
|
}
|
|
630
947
|
|
|
631
|
-
public $
|
|
632
|
-
|
|
633
|
-
|
|
948
|
+
public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
|
|
949
|
+
this.$check();
|
|
950
|
+
|
|
951
|
+
if ('abilities' in patch.set) {
|
|
952
|
+
this.$checkIncomingAbilities(patch.set.abilities);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
super.$jsonAcceptPatch(patch, allowConflict);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private $checkIncomingAbilities(incoming: any) {
|
|
959
|
+
const tilmeld = enforceTilmeld(this);
|
|
634
960
|
|
|
635
961
|
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
962
|
+
!Array.isArray(incoming) ||
|
|
963
|
+
incoming.find((ability) => typeof ability !== 'string')
|
|
964
|
+
) {
|
|
965
|
+
throw new BadDataError('Abilities must be an array of strings.');
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (
|
|
969
|
+
incoming.includes('system/admin') !==
|
|
970
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
971
|
+
(!tilmeld.currentUser ||
|
|
972
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
641
973
|
) {
|
|
642
974
|
throw new BadDataError(
|
|
643
|
-
"You don't have the authority to
|
|
975
|
+
"You don't have the authority to grant or revoke system/admin.",
|
|
644
976
|
);
|
|
645
977
|
}
|
|
646
978
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
979
|
+
if (
|
|
980
|
+
incoming.includes('tilmeld/admin') !==
|
|
981
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
982
|
+
(!tilmeld.currentUser ||
|
|
983
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
984
|
+
) {
|
|
985
|
+
throw new BadDataError(
|
|
986
|
+
"You don't have the authority to grant or revoke tilmeld/admin.",
|
|
987
|
+
);
|
|
988
|
+
}
|
|
654
989
|
|
|
655
990
|
if (
|
|
656
|
-
'
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
!tilmeld.gatekeeper('system/admin')
|
|
991
|
+
incoming.includes('tilmeld/switch') !==
|
|
992
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
993
|
+
(!tilmeld.currentUser ||
|
|
994
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
661
995
|
) {
|
|
662
996
|
throw new BadDataError(
|
|
663
|
-
"You don't have the authority to
|
|
997
|
+
"You don't have the authority to grant or revoke tilmeld/switch.",
|
|
664
998
|
);
|
|
665
999
|
}
|
|
666
1000
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1001
|
+
if (
|
|
1002
|
+
xor(
|
|
1003
|
+
incoming.filter((ability: string) => ability.startsWith('tilmeld/')),
|
|
1004
|
+
this.$data.abilities?.filter((ability: string) =>
|
|
1005
|
+
ability.startsWith('tilmeld/'),
|
|
1006
|
+
) ?? [],
|
|
1007
|
+
).length &&
|
|
1008
|
+
(!tilmeld.currentUser ||
|
|
1009
|
+
(!tilmeld.currentUser.abilities?.includes('system/admin') &&
|
|
1010
|
+
!tilmeld.currentUser.abilities?.includes('tilmeld/admin')))
|
|
1011
|
+
) {
|
|
1012
|
+
throw new BadDataError(
|
|
1013
|
+
"You don't have the authority to grant or revoke Tilmeld abilities.",
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
670
1017
|
|
|
671
|
-
public $putData(
|
|
672
|
-
|
|
1018
|
+
public $putData(
|
|
1019
|
+
data: EntityData,
|
|
1020
|
+
sdata?: SerializedEntityData,
|
|
1021
|
+
source?: 'server',
|
|
1022
|
+
) {
|
|
1023
|
+
super.$putData(data, sdata, source);
|
|
1024
|
+
// TODO: Fix these being added by the drivers.
|
|
1025
|
+
// @ts-ignore
|
|
1026
|
+
delete this.$data.acRead;
|
|
1027
|
+
// @ts-ignore
|
|
1028
|
+
delete this.$data.acWrite;
|
|
1029
|
+
// @ts-ignore
|
|
1030
|
+
delete this.$data.acFull;
|
|
1031
|
+
if (source === 'server') {
|
|
1032
|
+
this.$originalEmail = this.$data.email;
|
|
1033
|
+
this.$originalUsername = this.$data.username;
|
|
1034
|
+
}
|
|
673
1035
|
this.$updateDataProtection();
|
|
674
1036
|
}
|
|
675
1037
|
|
|
@@ -679,8 +1041,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
679
1041
|
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
680
1042
|
*/
|
|
681
1043
|
public $updateDataProtection(givenUser?: User & UserData) {
|
|
682
|
-
const tilmeld = this
|
|
1044
|
+
const tilmeld = enforceTilmeld(this);
|
|
683
1045
|
let user = givenUser ?? tilmeld.User.current();
|
|
1046
|
+
let [_username, domain] = tilmeld.config.domainSupport
|
|
1047
|
+
? splitn(this.$originalUsername ?? this.$data.username ?? '', '@', 2)
|
|
1048
|
+
: [this.$originalUsername ?? this.$data.username, null];
|
|
1049
|
+
// Lowercase the domain part.
|
|
1050
|
+
if (domain) {
|
|
1051
|
+
domain = domain.toLowerCase();
|
|
1052
|
+
}
|
|
684
1053
|
|
|
685
1054
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
686
1055
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
@@ -698,53 +1067,164 @@ export default class User extends AbleObject<UserData> {
|
|
|
698
1067
|
? this.$data.abilities
|
|
699
1068
|
: user?.abilities) ?? [];
|
|
700
1069
|
const isNewUser = this.guid == null;
|
|
1070
|
+
const isDomainUser =
|
|
1071
|
+
tilmeld.config.domainSupport &&
|
|
1072
|
+
!isCurrentUser &&
|
|
1073
|
+
user != null &&
|
|
1074
|
+
domain &&
|
|
1075
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
701
1076
|
|
|
702
1077
|
if (isCurrentUser) {
|
|
703
1078
|
// Users can check to see what abilities they have.
|
|
704
1079
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
705
1080
|
this.$clientEnabledMethods.push('$changePassword');
|
|
1081
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
706
1082
|
this.$clientEnabledMethods.push('$logout');
|
|
707
|
-
this.$clientEnabledMethods.push('$
|
|
1083
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1084
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
1085
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
1086
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (isDomainUser) {
|
|
1090
|
+
// Domain admins can check to see what abilities their users have.
|
|
1091
|
+
this.$clientEnabledMethods.push('$gatekeeper');
|
|
1092
|
+
this.$clientEnabledMethods.push('$changePassword');
|
|
1093
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
1094
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1095
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
1096
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
1097
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
708
1098
|
}
|
|
709
1099
|
|
|
710
1100
|
if (
|
|
711
1101
|
user != null &&
|
|
712
|
-
(abilities.
|
|
713
|
-
abilities.
|
|
1102
|
+
(abilities.includes('tilmeld/admin') ||
|
|
1103
|
+
abilities.includes('system/admin'))
|
|
714
1104
|
) {
|
|
715
1105
|
// Users who can edit other users can see most of their data.
|
|
716
|
-
this.$privateData = ['password', 'salt'];
|
|
1106
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
1107
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1108
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
717
1109
|
this.$allowlistData = undefined;
|
|
1110
|
+
} else if (isDomainUser) {
|
|
1111
|
+
// Domain admins can see their users' data, and edit some of it.
|
|
1112
|
+
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
1113
|
+
this.$allowlistData.push('username');
|
|
1114
|
+
}
|
|
1115
|
+
this.$allowlistData.push('avatar');
|
|
1116
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
1117
|
+
this.$allowlistData.push('nameFirst');
|
|
1118
|
+
this.$allowlistData.push('nameMiddle');
|
|
1119
|
+
this.$allowlistData.push('nameLast');
|
|
1120
|
+
this.$allowlistData.push('name');
|
|
1121
|
+
}
|
|
1122
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
1123
|
+
this.$allowlistData.push('email');
|
|
1124
|
+
}
|
|
1125
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
1126
|
+
this.$allowlistData.push('phone');
|
|
1127
|
+
}
|
|
1128
|
+
this.$allowlistData.push('enabled');
|
|
1129
|
+
this.$allowlistData.push('passwordTemp');
|
|
1130
|
+
this.$privateData = [
|
|
1131
|
+
// The things commented are there to show what is _not_ private.
|
|
1132
|
+
// 'abilities',
|
|
1133
|
+
// 'username',
|
|
1134
|
+
// 'nameFirst',
|
|
1135
|
+
// 'nameMiddle',
|
|
1136
|
+
// 'nameLast',
|
|
1137
|
+
// 'name',
|
|
1138
|
+
// 'email',
|
|
1139
|
+
// 'avatar',
|
|
1140
|
+
// 'phone',
|
|
1141
|
+
// 'group',
|
|
1142
|
+
// 'groups',
|
|
1143
|
+
// 'inheritAbilities',
|
|
1144
|
+
// 'enabled',
|
|
1145
|
+
'secret',
|
|
1146
|
+
'emailChangeDate',
|
|
1147
|
+
'newEmailSecret',
|
|
1148
|
+
// 'newEmailAddress',
|
|
1149
|
+
'cancelEmailSecret',
|
|
1150
|
+
'cancelEmailAddress',
|
|
1151
|
+
'recoverSecret',
|
|
1152
|
+
'recoverSecretDate',
|
|
1153
|
+
'salt',
|
|
1154
|
+
'password',
|
|
1155
|
+
// 'passwordTemp',
|
|
1156
|
+
// 'revokeTokenDate',
|
|
1157
|
+
'totpSecret',
|
|
1158
|
+
];
|
|
718
1159
|
} else if (isCurrentUser || isNewUser) {
|
|
719
1160
|
// Users can see their own data, and edit some of it.
|
|
720
1161
|
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
721
1162
|
this.$allowlistData.push('username');
|
|
722
1163
|
}
|
|
723
1164
|
this.$allowlistData.push('avatar');
|
|
724
|
-
if (tilmeld.config.userFields.
|
|
1165
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
725
1166
|
this.$allowlistData.push('nameFirst');
|
|
726
1167
|
this.$allowlistData.push('nameMiddle');
|
|
727
1168
|
this.$allowlistData.push('nameLast');
|
|
728
1169
|
this.$allowlistData.push('name');
|
|
729
1170
|
}
|
|
730
|
-
if (tilmeld.config.userFields.
|
|
1171
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
731
1172
|
this.$allowlistData.push('email');
|
|
732
1173
|
}
|
|
733
|
-
if (tilmeld.config.userFields.
|
|
1174
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
734
1175
|
this.$allowlistData.push('phone');
|
|
735
1176
|
}
|
|
736
1177
|
this.$privateData = [
|
|
1178
|
+
// The things commented are there to show what is _not_ private.
|
|
1179
|
+
// 'abilities',
|
|
1180
|
+
// 'username',
|
|
1181
|
+
// 'nameFirst',
|
|
1182
|
+
// 'nameMiddle',
|
|
1183
|
+
// 'nameLast',
|
|
1184
|
+
// 'name',
|
|
1185
|
+
// 'email',
|
|
1186
|
+
// 'avatar',
|
|
1187
|
+
// 'phone',
|
|
1188
|
+
// 'group',
|
|
1189
|
+
// 'groups',
|
|
1190
|
+
// 'inheritAbilities',
|
|
1191
|
+
'enabled',
|
|
737
1192
|
'secret',
|
|
1193
|
+
'emailChangeDate',
|
|
738
1194
|
'newEmailSecret',
|
|
739
|
-
'
|
|
1195
|
+
// 'newEmailAddress',
|
|
740
1196
|
'cancelEmailSecret',
|
|
741
|
-
'
|
|
1197
|
+
'cancelEmailAddress',
|
|
742
1198
|
'recoverSecret',
|
|
743
1199
|
'recoverSecretDate',
|
|
744
|
-
'password',
|
|
745
1200
|
'salt',
|
|
1201
|
+
'password',
|
|
1202
|
+
'passwordTemp',
|
|
1203
|
+
// 'revokeTokenDate',
|
|
1204
|
+
'totpSecret',
|
|
746
1205
|
];
|
|
747
1206
|
}
|
|
1207
|
+
|
|
1208
|
+
if (
|
|
1209
|
+
user != null &&
|
|
1210
|
+
!isCurrentUser &&
|
|
1211
|
+
(abilities.includes('tilmeld/switch') ||
|
|
1212
|
+
abilities.includes('system/admin'))
|
|
1213
|
+
) {
|
|
1214
|
+
// Users with this ability can switch to other users.
|
|
1215
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
public $updateGroupDataProtection() {
|
|
1220
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
1221
|
+
this.$data.group.$updateDataProtection();
|
|
1222
|
+
}
|
|
1223
|
+
for (let group of this.$data.groups ?? []) {
|
|
1224
|
+
if (!group.$asleep()) {
|
|
1225
|
+
group.$updateDataProtection();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
748
1228
|
}
|
|
749
1229
|
|
|
750
1230
|
/**
|
|
@@ -761,35 +1241,43 @@ export default class User extends AbleObject<UserData> {
|
|
|
761
1241
|
* @param ability The ability.
|
|
762
1242
|
* @returns True or false.
|
|
763
1243
|
*/
|
|
764
|
-
public $gatekeeper(ability?: string): boolean {
|
|
765
|
-
|
|
1244
|
+
public async $gatekeeper(ability?: string): Promise<boolean> {
|
|
1245
|
+
await this.$wake();
|
|
1246
|
+
const tilmeld = enforceTilmeld(this);
|
|
766
1247
|
if (ability == null) {
|
|
767
1248
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
768
1249
|
}
|
|
769
1250
|
// Check the cache to see if we've already checked this user.
|
|
770
1251
|
if (this.$gatekeeperCache == null) {
|
|
771
|
-
|
|
772
|
-
if (this.$data.inheritAbilities) {
|
|
773
|
-
for (let curGroup of this.$data.groups ?? []) {
|
|
774
|
-
if (curGroup.enabled) {
|
|
775
|
-
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
if (
|
|
779
|
-
this.$data.group != null &&
|
|
780
|
-
this.$data.group.cdate != null &&
|
|
781
|
-
this.$data.group.enabled
|
|
782
|
-
) {
|
|
783
|
-
abilities = abilities.concat(this.$data.group.abilities ?? []);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
this.$gatekeeperCache = Object.fromEntries(
|
|
787
|
-
abilities.map((ability) => [ability, true])
|
|
788
|
-
);
|
|
1252
|
+
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
789
1253
|
}
|
|
790
1254
|
return (
|
|
791
1255
|
(ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
792
|
-
!!this.$
|
|
1256
|
+
!!this.$data.abilities?.includes('system/admin')
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Build a gatekeeper cache object.
|
|
1262
|
+
*/
|
|
1263
|
+
public async $getGatekeeperCache() {
|
|
1264
|
+
await this.$wake();
|
|
1265
|
+
let abilities = this.$data.abilities ?? [];
|
|
1266
|
+
if (this.$data.inheritAbilities) {
|
|
1267
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1268
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
1269
|
+
if (curGroup.enabled) {
|
|
1270
|
+
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
await this.$data.group?.$wake();
|
|
1274
|
+
const group = this.$data.group;
|
|
1275
|
+
if (group != null && group.cdate != null && group.enabled) {
|
|
1276
|
+
abilities = abilities.concat(group.abilities ?? []);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return Object.fromEntries(
|
|
1280
|
+
abilities.map((ability) => [ability, true as true]),
|
|
793
1281
|
);
|
|
794
1282
|
}
|
|
795
1283
|
|
|
@@ -805,8 +1293,8 @@ export default class User extends AbleObject<UserData> {
|
|
|
805
1293
|
*
|
|
806
1294
|
* @returns True on success, false on failure.
|
|
807
1295
|
*/
|
|
808
|
-
public async $sendEmailVerification() {
|
|
809
|
-
const tilmeld = this
|
|
1296
|
+
public async $sendEmailVerification(): Promise<boolean> {
|
|
1297
|
+
const tilmeld = enforceTilmeld(this);
|
|
810
1298
|
if (this.guid == null) {
|
|
811
1299
|
return false;
|
|
812
1300
|
}
|
|
@@ -818,7 +1306,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
818
1306
|
|
|
819
1307
|
if (this.$data.secret != null) {
|
|
820
1308
|
const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(
|
|
821
|
-
this.guid
|
|
1309
|
+
this.guid,
|
|
822
1310
|
)}&secret=${encodeURIComponent(this.$data.secret)}`;
|
|
823
1311
|
success =
|
|
824
1312
|
success &&
|
|
@@ -832,17 +1320,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
832
1320
|
address: this.$data.email ?? '',
|
|
833
1321
|
},
|
|
834
1322
|
},
|
|
835
|
-
locals: {
|
|
836
|
-
verifyLink: link,
|
|
837
|
-
},
|
|
1323
|
+
locals: { verifyLink: link },
|
|
838
1324
|
},
|
|
839
|
-
this
|
|
1325
|
+
this,
|
|
840
1326
|
));
|
|
841
1327
|
}
|
|
842
1328
|
|
|
843
1329
|
if (this.$data.newEmailSecret != null) {
|
|
844
1330
|
const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(
|
|
845
|
-
this.guid
|
|
1331
|
+
this.guid,
|
|
846
1332
|
)}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
|
|
847
1333
|
success =
|
|
848
1334
|
success &&
|
|
@@ -862,13 +1348,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
862
1348
|
newEmail: this.$data.newEmailAddress,
|
|
863
1349
|
},
|
|
864
1350
|
},
|
|
865
|
-
this
|
|
1351
|
+
this,
|
|
866
1352
|
));
|
|
867
1353
|
}
|
|
868
1354
|
|
|
869
1355
|
if (this.$data.cancelEmailSecret != null) {
|
|
870
1356
|
const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(
|
|
871
|
-
this.guid
|
|
1357
|
+
this.guid,
|
|
872
1358
|
)}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
|
|
873
1359
|
success =
|
|
874
1360
|
success &&
|
|
@@ -888,7 +1374,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
888
1374
|
newEmail: this.$data.newEmailAddress ?? this.$data.email,
|
|
889
1375
|
},
|
|
890
1376
|
},
|
|
891
|
-
this
|
|
1377
|
+
this,
|
|
892
1378
|
));
|
|
893
1379
|
}
|
|
894
1380
|
|
|
@@ -901,13 +1387,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
901
1387
|
* @param group The group.
|
|
902
1388
|
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
903
1389
|
*/
|
|
904
|
-
public $addGroup(group:
|
|
905
|
-
|
|
906
|
-
|
|
1390
|
+
public async $addGroup(group: GroupClass & GroupData) {
|
|
1391
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1392
|
+
let groups = this.$data.groups;
|
|
1393
|
+
if (groups == null) {
|
|
1394
|
+
groups = [];
|
|
907
1395
|
}
|
|
908
|
-
if (!group.$inArray(
|
|
909
|
-
|
|
910
|
-
|
|
1396
|
+
if (!group.$inArray(groups)) {
|
|
1397
|
+
groups.push(group);
|
|
1398
|
+
this.$data.groups = groups;
|
|
1399
|
+
return groups;
|
|
911
1400
|
}
|
|
912
1401
|
return true;
|
|
913
1402
|
}
|
|
@@ -919,17 +1408,23 @@ export default class User extends AbleObject<UserData> {
|
|
|
919
1408
|
* @returns True if the passwords match, otherwise false.
|
|
920
1409
|
*/
|
|
921
1410
|
public $checkPassword(password: string) {
|
|
922
|
-
const tilmeld = this
|
|
1411
|
+
const tilmeld = enforceTilmeld(this);
|
|
923
1412
|
switch (tilmeld.config.pwMethod) {
|
|
924
1413
|
case 'plain':
|
|
925
1414
|
return this.$data.password === password;
|
|
926
1415
|
case 'digest':
|
|
927
|
-
return
|
|
1416
|
+
return (
|
|
1417
|
+
this.$data.password ===
|
|
1418
|
+
crypto.createHash('sha256').update(password).digest('base64')
|
|
1419
|
+
);
|
|
928
1420
|
case 'salt':
|
|
929
1421
|
default:
|
|
930
1422
|
return (
|
|
931
1423
|
this.$data.password ===
|
|
932
|
-
|
|
1424
|
+
crypto
|
|
1425
|
+
.createHash('sha256')
|
|
1426
|
+
.update(password + this.$data.salt)
|
|
1427
|
+
.digest('base64')
|
|
933
1428
|
);
|
|
934
1429
|
}
|
|
935
1430
|
}
|
|
@@ -940,10 +1435,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
940
1435
|
* @param group The group.
|
|
941
1436
|
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
942
1437
|
*/
|
|
943
|
-
public $delGroup(group:
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1438
|
+
public async $delGroup(group: GroupClass & GroupData) {
|
|
1439
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1440
|
+
let groups = this.$data.groups || [];
|
|
1441
|
+
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
1442
|
+
const newGroups: (GroupClass & GroupData)[] = [];
|
|
1443
|
+
for (let curGroup of groups) {
|
|
947
1444
|
if (!group.$is(curGroup)) {
|
|
948
1445
|
newGroups.push(curGroup);
|
|
949
1446
|
}
|
|
@@ -960,7 +1457,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
960
1457
|
* @param group The group, or the group's GUID.
|
|
961
1458
|
* @returns True or false.
|
|
962
1459
|
*/
|
|
963
|
-
public $inGroup(group: (
|
|
1460
|
+
public async $inGroup(group: (GroupClass & GroupData) | string) {
|
|
964
1461
|
if (typeof group === 'string') {
|
|
965
1462
|
if (this.$data.group?.guid === group) {
|
|
966
1463
|
return true;
|
|
@@ -983,16 +1480,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
983
1480
|
* @param group The group, or the group's GUID.
|
|
984
1481
|
* @returns True or false.
|
|
985
1482
|
*/
|
|
986
|
-
public $isDescendant(group: (
|
|
1483
|
+
public async $isDescendant(group: (GroupClass & GroupData) | string) {
|
|
987
1484
|
// Check to see if the user is in a descendant group of the given group.
|
|
1485
|
+
await this.$data.group?.$wake();
|
|
988
1486
|
if (
|
|
1487
|
+
this.$data.group != null &&
|
|
989
1488
|
this.$data.group?.cdate != null &&
|
|
990
|
-
this.$data.group
|
|
1489
|
+
(await this.$data.group?.$isDescendant(group))
|
|
991
1490
|
) {
|
|
992
1491
|
return true;
|
|
993
1492
|
}
|
|
1493
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
994
1494
|
for (let curGroup of this.$data.groups ?? []) {
|
|
995
|
-
if (curGroup.$isDescendant(group)) {
|
|
1495
|
+
if (await curGroup.$isDescendant(group)) {
|
|
996
1496
|
return true;
|
|
997
1497
|
}
|
|
998
1498
|
}
|
|
@@ -1008,17 +1508,34 @@ export default class User extends AbleObject<UserData> {
|
|
|
1008
1508
|
public async $changePassword(data: {
|
|
1009
1509
|
newPassword: string;
|
|
1010
1510
|
currentPassword: string;
|
|
1511
|
+
revokeCurrentTokens?: boolean;
|
|
1011
1512
|
}): Promise<{ result: boolean; message: string }> {
|
|
1513
|
+
this.$nymph.config.debugLog(
|
|
1514
|
+
'tilmeld',
|
|
1515
|
+
`Password change request for "${this.$data.username}".`,
|
|
1516
|
+
);
|
|
1012
1517
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
1013
1518
|
return { result: false, message: 'Please specify a password.' };
|
|
1014
1519
|
}
|
|
1015
1520
|
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
1521
|
+
this.$nymph.config.debugLog(
|
|
1522
|
+
'tilmeld',
|
|
1523
|
+
`Incorrect password for change request for "${this.$data.username}".`,
|
|
1524
|
+
);
|
|
1016
1525
|
return { result: false, message: 'Incorrect password.' };
|
|
1017
1526
|
}
|
|
1018
1527
|
this.$data.passwordTemp = data.newPassword;
|
|
1019
|
-
if (
|
|
1528
|
+
if (data.revokeCurrentTokens) {
|
|
1529
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1530
|
+
}
|
|
1531
|
+
try {
|
|
1532
|
+
await this.$save();
|
|
1533
|
+
if (data.revokeCurrentTokens) {
|
|
1534
|
+
const tilmeld = enforceTilmeld(this);
|
|
1535
|
+
await tilmeld.login(this, true);
|
|
1536
|
+
}
|
|
1020
1537
|
return { result: true, message: 'Your password has been changed.' };
|
|
1021
|
-
}
|
|
1538
|
+
} catch (e: any) {
|
|
1022
1539
|
return { result: false, message: "Couldn't save new password." };
|
|
1023
1540
|
}
|
|
1024
1541
|
}
|
|
@@ -1030,7 +1547,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1030
1547
|
* @returns The resulting password or hash which is stored in the entity.
|
|
1031
1548
|
*/
|
|
1032
1549
|
public $password(password: string) {
|
|
1033
|
-
const tilmeld = this
|
|
1550
|
+
const tilmeld = enforceTilmeld(this);
|
|
1034
1551
|
switch (tilmeld.config.pwMethod) {
|
|
1035
1552
|
case 'plain':
|
|
1036
1553
|
delete this.$data.salt;
|
|
@@ -1038,34 +1555,275 @@ export default class User extends AbleObject<UserData> {
|
|
|
1038
1555
|
break;
|
|
1039
1556
|
case 'digest':
|
|
1040
1557
|
delete this.$data.salt;
|
|
1041
|
-
this.$data.password =
|
|
1558
|
+
this.$data.password = crypto
|
|
1559
|
+
.createHash('sha256')
|
|
1560
|
+
.update(password)
|
|
1561
|
+
.digest('base64');
|
|
1042
1562
|
break;
|
|
1043
1563
|
case 'salt':
|
|
1044
1564
|
default:
|
|
1045
1565
|
this.$data.salt = nanoid();
|
|
1046
|
-
this.$data.password =
|
|
1047
|
-
|
|
1048
|
-
|
|
1566
|
+
this.$data.password = crypto
|
|
1567
|
+
.createHash('sha256')
|
|
1568
|
+
.update(password + this.$data.salt)
|
|
1569
|
+
.digest('base64');
|
|
1049
1570
|
break;
|
|
1050
1571
|
}
|
|
1051
1572
|
return this.$data.password;
|
|
1052
1573
|
}
|
|
1053
1574
|
|
|
1575
|
+
/**
|
|
1576
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
1577
|
+
*
|
|
1578
|
+
* @param data The input data from the client.
|
|
1579
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1580
|
+
*/
|
|
1581
|
+
public async $revokeCurrentTokens(data: {
|
|
1582
|
+
password: string;
|
|
1583
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1584
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1585
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1589
|
+
|
|
1590
|
+
try {
|
|
1591
|
+
await this.$save();
|
|
1592
|
+
const tilmeld = enforceTilmeld(this);
|
|
1593
|
+
await tilmeld.login(this, true);
|
|
1594
|
+
return {
|
|
1595
|
+
result: true,
|
|
1596
|
+
message: 'You have logged out of all other sessions.',
|
|
1597
|
+
};
|
|
1598
|
+
} catch (e: any) {
|
|
1599
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
public $hasTOTPSecret(): boolean {
|
|
1604
|
+
return this.$data.totpSecret != null;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* Check the given code against the user's TOTP secret.
|
|
1609
|
+
*
|
|
1610
|
+
* @param code The code in question.
|
|
1611
|
+
* @returns True if the code is valid, otherwise false.
|
|
1612
|
+
*/
|
|
1613
|
+
public $checkTOTPCode(code: string) {
|
|
1614
|
+
if (this.$data.totpSecret == null) {
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
const tilmeld = enforceTilmeld(this);
|
|
1619
|
+
|
|
1620
|
+
const totp = new TOTP({
|
|
1621
|
+
issuer: tilmeld.config.appName,
|
|
1622
|
+
label: this.$data.username,
|
|
1623
|
+
algorithm: 'SHA1',
|
|
1624
|
+
digits: 6,
|
|
1625
|
+
period: 30,
|
|
1626
|
+
secret: this.$data.totpSecret,
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
1634
|
+
*
|
|
1635
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
1636
|
+
*/
|
|
1637
|
+
public async $getNewTOTPSecret(): Promise<{
|
|
1638
|
+
uri: string;
|
|
1639
|
+
qrcode: string;
|
|
1640
|
+
secret: string;
|
|
1641
|
+
}> {
|
|
1642
|
+
const tilmeld = enforceTilmeld(this);
|
|
1643
|
+
|
|
1644
|
+
if (this.$data.totpSecret != null) {
|
|
1645
|
+
throw new BadDataError('You already have a 2FA secret.');
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
const secret = new Secret();
|
|
1649
|
+
|
|
1650
|
+
const totp = new TOTP({
|
|
1651
|
+
issuer: tilmeld.config.appName,
|
|
1652
|
+
label: this.$data.username,
|
|
1653
|
+
algorithm: 'SHA1',
|
|
1654
|
+
digits: 6,
|
|
1655
|
+
period: 30,
|
|
1656
|
+
secret,
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
const uri = totp.toString();
|
|
1660
|
+
|
|
1661
|
+
const qrcode = await toDataURL(uri);
|
|
1662
|
+
|
|
1663
|
+
return { uri, qrcode, secret: secret.base32 };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1668
|
+
*
|
|
1669
|
+
* @param data The input data from the client.
|
|
1670
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1671
|
+
*/
|
|
1672
|
+
public async $saveTOTPSecret(data: {
|
|
1673
|
+
password: string;
|
|
1674
|
+
secret: string;
|
|
1675
|
+
code: string;
|
|
1676
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1677
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1678
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (this.$data.totpSecret != null) {
|
|
1682
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (
|
|
1686
|
+
data.secret == null ||
|
|
1687
|
+
typeof data.secret !== 'string' ||
|
|
1688
|
+
data.secret === ''
|
|
1689
|
+
) {
|
|
1690
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (
|
|
1694
|
+
data.code == null ||
|
|
1695
|
+
typeof data.code !== 'string' ||
|
|
1696
|
+
data.code.length !== 6
|
|
1697
|
+
) {
|
|
1698
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
const tilmeld = enforceTilmeld(this);
|
|
1702
|
+
|
|
1703
|
+
const totp = new TOTP({
|
|
1704
|
+
issuer: tilmeld.config.appName,
|
|
1705
|
+
label: this.$data.username,
|
|
1706
|
+
algorithm: 'SHA1',
|
|
1707
|
+
digits: 6,
|
|
1708
|
+
period: 30,
|
|
1709
|
+
secret: data.secret,
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1713
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
this.$data.totpSecret = data.secret;
|
|
1717
|
+
|
|
1718
|
+
try {
|
|
1719
|
+
await this.$save();
|
|
1720
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1721
|
+
} catch (e: any) {
|
|
1722
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1728
|
+
* account.
|
|
1729
|
+
*
|
|
1730
|
+
* @param data The input data from the client.
|
|
1731
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1732
|
+
*/
|
|
1733
|
+
public async $removeTOTPSecret(data?: {
|
|
1734
|
+
password: string;
|
|
1735
|
+
code: string;
|
|
1736
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1737
|
+
const tilmeld = enforceTilmeld(this);
|
|
1738
|
+
|
|
1739
|
+
if (data) {
|
|
1740
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1741
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
if (
|
|
1745
|
+
data.code == null ||
|
|
1746
|
+
typeof data.code !== 'string' ||
|
|
1747
|
+
data.code.length !== 6
|
|
1748
|
+
) {
|
|
1749
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
if (this.$data.totpSecret == null) {
|
|
1753
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const totp = new TOTP({
|
|
1757
|
+
issuer: tilmeld.config.appName,
|
|
1758
|
+
label: this.$data.username,
|
|
1759
|
+
algorithm: 'SHA1',
|
|
1760
|
+
digits: 6,
|
|
1761
|
+
period: 30,
|
|
1762
|
+
secret: this.$data.totpSecret,
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1766
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1767
|
+
}
|
|
1768
|
+
} else if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1769
|
+
return {
|
|
1770
|
+
result: false,
|
|
1771
|
+
message: 'You must provide your password and 2FA code.',
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
delete this.$data.totpSecret;
|
|
1776
|
+
|
|
1777
|
+
try {
|
|
1778
|
+
await this.$save();
|
|
1779
|
+
return { result: true, message: 'Two factor secret has been removed.' };
|
|
1780
|
+
} catch (e: any) {
|
|
1781
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1054
1785
|
/**
|
|
1055
1786
|
* Check that a username is valid.
|
|
1056
1787
|
*
|
|
1057
1788
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1058
1789
|
*/
|
|
1059
1790
|
public async $checkUsername(): Promise<{ result: boolean; message: string }> {
|
|
1060
|
-
const tilmeld = this
|
|
1791
|
+
const tilmeld = enforceTilmeld(this);
|
|
1792
|
+
|
|
1793
|
+
this.$nymph.config.debugInfo(
|
|
1794
|
+
'tilmeld',
|
|
1795
|
+
`Username check "${this.$data.username}".`,
|
|
1796
|
+
);
|
|
1797
|
+
|
|
1061
1798
|
if (!tilmeld.config.emailUsernames) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1799
|
+
let [username, domain] = tilmeld.config.domainSupport
|
|
1800
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1801
|
+
: [this.$data.username, null];
|
|
1802
|
+
|
|
1803
|
+
// Lowercase the domain part.
|
|
1804
|
+
if (domain) {
|
|
1805
|
+
domain = domain.toLowerCase();
|
|
1806
|
+
this.$data.username = `${username}@${domain}`;
|
|
1064
1807
|
}
|
|
1808
|
+
|
|
1065
1809
|
if (
|
|
1066
|
-
|
|
1067
|
-
this.$data.username.length
|
|
1810
|
+
this.$data.username == null ||
|
|
1811
|
+
!this.$data.username.length ||
|
|
1812
|
+
username == null ||
|
|
1813
|
+
!username.length
|
|
1068
1814
|
) {
|
|
1815
|
+
return { result: false, message: 'Please specify a username.' };
|
|
1816
|
+
}
|
|
1817
|
+
if (username.length < tilmeld.config.minUsernameLength) {
|
|
1818
|
+
return {
|
|
1819
|
+
result: false,
|
|
1820
|
+
message:
|
|
1821
|
+
'Usernames must be at least ' +
|
|
1822
|
+
tilmeld.config.minUsernameLength +
|
|
1823
|
+
' characters.',
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
if (username.length > tilmeld.config.maxUsernameLength) {
|
|
1069
1827
|
return {
|
|
1070
1828
|
result: false,
|
|
1071
1829
|
message:
|
|
@@ -1076,21 +1834,32 @@ export default class User extends AbleObject<UserData> {
|
|
|
1076
1834
|
}
|
|
1077
1835
|
|
|
1078
1836
|
if (
|
|
1079
|
-
difference(
|
|
1080
|
-
|
|
1081
|
-
tilmeld.config.validChars.split('')
|
|
1082
|
-
).length
|
|
1837
|
+
difference(username.split(''), tilmeld.config.validChars.split(''))
|
|
1838
|
+
.length
|
|
1083
1839
|
) {
|
|
1084
|
-
return {
|
|
1085
|
-
result: false,
|
|
1086
|
-
message: tilmeld.config.validCharsNotice,
|
|
1087
|
-
};
|
|
1840
|
+
return { result: false, message: tilmeld.config.validCharsNotice };
|
|
1088
1841
|
}
|
|
1089
|
-
if (!tilmeld.config.validRegex.test(
|
|
1090
|
-
return {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1842
|
+
if (!tilmeld.config.validRegex.test(username)) {
|
|
1843
|
+
return { result: false, message: tilmeld.config.validRegexNotice };
|
|
1844
|
+
}
|
|
1845
|
+
if (domain != null) {
|
|
1846
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
1847
|
+
return {
|
|
1848
|
+
result: false,
|
|
1849
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
if (
|
|
1853
|
+
!this.$skipAcWhenSaving &&
|
|
1854
|
+
!this.$is(tilmeld.currentUser) &&
|
|
1855
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
1856
|
+
) {
|
|
1857
|
+
return {
|
|
1858
|
+
result: false,
|
|
1859
|
+
message:
|
|
1860
|
+
"You don't have the authority to manage users on this domain.",
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1094
1863
|
}
|
|
1095
1864
|
|
|
1096
1865
|
const selector: Selector = {
|
|
@@ -1105,12 +1874,24 @@ export default class User extends AbleObject<UserData> {
|
|
|
1105
1874
|
}
|
|
1106
1875
|
const test = await this.$nymph.getEntity(
|
|
1107
1876
|
{ class: tilmeld.User, skipAc: true },
|
|
1108
|
-
selector
|
|
1877
|
+
selector,
|
|
1109
1878
|
);
|
|
1110
1879
|
if (test != null) {
|
|
1111
1880
|
return { result: false, message: 'That username is taken.' };
|
|
1112
1881
|
}
|
|
1113
1882
|
|
|
1883
|
+
for (let callback of (this.constructor as typeof User)
|
|
1884
|
+
.checkUsernameCallbacks) {
|
|
1885
|
+
if (callback) {
|
|
1886
|
+
const result = await callback(this, {
|
|
1887
|
+
username: this.$data.username,
|
|
1888
|
+
});
|
|
1889
|
+
if (!result.result) {
|
|
1890
|
+
return { message: 'Username is not available.', ...result };
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1114
1895
|
return {
|
|
1115
1896
|
result: true,
|
|
1116
1897
|
message:
|
|
@@ -1121,10 +1902,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
1121
1902
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
1122
1903
|
return { result: false, message: 'Please specify an email.' };
|
|
1123
1904
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1905
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1906
|
+
return {
|
|
1907
|
+
result: false,
|
|
1908
|
+
message:
|
|
1909
|
+
'Emails must be at least ' +
|
|
1910
|
+
tilmeld.config.minUsernameLength +
|
|
1911
|
+
' characters.',
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1128
1915
|
return {
|
|
1129
1916
|
result: false,
|
|
1130
1917
|
message:
|
|
@@ -1144,7 +1931,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1144
1931
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1145
1932
|
*/
|
|
1146
1933
|
public async $checkEmail(): Promise<{ result: boolean; message: string }> {
|
|
1147
|
-
const tilmeld = this
|
|
1934
|
+
const tilmeld = enforceTilmeld(this);
|
|
1935
|
+
|
|
1936
|
+
this.$nymph.config.debugInfo(
|
|
1937
|
+
'tilmeld',
|
|
1938
|
+
`Email check "${this.$data.email}".`,
|
|
1939
|
+
);
|
|
1940
|
+
|
|
1148
1941
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
1149
1942
|
if (tilmeld.config.verifyEmail) {
|
|
1150
1943
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -1153,10 +1946,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1153
1946
|
}
|
|
1154
1947
|
}
|
|
1155
1948
|
if (!tilmeld.config.validEmailRegex.test(this.$data.email)) {
|
|
1156
|
-
return {
|
|
1157
|
-
result: false,
|
|
1158
|
-
message: tilmeld.config.validEmailRegexNotice,
|
|
1159
|
-
};
|
|
1949
|
+
return { result: false, message: tilmeld.config.validEmailRegexNotice };
|
|
1160
1950
|
}
|
|
1161
1951
|
const selector: Selector = {
|
|
1162
1952
|
type: '&',
|
|
@@ -1167,7 +1957,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1167
1957
|
}
|
|
1168
1958
|
const test = await this.$nymph.getEntity(
|
|
1169
1959
|
{ class: tilmeld.User, skipAc: true },
|
|
1170
|
-
selector
|
|
1960
|
+
selector,
|
|
1171
1961
|
);
|
|
1172
1962
|
if (test != null) {
|
|
1173
1963
|
return {
|
|
@@ -1189,7 +1979,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1189
1979
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1190
1980
|
*/
|
|
1191
1981
|
public async $checkPhone(): Promise<{ result: boolean; message: string }> {
|
|
1192
|
-
const tilmeld = this
|
|
1982
|
+
const tilmeld = enforceTilmeld(this);
|
|
1983
|
+
|
|
1984
|
+
this.$nymph.config.debugInfo(
|
|
1985
|
+
'tilmeld',
|
|
1986
|
+
`Phone check "${this.$data.phone}".`,
|
|
1987
|
+
);
|
|
1988
|
+
|
|
1193
1989
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
1194
1990
|
return { result: false, message: 'Please specify a phone number.' };
|
|
1195
1991
|
}
|
|
@@ -1211,7 +2007,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1211
2007
|
}
|
|
1212
2008
|
const test = await this.$nymph.getEntity(
|
|
1213
2009
|
{ class: tilmeld.User, skipAc: true },
|
|
1214
|
-
selector
|
|
2010
|
+
selector,
|
|
1215
2011
|
);
|
|
1216
2012
|
if (test != null) {
|
|
1217
2013
|
return { result: false, message: 'Phone number is in use.' };
|
|
@@ -1228,7 +2024,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1228
2024
|
password: string;
|
|
1229
2025
|
additionalData?: { [k: string]: any };
|
|
1230
2026
|
}): Promise<{ result: boolean; loggedin: boolean; message: string }> {
|
|
1231
|
-
let tilmeld = this
|
|
2027
|
+
let tilmeld = enforceTilmeld(this);
|
|
1232
2028
|
if (!tilmeld.config.allowRegistration) {
|
|
1233
2029
|
return {
|
|
1234
2030
|
result: false,
|
|
@@ -1244,26 +2040,11 @@ export default class User extends AbleObject<UserData> {
|
|
|
1244
2040
|
};
|
|
1245
2041
|
}
|
|
1246
2042
|
|
|
1247
|
-
|
|
1248
|
-
for (let callback of (this.constructor as typeof User)
|
|
1249
|
-
.beforeRegisterCallbacks) {
|
|
1250
|
-
if (callback) {
|
|
1251
|
-
await callback(this, data);
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
} catch (e: any) {
|
|
2043
|
+
if (!('password' in data) || !data.password.length) {
|
|
1255
2044
|
return {
|
|
1256
2045
|
result: false,
|
|
1257
2046
|
loggedin: false,
|
|
1258
|
-
message:
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
if (!('password' in data) || !data.password.length) {
|
|
1263
|
-
return {
|
|
1264
|
-
result: false,
|
|
1265
|
-
loggedin: false,
|
|
1266
|
-
message: 'Password is a required field.',
|
|
2047
|
+
message: 'Password is a required field.',
|
|
1267
2048
|
};
|
|
1268
2049
|
}
|
|
1269
2050
|
const unCheck = await this.$checkUsername();
|
|
@@ -1272,7 +2053,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1272
2053
|
}
|
|
1273
2054
|
|
|
1274
2055
|
this.$password(data.password);
|
|
1275
|
-
if (tilmeld.config.regFields.
|
|
2056
|
+
if (tilmeld.config.regFields.includes('name')) {
|
|
1276
2057
|
this.$data.name =
|
|
1277
2058
|
this.$data.nameFirst +
|
|
1278
2059
|
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
@@ -1285,231 +2066,229 @@ export default class User extends AbleObject<UserData> {
|
|
|
1285
2066
|
this.$data.email = this.$data.username;
|
|
1286
2067
|
}
|
|
1287
2068
|
|
|
1288
|
-
// Start transaction.
|
|
1289
|
-
const transaction = 'tilmeld-register-' + nanoid();
|
|
1290
|
-
const nymph = this.$nymph;
|
|
1291
|
-
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1292
|
-
this.$nymph = tnymph;
|
|
1293
|
-
tilmeld = this.$nymph.tilmeld as Tilmeld;
|
|
1294
|
-
|
|
1295
2069
|
try {
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
);
|
|
1312
|
-
if (parent != null) {
|
|
1313
|
-
generatedPrimaryGroup.parent = parent;
|
|
1314
|
-
}
|
|
1315
|
-
if (!(await generatedPrimaryGroup.$saveSkipAC())) {
|
|
1316
|
-
await tnymph.rollback(transaction);
|
|
1317
|
-
this.$nymph = nymph;
|
|
1318
|
-
return {
|
|
1319
|
-
result: false,
|
|
1320
|
-
loggedin: false,
|
|
1321
|
-
message: 'Error creating primary group for user.',
|
|
1322
|
-
};
|
|
1323
|
-
}
|
|
1324
|
-
this.$data.group = generatedPrimaryGroup;
|
|
1325
|
-
} else {
|
|
1326
|
-
// Add the default primary.
|
|
1327
|
-
const group = await tnymph.getEntity(
|
|
1328
|
-
{ class: tilmeld.Group },
|
|
1329
|
-
{
|
|
1330
|
-
type: '&',
|
|
1331
|
-
equal: ['defaultPrimary', true],
|
|
2070
|
+
const result = await transaction(
|
|
2071
|
+
this.$nymph,
|
|
2072
|
+
'tilmeld-register-' + nanoid(),
|
|
2073
|
+
async (nymph) => {
|
|
2074
|
+
this.$setNymph(nymph);
|
|
2075
|
+
const tilmeld = enforceTilmeld(this);
|
|
2076
|
+
|
|
2077
|
+
let message = '';
|
|
2078
|
+
let loggedin = false;
|
|
2079
|
+
|
|
2080
|
+
for (let callback of (this.constructor as typeof User)
|
|
2081
|
+
.beforeRegisterCallbacks) {
|
|
2082
|
+
if (callback) {
|
|
2083
|
+
await callback(this, data);
|
|
2084
|
+
}
|
|
1332
2085
|
}
|
|
1333
|
-
);
|
|
1334
|
-
if (group != null) {
|
|
1335
|
-
this.$data.group = group;
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
2086
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
} else {
|
|
1350
|
-
// Add the default secondaries.
|
|
1351
|
-
this.$data.groups = await tnymph.getEntities(
|
|
1352
|
-
{ class: tilmeld.Group },
|
|
1353
|
-
{
|
|
1354
|
-
type: '&',
|
|
1355
|
-
equal: ['defaultSecondary', true],
|
|
2087
|
+
if (
|
|
2088
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2089
|
+
tilmeld.config.verifyEmail
|
|
2090
|
+
) {
|
|
2091
|
+
// The user will be enabled after verifying their e-mail address.
|
|
2092
|
+
if (!tilmeld.config.unverifiedAccess) {
|
|
2093
|
+
this.$data.enabled = false;
|
|
2094
|
+
}
|
|
2095
|
+
} else {
|
|
2096
|
+
this.$data.enabled = true;
|
|
1356
2097
|
}
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
2098
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
if (!otherUsers.length) {
|
|
1381
|
-
this.$grant('system/admin');
|
|
1382
|
-
this.$data.enabled = true;
|
|
1383
|
-
madeAdmin = true;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
2099
|
+
// If createAdmin is true and there are no other users, grant
|
|
2100
|
+
// "system/admin".
|
|
2101
|
+
let madeAdmin = false;
|
|
2102
|
+
if (tilmeld.config.createAdmin) {
|
|
2103
|
+
const otherUsers = await this.$nymph.getEntities({
|
|
2104
|
+
class: tilmeld.User,
|
|
2105
|
+
skipAc: true,
|
|
2106
|
+
limit: 1,
|
|
2107
|
+
return: 'guid',
|
|
2108
|
+
});
|
|
2109
|
+
// Make sure it's not just null, cause that means an error.
|
|
2110
|
+
if (otherUsers == null) {
|
|
2111
|
+
throw new Error('An error occurred.');
|
|
2112
|
+
}
|
|
2113
|
+
if (!otherUsers.length) {
|
|
2114
|
+
this.$grant('system/admin');
|
|
2115
|
+
this.$data.enabled = true;
|
|
2116
|
+
madeAdmin = true;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
1386
2119
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
await tilmeld.config.sendEmail(
|
|
1391
|
-
tilmeld,
|
|
1392
|
-
{
|
|
1393
|
-
template: 'UserRegistered',
|
|
1394
|
-
message: {
|
|
1395
|
-
to: tilmeld.config.userRegisteredRecipient,
|
|
1396
|
-
},
|
|
1397
|
-
locals: {
|
|
1398
|
-
userUsername: this.$data.username,
|
|
1399
|
-
userName: this.$data.name,
|
|
1400
|
-
userFirstName: this.$data.nameFirst,
|
|
1401
|
-
userLastName: this.$data.nameLast,
|
|
1402
|
-
userEmail: this.$data.email,
|
|
1403
|
-
userPhone: this.$data.phone,
|
|
1404
|
-
},
|
|
1405
|
-
},
|
|
1406
|
-
this
|
|
2120
|
+
this.$nymph.config.debugLog(
|
|
2121
|
+
'tilmeld',
|
|
2122
|
+
`Registering new user "${this.$data.username}".`,
|
|
1407
2123
|
);
|
|
1408
|
-
}
|
|
1409
2124
|
|
|
1410
|
-
|
|
1411
|
-
let loggedin = false;
|
|
1412
|
-
|
|
1413
|
-
// Save the primary group.
|
|
1414
|
-
if (generatedPrimaryGroup != null) {
|
|
1415
|
-
generatedPrimaryGroup.user = this;
|
|
1416
|
-
if (!(await generatedPrimaryGroup.$saveSkipAC())) {
|
|
1417
|
-
await tnymph.rollback(transaction);
|
|
1418
|
-
this.$nymph = nymph;
|
|
1419
|
-
return {
|
|
1420
|
-
result: false,
|
|
1421
|
-
loggedin: false,
|
|
1422
|
-
message:
|
|
1423
|
-
'Your primary group could not be assigned. Please try again later.',
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
2125
|
+
await this.$saveSkipAC();
|
|
1427
2126
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
2127
|
+
// Finish up.
|
|
2128
|
+
if (
|
|
2129
|
+
tilmeld.config.verifyEmail &&
|
|
2130
|
+
!tilmeld.config.unverifiedAccess &&
|
|
2131
|
+
!madeAdmin &&
|
|
2132
|
+
tilmeld.config.userFields.includes('email')
|
|
2133
|
+
) {
|
|
2134
|
+
message +=
|
|
2135
|
+
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
2136
|
+
'with a verification link for you to finish registration.';
|
|
2137
|
+
} else if (
|
|
2138
|
+
tilmeld.config.verifyEmail &&
|
|
2139
|
+
tilmeld.config.unverifiedAccess &&
|
|
2140
|
+
!madeAdmin &&
|
|
2141
|
+
tilmeld.config.userFields.includes('email')
|
|
2142
|
+
) {
|
|
2143
|
+
if (!(await tilmeld.login(this, true))) {
|
|
2144
|
+
throw new Error('An error occurred trying to log you in.');
|
|
2145
|
+
}
|
|
2146
|
+
message +=
|
|
2147
|
+
"You're now logged in! An email has been sent to " +
|
|
2148
|
+
`${this.$data.email} with a verification link for you to finish ` +
|
|
2149
|
+
'registration.';
|
|
2150
|
+
loggedin = true;
|
|
2151
|
+
} else {
|
|
2152
|
+
if (!(await tilmeld.login(this, true))) {
|
|
2153
|
+
throw new Error('An error occurred trying to log you in.');
|
|
2154
|
+
}
|
|
2155
|
+
message += "You're now registered and logged in!";
|
|
2156
|
+
loggedin = true;
|
|
1444
2157
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (!tilmeld.login(this, true)) {
|
|
1452
|
-
throw new Error('An error occurred trying to log you in.');
|
|
2158
|
+
|
|
2159
|
+
for (let callback of (this.constructor as typeof User)
|
|
2160
|
+
.afterRegisterCallbacks) {
|
|
2161
|
+
if (callback) {
|
|
2162
|
+
await callback(this, { loggedin, message });
|
|
2163
|
+
}
|
|
1453
2164
|
}
|
|
1454
|
-
message += "You're now registered and logged in!";
|
|
1455
|
-
loggedin = true;
|
|
1456
|
-
}
|
|
1457
2165
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
2166
|
+
return { result: true, loggedin, message };
|
|
2167
|
+
},
|
|
2168
|
+
async (nymph) => {
|
|
2169
|
+
this.$setNymph(nymph);
|
|
2170
|
+
},
|
|
2171
|
+
);
|
|
2172
|
+
|
|
2173
|
+
// Send the new user registered email.
|
|
2174
|
+
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
2175
|
+
try {
|
|
2176
|
+
if (
|
|
2177
|
+
!(await tilmeld.config.sendEmail(
|
|
2178
|
+
tilmeld,
|
|
2179
|
+
{
|
|
2180
|
+
template: 'UserRegistered',
|
|
2181
|
+
message: { to: tilmeld.config.userRegisteredRecipient },
|
|
2182
|
+
locals: {
|
|
2183
|
+
userUsername: this.$data.username,
|
|
2184
|
+
userName: this.$data.name,
|
|
2185
|
+
userFirstName: this.$data.nameFirst,
|
|
2186
|
+
userLastName: this.$data.nameLast,
|
|
2187
|
+
userEmail: this.$data.email,
|
|
2188
|
+
userPhone: this.$data.phone,
|
|
2189
|
+
},
|
|
2190
|
+
},
|
|
2191
|
+
this,
|
|
2192
|
+
))
|
|
2193
|
+
) {
|
|
2194
|
+
throw new Error('Failed to send.');
|
|
1465
2195
|
}
|
|
2196
|
+
} catch (e: any) {
|
|
2197
|
+
this.$nymph.config.debugError(
|
|
2198
|
+
'tilmeld',
|
|
2199
|
+
`Couldn't send new user registered email for "${this.$data.username}": ${e.message}`,
|
|
2200
|
+
);
|
|
1466
2201
|
}
|
|
2202
|
+
}
|
|
1467
2203
|
|
|
2204
|
+
if (result.loggedin) {
|
|
1468
2205
|
try {
|
|
1469
|
-
await
|
|
1470
|
-
this.$nymph = nymph;
|
|
1471
|
-
this.$nymph.tilmeld?.fillSession(this);
|
|
2206
|
+
await this.$nymph.tilmeld?.fillSession(this);
|
|
1472
2207
|
} catch (e: any) {
|
|
1473
|
-
|
|
2208
|
+
return {
|
|
2209
|
+
result: true,
|
|
2210
|
+
loggedin: false,
|
|
2211
|
+
message: `You are registered, but an error occurred logging you in: ${e.message}`,
|
|
2212
|
+
};
|
|
1474
2213
|
}
|
|
1475
|
-
|
|
1476
|
-
return {
|
|
1477
|
-
result: true,
|
|
1478
|
-
loggedin,
|
|
1479
|
-
message,
|
|
1480
|
-
};
|
|
1481
|
-
} else {
|
|
1482
|
-
await tnymph.rollback(transaction);
|
|
1483
|
-
this.$nymph = nymph;
|
|
1484
|
-
return {
|
|
1485
|
-
result: false,
|
|
1486
|
-
loggedin: false,
|
|
1487
|
-
message: 'Error registering user.',
|
|
1488
|
-
};
|
|
1489
2214
|
}
|
|
2215
|
+
|
|
2216
|
+
return result;
|
|
1490
2217
|
} catch (e: any) {
|
|
1491
|
-
|
|
1492
|
-
this.$nymph = nymph;
|
|
1493
|
-
throw e;
|
|
2218
|
+
return { result: false, loggedin: false, message: e.message };
|
|
1494
2219
|
}
|
|
1495
2220
|
}
|
|
1496
2221
|
|
|
1497
2222
|
public async $save() {
|
|
1498
|
-
let tilmeld = this
|
|
2223
|
+
let tilmeld = enforceTilmeld(this);
|
|
1499
2224
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
1500
|
-
|
|
2225
|
+
throw new BadUsernameError('Username must not be empty.');
|
|
1501
2226
|
}
|
|
1502
2227
|
|
|
1503
2228
|
if (
|
|
2229
|
+
!this.$skipAcWhenSaving &&
|
|
1504
2230
|
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1505
2231
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1506
|
-
this.$
|
|
2232
|
+
this.$data.abilities?.includes('system/admin')
|
|
1507
2233
|
) {
|
|
1508
|
-
throw new
|
|
1509
|
-
"You don't have the authority to modify system admins."
|
|
2234
|
+
throw new AccessControlError(
|
|
2235
|
+
"You don't have the authority to modify system admins.",
|
|
1510
2236
|
);
|
|
1511
2237
|
}
|
|
1512
2238
|
|
|
2239
|
+
// Lowercase the domain part.
|
|
2240
|
+
if (tilmeld.config.domainSupport) {
|
|
2241
|
+
const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
2242
|
+
if (domain) {
|
|
2243
|
+
this.$data.username = `${username}@${domain.toLowerCase()}`;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
if (tilmeld.config.domainSupport) {
|
|
2248
|
+
const [_username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
2249
|
+
const [_originalUsername, originalDomain] = splitn(
|
|
2250
|
+
this.$originalUsername ?? '',
|
|
2251
|
+
'@',
|
|
2252
|
+
2,
|
|
2253
|
+
);
|
|
2254
|
+
|
|
2255
|
+
if (
|
|
2256
|
+
this.guid != null &&
|
|
2257
|
+
((originalDomain == null && domain != null) ||
|
|
2258
|
+
(domain == null && originalDomain != null))
|
|
2259
|
+
) {
|
|
2260
|
+
throw new AccessControlError(
|
|
2261
|
+
"Users can't switch to or from having a domain once created.",
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
if (
|
|
2266
|
+
!this.$skipAcWhenSaving &&
|
|
2267
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
2268
|
+
!tilmeld.gatekeeper('system/admin')
|
|
2269
|
+
) {
|
|
2270
|
+
if (
|
|
2271
|
+
domain != null &&
|
|
2272
|
+
!this.$is(tilmeld.currentUser) &&
|
|
2273
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
2274
|
+
) {
|
|
2275
|
+
throw new AccessControlError(
|
|
2276
|
+
"You don't have the authority to modify users on this domain.",
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
if (
|
|
2280
|
+
originalDomain != null &&
|
|
2281
|
+
originalDomain !== domain &&
|
|
2282
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
2283
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))
|
|
2284
|
+
) {
|
|
2285
|
+
throw new AccessControlError(
|
|
2286
|
+
"You don't have the authority to modify users on this domain.",
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
1513
2292
|
let sendVerification = false;
|
|
1514
2293
|
|
|
1515
2294
|
// Formatting.
|
|
@@ -1528,49 +2307,88 @@ export default class User extends AbleObject<UserData> {
|
|
|
1528
2307
|
this.$data.enabled = !!this.$data.enabled;
|
|
1529
2308
|
|
|
1530
2309
|
// Clear empty values.
|
|
1531
|
-
if (
|
|
2310
|
+
if (
|
|
2311
|
+
this.$data.nameFirst === '' ||
|
|
2312
|
+
!tilmeld.config.userFields.includes('name')
|
|
2313
|
+
) {
|
|
1532
2314
|
delete this.$data.nameFirst;
|
|
1533
2315
|
}
|
|
1534
|
-
if (
|
|
2316
|
+
if (
|
|
2317
|
+
this.$data.nameMiddle === '' ||
|
|
2318
|
+
!tilmeld.config.userFields.includes('name')
|
|
2319
|
+
) {
|
|
1535
2320
|
delete this.$data.nameMiddle;
|
|
1536
2321
|
}
|
|
1537
|
-
if (
|
|
2322
|
+
if (
|
|
2323
|
+
this.$data.nameLast === '' ||
|
|
2324
|
+
!tilmeld.config.userFields.includes('name')
|
|
2325
|
+
) {
|
|
1538
2326
|
delete this.$data.nameLast;
|
|
1539
2327
|
}
|
|
1540
|
-
if (this.$data.name === '') {
|
|
2328
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
1541
2329
|
delete this.$data.name;
|
|
1542
2330
|
}
|
|
1543
|
-
if (
|
|
2331
|
+
if (
|
|
2332
|
+
this.$data.email === '' ||
|
|
2333
|
+
!tilmeld.config.userFields.includes('email')
|
|
2334
|
+
) {
|
|
1544
2335
|
delete this.$data.email;
|
|
1545
2336
|
}
|
|
1546
2337
|
if (this.$data.avatar === '') {
|
|
1547
2338
|
delete this.$data.avatar;
|
|
1548
2339
|
}
|
|
1549
|
-
if (
|
|
2340
|
+
if (
|
|
2341
|
+
this.$data.phone === '' ||
|
|
2342
|
+
!tilmeld.config.userFields.includes('phone')
|
|
2343
|
+
) {
|
|
1550
2344
|
delete this.$data.phone;
|
|
1551
2345
|
}
|
|
1552
|
-
if (
|
|
2346
|
+
if (
|
|
2347
|
+
this.$data.secret === '' ||
|
|
2348
|
+
!tilmeld.config.userFields.includes('email')
|
|
2349
|
+
) {
|
|
1553
2350
|
delete this.$data.secret;
|
|
1554
2351
|
}
|
|
1555
|
-
if (
|
|
2352
|
+
if (
|
|
2353
|
+
this.$data.emailChangeDate === 0 ||
|
|
2354
|
+
!tilmeld.config.userFields.includes('email')
|
|
2355
|
+
) {
|
|
1556
2356
|
delete this.$data.emailChangeDate;
|
|
1557
2357
|
}
|
|
1558
|
-
if (
|
|
2358
|
+
if (
|
|
2359
|
+
this.$data.newEmailSecret === '' ||
|
|
2360
|
+
!tilmeld.config.userFields.includes('email')
|
|
2361
|
+
) {
|
|
1559
2362
|
delete this.$data.newEmailSecret;
|
|
1560
2363
|
}
|
|
1561
|
-
if (
|
|
2364
|
+
if (
|
|
2365
|
+
this.$data.newEmailAddress === '' ||
|
|
2366
|
+
!tilmeld.config.userFields.includes('email')
|
|
2367
|
+
) {
|
|
1562
2368
|
delete this.$data.newEmailAddress;
|
|
1563
2369
|
}
|
|
1564
|
-
if (
|
|
2370
|
+
if (
|
|
2371
|
+
this.$data.cancelEmailSecret === '' ||
|
|
2372
|
+
!tilmeld.config.userFields.includes('email')
|
|
2373
|
+
) {
|
|
1565
2374
|
delete this.$data.cancelEmailSecret;
|
|
1566
2375
|
}
|
|
1567
|
-
if (
|
|
2376
|
+
if (
|
|
2377
|
+
this.$data.cancelEmailAddress === '' ||
|
|
2378
|
+
!tilmeld.config.userFields.includes('email')
|
|
2379
|
+
) {
|
|
1568
2380
|
delete this.$data.cancelEmailAddress;
|
|
1569
2381
|
}
|
|
1570
|
-
if (
|
|
2382
|
+
if (
|
|
2383
|
+
this.$data.recoverSecret === '' ||
|
|
2384
|
+
!tilmeld.config.userFields.includes('email')
|
|
2385
|
+
) {
|
|
1571
2386
|
delete this.$data.recoverSecret;
|
|
1572
2387
|
}
|
|
1573
|
-
if (
|
|
2388
|
+
if (
|
|
2389
|
+
this.$data.recoverSecretDate === 0 ||
|
|
2390
|
+
!tilmeld.config.userFields.includes('email')
|
|
2391
|
+
) {
|
|
1574
2392
|
delete this.$data.recoverSecretDate;
|
|
1575
2393
|
}
|
|
1576
2394
|
if (this.$data.password === '') {
|
|
@@ -1588,7 +2406,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1588
2406
|
if (!unCheck.result) {
|
|
1589
2407
|
throw new BadUsernameError(unCheck.message);
|
|
1590
2408
|
}
|
|
1591
|
-
if (
|
|
2409
|
+
if (
|
|
2410
|
+
!tilmeld.config.emailUsernames &&
|
|
2411
|
+
tilmeld.config.userFields.includes('email')
|
|
2412
|
+
) {
|
|
1592
2413
|
const emCheck = await this.$checkEmail();
|
|
1593
2414
|
if (!emCheck.result) {
|
|
1594
2415
|
throw new BadEmailError(emCheck.message);
|
|
@@ -1596,7 +2417,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1596
2417
|
}
|
|
1597
2418
|
|
|
1598
2419
|
// Email changes.
|
|
1599
|
-
if (
|
|
2420
|
+
if (
|
|
2421
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
2422
|
+
tilmeld.config.userFields.includes('email')
|
|
2423
|
+
) {
|
|
1600
2424
|
// The user isn't an admin, so email address changes should contain some
|
|
1601
2425
|
// security measures.
|
|
1602
2426
|
if (tilmeld.config.verifyEmail) {
|
|
@@ -1604,7 +2428,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1604
2428
|
if (this.guid == null) {
|
|
1605
2429
|
// If this is the first user, they'll be made an admin, so they don't
|
|
1606
2430
|
// need to verify.
|
|
1607
|
-
if (
|
|
2431
|
+
if (!this.$data.abilities?.includes('system/admin')) {
|
|
1608
2432
|
this.$data.secret = nanoid();
|
|
1609
2433
|
sendVerification = true;
|
|
1610
2434
|
}
|
|
@@ -1617,17 +2441,17 @@ export default class User extends AbleObject<UserData> {
|
|
|
1617
2441
|
tilmeld.config.emailRateLimit !== '' &&
|
|
1618
2442
|
this.$data.emailChangeDate != null &&
|
|
1619
2443
|
this.$data.emailChangeDate >=
|
|
1620
|
-
strtotime('-' + tilmeld.config.emailRateLimit) * 1000
|
|
2444
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000
|
|
1621
2445
|
) {
|
|
1622
2446
|
throw new EmailChangeRateLimitExceededError(
|
|
1623
2447
|
'You already changed your email address recently. Please wait until ' +
|
|
1624
2448
|
new Date(
|
|
1625
|
-
strtotime(
|
|
2449
|
+
(strtotime(
|
|
1626
2450
|
'+' + tilmeld.config.emailRateLimit,
|
|
1627
|
-
Math.floor(this.$data.emailChangeDate / 1000)
|
|
1628
|
-
) * 1000
|
|
2451
|
+
Math.floor(this.$data.emailChangeDate / 1000),
|
|
2452
|
+
) || 0) * 1000,
|
|
1629
2453
|
).toString() +
|
|
1630
|
-
' to change your email address again.'
|
|
2454
|
+
' to change your email address again.',
|
|
1631
2455
|
);
|
|
1632
2456
|
} else {
|
|
1633
2457
|
if (
|
|
@@ -1637,7 +2461,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1637
2461
|
// an email change.
|
|
1638
2462
|
(this.$data.emailChangeDate == null ||
|
|
1639
2463
|
this.$data.emailChangeDate <
|
|
1640
|
-
strtotime('-' + tilmeld.config.emailRateLimit) * 1000)
|
|
2464
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)
|
|
1641
2465
|
) {
|
|
1642
2466
|
// Save the old email in case the cancel change link is clicked.
|
|
1643
2467
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
@@ -1663,7 +2487,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1663
2487
|
// email change.
|
|
1664
2488
|
(this.$data.emailChangeDate == null ||
|
|
1665
2489
|
this.$data.emailChangeDate <
|
|
1666
|
-
strtotime('-' + tilmeld.config.emailRateLimit) * 1000)
|
|
2490
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)
|
|
1667
2491
|
) {
|
|
1668
2492
|
// The user doesn't need to verify their new email address, but should
|
|
1669
2493
|
// be able to cancel the email change from their old address.
|
|
@@ -1682,77 +2506,167 @@ export default class User extends AbleObject<UserData> {
|
|
|
1682
2506
|
throw new BadDataError('A password is required.');
|
|
1683
2507
|
}
|
|
1684
2508
|
|
|
1685
|
-
try {
|
|
1686
|
-
tilmeld.config.validatorUser(this);
|
|
1687
|
-
} catch (e: any) {
|
|
1688
|
-
throw new BadDataError(e?.message);
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
// Start transaction.
|
|
1692
|
-
const transaction = 'tilmeld-save-' + nanoid();
|
|
1693
|
-
const nymph = this.$nymph;
|
|
1694
|
-
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1695
|
-
this.$nymph = tnymph;
|
|
1696
|
-
tilmeld = this.$nymph.tilmeld as Tilmeld;
|
|
1697
|
-
|
|
1698
|
-
if (
|
|
1699
|
-
this.$data.group != null &&
|
|
1700
|
-
this.$data.group.user != null &&
|
|
1701
|
-
this.$is(this.$data.group.user)
|
|
1702
|
-
) {
|
|
1703
|
-
try {
|
|
1704
|
-
// Update the user's generated primary group.
|
|
1705
|
-
this.$data.group.groupname = this.$data.username;
|
|
1706
|
-
this.$data.group.avatar = this.$data.avatar;
|
|
1707
|
-
this.$data.group.email = this.$data.email;
|
|
1708
|
-
this.$data.group.name = this.$data.name;
|
|
1709
|
-
this.$data.group.phone = this.$data.phone;
|
|
1710
|
-
await this.$data.group.$saveSkipAC();
|
|
1711
|
-
} catch (e: any) {
|
|
1712
|
-
await tnymph.rollback(transaction);
|
|
1713
|
-
this.$nymph = nymph;
|
|
1714
|
-
throw e;
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
let ret: boolean;
|
|
1719
2509
|
let preGuid = this.guid;
|
|
1720
2510
|
let preCdate = this.cdate;
|
|
1721
2511
|
let preMdate = this.mdate;
|
|
2512
|
+
return await transaction(
|
|
2513
|
+
this.$nymph,
|
|
2514
|
+
'tilmeld-save-user-' + (this.guid || nanoid()),
|
|
2515
|
+
async (nymph) => {
|
|
2516
|
+
this.$setNymph(nymph);
|
|
2517
|
+
const tilmeld = enforceTilmeld(this);
|
|
2518
|
+
const Group = this.$nymph.getEntityClass(GroupClass);
|
|
2519
|
+
|
|
2520
|
+
let group = this.$data.group;
|
|
2521
|
+
if (group != null) {
|
|
2522
|
+
// Make sure to get fresh group data.
|
|
2523
|
+
if (group.$asleep()) {
|
|
2524
|
+
await group.$wake();
|
|
2525
|
+
} else {
|
|
2526
|
+
if (!(await group.$refresh())) {
|
|
2527
|
+
throw new Error("Couldn't retrieve primary group info.");
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
if (group == null && this.guid == null) {
|
|
2532
|
+
if (tilmeld.config.generatePrimary) {
|
|
2533
|
+
// Generate a new primary group for the user.
|
|
2534
|
+
group = await Group.factory();
|
|
2535
|
+
group.user = this;
|
|
2536
|
+
const parent = await this.$nymph.getEntity(
|
|
2537
|
+
{ class: Group },
|
|
2538
|
+
{ type: '&', equal: ['defaultPrimary', true] },
|
|
2539
|
+
);
|
|
2540
|
+
if (parent != null) {
|
|
2541
|
+
group.parent = parent;
|
|
2542
|
+
}
|
|
2543
|
+
} else {
|
|
2544
|
+
// Add the default primary.
|
|
2545
|
+
const group = await this.$nymph.getEntity(
|
|
2546
|
+
{ class: Group },
|
|
2547
|
+
{ type: '&', equal: ['defaultPrimary', true] },
|
|
2548
|
+
);
|
|
2549
|
+
if (group != null) {
|
|
2550
|
+
this.$data.group = group;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
if (group != null && group.user != null && this.$is(group.user)) {
|
|
2555
|
+
// Update the user's generated primary group.
|
|
2556
|
+
let changed = false;
|
|
2557
|
+
if (group.groupname !== this.$data.username) {
|
|
2558
|
+
group.groupname = this.$data.username;
|
|
2559
|
+
changed = true;
|
|
2560
|
+
}
|
|
2561
|
+
if (group.avatar !== this.$data.avatar) {
|
|
2562
|
+
group.avatar = this.$data.avatar;
|
|
2563
|
+
changed = true;
|
|
2564
|
+
}
|
|
2565
|
+
if (
|
|
2566
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2567
|
+
group.email !== this.$data.email
|
|
2568
|
+
) {
|
|
2569
|
+
group.email = this.$data.email;
|
|
2570
|
+
changed = true;
|
|
2571
|
+
}
|
|
2572
|
+
if (
|
|
2573
|
+
tilmeld.config.userFields.includes('name') &&
|
|
2574
|
+
group.name !== this.$data.name
|
|
2575
|
+
) {
|
|
2576
|
+
group.name = this.$data.name;
|
|
2577
|
+
changed = true;
|
|
2578
|
+
}
|
|
2579
|
+
if (
|
|
2580
|
+
tilmeld.config.userFields.includes('phone') &&
|
|
2581
|
+
group.phone !== this.$data.phone
|
|
2582
|
+
) {
|
|
2583
|
+
group.phone = this.$data.phone;
|
|
2584
|
+
changed = true;
|
|
2585
|
+
}
|
|
2586
|
+
if (changed || group.guid == null) {
|
|
2587
|
+
await group.$saveSkipAC();
|
|
2588
|
+
}
|
|
2589
|
+
this.$data.group = group;
|
|
2590
|
+
}
|
|
1722
2591
|
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2592
|
+
if (this.$data.groups == null || this.$data.groups.length === 0) {
|
|
2593
|
+
// Add secondary groups.
|
|
2594
|
+
if (
|
|
2595
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2596
|
+
tilmeld.config.verifyEmail &&
|
|
2597
|
+
tilmeld.config.unverifiedAccess &&
|
|
2598
|
+
this.$data.secret != null
|
|
2599
|
+
) {
|
|
2600
|
+
// Add the default secondaries for unverified users.
|
|
2601
|
+
this.$data.groups = await this.$nymph.getEntities(
|
|
2602
|
+
{ class: Group },
|
|
2603
|
+
{ type: '&', equal: ['unverifiedSecondary', true] },
|
|
2604
|
+
);
|
|
2605
|
+
} else {
|
|
2606
|
+
// Add the default secondaries.
|
|
2607
|
+
this.$data.groups = await this.$nymph.getEntities(
|
|
2608
|
+
{ class: Group },
|
|
2609
|
+
{ type: '&', equal: ['defaultSecondary', true] },
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
try {
|
|
2615
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
2616
|
+
} catch (e: any) {
|
|
2617
|
+
throw new BadDataError(e?.message);
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
for (let callback of (this.constructor as typeof User)
|
|
2621
|
+
.beforeSaveCallbacks) {
|
|
2622
|
+
if (callback) {
|
|
2623
|
+
await callback(this);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
await super.$save();
|
|
2628
|
+
|
|
2629
|
+
if (sendVerification) {
|
|
2630
|
+
// The email has changed, so send a new verification email.
|
|
2631
|
+
if (!(await this.$sendEmailVerification())) {
|
|
2632
|
+
throw new Error("Couldn't send verification email.");
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
this.$descendantGroups = undefined;
|
|
2637
|
+
this.$gatekeeperCache = undefined;
|
|
2638
|
+
|
|
2639
|
+
for (let callback of (this.constructor as typeof User)
|
|
2640
|
+
.afterSaveCallbacks) {
|
|
2641
|
+
if (callback) {
|
|
2642
|
+
await callback(this);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
},
|
|
2646
|
+
async (nymph, committed) => {
|
|
2647
|
+
this.$setNymph(nymph);
|
|
2648
|
+
|
|
2649
|
+
if (committed) {
|
|
2650
|
+
this.$originalEmail = this.$data.email;
|
|
2651
|
+
this.$originalUsername = this.$data.username;
|
|
2652
|
+
|
|
2653
|
+
try {
|
|
2654
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
2655
|
+
|
|
2656
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
2657
|
+
// Update the user in the session cache.
|
|
2658
|
+
await tilmeld.fillSession(this);
|
|
2659
|
+
}
|
|
2660
|
+
} catch (e: any) {
|
|
2661
|
+
// Ignore.
|
|
2662
|
+
}
|
|
2663
|
+
} else {
|
|
1735
2664
|
this.guid = preGuid;
|
|
1736
2665
|
this.cdate = preCdate;
|
|
1737
2666
|
this.mdate = preMdate;
|
|
1738
|
-
this.$nymph = nymph;
|
|
1739
|
-
throw new Error("Couldn't send verification email.");
|
|
1740
2667
|
}
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
this.$descendantGroups = undefined;
|
|
1744
|
-
this.$gatekeeperCache = undefined;
|
|
1745
|
-
await tnymph.commit(transaction);
|
|
1746
|
-
|
|
1747
|
-
if ((nymph.tilmeld as Tilmeld).User.current(true).$is(this)) {
|
|
1748
|
-
// Update the user in the session cache.
|
|
1749
|
-
(nymph.tilmeld as Tilmeld).fillSession(this);
|
|
1750
|
-
}
|
|
1751
|
-
} else {
|
|
1752
|
-
await tnymph.rollback(transaction);
|
|
1753
|
-
}
|
|
1754
|
-
this.$nymph = nymph;
|
|
1755
|
-
return ret;
|
|
2668
|
+
},
|
|
2669
|
+
);
|
|
1756
2670
|
}
|
|
1757
2671
|
|
|
1758
2672
|
/**
|
|
@@ -1760,7 +2674,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1760
2674
|
*/
|
|
1761
2675
|
public async $saveSkipAC() {
|
|
1762
2676
|
this.$skipAcWhenSaving = true;
|
|
1763
|
-
|
|
2677
|
+
await this.$save();
|
|
1764
2678
|
}
|
|
1765
2679
|
|
|
1766
2680
|
public $tilmeldSaveSkipAC() {
|
|
@@ -1771,54 +2685,143 @@ export default class User extends AbleObject<UserData> {
|
|
|
1771
2685
|
return false;
|
|
1772
2686
|
}
|
|
1773
2687
|
|
|
2688
|
+
@transactional
|
|
1774
2689
|
public async $delete() {
|
|
1775
|
-
const tilmeld = this
|
|
1776
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1777
|
-
throw new
|
|
2690
|
+
const tilmeld = enforceTilmeld(this);
|
|
2691
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2692
|
+
throw new AccessControlError(
|
|
2693
|
+
"You don't have the authority to delete users.",
|
|
2694
|
+
);
|
|
1778
2695
|
}
|
|
1779
2696
|
if (
|
|
2697
|
+
!this.$skipAcWhenDeleting &&
|
|
1780
2698
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1781
|
-
this.$
|
|
2699
|
+
this.$data.abilities?.includes('system/admin')
|
|
1782
2700
|
) {
|
|
1783
|
-
throw new
|
|
1784
|
-
"You don't have the authority to delete system admins."
|
|
2701
|
+
throw new AccessControlError(
|
|
2702
|
+
"You don't have the authority to delete system admins.",
|
|
1785
2703
|
);
|
|
1786
2704
|
}
|
|
2705
|
+
if (this.$data.username !== this.$originalUsername) {
|
|
2706
|
+
throw new AccessControlError(
|
|
2707
|
+
"You can't delete a user while switching their username.",
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
if (!(await this.$refresh())) {
|
|
2712
|
+
throw new BadDataError('User could not be refreshed.');
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
if (this.$data.group != null) {
|
|
2716
|
+
if (this.$data.group.$asleep()) {
|
|
2717
|
+
await this.$data.group.$wake();
|
|
2718
|
+
} else {
|
|
2719
|
+
if (!(await this.$data.group.$refresh())) {
|
|
2720
|
+
throw new BadDataError('Group could not be refreshed.');
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
for (let callback of (this.constructor as typeof User)
|
|
2726
|
+
.beforeDeleteCallbacks) {
|
|
2727
|
+
if (callback) {
|
|
2728
|
+
await callback(this);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
|
|
1787
2732
|
if (tilmeld.User.current(true).$is(this)) {
|
|
1788
2733
|
await this.$logout();
|
|
1789
2734
|
}
|
|
1790
|
-
|
|
2735
|
+
|
|
2736
|
+
if (this.$data.group != null && this.$is(this.$data.group.user)) {
|
|
2737
|
+
// Read the skip ac value here, because super.$delete changes it.
|
|
2738
|
+
const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
|
|
2739
|
+
|
|
2740
|
+
// Delete the user.
|
|
2741
|
+
await super.$delete();
|
|
2742
|
+
// Delete the generated group.
|
|
2743
|
+
if ($skipAcWhenDeleting) {
|
|
2744
|
+
await this.$data.group.$deleteSkipAC();
|
|
2745
|
+
} else {
|
|
2746
|
+
await this.$data.group.$delete();
|
|
2747
|
+
}
|
|
2748
|
+
} else {
|
|
2749
|
+
await super.$delete();
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
for (let callback of (this.constructor as typeof User)
|
|
2753
|
+
.afterDeleteCallbacks) {
|
|
2754
|
+
if (callback) {
|
|
2755
|
+
await callback(this);
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
/*
|
|
2761
|
+
* This should *never* be accessible on the client.
|
|
2762
|
+
*/
|
|
2763
|
+
async $deleteSkipAC() {
|
|
2764
|
+
this.$skipAcWhenDeleting = true;
|
|
2765
|
+
await this.$delete();
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
$tilmeldDeleteSkipAC() {
|
|
2769
|
+
if (this.$skipAcWhenDeleting) {
|
|
2770
|
+
this.$skipAcWhenDeleting = false;
|
|
2771
|
+
return true;
|
|
2772
|
+
}
|
|
2773
|
+
return false;
|
|
1791
2774
|
}
|
|
1792
2775
|
|
|
1793
2776
|
public static on<T extends EventType>(
|
|
1794
2777
|
event: T,
|
|
1795
|
-
callback: T extends '
|
|
1796
|
-
?
|
|
1797
|
-
: T extends '
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2778
|
+
callback: T extends 'checkUsername'
|
|
2779
|
+
? TilmeldCheckUsernameCallback
|
|
2780
|
+
: T extends 'beforeRegister'
|
|
2781
|
+
? TilmeldBeforeRegisterCallback
|
|
2782
|
+
: T extends 'afterRegister'
|
|
2783
|
+
? TilmeldAfterRegisterCallback
|
|
2784
|
+
: T extends 'beforeSave'
|
|
2785
|
+
? TilmeldBeforeSaveCallback
|
|
2786
|
+
: T extends 'afterSave'
|
|
2787
|
+
? TilmeldAfterSaveCallback
|
|
2788
|
+
: T extends 'beforeLogin'
|
|
2789
|
+
? TilmeldBeforeLoginCallback
|
|
2790
|
+
: T extends 'afterLogin'
|
|
2791
|
+
? TilmeldAfterLoginCallback
|
|
2792
|
+
: T extends 'beforeLogout'
|
|
2793
|
+
? TilmeldBeforeLogoutCallback
|
|
2794
|
+
: T extends 'afterLogout'
|
|
2795
|
+
? TilmeldAfterLogoutCallback
|
|
2796
|
+
: T extends 'beforeDelete'
|
|
2797
|
+
? TilmeldBeforeDeleteCallback
|
|
2798
|
+
: T extends 'afterDelete'
|
|
2799
|
+
? TilmeldAfterDeleteCallback
|
|
2800
|
+
: never,
|
|
1808
2801
|
) {
|
|
1809
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
1810
|
-
? '
|
|
1811
|
-
: T extends '
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2802
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2803
|
+
? 'checkUsernameCallbacks'
|
|
2804
|
+
: T extends 'beforeRegister'
|
|
2805
|
+
? 'beforeRegisterCallbacks'
|
|
2806
|
+
: T extends 'afterRegister'
|
|
2807
|
+
? 'afterRegisterCallbacks'
|
|
2808
|
+
: T extends 'beforeSave'
|
|
2809
|
+
? 'beforeSaveCallbacks'
|
|
2810
|
+
: T extends 'afterSave'
|
|
2811
|
+
? 'afterSaveCallbacks'
|
|
2812
|
+
: T extends 'beforeLogin'
|
|
2813
|
+
? 'beforeLoginCallbacks'
|
|
2814
|
+
: T extends 'afterLogin'
|
|
2815
|
+
? 'afterLoginCallbacks'
|
|
2816
|
+
: T extends 'beforeLogout'
|
|
2817
|
+
? 'beforeLogoutCallbacks'
|
|
2818
|
+
: T extends 'afterLogout'
|
|
2819
|
+
? 'afterLogoutCallbacks'
|
|
2820
|
+
: T extends 'beforeDelete'
|
|
2821
|
+
? 'beforeDeleteCallbacks'
|
|
2822
|
+
: T extends 'afterDelete'
|
|
2823
|
+
? 'afterDeleteCallbacks'
|
|
2824
|
+
: never;
|
|
1822
2825
|
if (!(prop in this)) {
|
|
1823
2826
|
throw new Error('Invalid event type.');
|
|
1824
2827
|
}
|
|
@@ -1829,33 +2832,53 @@ export default class User extends AbleObject<UserData> {
|
|
|
1829
2832
|
|
|
1830
2833
|
public static off<T extends EventType>(
|
|
1831
2834
|
event: T,
|
|
1832
|
-
callback: T extends '
|
|
1833
|
-
?
|
|
1834
|
-
: T extends '
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2835
|
+
callback: T extends 'checkUsername'
|
|
2836
|
+
? TilmeldCheckUsernameCallback
|
|
2837
|
+
: T extends 'beforeRegister'
|
|
2838
|
+
? TilmeldBeforeRegisterCallback
|
|
2839
|
+
: T extends 'afterRegister'
|
|
2840
|
+
? TilmeldAfterRegisterCallback
|
|
2841
|
+
: T extends 'beforeSave'
|
|
2842
|
+
? TilmeldBeforeSaveCallback
|
|
2843
|
+
: T extends 'afterSave'
|
|
2844
|
+
? TilmeldAfterSaveCallback
|
|
2845
|
+
: T extends 'beforeLogin'
|
|
2846
|
+
? TilmeldBeforeLoginCallback
|
|
2847
|
+
: T extends 'afterLogin'
|
|
2848
|
+
? TilmeldAfterLoginCallback
|
|
2849
|
+
: T extends 'beforeLogout'
|
|
2850
|
+
? TilmeldBeforeLogoutCallback
|
|
2851
|
+
: T extends 'afterLogout'
|
|
2852
|
+
? TilmeldAfterLogoutCallback
|
|
2853
|
+
: T extends 'beforeDelete'
|
|
2854
|
+
? TilmeldBeforeDeleteCallback
|
|
2855
|
+
: T extends 'afterDelete'
|
|
2856
|
+
? TilmeldAfterDeleteCallback
|
|
2857
|
+
: never,
|
|
1845
2858
|
) {
|
|
1846
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
1847
|
-
? '
|
|
1848
|
-
: T extends '
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2859
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2860
|
+
? 'checkUsernameCallbacks'
|
|
2861
|
+
: T extends 'beforeRegister'
|
|
2862
|
+
? 'beforeRegisterCallbacks'
|
|
2863
|
+
: T extends 'afterRegister'
|
|
2864
|
+
? 'afterRegisterCallbacks'
|
|
2865
|
+
: T extends 'beforeSave'
|
|
2866
|
+
? 'beforeSaveCallbacks'
|
|
2867
|
+
: T extends 'afterSave'
|
|
2868
|
+
? 'afterSaveCallbacks'
|
|
2869
|
+
: T extends 'beforeLogin'
|
|
2870
|
+
? 'beforeLoginCallbacks'
|
|
2871
|
+
: T extends 'afterLogin'
|
|
2872
|
+
? 'afterLoginCallbacks'
|
|
2873
|
+
: T extends 'beforeLogout'
|
|
2874
|
+
? 'beforeLogoutCallbacks'
|
|
2875
|
+
: T extends 'afterLogout'
|
|
2876
|
+
? 'afterLogoutCallbacks'
|
|
2877
|
+
: T extends 'beforeDelete'
|
|
2878
|
+
? 'beforeDeleteCallbacks'
|
|
2879
|
+
: T extends 'afterDelete'
|
|
2880
|
+
? 'afterDeleteCallbacks'
|
|
2881
|
+
: never;
|
|
1859
2882
|
if (!(prop in this)) {
|
|
1860
2883
|
return false;
|
|
1861
2884
|
}
|