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