@nymphjs/tilmeld 1.0.0-beta.9 → 1.0.0-beta.91
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 +414 -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 +181 -15
- package/dist/Group.js +551 -192
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +83 -13
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +241 -12
- package/dist/Tilmeld.js +636 -254
- package/dist/Tilmeld.js.map +1 -1
- package/dist/Tilmeld.types.d.ts +2 -2
- package/dist/Tilmeld.types.js +1 -2
- package/dist/User.d.ts +395 -22
- package/dist/User.js +1316 -365
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +132 -13
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +287 -7
- package/dist/conf/d.js +1 -2
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +188 -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 +11 -10
- package/dist/index.js +16 -35
- package/dist/index.js.map +1 -1
- package/dist/tilmeldJoiProps.js +13 -18
- package/dist/tilmeldJoiProps.js.map +1 -1
- package/jest.config.js +11 -2
- package/package.json +34 -31
- package/src/AbleObject.ts +2 -2
- package/src/Group.test.ts +104 -6
- package/src/Group.ts +661 -226
- package/src/Tilmeld.ts +420 -254
- package/src/Tilmeld.types.ts +2 -2
- package/src/User.test.ts +163 -6
- package/src/User.ts +1557 -454
- package/src/conf/d.ts +63 -17
- package/src/conf/defaults.ts +162 -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 +12 -10
- package/src/tilmeldJoiProps.ts +11 -11
- 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,10 +335,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
256
335
|
* verification, don't set this.
|
|
257
336
|
*/
|
|
258
337
|
public $originalEmail?: string;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
338
|
+
/**
|
|
339
|
+
* Used to save the current username for domain admin permissions.
|
|
340
|
+
*/
|
|
341
|
+
public $originalUsername?: string;
|
|
263
342
|
|
|
264
343
|
static async factoryUsername(username?: string): Promise<User & UserData> {
|
|
265
344
|
const entity = new this();
|
|
@@ -271,7 +350,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
271
350
|
{
|
|
272
351
|
type: '&',
|
|
273
352
|
ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
274
|
-
}
|
|
353
|
+
},
|
|
275
354
|
);
|
|
276
355
|
if (entity != null) {
|
|
277
356
|
return entity;
|
|
@@ -280,34 +359,18 @@ export default class User extends AbleObject<UserData> {
|
|
|
280
359
|
return entity;
|
|
281
360
|
}
|
|
282
361
|
|
|
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
362
|
public static current(returnObjectIfNotExist: true): User & UserData;
|
|
301
363
|
public static current(
|
|
302
|
-
returnObjectIfNotExist?: false
|
|
364
|
+
returnObjectIfNotExist?: false,
|
|
303
365
|
): (User & UserData) | null;
|
|
304
366
|
public static current(
|
|
305
|
-
returnObjectIfNotExist?: boolean
|
|
367
|
+
returnObjectIfNotExist?: boolean,
|
|
306
368
|
): (User & UserData) | null {
|
|
307
|
-
|
|
369
|
+
const tilmeld = enforceTilmeld(this);
|
|
370
|
+
if (tilmeld.currentUser == null) {
|
|
308
371
|
return returnObjectIfNotExist ? this.factorySync() : null;
|
|
309
372
|
}
|
|
310
|
-
return
|
|
373
|
+
return tilmeld.currentUser;
|
|
311
374
|
}
|
|
312
375
|
|
|
313
376
|
/**
|
|
@@ -320,7 +383,8 @@ export default class User extends AbleObject<UserData> {
|
|
|
320
383
|
recoveryType: 'username' | 'password';
|
|
321
384
|
account: string;
|
|
322
385
|
}): Promise<{ result: boolean; message: string }> {
|
|
323
|
-
|
|
386
|
+
const tilmeld = enforceTilmeld(this);
|
|
387
|
+
if (!tilmeld.config.pwRecovery) {
|
|
324
388
|
return {
|
|
325
389
|
result: false,
|
|
326
390
|
message: 'Account recovery is not allowed.',
|
|
@@ -330,18 +394,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
330
394
|
let user: User & UserData;
|
|
331
395
|
const options: EmailOptions = {};
|
|
332
396
|
|
|
333
|
-
if (
|
|
334
|
-
!this.tilmeld.config.emailUsernames &&
|
|
335
|
-
data.recoveryType === 'username'
|
|
336
|
-
) {
|
|
397
|
+
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
337
398
|
// Create a username recovery email.
|
|
338
399
|
|
|
339
400
|
const getUser = await this.nymph.getEntity(
|
|
340
|
-
{ class:
|
|
401
|
+
{ class: tilmeld.User, skipAc: true },
|
|
341
402
|
{
|
|
342
403
|
type: '&',
|
|
343
404
|
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
344
|
-
}
|
|
405
|
+
},
|
|
345
406
|
);
|
|
346
407
|
|
|
347
408
|
if (getUser == null) {
|
|
@@ -364,7 +425,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
364
425
|
} else if (data.recoveryType === 'password') {
|
|
365
426
|
// Create a password recovery email.
|
|
366
427
|
|
|
367
|
-
const getUser = await
|
|
428
|
+
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
368
429
|
|
|
369
430
|
if (getUser.guid == null) {
|
|
370
431
|
return {
|
|
@@ -391,14 +452,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
391
452
|
};
|
|
392
453
|
options.locals = {
|
|
393
454
|
recoverCode: getUser.recoverSecret,
|
|
394
|
-
timeLimit:
|
|
455
|
+
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
395
456
|
};
|
|
396
457
|
} else {
|
|
397
458
|
return { result: false, message: 'Invalid recovery type.' };
|
|
398
459
|
}
|
|
399
460
|
|
|
461
|
+
this.nymph.config.debugLog(
|
|
462
|
+
'tilmeld',
|
|
463
|
+
`User account recovery (${options.template}) request for "${user.username}".`,
|
|
464
|
+
);
|
|
465
|
+
|
|
400
466
|
// Send the email.
|
|
401
|
-
if (await
|
|
467
|
+
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
402
468
|
return {
|
|
403
469
|
result: true,
|
|
404
470
|
message:
|
|
@@ -421,22 +487,23 @@ export default class User extends AbleObject<UserData> {
|
|
|
421
487
|
secret: string;
|
|
422
488
|
password: string;
|
|
423
489
|
}): Promise<{ result: boolean; message: string }> {
|
|
424
|
-
|
|
490
|
+
const tilmeld = enforceTilmeld(this);
|
|
491
|
+
if (!tilmeld.config.pwRecovery) {
|
|
425
492
|
return {
|
|
426
493
|
result: false,
|
|
427
494
|
message: 'Account recovery is not allowed.',
|
|
428
495
|
};
|
|
429
496
|
}
|
|
430
497
|
|
|
431
|
-
const user = await
|
|
498
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
432
499
|
|
|
433
500
|
if (
|
|
434
501
|
user.guid == null ||
|
|
435
502
|
user.recoverSecret == null ||
|
|
436
503
|
data.secret !== user.recoverSecret ||
|
|
437
504
|
strtotime(
|
|
438
|
-
'+' +
|
|
439
|
-
Math.floor((user.recoverSecretDate ?? 0) / 1000)
|
|
505
|
+
'+' + tilmeld.config.pwRecoveryTimeLimit,
|
|
506
|
+
Math.floor((user.recoverSecretDate ?? 0) / 1000),
|
|
440
507
|
) *
|
|
441
508
|
1000 <
|
|
442
509
|
Date.now()
|
|
@@ -451,6 +518,11 @@ export default class User extends AbleObject<UserData> {
|
|
|
451
518
|
return { result: false, message: 'Password cannot be empty.' };
|
|
452
519
|
}
|
|
453
520
|
|
|
521
|
+
this.nymph.config.debugLog(
|
|
522
|
+
'tilmeld',
|
|
523
|
+
`User account recovery for "${user.username}".`,
|
|
524
|
+
);
|
|
525
|
+
|
|
454
526
|
user.$password(data.password);
|
|
455
527
|
delete user.recoverSecret;
|
|
456
528
|
delete user.recoverSecretDate;
|
|
@@ -466,27 +538,31 @@ export default class User extends AbleObject<UserData> {
|
|
|
466
538
|
}
|
|
467
539
|
|
|
468
540
|
public static getClientConfig() {
|
|
541
|
+
const tilmeld = enforceTilmeld(this);
|
|
469
542
|
return {
|
|
470
|
-
regFields:
|
|
471
|
-
userFields:
|
|
472
|
-
emailUsernames:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
543
|
+
regFields: tilmeld.config.regFields,
|
|
544
|
+
userFields: tilmeld.config.userFields,
|
|
545
|
+
emailUsernames: tilmeld.config.emailUsernames,
|
|
546
|
+
domainSupport: tilmeld.config.domainSupport,
|
|
547
|
+
allowRegistration: tilmeld.config.allowRegistration,
|
|
548
|
+
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
549
|
+
pwRecovery: tilmeld.config.pwRecovery,
|
|
550
|
+
verifyEmail: tilmeld.config.verifyEmail,
|
|
551
|
+
unverifiedAccess: tilmeld.config.unverifiedAccess,
|
|
478
552
|
};
|
|
479
553
|
}
|
|
480
554
|
|
|
481
555
|
public static async loginUser(data: {
|
|
482
556
|
username: string;
|
|
483
557
|
password: string;
|
|
558
|
+
code?: string;
|
|
484
559
|
additionalData?: { [k: string]: any };
|
|
485
560
|
}) {
|
|
561
|
+
const tilmeld = enforceTilmeld(this);
|
|
486
562
|
if (!('username' in data) || !data.username.length) {
|
|
487
563
|
return { result: false, message: 'Incorrect login/password.' };
|
|
488
564
|
}
|
|
489
|
-
const user = await
|
|
565
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
490
566
|
const result: { result: boolean; message: string; user?: User & UserData } =
|
|
491
567
|
await user.$login(data);
|
|
492
568
|
if (result.result) {
|
|
@@ -496,25 +572,149 @@ export default class User extends AbleObject<UserData> {
|
|
|
496
572
|
return result;
|
|
497
573
|
}
|
|
498
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Get the users in a domain. This is only accessible to the domain's admins.
|
|
577
|
+
*/
|
|
578
|
+
public static async getDomainUsers(
|
|
579
|
+
domain: string,
|
|
580
|
+
options?: {
|
|
581
|
+
limit?: number;
|
|
582
|
+
offset?: number;
|
|
583
|
+
sort?: string;
|
|
584
|
+
reverse?: boolean;
|
|
585
|
+
},
|
|
586
|
+
) {
|
|
587
|
+
const tilmeld = enforceTilmeld(this);
|
|
588
|
+
|
|
589
|
+
if (!tilmeld.config.domainSupport) {
|
|
590
|
+
throw new AccessControlError('Domain support is not enabled.');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (!domain) {
|
|
594
|
+
throw new BadDataError('Domain is required.');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
domain = domain.toLowerCase();
|
|
598
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
599
|
+
throw new BadDataError(tilmeld.config.validDomainRegexNotice);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
603
|
+
throw new AccessControlError(
|
|
604
|
+
'You do not have the authority to manage this domain.',
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return await this.nymph.getEntities(
|
|
609
|
+
{
|
|
610
|
+
class: this.nymph.getEntityClass(User),
|
|
611
|
+
...(options?.limit != null
|
|
612
|
+
? {
|
|
613
|
+
limit: options.limit,
|
|
614
|
+
}
|
|
615
|
+
: {}),
|
|
616
|
+
...(options?.offset != null
|
|
617
|
+
? {
|
|
618
|
+
offset: options.offset,
|
|
619
|
+
}
|
|
620
|
+
: {}),
|
|
621
|
+
...(options?.sort != null
|
|
622
|
+
? {
|
|
623
|
+
sort: options.sort,
|
|
624
|
+
}
|
|
625
|
+
: {}),
|
|
626
|
+
...(options?.reverse != null
|
|
627
|
+
? {
|
|
628
|
+
reverse: options.reverse,
|
|
629
|
+
}
|
|
630
|
+
: {}),
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
type: '&',
|
|
634
|
+
like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
|
|
635
|
+
},
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
constructor() {
|
|
640
|
+
super();
|
|
641
|
+
|
|
642
|
+
// Defaults.
|
|
643
|
+
this.$data.enabled = true;
|
|
644
|
+
this.$data.abilities = [];
|
|
645
|
+
this.$data.groups = [];
|
|
646
|
+
this.$data.inheritAbilities = true;
|
|
647
|
+
this.$updateDataProtection();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
$setNymph(nymph: Nymph) {
|
|
651
|
+
this.$nymph = nymph;
|
|
652
|
+
if (!this.$asleep()) {
|
|
653
|
+
if (this.$data.group && this.$data.group.$nymph !== nymph) {
|
|
654
|
+
this.$data.group.$setNymph(nymph);
|
|
655
|
+
}
|
|
656
|
+
for (const group of this.$data.groups ?? []) {
|
|
657
|
+
if (group && group.$nymph !== nymph) {
|
|
658
|
+
group.$setNymph(nymph);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async $getUniques(): Promise<string[]> {
|
|
665
|
+
const tilmeld = enforceTilmeld(this);
|
|
666
|
+
const uniques = [`u:${this.$data.username}`];
|
|
667
|
+
if (
|
|
668
|
+
!tilmeld.config.emailUsernames &&
|
|
669
|
+
tilmeld.config.userFields.includes('email')
|
|
670
|
+
) {
|
|
671
|
+
uniques.push(`e:${this.$data.email}`);
|
|
672
|
+
|
|
673
|
+
if (this.$data.newEmailAddress != null) {
|
|
674
|
+
uniques.push(`e:${this.$data.newEmailAddress}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return uniques;
|
|
678
|
+
}
|
|
679
|
+
|
|
499
680
|
public async $login(data: {
|
|
500
681
|
username: string;
|
|
501
682
|
password: string;
|
|
683
|
+
code?: string;
|
|
502
684
|
additionalData?: { [k: string]: any };
|
|
503
685
|
}) {
|
|
504
|
-
const tilmeld = this
|
|
686
|
+
const tilmeld = enforceTilmeld(this);
|
|
505
687
|
if (this.guid == null) {
|
|
506
688
|
return { result: false, message: 'Incorrect login/password.' };
|
|
507
689
|
}
|
|
508
690
|
if (!this.$data.enabled) {
|
|
509
691
|
return { result: false, message: 'This user is disabled.' };
|
|
510
692
|
}
|
|
511
|
-
if (this.$gatekeeper()) {
|
|
693
|
+
if (await this.$gatekeeper()) {
|
|
512
694
|
return { result: true, message: 'You are already logged in.' };
|
|
513
695
|
}
|
|
514
696
|
if (!this.$checkPassword(data.password)) {
|
|
515
697
|
return { result: false, message: 'Incorrect login/password.' };
|
|
516
698
|
}
|
|
517
699
|
|
|
700
|
+
if (this.$data.totpSecret != null) {
|
|
701
|
+
if (
|
|
702
|
+
data.code == null ||
|
|
703
|
+
typeof data.code !== 'string' ||
|
|
704
|
+
data.code.length !== 6
|
|
705
|
+
) {
|
|
706
|
+
return {
|
|
707
|
+
result: false,
|
|
708
|
+
needTOTP: true,
|
|
709
|
+
message: 'You need to provide a 2FA code.',
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!this.$checkTOTPCode(data.code)) {
|
|
714
|
+
return { result: false, message: 'Incorrect 2FA code.' };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
518
718
|
try {
|
|
519
719
|
for (let callback of (this.constructor as typeof User)
|
|
520
720
|
.beforeLoginCallbacks) {
|
|
@@ -529,8 +729,90 @@ export default class User extends AbleObject<UserData> {
|
|
|
529
729
|
};
|
|
530
730
|
}
|
|
531
731
|
|
|
732
|
+
this.$nymph.config.debugLog(
|
|
733
|
+
'tilmeld',
|
|
734
|
+
`User login for "${this.$data.username}".`,
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
// Authentication was successful, attempt to login.
|
|
738
|
+
if (!(await tilmeld.login(this, true))) {
|
|
739
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
for (let callback of (this.constructor as typeof User)
|
|
743
|
+
.afterLoginCallbacks) {
|
|
744
|
+
if (callback) {
|
|
745
|
+
await callback(this);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Login was successful.
|
|
750
|
+
return { result: true, message: 'You are logged in.' };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
|
|
754
|
+
const tilmeld = enforceTilmeld(this);
|
|
755
|
+
if (this.guid == null) {
|
|
756
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
let user = tilmeld.User.current();
|
|
760
|
+
|
|
761
|
+
if (!user || user.guid == null) {
|
|
762
|
+
return { result: false, message: "You're not logged in." };
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
766
|
+
|
|
767
|
+
if (isCurrentUser) {
|
|
768
|
+
return { result: false, message: "You can't switch to yourself." };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const toAbilities = this.$data.abilities || [];
|
|
772
|
+
const fromAbilities = user.abilities || [];
|
|
773
|
+
|
|
774
|
+
if (
|
|
775
|
+
!fromAbilities.includes('tilmeld/switch') &&
|
|
776
|
+
!fromAbilities.includes('system/admin')
|
|
777
|
+
) {
|
|
778
|
+
return {
|
|
779
|
+
result: false,
|
|
780
|
+
message: "You don't have permission to switch users.",
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (
|
|
785
|
+
toAbilities.includes('system/admin') ||
|
|
786
|
+
toAbilities.includes('tilmeld/admin')
|
|
787
|
+
) {
|
|
788
|
+
return { result: false, message: "You can't switch to an admin." };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
for (let callback of (this.constructor as typeof User)
|
|
793
|
+
.beforeLoginCallbacks) {
|
|
794
|
+
if (callback) {
|
|
795
|
+
await callback(this, {
|
|
796
|
+
username: this.$data.username || '',
|
|
797
|
+
password: '',
|
|
798
|
+
...(data || {}),
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
} catch (e: any) {
|
|
803
|
+
return {
|
|
804
|
+
result: false,
|
|
805
|
+
message: e.message,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
this.$nymph.config.debugLog(
|
|
810
|
+
'tilmeld',
|
|
811
|
+
`User login switch for "${user.username}" to "${this.$data.username}".`,
|
|
812
|
+
);
|
|
813
|
+
|
|
532
814
|
// Authentication was successful, attempt to login.
|
|
533
|
-
if (!tilmeld.
|
|
815
|
+
if (!(await tilmeld.loginSwitch(this, true))) {
|
|
534
816
|
return { result: false, message: 'Incorrect login/password.' };
|
|
535
817
|
}
|
|
536
818
|
|
|
@@ -550,7 +832,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
550
832
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
551
833
|
*/
|
|
552
834
|
public async $logout() {
|
|
553
|
-
const tilmeld = this
|
|
835
|
+
const tilmeld = enforceTilmeld(this);
|
|
554
836
|
|
|
555
837
|
try {
|
|
556
838
|
for (let callback of (this.constructor as typeof User)
|
|
@@ -566,7 +848,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
566
848
|
};
|
|
567
849
|
}
|
|
568
850
|
|
|
569
|
-
|
|
851
|
+
this.$nymph.config.debugLog(
|
|
852
|
+
'tilmeld',
|
|
853
|
+
`User logout for "${this.$data.username}".`,
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
await tilmeld.logout();
|
|
570
857
|
|
|
571
858
|
for (let callback of (this.constructor as typeof User)
|
|
572
859
|
.afterLogoutCallbacks) {
|
|
@@ -587,89 +874,176 @@ export default class User extends AbleObject<UserData> {
|
|
|
587
874
|
}
|
|
588
875
|
return (
|
|
589
876
|
'https://secure.gravatar.com/avatar/' +
|
|
590
|
-
|
|
877
|
+
crypto
|
|
878
|
+
.createHash('sha256')
|
|
879
|
+
.update(this.$data.email.trim().toLowerCase())
|
|
880
|
+
.digest('hex')
|
|
881
|
+
.toLowerCase() +
|
|
591
882
|
'?d=identicon&s=40'
|
|
592
883
|
);
|
|
593
884
|
}
|
|
594
885
|
|
|
886
|
+
public $getGid() {
|
|
887
|
+
if (this.$data.group) {
|
|
888
|
+
return this.$data.group.guid;
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
public $getGids() {
|
|
894
|
+
if (this.$data.groups) {
|
|
895
|
+
return this.$data.groups
|
|
896
|
+
.map((group) => group.guid)
|
|
897
|
+
.filter((guid) => guid != null) as string[];
|
|
898
|
+
}
|
|
899
|
+
return null;
|
|
900
|
+
}
|
|
901
|
+
|
|
595
902
|
/**
|
|
596
903
|
* Get the user's group descendants.
|
|
597
904
|
*/
|
|
598
|
-
public async $getDescendantGroups(): Promise<(
|
|
905
|
+
public async $getDescendantGroups(): Promise<(GroupClass & GroupData)[]> {
|
|
599
906
|
if (this.$descendantGroups == null) {
|
|
600
907
|
this.$descendantGroups = [];
|
|
601
908
|
if (this.$data.group != null) {
|
|
909
|
+
await this.$data.group.$wake();
|
|
602
910
|
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
603
911
|
}
|
|
912
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
604
913
|
for (let curGroup of this.$data.groups ?? []) {
|
|
605
914
|
this.$descendantGroups = this.$descendantGroups?.concat(
|
|
606
|
-
await curGroup.$getDescendants()
|
|
915
|
+
await curGroup.$getDescendants(),
|
|
607
916
|
);
|
|
608
917
|
}
|
|
609
918
|
}
|
|
610
919
|
return this.$descendantGroups;
|
|
611
920
|
}
|
|
612
921
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (
|
|
618
|
-
this.$
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
922
|
+
public $jsonAcceptData(input: EntityJson, allowConflict = false) {
|
|
923
|
+
const tilmeld = enforceTilmeld(this);
|
|
924
|
+
this.$check();
|
|
925
|
+
|
|
926
|
+
if ('abilities' in input.data) {
|
|
927
|
+
this.$checkIncomingAbilities(input.data.abilities);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// If the user is a new domain user, the data protection needs to be updated
|
|
931
|
+
// before the data is accepted, or else some things won't be accepted
|
|
932
|
+
// correctly (enabled, passwordTemp).
|
|
933
|
+
let reloadDataProtection = false;
|
|
934
|
+
if (tilmeld.config.domainSupport) {
|
|
935
|
+
let [_username, domain] = splitn(input.data.username ?? '', '@', 2);
|
|
936
|
+
// Lowercase the domain part.
|
|
937
|
+
if (domain) {
|
|
938
|
+
domain = domain.toLowerCase();
|
|
626
939
|
}
|
|
940
|
+
const user = tilmeld.User.current();
|
|
941
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
942
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
943
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
944
|
+
const abilities =
|
|
945
|
+
(isCurrentUser || user == null
|
|
946
|
+
? this.$data.abilities
|
|
947
|
+
: user?.abilities) ?? [];
|
|
948
|
+
const isNewUser = input.guid == null;
|
|
949
|
+
const isDomainUser =
|
|
950
|
+
!isCurrentUser &&
|
|
951
|
+
user != null &&
|
|
952
|
+
domain &&
|
|
953
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
954
|
+
|
|
955
|
+
reloadDataProtection = !!(isNewUser && isDomainUser);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
959
|
+
|
|
960
|
+
if (reloadDataProtection) {
|
|
961
|
+
// Load it again after data protection is run.
|
|
962
|
+
this.$updateDataProtection();
|
|
963
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
627
964
|
}
|
|
628
|
-
return this.$descendantGroups;
|
|
629
965
|
}
|
|
630
966
|
|
|
631
|
-
public $
|
|
632
|
-
|
|
633
|
-
|
|
967
|
+
public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
|
|
968
|
+
this.$check();
|
|
969
|
+
|
|
970
|
+
if ('abilities' in patch.set) {
|
|
971
|
+
this.$checkIncomingAbilities(patch.set.abilities);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
super.$jsonAcceptPatch(patch, allowConflict);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private $checkIncomingAbilities(incoming: any) {
|
|
978
|
+
const tilmeld = enforceTilmeld(this);
|
|
634
979
|
|
|
635
980
|
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
981
|
+
!Array.isArray(incoming) ||
|
|
982
|
+
incoming.find((ability) => typeof ability !== 'string')
|
|
983
|
+
) {
|
|
984
|
+
throw new BadDataError('Abilities must be an array of strings.');
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (
|
|
988
|
+
incoming.includes('system/admin') !==
|
|
989
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
990
|
+
(!tilmeld.currentUser ||
|
|
991
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
641
992
|
) {
|
|
642
993
|
throw new BadDataError(
|
|
643
|
-
"You don't have the authority to
|
|
994
|
+
"You don't have the authority to grant or revoke system/admin.",
|
|
644
995
|
);
|
|
645
996
|
}
|
|
646
997
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
998
|
+
if (
|
|
999
|
+
incoming.includes('tilmeld/admin') !==
|
|
1000
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
1001
|
+
(!tilmeld.currentUser ||
|
|
1002
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
1003
|
+
) {
|
|
1004
|
+
throw new BadDataError(
|
|
1005
|
+
"You don't have the authority to grant or revoke tilmeld/admin.",
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
654
1008
|
|
|
655
1009
|
if (
|
|
656
|
-
'
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
!tilmeld.gatekeeper('system/admin')
|
|
1010
|
+
incoming.includes('tilmeld/switch') !==
|
|
1011
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
1012
|
+
(!tilmeld.currentUser ||
|
|
1013
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
661
1014
|
) {
|
|
662
1015
|
throw new BadDataError(
|
|
663
|
-
"You don't have the authority to
|
|
1016
|
+
"You don't have the authority to grant or revoke tilmeld/switch.",
|
|
664
1017
|
);
|
|
665
1018
|
}
|
|
666
1019
|
|
|
667
|
-
|
|
668
|
-
|
|
1020
|
+
if (
|
|
1021
|
+
xor(
|
|
1022
|
+
incoming.filter((ability: string) => ability.startsWith('tilmeld/')),
|
|
1023
|
+
this.$data.abilities?.filter((ability: string) =>
|
|
1024
|
+
ability.startsWith('tilmeld/'),
|
|
1025
|
+
) ?? [],
|
|
1026
|
+
).length &&
|
|
1027
|
+
(!tilmeld.currentUser ||
|
|
1028
|
+
(!tilmeld.currentUser.abilities?.includes('system/admin') &&
|
|
1029
|
+
!tilmeld.currentUser.abilities?.includes('tilmeld/admin')))
|
|
1030
|
+
) {
|
|
1031
|
+
throw new BadDataError(
|
|
1032
|
+
"You don't have the authority to grant or revoke Tilmeld abilities.",
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
669
1035
|
}
|
|
670
1036
|
|
|
671
|
-
public $putData(
|
|
672
|
-
|
|
1037
|
+
public $putData(
|
|
1038
|
+
data: EntityData,
|
|
1039
|
+
sdata?: SerializedEntityData,
|
|
1040
|
+
source?: 'server',
|
|
1041
|
+
) {
|
|
1042
|
+
super.$putData(data, sdata, source);
|
|
1043
|
+
if (source === 'server') {
|
|
1044
|
+
this.$originalEmail = this.$data.email;
|
|
1045
|
+
this.$originalUsername = this.$data.username;
|
|
1046
|
+
}
|
|
673
1047
|
this.$updateDataProtection();
|
|
674
1048
|
}
|
|
675
1049
|
|
|
@@ -679,8 +1053,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
679
1053
|
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
680
1054
|
*/
|
|
681
1055
|
public $updateDataProtection(givenUser?: User & UserData) {
|
|
682
|
-
const tilmeld = this
|
|
1056
|
+
const tilmeld = enforceTilmeld(this);
|
|
683
1057
|
let user = givenUser ?? tilmeld.User.current();
|
|
1058
|
+
let [_username, domain] = tilmeld.config.domainSupport
|
|
1059
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1060
|
+
: [this.$data.username, null];
|
|
1061
|
+
// Lowercase the domain part.
|
|
1062
|
+
if (domain) {
|
|
1063
|
+
domain = domain.toLowerCase();
|
|
1064
|
+
}
|
|
684
1065
|
|
|
685
1066
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
686
1067
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
@@ -698,53 +1079,165 @@ export default class User extends AbleObject<UserData> {
|
|
|
698
1079
|
? this.$data.abilities
|
|
699
1080
|
: user?.abilities) ?? [];
|
|
700
1081
|
const isNewUser = this.guid == null;
|
|
1082
|
+
const isDomainUser =
|
|
1083
|
+
tilmeld.config.domainSupport &&
|
|
1084
|
+
!isCurrentUser &&
|
|
1085
|
+
user != null &&
|
|
1086
|
+
domain &&
|
|
1087
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
701
1088
|
|
|
702
1089
|
if (isCurrentUser) {
|
|
703
1090
|
// Users can check to see what abilities they have.
|
|
704
1091
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
705
1092
|
this.$clientEnabledMethods.push('$changePassword');
|
|
1093
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
706
1094
|
this.$clientEnabledMethods.push('$logout');
|
|
707
1095
|
this.$clientEnabledMethods.push('$sendEmailVerification');
|
|
1096
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1097
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
1098
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
1099
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (isDomainUser) {
|
|
1103
|
+
// Domain admins can check to see what abilities their users have.
|
|
1104
|
+
this.$clientEnabledMethods.push('$gatekeeper');
|
|
1105
|
+
this.$clientEnabledMethods.push('$changePassword');
|
|
1106
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
1107
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1108
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
1109
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
1110
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
708
1111
|
}
|
|
709
1112
|
|
|
710
1113
|
if (
|
|
711
1114
|
user != null &&
|
|
712
|
-
(abilities.
|
|
713
|
-
abilities.
|
|
1115
|
+
(abilities.includes('tilmeld/admin') ||
|
|
1116
|
+
abilities.includes('system/admin'))
|
|
714
1117
|
) {
|
|
715
1118
|
// Users who can edit other users can see most of their data.
|
|
716
|
-
this.$privateData = ['password', 'salt'];
|
|
1119
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
1120
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
1121
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
717
1122
|
this.$allowlistData = undefined;
|
|
718
|
-
} else if (
|
|
719
|
-
//
|
|
1123
|
+
} else if (isDomainUser) {
|
|
1124
|
+
// Domain admins can see their users' data, and edit some of it.
|
|
720
1125
|
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
721
1126
|
this.$allowlistData.push('username');
|
|
722
1127
|
}
|
|
723
1128
|
this.$allowlistData.push('avatar');
|
|
724
|
-
if (tilmeld.config.userFields.
|
|
1129
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
725
1130
|
this.$allowlistData.push('nameFirst');
|
|
726
1131
|
this.$allowlistData.push('nameMiddle');
|
|
727
1132
|
this.$allowlistData.push('nameLast');
|
|
728
1133
|
this.$allowlistData.push('name');
|
|
729
1134
|
}
|
|
730
|
-
if (tilmeld.config.userFields.
|
|
1135
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
731
1136
|
this.$allowlistData.push('email');
|
|
732
1137
|
}
|
|
733
|
-
if (tilmeld.config.userFields.
|
|
1138
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
734
1139
|
this.$allowlistData.push('phone');
|
|
735
1140
|
}
|
|
1141
|
+
this.$allowlistData.push('enabled');
|
|
1142
|
+
this.$allowlistData.push('passwordTemp');
|
|
736
1143
|
this.$privateData = [
|
|
1144
|
+
// The things commented are there to show what is _not_ private.
|
|
1145
|
+
// 'abilities',
|
|
1146
|
+
// 'username',
|
|
1147
|
+
// 'nameFirst',
|
|
1148
|
+
// 'nameMiddle',
|
|
1149
|
+
// 'nameLast',
|
|
1150
|
+
// 'name',
|
|
1151
|
+
// 'email',
|
|
1152
|
+
// 'avatar',
|
|
1153
|
+
// 'phone',
|
|
1154
|
+
// 'group',
|
|
1155
|
+
// 'groups',
|
|
1156
|
+
// 'inheritAbilities',
|
|
1157
|
+
// 'enabled',
|
|
737
1158
|
'secret',
|
|
1159
|
+
'emailChangeDate',
|
|
738
1160
|
'newEmailSecret',
|
|
739
|
-
'
|
|
1161
|
+
// 'newEmailAddress',
|
|
740
1162
|
'cancelEmailSecret',
|
|
741
|
-
'
|
|
1163
|
+
'cancelEmailAddress',
|
|
742
1164
|
'recoverSecret',
|
|
743
1165
|
'recoverSecretDate',
|
|
1166
|
+
'salt',
|
|
744
1167
|
'password',
|
|
1168
|
+
// 'passwordTemp',
|
|
1169
|
+
// 'revokeTokenDate',
|
|
1170
|
+
'totpSecret',
|
|
1171
|
+
];
|
|
1172
|
+
} else if (isCurrentUser || isNewUser) {
|
|
1173
|
+
// Users can see their own data, and edit some of it.
|
|
1174
|
+
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
1175
|
+
this.$allowlistData.push('username');
|
|
1176
|
+
}
|
|
1177
|
+
this.$allowlistData.push('avatar');
|
|
1178
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
1179
|
+
this.$allowlistData.push('nameFirst');
|
|
1180
|
+
this.$allowlistData.push('nameMiddle');
|
|
1181
|
+
this.$allowlistData.push('nameLast');
|
|
1182
|
+
this.$allowlistData.push('name');
|
|
1183
|
+
}
|
|
1184
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
1185
|
+
this.$allowlistData.push('email');
|
|
1186
|
+
}
|
|
1187
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
1188
|
+
this.$allowlistData.push('phone');
|
|
1189
|
+
}
|
|
1190
|
+
this.$privateData = [
|
|
1191
|
+
// The things commented are there to show what is _not_ private.
|
|
1192
|
+
// 'abilities',
|
|
1193
|
+
// 'username',
|
|
1194
|
+
// 'nameFirst',
|
|
1195
|
+
// 'nameMiddle',
|
|
1196
|
+
// 'nameLast',
|
|
1197
|
+
// 'name',
|
|
1198
|
+
// 'email',
|
|
1199
|
+
// 'avatar',
|
|
1200
|
+
// 'phone',
|
|
1201
|
+
// 'group',
|
|
1202
|
+
// 'groups',
|
|
1203
|
+
// 'inheritAbilities',
|
|
1204
|
+
'enabled',
|
|
1205
|
+
'secret',
|
|
1206
|
+
'emailChangeDate',
|
|
1207
|
+
'newEmailSecret',
|
|
1208
|
+
// 'newEmailAddress',
|
|
1209
|
+
'cancelEmailSecret',
|
|
1210
|
+
'cancelEmailAddress',
|
|
1211
|
+
'recoverSecret',
|
|
1212
|
+
'recoverSecretDate',
|
|
745
1213
|
'salt',
|
|
1214
|
+
'password',
|
|
1215
|
+
'passwordTemp',
|
|
1216
|
+
// 'revokeTokenDate',
|
|
1217
|
+
'totpSecret',
|
|
746
1218
|
];
|
|
747
1219
|
}
|
|
1220
|
+
|
|
1221
|
+
if (
|
|
1222
|
+
user != null &&
|
|
1223
|
+
!isCurrentUser &&
|
|
1224
|
+
(abilities.includes('tilmeld/switch') ||
|
|
1225
|
+
abilities.includes('system/admin'))
|
|
1226
|
+
) {
|
|
1227
|
+
// Users with this ability can switch to other users.
|
|
1228
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
public $updateGroupDataProtection() {
|
|
1233
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
1234
|
+
this.$data.group.$updateDataProtection();
|
|
1235
|
+
}
|
|
1236
|
+
for (let group of this.$data.groups ?? []) {
|
|
1237
|
+
if (!group.$asleep()) {
|
|
1238
|
+
group.$updateDataProtection();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
748
1241
|
}
|
|
749
1242
|
|
|
750
1243
|
/**
|
|
@@ -761,35 +1254,43 @@ export default class User extends AbleObject<UserData> {
|
|
|
761
1254
|
* @param ability The ability.
|
|
762
1255
|
* @returns True or false.
|
|
763
1256
|
*/
|
|
764
|
-
public $gatekeeper(ability?: string): boolean {
|
|
765
|
-
|
|
1257
|
+
public async $gatekeeper(ability?: string): Promise<boolean> {
|
|
1258
|
+
await this.$wake();
|
|
1259
|
+
const tilmeld = enforceTilmeld(this);
|
|
766
1260
|
if (ability == null) {
|
|
767
1261
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
768
1262
|
}
|
|
769
1263
|
// Check the cache to see if we've already checked this user.
|
|
770
1264
|
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
|
-
);
|
|
1265
|
+
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
789
1266
|
}
|
|
790
1267
|
return (
|
|
791
1268
|
(ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
792
|
-
!!this.$
|
|
1269
|
+
!!this.$data.abilities?.includes('system/admin')
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Build a gatekeeper cache object.
|
|
1275
|
+
*/
|
|
1276
|
+
public async $getGatekeeperCache() {
|
|
1277
|
+
await this.$wake();
|
|
1278
|
+
let abilities = this.$data.abilities ?? [];
|
|
1279
|
+
if (this.$data.inheritAbilities) {
|
|
1280
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1281
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
1282
|
+
if (curGroup.enabled) {
|
|
1283
|
+
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
await this.$data.group?.$wake();
|
|
1287
|
+
const group = this.$data.group;
|
|
1288
|
+
if (group != null && group.cdate != null && group.enabled) {
|
|
1289
|
+
abilities = abilities.concat(group.abilities ?? []);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return Object.fromEntries(
|
|
1293
|
+
abilities.map((ability) => [ability, true as true]),
|
|
793
1294
|
);
|
|
794
1295
|
}
|
|
795
1296
|
|
|
@@ -806,7 +1307,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
806
1307
|
* @returns True on success, false on failure.
|
|
807
1308
|
*/
|
|
808
1309
|
public async $sendEmailVerification() {
|
|
809
|
-
const tilmeld = this
|
|
1310
|
+
const tilmeld = enforceTilmeld(this);
|
|
810
1311
|
if (this.guid == null) {
|
|
811
1312
|
return false;
|
|
812
1313
|
}
|
|
@@ -818,7 +1319,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
818
1319
|
|
|
819
1320
|
if (this.$data.secret != null) {
|
|
820
1321
|
const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(
|
|
821
|
-
this.guid
|
|
1322
|
+
this.guid,
|
|
822
1323
|
)}&secret=${encodeURIComponent(this.$data.secret)}`;
|
|
823
1324
|
success =
|
|
824
1325
|
success &&
|
|
@@ -836,13 +1337,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
836
1337
|
verifyLink: link,
|
|
837
1338
|
},
|
|
838
1339
|
},
|
|
839
|
-
this
|
|
1340
|
+
this,
|
|
840
1341
|
));
|
|
841
1342
|
}
|
|
842
1343
|
|
|
843
1344
|
if (this.$data.newEmailSecret != null) {
|
|
844
1345
|
const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(
|
|
845
|
-
this.guid
|
|
1346
|
+
this.guid,
|
|
846
1347
|
)}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
|
|
847
1348
|
success =
|
|
848
1349
|
success &&
|
|
@@ -862,13 +1363,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
862
1363
|
newEmail: this.$data.newEmailAddress,
|
|
863
1364
|
},
|
|
864
1365
|
},
|
|
865
|
-
this
|
|
1366
|
+
this,
|
|
866
1367
|
));
|
|
867
1368
|
}
|
|
868
1369
|
|
|
869
1370
|
if (this.$data.cancelEmailSecret != null) {
|
|
870
1371
|
const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(
|
|
871
|
-
this.guid
|
|
1372
|
+
this.guid,
|
|
872
1373
|
)}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
|
|
873
1374
|
success =
|
|
874
1375
|
success &&
|
|
@@ -888,7 +1389,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
888
1389
|
newEmail: this.$data.newEmailAddress ?? this.$data.email,
|
|
889
1390
|
},
|
|
890
1391
|
},
|
|
891
|
-
this
|
|
1392
|
+
this,
|
|
892
1393
|
));
|
|
893
1394
|
}
|
|
894
1395
|
|
|
@@ -901,13 +1402,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
901
1402
|
* @param group The group.
|
|
902
1403
|
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
903
1404
|
*/
|
|
904
|
-
public $addGroup(group:
|
|
905
|
-
|
|
906
|
-
|
|
1405
|
+
public async $addGroup(group: GroupClass & GroupData) {
|
|
1406
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1407
|
+
let groups = this.$data.groups;
|
|
1408
|
+
if (groups == null) {
|
|
1409
|
+
groups = [];
|
|
907
1410
|
}
|
|
908
|
-
if (!group.$inArray(
|
|
909
|
-
|
|
910
|
-
|
|
1411
|
+
if (!group.$inArray(groups)) {
|
|
1412
|
+
groups.push(group);
|
|
1413
|
+
this.$data.groups = groups;
|
|
1414
|
+
return groups;
|
|
911
1415
|
}
|
|
912
1416
|
return true;
|
|
913
1417
|
}
|
|
@@ -919,17 +1423,23 @@ export default class User extends AbleObject<UserData> {
|
|
|
919
1423
|
* @returns True if the passwords match, otherwise false.
|
|
920
1424
|
*/
|
|
921
1425
|
public $checkPassword(password: string) {
|
|
922
|
-
const tilmeld = this
|
|
1426
|
+
const tilmeld = enforceTilmeld(this);
|
|
923
1427
|
switch (tilmeld.config.pwMethod) {
|
|
924
1428
|
case 'plain':
|
|
925
1429
|
return this.$data.password === password;
|
|
926
1430
|
case 'digest':
|
|
927
|
-
return
|
|
1431
|
+
return (
|
|
1432
|
+
this.$data.password ===
|
|
1433
|
+
crypto.createHash('sha256').update(password).digest('base64')
|
|
1434
|
+
);
|
|
928
1435
|
case 'salt':
|
|
929
1436
|
default:
|
|
930
1437
|
return (
|
|
931
1438
|
this.$data.password ===
|
|
932
|
-
|
|
1439
|
+
crypto
|
|
1440
|
+
.createHash('sha256')
|
|
1441
|
+
.update(password + this.$data.salt)
|
|
1442
|
+
.digest('base64')
|
|
933
1443
|
);
|
|
934
1444
|
}
|
|
935
1445
|
}
|
|
@@ -940,10 +1450,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
940
1450
|
* @param group The group.
|
|
941
1451
|
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
942
1452
|
*/
|
|
943
|
-
public $delGroup(group:
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1453
|
+
public async $delGroup(group: GroupClass & GroupData) {
|
|
1454
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1455
|
+
let groups = this.$data.groups || [];
|
|
1456
|
+
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
1457
|
+
const newGroups: (GroupClass & GroupData)[] = [];
|
|
1458
|
+
for (let curGroup of groups) {
|
|
947
1459
|
if (!group.$is(curGroup)) {
|
|
948
1460
|
newGroups.push(curGroup);
|
|
949
1461
|
}
|
|
@@ -960,7 +1472,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
960
1472
|
* @param group The group, or the group's GUID.
|
|
961
1473
|
* @returns True or false.
|
|
962
1474
|
*/
|
|
963
|
-
public $inGroup(group: (
|
|
1475
|
+
public async $inGroup(group: (GroupClass & GroupData) | string) {
|
|
964
1476
|
if (typeof group === 'string') {
|
|
965
1477
|
if (this.$data.group?.guid === group) {
|
|
966
1478
|
return true;
|
|
@@ -983,16 +1495,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
983
1495
|
* @param group The group, or the group's GUID.
|
|
984
1496
|
* @returns True or false.
|
|
985
1497
|
*/
|
|
986
|
-
public $isDescendant(group: (
|
|
1498
|
+
public async $isDescendant(group: (GroupClass & GroupData) | string) {
|
|
987
1499
|
// Check to see if the user is in a descendant group of the given group.
|
|
1500
|
+
await this.$data.group?.$wake();
|
|
988
1501
|
if (
|
|
1502
|
+
this.$data.group != null &&
|
|
989
1503
|
this.$data.group?.cdate != null &&
|
|
990
|
-
this.$data.group
|
|
1504
|
+
(await this.$data.group?.$isDescendant(group))
|
|
991
1505
|
) {
|
|
992
1506
|
return true;
|
|
993
1507
|
}
|
|
1508
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
994
1509
|
for (let curGroup of this.$data.groups ?? []) {
|
|
995
|
-
if (curGroup.$isDescendant(group)) {
|
|
1510
|
+
if (await curGroup.$isDescendant(group)) {
|
|
996
1511
|
return true;
|
|
997
1512
|
}
|
|
998
1513
|
}
|
|
@@ -1008,15 +1523,31 @@ export default class User extends AbleObject<UserData> {
|
|
|
1008
1523
|
public async $changePassword(data: {
|
|
1009
1524
|
newPassword: string;
|
|
1010
1525
|
currentPassword: string;
|
|
1526
|
+
revokeCurrentTokens?: boolean;
|
|
1011
1527
|
}): Promise<{ result: boolean; message: string }> {
|
|
1528
|
+
this.$nymph.config.debugLog(
|
|
1529
|
+
'tilmeld',
|
|
1530
|
+
`Password change request for "${this.$data.username}".`,
|
|
1531
|
+
);
|
|
1012
1532
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
1013
1533
|
return { result: false, message: 'Please specify a password.' };
|
|
1014
1534
|
}
|
|
1015
1535
|
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
1536
|
+
this.$nymph.config.debugLog(
|
|
1537
|
+
'tilmeld',
|
|
1538
|
+
`Incorrect password for change request for "${this.$data.username}".`,
|
|
1539
|
+
);
|
|
1016
1540
|
return { result: false, message: 'Incorrect password.' };
|
|
1017
1541
|
}
|
|
1018
1542
|
this.$data.passwordTemp = data.newPassword;
|
|
1543
|
+
if (data.revokeCurrentTokens) {
|
|
1544
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1545
|
+
}
|
|
1019
1546
|
if (await this.$save()) {
|
|
1547
|
+
if (data.revokeCurrentTokens) {
|
|
1548
|
+
const tilmeld = enforceTilmeld(this);
|
|
1549
|
+
await tilmeld.login(this, true);
|
|
1550
|
+
}
|
|
1020
1551
|
return { result: true, message: 'Your password has been changed.' };
|
|
1021
1552
|
} else {
|
|
1022
1553
|
return { result: false, message: "Couldn't save new password." };
|
|
@@ -1030,7 +1561,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1030
1561
|
* @returns The resulting password or hash which is stored in the entity.
|
|
1031
1562
|
*/
|
|
1032
1563
|
public $password(password: string) {
|
|
1033
|
-
const tilmeld = this
|
|
1564
|
+
const tilmeld = enforceTilmeld(this);
|
|
1034
1565
|
switch (tilmeld.config.pwMethod) {
|
|
1035
1566
|
case 'plain':
|
|
1036
1567
|
delete this.$data.salt;
|
|
@@ -1038,34 +1569,275 @@ export default class User extends AbleObject<UserData> {
|
|
|
1038
1569
|
break;
|
|
1039
1570
|
case 'digest':
|
|
1040
1571
|
delete this.$data.salt;
|
|
1041
|
-
this.$data.password =
|
|
1572
|
+
this.$data.password = crypto
|
|
1573
|
+
.createHash('sha256')
|
|
1574
|
+
.update(password)
|
|
1575
|
+
.digest('base64');
|
|
1042
1576
|
break;
|
|
1043
1577
|
case 'salt':
|
|
1044
1578
|
default:
|
|
1045
1579
|
this.$data.salt = nanoid();
|
|
1046
|
-
this.$data.password =
|
|
1047
|
-
|
|
1048
|
-
|
|
1580
|
+
this.$data.password = crypto
|
|
1581
|
+
.createHash('sha256')
|
|
1582
|
+
.update(password + this.$data.salt)
|
|
1583
|
+
.digest('base64');
|
|
1049
1584
|
break;
|
|
1050
1585
|
}
|
|
1051
1586
|
return this.$data.password;
|
|
1052
1587
|
}
|
|
1053
1588
|
|
|
1589
|
+
/**
|
|
1590
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
1591
|
+
*
|
|
1592
|
+
* @param data The input data from the client.
|
|
1593
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1594
|
+
*/
|
|
1595
|
+
public async $revokeCurrentTokens(data: {
|
|
1596
|
+
password: string;
|
|
1597
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1598
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1599
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1603
|
+
|
|
1604
|
+
if (await this.$save()) {
|
|
1605
|
+
const tilmeld = enforceTilmeld(this);
|
|
1606
|
+
await tilmeld.login(this, true);
|
|
1607
|
+
return {
|
|
1608
|
+
result: true,
|
|
1609
|
+
message: 'You have logged out of all other sessions.',
|
|
1610
|
+
};
|
|
1611
|
+
} else {
|
|
1612
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
public async $hasTOTPSecret() {
|
|
1617
|
+
return this.$data.totpSecret != null;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Check the given code against the user's TOTP secret.
|
|
1622
|
+
*
|
|
1623
|
+
* @param code The code in question.
|
|
1624
|
+
* @returns True if the code is valid, otherwise false.
|
|
1625
|
+
*/
|
|
1626
|
+
public $checkTOTPCode(code: string) {
|
|
1627
|
+
if (this.$data.totpSecret == null) {
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
const tilmeld = enforceTilmeld(this);
|
|
1632
|
+
|
|
1633
|
+
const totp = new TOTP({
|
|
1634
|
+
issuer: tilmeld.config.appName,
|
|
1635
|
+
label: this.$data.username,
|
|
1636
|
+
algorithm: 'SHA1',
|
|
1637
|
+
digits: 6,
|
|
1638
|
+
period: 30,
|
|
1639
|
+
secret: this.$data.totpSecret,
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
1647
|
+
*
|
|
1648
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
1649
|
+
*/
|
|
1650
|
+
public async $getNewTOTPSecret() {
|
|
1651
|
+
const tilmeld = enforceTilmeld(this);
|
|
1652
|
+
|
|
1653
|
+
if (this.$data.totpSecret != null) {
|
|
1654
|
+
throw new BadDataError('You already have a 2FA secret.');
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const secret = new Secret();
|
|
1658
|
+
|
|
1659
|
+
const totp = new TOTP({
|
|
1660
|
+
issuer: tilmeld.config.appName,
|
|
1661
|
+
label: this.$data.username,
|
|
1662
|
+
algorithm: 'SHA1',
|
|
1663
|
+
digits: 6,
|
|
1664
|
+
period: 30,
|
|
1665
|
+
secret,
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
const uri = totp.toString();
|
|
1669
|
+
|
|
1670
|
+
const qrcode = await toDataURL(uri);
|
|
1671
|
+
|
|
1672
|
+
return {
|
|
1673
|
+
uri,
|
|
1674
|
+
qrcode,
|
|
1675
|
+
secret: secret.base32,
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1681
|
+
*
|
|
1682
|
+
* @param data The input data from the client.
|
|
1683
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1684
|
+
*/
|
|
1685
|
+
public async $saveTOTPSecret(data: {
|
|
1686
|
+
password: string;
|
|
1687
|
+
secret: string;
|
|
1688
|
+
code: string;
|
|
1689
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1690
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1691
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (this.$data.totpSecret != null) {
|
|
1695
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (
|
|
1699
|
+
data.secret == null ||
|
|
1700
|
+
typeof data.secret !== 'string' ||
|
|
1701
|
+
data.secret === ''
|
|
1702
|
+
) {
|
|
1703
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
if (
|
|
1707
|
+
data.code == null ||
|
|
1708
|
+
typeof data.code !== 'string' ||
|
|
1709
|
+
data.code.length !== 6
|
|
1710
|
+
) {
|
|
1711
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const tilmeld = enforceTilmeld(this);
|
|
1715
|
+
|
|
1716
|
+
const totp = new TOTP({
|
|
1717
|
+
issuer: tilmeld.config.appName,
|
|
1718
|
+
label: this.$data.username,
|
|
1719
|
+
algorithm: 'SHA1',
|
|
1720
|
+
digits: 6,
|
|
1721
|
+
period: 30,
|
|
1722
|
+
secret: data.secret,
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1726
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
this.$data.totpSecret = data.secret;
|
|
1730
|
+
|
|
1731
|
+
if (await this.$save()) {
|
|
1732
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1733
|
+
} else {
|
|
1734
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1740
|
+
* account.
|
|
1741
|
+
*
|
|
1742
|
+
* @param data The input data from the client.
|
|
1743
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1744
|
+
*/
|
|
1745
|
+
public async $removeTOTPSecret(data?: {
|
|
1746
|
+
password: string;
|
|
1747
|
+
code: string;
|
|
1748
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1749
|
+
const tilmeld = enforceTilmeld(this);
|
|
1750
|
+
|
|
1751
|
+
if (data) {
|
|
1752
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1753
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
if (
|
|
1757
|
+
data.code == null ||
|
|
1758
|
+
typeof data.code !== 'string' ||
|
|
1759
|
+
data.code.length !== 6
|
|
1760
|
+
) {
|
|
1761
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (this.$data.totpSecret == null) {
|
|
1765
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
const totp = new TOTP({
|
|
1769
|
+
issuer: tilmeld.config.appName,
|
|
1770
|
+
label: this.$data.username,
|
|
1771
|
+
algorithm: 'SHA1',
|
|
1772
|
+
digits: 6,
|
|
1773
|
+
period: 30,
|
|
1774
|
+
secret: this.$data.totpSecret,
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1778
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1779
|
+
}
|
|
1780
|
+
} else if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1781
|
+
return {
|
|
1782
|
+
result: false,
|
|
1783
|
+
message: 'You must provide your password and 2FA code.',
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
delete this.$data.totpSecret;
|
|
1788
|
+
|
|
1789
|
+
if (await this.$save()) {
|
|
1790
|
+
return {
|
|
1791
|
+
result: true,
|
|
1792
|
+
message: 'Two factor secret has been removed.',
|
|
1793
|
+
};
|
|
1794
|
+
} else {
|
|
1795
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1054
1799
|
/**
|
|
1055
1800
|
* Check that a username is valid.
|
|
1056
1801
|
*
|
|
1057
1802
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1058
1803
|
*/
|
|
1059
1804
|
public async $checkUsername(): Promise<{ result: boolean; message: string }> {
|
|
1060
|
-
const tilmeld = this
|
|
1805
|
+
const tilmeld = enforceTilmeld(this);
|
|
1806
|
+
|
|
1807
|
+
this.$nymph.config.debugInfo(
|
|
1808
|
+
'tilmeld',
|
|
1809
|
+
`Username check "${this.$data.username}".`,
|
|
1810
|
+
);
|
|
1811
|
+
|
|
1061
1812
|
if (!tilmeld.config.emailUsernames) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1813
|
+
let [username, domain] = tilmeld.config.domainSupport
|
|
1814
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1815
|
+
: [this.$data.username, null];
|
|
1816
|
+
|
|
1817
|
+
// Lowercase the domain part.
|
|
1818
|
+
if (domain) {
|
|
1819
|
+
domain = domain.toLowerCase();
|
|
1820
|
+
this.$data.username = `${username}@${domain}`;
|
|
1064
1821
|
}
|
|
1822
|
+
|
|
1065
1823
|
if (
|
|
1066
|
-
|
|
1067
|
-
this.$data.username.length
|
|
1824
|
+
this.$data.username == null ||
|
|
1825
|
+
!this.$data.username.length ||
|
|
1826
|
+
username == null ||
|
|
1827
|
+
!username.length
|
|
1068
1828
|
) {
|
|
1829
|
+
return { result: false, message: 'Please specify a username.' };
|
|
1830
|
+
}
|
|
1831
|
+
if (username.length < tilmeld.config.minUsernameLength) {
|
|
1832
|
+
return {
|
|
1833
|
+
result: false,
|
|
1834
|
+
message:
|
|
1835
|
+
'Usernames must be at least ' +
|
|
1836
|
+
tilmeld.config.minUsernameLength +
|
|
1837
|
+
' characters.',
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
if (username.length > tilmeld.config.maxUsernameLength) {
|
|
1069
1841
|
return {
|
|
1070
1842
|
result: false,
|
|
1071
1843
|
message:
|
|
@@ -1076,22 +1848,38 @@ export default class User extends AbleObject<UserData> {
|
|
|
1076
1848
|
}
|
|
1077
1849
|
|
|
1078
1850
|
if (
|
|
1079
|
-
difference(
|
|
1080
|
-
|
|
1081
|
-
tilmeld.config.validChars.split('')
|
|
1082
|
-
).length
|
|
1851
|
+
difference(username.split(''), tilmeld.config.validChars.split(''))
|
|
1852
|
+
.length
|
|
1083
1853
|
) {
|
|
1084
1854
|
return {
|
|
1085
1855
|
result: false,
|
|
1086
1856
|
message: tilmeld.config.validCharsNotice,
|
|
1087
1857
|
};
|
|
1088
1858
|
}
|
|
1089
|
-
if (!tilmeld.config.validRegex.test(
|
|
1859
|
+
if (!tilmeld.config.validRegex.test(username)) {
|
|
1090
1860
|
return {
|
|
1091
1861
|
result: false,
|
|
1092
1862
|
message: tilmeld.config.validRegexNotice,
|
|
1093
1863
|
};
|
|
1094
1864
|
}
|
|
1865
|
+
if (domain != null) {
|
|
1866
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
1867
|
+
return {
|
|
1868
|
+
result: false,
|
|
1869
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
if (
|
|
1873
|
+
!this.$is(tilmeld.currentUser) &&
|
|
1874
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
1875
|
+
) {
|
|
1876
|
+
return {
|
|
1877
|
+
result: false,
|
|
1878
|
+
message:
|
|
1879
|
+
"You don't have the authority to manage users on this domain.",
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1095
1883
|
|
|
1096
1884
|
const selector: Selector = {
|
|
1097
1885
|
type: '&',
|
|
@@ -1105,12 +1893,24 @@ export default class User extends AbleObject<UserData> {
|
|
|
1105
1893
|
}
|
|
1106
1894
|
const test = await this.$nymph.getEntity(
|
|
1107
1895
|
{ class: tilmeld.User, skipAc: true },
|
|
1108
|
-
selector
|
|
1896
|
+
selector,
|
|
1109
1897
|
);
|
|
1110
1898
|
if (test != null) {
|
|
1111
1899
|
return { result: false, message: 'That username is taken.' };
|
|
1112
1900
|
}
|
|
1113
1901
|
|
|
1902
|
+
for (let callback of (this.constructor as typeof User)
|
|
1903
|
+
.checkUsernameCallbacks) {
|
|
1904
|
+
if (callback) {
|
|
1905
|
+
const result = await callback(this, {
|
|
1906
|
+
username: this.$data.username,
|
|
1907
|
+
});
|
|
1908
|
+
if (!result.result) {
|
|
1909
|
+
return { message: 'Username is not available.', ...result };
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1114
1914
|
return {
|
|
1115
1915
|
result: true,
|
|
1116
1916
|
message:
|
|
@@ -1121,10 +1921,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
1121
1921
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
1122
1922
|
return { result: false, message: 'Please specify an email.' };
|
|
1123
1923
|
}
|
|
1124
|
-
if (
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1924
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1925
|
+
return {
|
|
1926
|
+
result: false,
|
|
1927
|
+
message:
|
|
1928
|
+
'Emails must be at least ' +
|
|
1929
|
+
tilmeld.config.minUsernameLength +
|
|
1930
|
+
' characters.',
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1128
1934
|
return {
|
|
1129
1935
|
result: false,
|
|
1130
1936
|
message:
|
|
@@ -1144,7 +1950,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1144
1950
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1145
1951
|
*/
|
|
1146
1952
|
public async $checkEmail(): Promise<{ result: boolean; message: string }> {
|
|
1147
|
-
const tilmeld = this
|
|
1953
|
+
const tilmeld = enforceTilmeld(this);
|
|
1954
|
+
|
|
1955
|
+
this.$nymph.config.debugInfo(
|
|
1956
|
+
'tilmeld',
|
|
1957
|
+
`Email check "${this.$data.email}".`,
|
|
1958
|
+
);
|
|
1959
|
+
|
|
1148
1960
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
1149
1961
|
if (tilmeld.config.verifyEmail) {
|
|
1150
1962
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -1167,7 +1979,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1167
1979
|
}
|
|
1168
1980
|
const test = await this.$nymph.getEntity(
|
|
1169
1981
|
{ class: tilmeld.User, skipAc: true },
|
|
1170
|
-
selector
|
|
1982
|
+
selector,
|
|
1171
1983
|
);
|
|
1172
1984
|
if (test != null) {
|
|
1173
1985
|
return {
|
|
@@ -1189,7 +2001,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1189
2001
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1190
2002
|
*/
|
|
1191
2003
|
public async $checkPhone(): Promise<{ result: boolean; message: string }> {
|
|
1192
|
-
const tilmeld = this
|
|
2004
|
+
const tilmeld = enforceTilmeld(this);
|
|
2005
|
+
|
|
2006
|
+
this.$nymph.config.debugInfo(
|
|
2007
|
+
'tilmeld',
|
|
2008
|
+
`Phone check "${this.$data.phone}".`,
|
|
2009
|
+
);
|
|
2010
|
+
|
|
1193
2011
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
1194
2012
|
return { result: false, message: 'Please specify a phone number.' };
|
|
1195
2013
|
}
|
|
@@ -1211,7 +2029,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1211
2029
|
}
|
|
1212
2030
|
const test = await this.$nymph.getEntity(
|
|
1213
2031
|
{ class: tilmeld.User, skipAc: true },
|
|
1214
|
-
selector
|
|
2032
|
+
selector,
|
|
1215
2033
|
);
|
|
1216
2034
|
if (test != null) {
|
|
1217
2035
|
return { result: false, message: 'Phone number is in use.' };
|
|
@@ -1228,7 +2046,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1228
2046
|
password: string;
|
|
1229
2047
|
additionalData?: { [k: string]: any };
|
|
1230
2048
|
}): Promise<{ result: boolean; loggedin: boolean; message: string }> {
|
|
1231
|
-
let tilmeld = this
|
|
2049
|
+
let tilmeld = enforceTilmeld(this);
|
|
1232
2050
|
if (!tilmeld.config.allowRegistration) {
|
|
1233
2051
|
return {
|
|
1234
2052
|
result: false,
|
|
@@ -1244,21 +2062,6 @@ export default class User extends AbleObject<UserData> {
|
|
|
1244
2062
|
};
|
|
1245
2063
|
}
|
|
1246
2064
|
|
|
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
2065
|
if (!('password' in data) || !data.password.length) {
|
|
1263
2066
|
return {
|
|
1264
2067
|
result: false,
|
|
@@ -1272,7 +2075,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1272
2075
|
}
|
|
1273
2076
|
|
|
1274
2077
|
this.$password(data.password);
|
|
1275
|
-
if (tilmeld.config.regFields.
|
|
2078
|
+
if (tilmeld.config.regFields.includes('name')) {
|
|
1276
2079
|
this.$data.name =
|
|
1277
2080
|
this.$data.nameFirst +
|
|
1278
2081
|
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
@@ -1289,75 +2092,34 @@ export default class User extends AbleObject<UserData> {
|
|
|
1289
2092
|
const transaction = 'tilmeld-register-' + nanoid();
|
|
1290
2093
|
const nymph = this.$nymph;
|
|
1291
2094
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1292
|
-
this.$
|
|
1293
|
-
tilmeld = this
|
|
2095
|
+
this.$setNymph(tnymph);
|
|
2096
|
+
tilmeld = enforceTilmeld(this);
|
|
2097
|
+
|
|
2098
|
+
let message = '';
|
|
2099
|
+
let loggedin = false;
|
|
1294
2100
|
|
|
1295
2101
|
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],
|
|
2102
|
+
try {
|
|
2103
|
+
for (let callback of (this.constructor as typeof User)
|
|
2104
|
+
.beforeRegisterCallbacks) {
|
|
2105
|
+
if (callback) {
|
|
2106
|
+
await callback(this, data);
|
|
1332
2107
|
}
|
|
1333
|
-
);
|
|
1334
|
-
if (group != null) {
|
|
1335
|
-
this.$data.group = group;
|
|
1336
2108
|
}
|
|
2109
|
+
} catch (e: any) {
|
|
2110
|
+
await tnymph.rollback(transaction);
|
|
2111
|
+
this.$setNymph(nymph);
|
|
2112
|
+
return {
|
|
2113
|
+
result: false,
|
|
2114
|
+
loggedin: false,
|
|
2115
|
+
message: e.message,
|
|
2116
|
+
};
|
|
1337
2117
|
}
|
|
1338
2118
|
|
|
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) {
|
|
2119
|
+
if (
|
|
2120
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2121
|
+
tilmeld.config.verifyEmail
|
|
2122
|
+
) {
|
|
1361
2123
|
// The user will be enabled after verifying their e-mail address.
|
|
1362
2124
|
if (!tilmeld.config.unverifiedAccess) {
|
|
1363
2125
|
this.$data.enabled = false;
|
|
@@ -1366,7 +2128,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1366
2128
|
this.$data.enabled = true;
|
|
1367
2129
|
}
|
|
1368
2130
|
|
|
1369
|
-
// If
|
|
2131
|
+
// If createAdmin is true and there are no other users, grant
|
|
1370
2132
|
// "system/admin".
|
|
1371
2133
|
let madeAdmin = false;
|
|
1372
2134
|
if (tilmeld.config.createAdmin) {
|
|
@@ -1384,7 +2146,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
1384
2146
|
}
|
|
1385
2147
|
}
|
|
1386
2148
|
|
|
2149
|
+
this.$nymph.config.debugLog(
|
|
2150
|
+
'tilmeld',
|
|
2151
|
+
`Registering new user "${this.$data.username}".`,
|
|
2152
|
+
);
|
|
1387
2153
|
if (await this.$saveSkipAC()) {
|
|
2154
|
+
this.$nymph.config.debugLog(
|
|
2155
|
+
'tilmeld',
|
|
2156
|
+
`New user registered "${this.$data.username}".`,
|
|
2157
|
+
);
|
|
1388
2158
|
// Send the new user registered email.
|
|
1389
2159
|
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
1390
2160
|
await tilmeld.config.sendEmail(
|
|
@@ -1403,33 +2173,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
1403
2173
|
userPhone: this.$data.phone,
|
|
1404
2174
|
},
|
|
1405
2175
|
},
|
|
1406
|
-
this
|
|
2176
|
+
this,
|
|
1407
2177
|
);
|
|
1408
2178
|
}
|
|
1409
2179
|
|
|
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
2180
|
// Finish up.
|
|
1429
2181
|
if (
|
|
1430
2182
|
tilmeld.config.verifyEmail &&
|
|
1431
2183
|
!tilmeld.config.unverifiedAccess &&
|
|
1432
|
-
!madeAdmin
|
|
2184
|
+
!madeAdmin &&
|
|
2185
|
+
tilmeld.config.userFields.includes('email')
|
|
1433
2186
|
) {
|
|
1434
2187
|
message +=
|
|
1435
2188
|
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
@@ -1437,9 +2190,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1437
2190
|
} else if (
|
|
1438
2191
|
tilmeld.config.verifyEmail &&
|
|
1439
2192
|
tilmeld.config.unverifiedAccess &&
|
|
1440
|
-
!madeAdmin
|
|
2193
|
+
!madeAdmin &&
|
|
2194
|
+
tilmeld.config.userFields.includes('email')
|
|
1441
2195
|
) {
|
|
1442
|
-
if (!tilmeld.login(this, true)) {
|
|
2196
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1443
2197
|
throw new Error('An error occurred trying to log you in.');
|
|
1444
2198
|
}
|
|
1445
2199
|
message +=
|
|
@@ -1448,7 +2202,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1448
2202
|
'registration.';
|
|
1449
2203
|
loggedin = true;
|
|
1450
2204
|
} else {
|
|
1451
|
-
if (!tilmeld.login(this, true)) {
|
|
2205
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1452
2206
|
throw new Error('An error occurred trying to log you in.');
|
|
1453
2207
|
}
|
|
1454
2208
|
message += "You're now registered and logged in!";
|
|
@@ -1464,23 +2218,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1464
2218
|
});
|
|
1465
2219
|
}
|
|
1466
2220
|
}
|
|
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
2221
|
} else {
|
|
2222
|
+
this.$nymph.config.debugError(
|
|
2223
|
+
'tilmeld',
|
|
2224
|
+
`Error registering new user "${this.$data.username}".`,
|
|
2225
|
+
);
|
|
1482
2226
|
await tnymph.rollback(transaction);
|
|
1483
|
-
this.$nymph
|
|
2227
|
+
this.$setNymph(nymph);
|
|
1484
2228
|
return {
|
|
1485
2229
|
result: false,
|
|
1486
2230
|
loggedin: false,
|
|
@@ -1488,28 +2232,100 @@ export default class User extends AbleObject<UserData> {
|
|
|
1488
2232
|
};
|
|
1489
2233
|
}
|
|
1490
2234
|
} catch (e: any) {
|
|
2235
|
+
this.$nymph.config.debugError(
|
|
2236
|
+
'tilmeld',
|
|
2237
|
+
`Error registering new user "${this.$data.username}": ${e}`,
|
|
2238
|
+
);
|
|
1491
2239
|
await tnymph.rollback(transaction);
|
|
1492
|
-
this.$nymph
|
|
2240
|
+
this.$setNymph(nymph);
|
|
1493
2241
|
throw e;
|
|
1494
2242
|
}
|
|
2243
|
+
|
|
2244
|
+
try {
|
|
2245
|
+
await tnymph.commit(transaction);
|
|
2246
|
+
this.$setNymph(nymph);
|
|
2247
|
+
await this.$nymph.tilmeld?.fillSession(this);
|
|
2248
|
+
} catch (e: any) {
|
|
2249
|
+
throw e;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
return {
|
|
2253
|
+
result: true,
|
|
2254
|
+
loggedin,
|
|
2255
|
+
message,
|
|
2256
|
+
};
|
|
1495
2257
|
}
|
|
1496
2258
|
|
|
1497
2259
|
public async $save() {
|
|
1498
|
-
let tilmeld = this
|
|
2260
|
+
let tilmeld = enforceTilmeld(this);
|
|
1499
2261
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
1500
2262
|
return false;
|
|
1501
2263
|
}
|
|
1502
2264
|
|
|
1503
2265
|
if (
|
|
2266
|
+
!this.$skipAcWhenSaving &&
|
|
1504
2267
|
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1505
2268
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1506
|
-
this.$
|
|
2269
|
+
this.$data.abilities?.includes('system/admin')
|
|
1507
2270
|
) {
|
|
1508
|
-
throw new
|
|
1509
|
-
"You don't have the authority to modify system admins."
|
|
2271
|
+
throw new AccessControlError(
|
|
2272
|
+
"You don't have the authority to modify system admins.",
|
|
1510
2273
|
);
|
|
1511
2274
|
}
|
|
1512
2275
|
|
|
2276
|
+
// Lowercase the domain part.
|
|
2277
|
+
if (tilmeld.config.domainSupport) {
|
|
2278
|
+
const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
2279
|
+
if (domain) {
|
|
2280
|
+
this.$data.username = `${username}@${domain.toLowerCase()}`;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
if (tilmeld.config.domainSupport) {
|
|
2285
|
+
const [_username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
2286
|
+
const [_originalUsername, originalDomain] = splitn(
|
|
2287
|
+
this.$originalUsername ?? '',
|
|
2288
|
+
'@',
|
|
2289
|
+
2,
|
|
2290
|
+
);
|
|
2291
|
+
|
|
2292
|
+
if (
|
|
2293
|
+
this.guid != null &&
|
|
2294
|
+
((originalDomain == null && domain != null) ||
|
|
2295
|
+
(domain == null && originalDomain != null))
|
|
2296
|
+
) {
|
|
2297
|
+
throw new AccessControlError(
|
|
2298
|
+
"Users can't switch to or from having a domain once created.",
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
if (
|
|
2303
|
+
!this.$skipAcWhenSaving &&
|
|
2304
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
2305
|
+
!tilmeld.gatekeeper('system/admin')
|
|
2306
|
+
) {
|
|
2307
|
+
if (
|
|
2308
|
+
domain != null &&
|
|
2309
|
+
!this.$is(tilmeld.currentUser) &&
|
|
2310
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
2311
|
+
) {
|
|
2312
|
+
throw new AccessControlError(
|
|
2313
|
+
"You don't have the authority to modify users on this domain.",
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
if (
|
|
2317
|
+
originalDomain != null &&
|
|
2318
|
+
originalDomain !== domain &&
|
|
2319
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
2320
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))
|
|
2321
|
+
) {
|
|
2322
|
+
throw new AccessControlError(
|
|
2323
|
+
"You don't have the authority to modify users on this domain.",
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
1513
2329
|
let sendVerification = false;
|
|
1514
2330
|
|
|
1515
2331
|
// Formatting.
|
|
@@ -1528,49 +2344,88 @@ export default class User extends AbleObject<UserData> {
|
|
|
1528
2344
|
this.$data.enabled = !!this.$data.enabled;
|
|
1529
2345
|
|
|
1530
2346
|
// Clear empty values.
|
|
1531
|
-
if (
|
|
2347
|
+
if (
|
|
2348
|
+
this.$data.nameFirst === '' ||
|
|
2349
|
+
!tilmeld.config.userFields.includes('name')
|
|
2350
|
+
) {
|
|
1532
2351
|
delete this.$data.nameFirst;
|
|
1533
2352
|
}
|
|
1534
|
-
if (
|
|
2353
|
+
if (
|
|
2354
|
+
this.$data.nameMiddle === '' ||
|
|
2355
|
+
!tilmeld.config.userFields.includes('name')
|
|
2356
|
+
) {
|
|
1535
2357
|
delete this.$data.nameMiddle;
|
|
1536
2358
|
}
|
|
1537
|
-
if (
|
|
2359
|
+
if (
|
|
2360
|
+
this.$data.nameLast === '' ||
|
|
2361
|
+
!tilmeld.config.userFields.includes('name')
|
|
2362
|
+
) {
|
|
1538
2363
|
delete this.$data.nameLast;
|
|
1539
2364
|
}
|
|
1540
|
-
if (this.$data.name === '') {
|
|
2365
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
1541
2366
|
delete this.$data.name;
|
|
1542
2367
|
}
|
|
1543
|
-
if (
|
|
2368
|
+
if (
|
|
2369
|
+
this.$data.email === '' ||
|
|
2370
|
+
!tilmeld.config.userFields.includes('email')
|
|
2371
|
+
) {
|
|
1544
2372
|
delete this.$data.email;
|
|
1545
2373
|
}
|
|
1546
2374
|
if (this.$data.avatar === '') {
|
|
1547
2375
|
delete this.$data.avatar;
|
|
1548
2376
|
}
|
|
1549
|
-
if (
|
|
2377
|
+
if (
|
|
2378
|
+
this.$data.phone === '' ||
|
|
2379
|
+
!tilmeld.config.userFields.includes('phone')
|
|
2380
|
+
) {
|
|
1550
2381
|
delete this.$data.phone;
|
|
1551
2382
|
}
|
|
1552
|
-
if (
|
|
2383
|
+
if (
|
|
2384
|
+
this.$data.secret === '' ||
|
|
2385
|
+
!tilmeld.config.userFields.includes('email')
|
|
2386
|
+
) {
|
|
1553
2387
|
delete this.$data.secret;
|
|
1554
2388
|
}
|
|
1555
|
-
if (
|
|
2389
|
+
if (
|
|
2390
|
+
this.$data.emailChangeDate === 0 ||
|
|
2391
|
+
!tilmeld.config.userFields.includes('email')
|
|
2392
|
+
) {
|
|
1556
2393
|
delete this.$data.emailChangeDate;
|
|
1557
2394
|
}
|
|
1558
|
-
if (
|
|
2395
|
+
if (
|
|
2396
|
+
this.$data.newEmailSecret === '' ||
|
|
2397
|
+
!tilmeld.config.userFields.includes('email')
|
|
2398
|
+
) {
|
|
1559
2399
|
delete this.$data.newEmailSecret;
|
|
1560
2400
|
}
|
|
1561
|
-
if (
|
|
2401
|
+
if (
|
|
2402
|
+
this.$data.newEmailAddress === '' ||
|
|
2403
|
+
!tilmeld.config.userFields.includes('email')
|
|
2404
|
+
) {
|
|
1562
2405
|
delete this.$data.newEmailAddress;
|
|
1563
2406
|
}
|
|
1564
|
-
if (
|
|
2407
|
+
if (
|
|
2408
|
+
this.$data.cancelEmailSecret === '' ||
|
|
2409
|
+
!tilmeld.config.userFields.includes('email')
|
|
2410
|
+
) {
|
|
1565
2411
|
delete this.$data.cancelEmailSecret;
|
|
1566
2412
|
}
|
|
1567
|
-
if (
|
|
2413
|
+
if (
|
|
2414
|
+
this.$data.cancelEmailAddress === '' ||
|
|
2415
|
+
!tilmeld.config.userFields.includes('email')
|
|
2416
|
+
) {
|
|
1568
2417
|
delete this.$data.cancelEmailAddress;
|
|
1569
2418
|
}
|
|
1570
|
-
if (
|
|
2419
|
+
if (
|
|
2420
|
+
this.$data.recoverSecret === '' ||
|
|
2421
|
+
!tilmeld.config.userFields.includes('email')
|
|
2422
|
+
) {
|
|
1571
2423
|
delete this.$data.recoverSecret;
|
|
1572
2424
|
}
|
|
1573
|
-
if (
|
|
2425
|
+
if (
|
|
2426
|
+
this.$data.recoverSecretDate === 0 ||
|
|
2427
|
+
!tilmeld.config.userFields.includes('email')
|
|
2428
|
+
) {
|
|
1574
2429
|
delete this.$data.recoverSecretDate;
|
|
1575
2430
|
}
|
|
1576
2431
|
if (this.$data.password === '') {
|
|
@@ -1588,7 +2443,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1588
2443
|
if (!unCheck.result) {
|
|
1589
2444
|
throw new BadUsernameError(unCheck.message);
|
|
1590
2445
|
}
|
|
1591
|
-
if (
|
|
2446
|
+
if (
|
|
2447
|
+
!tilmeld.config.emailUsernames &&
|
|
2448
|
+
tilmeld.config.userFields.includes('email')
|
|
2449
|
+
) {
|
|
1592
2450
|
const emCheck = await this.$checkEmail();
|
|
1593
2451
|
if (!emCheck.result) {
|
|
1594
2452
|
throw new BadEmailError(emCheck.message);
|
|
@@ -1596,7 +2454,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1596
2454
|
}
|
|
1597
2455
|
|
|
1598
2456
|
// Email changes.
|
|
1599
|
-
if (
|
|
2457
|
+
if (
|
|
2458
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
2459
|
+
tilmeld.config.userFields.includes('email')
|
|
2460
|
+
) {
|
|
1600
2461
|
// The user isn't an admin, so email address changes should contain some
|
|
1601
2462
|
// security measures.
|
|
1602
2463
|
if (tilmeld.config.verifyEmail) {
|
|
@@ -1604,7 +2465,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1604
2465
|
if (this.guid == null) {
|
|
1605
2466
|
// If this is the first user, they'll be made an admin, so they don't
|
|
1606
2467
|
// need to verify.
|
|
1607
|
-
if (
|
|
2468
|
+
if (!this.$data.abilities?.includes('system/admin')) {
|
|
1608
2469
|
this.$data.secret = nanoid();
|
|
1609
2470
|
sendVerification = true;
|
|
1610
2471
|
}
|
|
@@ -1624,10 +2485,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
1624
2485
|
new Date(
|
|
1625
2486
|
strtotime(
|
|
1626
2487
|
'+' + tilmeld.config.emailRateLimit,
|
|
1627
|
-
Math.floor(this.$data.emailChangeDate / 1000)
|
|
1628
|
-
) * 1000
|
|
2488
|
+
Math.floor(this.$data.emailChangeDate / 1000),
|
|
2489
|
+
) * 1000,
|
|
1629
2490
|
).toString() +
|
|
1630
|
-
' to change your email address again.'
|
|
2491
|
+
' to change your email address again.',
|
|
1631
2492
|
);
|
|
1632
2493
|
} else {
|
|
1633
2494
|
if (
|
|
@@ -1682,51 +2543,148 @@ export default class User extends AbleObject<UserData> {
|
|
|
1682
2543
|
throw new BadDataError('A password is required.');
|
|
1683
2544
|
}
|
|
1684
2545
|
|
|
1685
|
-
try {
|
|
1686
|
-
tilmeld.config.validatorUser(this);
|
|
1687
|
-
} catch (e: any) {
|
|
1688
|
-
throw new BadDataError(e?.message);
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
2546
|
// Start transaction.
|
|
1692
|
-
const transaction = 'tilmeld-save-' + nanoid();
|
|
2547
|
+
const transaction = 'tilmeld-save-user-' + (this.guid || nanoid());
|
|
1693
2548
|
const nymph = this.$nymph;
|
|
1694
2549
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1695
|
-
this.$
|
|
1696
|
-
tilmeld = this
|
|
2550
|
+
this.$setNymph(tnymph);
|
|
2551
|
+
tilmeld = enforceTilmeld(this);
|
|
2552
|
+
const Group = tnymph.getEntityClass(GroupClass);
|
|
1697
2553
|
|
|
1698
|
-
|
|
1699
|
-
this.$data.group
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2554
|
+
try {
|
|
2555
|
+
let group = this.$data.group;
|
|
2556
|
+
await group?.$wake();
|
|
2557
|
+
if (group == null && this.guid == null) {
|
|
2558
|
+
if (tilmeld.config.generatePrimary) {
|
|
2559
|
+
// Generate a new primary group for the user.
|
|
2560
|
+
group = await Group.factory();
|
|
2561
|
+
group.user = this;
|
|
2562
|
+
const parent = await tnymph.getEntity(
|
|
2563
|
+
{ class: Group },
|
|
2564
|
+
{
|
|
2565
|
+
type: '&',
|
|
2566
|
+
equal: ['defaultPrimary', true],
|
|
2567
|
+
},
|
|
2568
|
+
);
|
|
2569
|
+
if (parent != null) {
|
|
2570
|
+
group.parent = parent;
|
|
2571
|
+
}
|
|
2572
|
+
} else {
|
|
2573
|
+
// Add the default primary.
|
|
2574
|
+
const group = await tnymph.getEntity(
|
|
2575
|
+
{ class: Group },
|
|
2576
|
+
{
|
|
2577
|
+
type: '&',
|
|
2578
|
+
equal: ['defaultPrimary', true],
|
|
2579
|
+
},
|
|
2580
|
+
);
|
|
2581
|
+
if (group != null) {
|
|
2582
|
+
this.$data.group = group;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
if (group != null && group.user != null && this.$is(group.user)) {
|
|
1704
2587
|
// Update the user's generated primary group.
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
2588
|
+
let changed = false;
|
|
2589
|
+
if (group.groupname !== this.$data.username) {
|
|
2590
|
+
group.groupname = this.$data.username;
|
|
2591
|
+
changed = true;
|
|
2592
|
+
}
|
|
2593
|
+
if (group.avatar !== this.$data.avatar) {
|
|
2594
|
+
group.avatar = this.$data.avatar;
|
|
2595
|
+
changed = true;
|
|
2596
|
+
}
|
|
2597
|
+
if (
|
|
2598
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2599
|
+
group.email !== this.$data.email
|
|
2600
|
+
) {
|
|
2601
|
+
group.email = this.$data.email;
|
|
2602
|
+
changed = true;
|
|
2603
|
+
}
|
|
2604
|
+
if (
|
|
2605
|
+
tilmeld.config.userFields.includes('name') &&
|
|
2606
|
+
group.name !== this.$data.name
|
|
2607
|
+
) {
|
|
2608
|
+
group.name = this.$data.name;
|
|
2609
|
+
changed = true;
|
|
2610
|
+
}
|
|
2611
|
+
if (
|
|
2612
|
+
tilmeld.config.userFields.includes('phone') &&
|
|
2613
|
+
group.phone !== this.$data.phone
|
|
2614
|
+
) {
|
|
2615
|
+
group.phone = this.$data.phone;
|
|
2616
|
+
changed = true;
|
|
2617
|
+
}
|
|
2618
|
+
if (changed || group.guid == null) {
|
|
2619
|
+
if (!(await group.$saveSkipAC())) {
|
|
2620
|
+
throw Error('Error updating primary group for user.');
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
this.$data.group = group;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
if (this.$data.groups == null || this.$data.groups.length === 0) {
|
|
2627
|
+
// Add secondary groups.
|
|
2628
|
+
if (
|
|
2629
|
+
tilmeld.config.userFields.includes('email') &&
|
|
2630
|
+
tilmeld.config.verifyEmail &&
|
|
2631
|
+
tilmeld.config.unverifiedAccess &&
|
|
2632
|
+
this.$data.secret != null
|
|
2633
|
+
) {
|
|
2634
|
+
// Add the default secondaries for unverified users.
|
|
2635
|
+
this.$data.groups = await tnymph.getEntities(
|
|
2636
|
+
{ class: Group },
|
|
2637
|
+
{
|
|
2638
|
+
type: '&',
|
|
2639
|
+
equal: ['unverifiedSecondary', true],
|
|
2640
|
+
},
|
|
2641
|
+
);
|
|
2642
|
+
} else {
|
|
2643
|
+
// Add the default secondaries.
|
|
2644
|
+
this.$data.groups = await tnymph.getEntities(
|
|
2645
|
+
{ class: Group },
|
|
2646
|
+
{
|
|
2647
|
+
type: '&',
|
|
2648
|
+
equal: ['defaultSecondary', true],
|
|
2649
|
+
},
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
try {
|
|
2655
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
1711
2656
|
} catch (e: any) {
|
|
1712
|
-
|
|
1713
|
-
this.$nymph = nymph;
|
|
1714
|
-
throw e;
|
|
2657
|
+
throw new BadDataError(e?.message);
|
|
1715
2658
|
}
|
|
2659
|
+
} catch (e: any) {
|
|
2660
|
+
await tnymph.rollback(transaction);
|
|
2661
|
+
this.$setNymph(nymph);
|
|
2662
|
+
throw e;
|
|
1716
2663
|
}
|
|
1717
2664
|
|
|
1718
|
-
let ret
|
|
2665
|
+
let ret = false;
|
|
1719
2666
|
let preGuid = this.guid;
|
|
1720
2667
|
let preCdate = this.cdate;
|
|
1721
2668
|
let preMdate = this.mdate;
|
|
1722
2669
|
|
|
1723
2670
|
try {
|
|
2671
|
+
for (let callback of (this.constructor as typeof User)
|
|
2672
|
+
.beforeSaveCallbacks) {
|
|
2673
|
+
if (callback) {
|
|
2674
|
+
await callback(this);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
1724
2678
|
ret = await super.$save();
|
|
1725
2679
|
} catch (e: any) {
|
|
1726
2680
|
await tnymph.rollback(transaction);
|
|
1727
|
-
this
|
|
2681
|
+
this.guid = preGuid;
|
|
2682
|
+
this.cdate = preCdate;
|
|
2683
|
+
this.mdate = preMdate;
|
|
2684
|
+
this.$setNymph(nymph);
|
|
1728
2685
|
throw e;
|
|
1729
2686
|
}
|
|
2687
|
+
|
|
1730
2688
|
if (ret) {
|
|
1731
2689
|
if (sendVerification) {
|
|
1732
2690
|
// The email has changed, so send a new verification email.
|
|
@@ -1735,23 +2693,48 @@ export default class User extends AbleObject<UserData> {
|
|
|
1735
2693
|
this.guid = preGuid;
|
|
1736
2694
|
this.cdate = preCdate;
|
|
1737
2695
|
this.mdate = preMdate;
|
|
1738
|
-
this.$nymph
|
|
2696
|
+
this.$setNymph(nymph);
|
|
1739
2697
|
throw new Error("Couldn't send verification email.");
|
|
1740
2698
|
}
|
|
1741
2699
|
}
|
|
1742
2700
|
|
|
1743
2701
|
this.$descendantGroups = undefined;
|
|
1744
2702
|
this.$gatekeeperCache = undefined;
|
|
1745
|
-
await tnymph.commit(transaction);
|
|
1746
2703
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2704
|
+
try {
|
|
2705
|
+
for (let callback of (this.constructor as typeof User)
|
|
2706
|
+
.afterSaveCallbacks) {
|
|
2707
|
+
if (callback) {
|
|
2708
|
+
await callback(this);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
} catch (e: any) {
|
|
2712
|
+
await tnymph.rollback(transaction);
|
|
2713
|
+
this.guid = preGuid;
|
|
2714
|
+
this.cdate = preCdate;
|
|
2715
|
+
this.mdate = preMdate;
|
|
2716
|
+
this.$setNymph(nymph);
|
|
2717
|
+
throw e;
|
|
1750
2718
|
}
|
|
2719
|
+
|
|
2720
|
+
ret = await tnymph.commit(transaction);
|
|
1751
2721
|
} else {
|
|
1752
2722
|
await tnymph.rollback(transaction);
|
|
1753
2723
|
}
|
|
1754
|
-
this.$nymph
|
|
2724
|
+
this.$setNymph(nymph);
|
|
2725
|
+
|
|
2726
|
+
if (ret) {
|
|
2727
|
+
this.$originalEmail = this.$data.email;
|
|
2728
|
+
this.$originalUsername = this.$data.username;
|
|
2729
|
+
|
|
2730
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
2731
|
+
|
|
2732
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
2733
|
+
// Update the user in the session cache.
|
|
2734
|
+
await tilmeld.fillSession(this);
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
1755
2738
|
return ret;
|
|
1756
2739
|
}
|
|
1757
2740
|
|
|
@@ -1772,53 +2755,153 @@ export default class User extends AbleObject<UserData> {
|
|
|
1772
2755
|
}
|
|
1773
2756
|
|
|
1774
2757
|
public async $delete() {
|
|
1775
|
-
const tilmeld = this
|
|
1776
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1777
|
-
throw new
|
|
2758
|
+
const tilmeld = enforceTilmeld(this);
|
|
2759
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2760
|
+
throw new AccessControlError(
|
|
2761
|
+
"You don't have the authority to delete users.",
|
|
2762
|
+
);
|
|
1778
2763
|
}
|
|
1779
2764
|
if (
|
|
2765
|
+
!this.$skipAcWhenDeleting &&
|
|
1780
2766
|
!tilmeld.gatekeeper('system/admin') &&
|
|
1781
|
-
this.$
|
|
2767
|
+
this.$data.abilities?.includes('system/admin')
|
|
1782
2768
|
) {
|
|
1783
|
-
throw new
|
|
1784
|
-
"You don't have the authority to delete system admins."
|
|
2769
|
+
throw new AccessControlError(
|
|
2770
|
+
"You don't have the authority to delete system admins.",
|
|
1785
2771
|
);
|
|
1786
2772
|
}
|
|
1787
|
-
if (
|
|
1788
|
-
|
|
2773
|
+
if (this.$data.username !== this.$originalUsername) {
|
|
2774
|
+
throw new AccessControlError(
|
|
2775
|
+
"You can't delete a user while switching their username.",
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
const transaction = 'tilmeld-delete-user-' + this.guid;
|
|
2780
|
+
const nymph = this.$nymph;
|
|
2781
|
+
const tnymph = await nymph.startTransaction(transaction);
|
|
2782
|
+
this.$setNymph(tnymph);
|
|
2783
|
+
|
|
2784
|
+
let success = false;
|
|
2785
|
+
|
|
2786
|
+
try {
|
|
2787
|
+
for (let callback of (this.constructor as typeof User)
|
|
2788
|
+
.beforeDeleteCallbacks) {
|
|
2789
|
+
if (callback) {
|
|
2790
|
+
await callback(this);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
2795
|
+
await this.$logout();
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
if (this.$data.group != null && this.$is(this.$data.group.user)) {
|
|
2799
|
+
// Read the skip ac value here, because super.$delete changes it.
|
|
2800
|
+
const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
|
|
2801
|
+
|
|
2802
|
+
// Delete the user.
|
|
2803
|
+
success = await super.$delete();
|
|
2804
|
+
if (success) {
|
|
2805
|
+
// Delete the generated group.
|
|
2806
|
+
success = $skipAcWhenDeleting
|
|
2807
|
+
? await this.$data.group.$deleteSkipAC()
|
|
2808
|
+
: await this.$data.group.$delete();
|
|
2809
|
+
}
|
|
2810
|
+
} else {
|
|
2811
|
+
success = await super.$delete();
|
|
2812
|
+
}
|
|
2813
|
+
} catch (e: any) {
|
|
2814
|
+
await tnymph.rollback(transaction);
|
|
2815
|
+
this.$setNymph(nymph);
|
|
2816
|
+
throw e;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
if (success) {
|
|
2820
|
+
try {
|
|
2821
|
+
for (let callback of (this.constructor as typeof User)
|
|
2822
|
+
.afterDeleteCallbacks) {
|
|
2823
|
+
if (callback) {
|
|
2824
|
+
await callback(this);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
} catch (e: any) {
|
|
2828
|
+
await tnymph.rollback(transaction);
|
|
2829
|
+
this.$setNymph(nymph);
|
|
2830
|
+
throw e;
|
|
2831
|
+
}
|
|
2832
|
+
success = await tnymph.commit(transaction);
|
|
2833
|
+
} else {
|
|
2834
|
+
await tnymph.rollback(transaction);
|
|
2835
|
+
}
|
|
2836
|
+
this.$setNymph(nymph);
|
|
2837
|
+
return success;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
/*
|
|
2841
|
+
* This should *never* be accessible on the client.
|
|
2842
|
+
*/
|
|
2843
|
+
async $deleteSkipAC() {
|
|
2844
|
+
this.$skipAcWhenDeleting = true;
|
|
2845
|
+
return await this.$delete();
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
$tilmeldDeleteSkipAC() {
|
|
2849
|
+
if (this.$skipAcWhenDeleting) {
|
|
2850
|
+
this.$skipAcWhenDeleting = false;
|
|
2851
|
+
return true;
|
|
1789
2852
|
}
|
|
1790
|
-
return
|
|
2853
|
+
return false;
|
|
1791
2854
|
}
|
|
1792
2855
|
|
|
1793
2856
|
public static on<T extends EventType>(
|
|
1794
2857
|
event: T,
|
|
1795
|
-
callback: T extends '
|
|
1796
|
-
?
|
|
1797
|
-
: T extends '
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2858
|
+
callback: T extends 'checkUsername'
|
|
2859
|
+
? TilmeldCheckUsernameCallback
|
|
2860
|
+
: T extends 'beforeRegister'
|
|
2861
|
+
? TilmeldBeforeRegisterCallback
|
|
2862
|
+
: T extends 'afterRegister'
|
|
2863
|
+
? TilmeldAfterRegisterCallback
|
|
2864
|
+
: T extends 'beforeSave'
|
|
2865
|
+
? TilmeldBeforeSaveCallback
|
|
2866
|
+
: T extends 'afterSave'
|
|
2867
|
+
? TilmeldAfterSaveCallback
|
|
2868
|
+
: T extends 'beforeLogin'
|
|
2869
|
+
? TilmeldBeforeLoginCallback
|
|
2870
|
+
: T extends 'afterLogin'
|
|
2871
|
+
? TilmeldAfterLoginCallback
|
|
2872
|
+
: T extends 'beforeLogout'
|
|
2873
|
+
? TilmeldBeforeLogoutCallback
|
|
2874
|
+
: T extends 'afterLogout'
|
|
2875
|
+
? TilmeldAfterLogoutCallback
|
|
2876
|
+
: T extends 'beforeDelete'
|
|
2877
|
+
? TilmeldBeforeDeleteCallback
|
|
2878
|
+
: T extends 'afterDelete'
|
|
2879
|
+
? TilmeldAfterDeleteCallback
|
|
2880
|
+
: never,
|
|
1808
2881
|
) {
|
|
1809
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
1810
|
-
? '
|
|
1811
|
-
: T extends '
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
2882
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2883
|
+
? 'checkUsernameCallbacks'
|
|
2884
|
+
: T extends 'beforeRegister'
|
|
2885
|
+
? 'beforeRegisterCallbacks'
|
|
2886
|
+
: T extends 'afterRegister'
|
|
2887
|
+
? 'afterRegisterCallbacks'
|
|
2888
|
+
: T extends 'beforeSave'
|
|
2889
|
+
? 'beforeSaveCallbacks'
|
|
2890
|
+
: T extends 'afterSave'
|
|
2891
|
+
? 'afterSaveCallbacks'
|
|
2892
|
+
: T extends 'beforeLogin'
|
|
2893
|
+
? 'beforeLoginCallbacks'
|
|
2894
|
+
: T extends 'afterLogin'
|
|
2895
|
+
? 'afterLoginCallbacks'
|
|
2896
|
+
: T extends 'beforeLogout'
|
|
2897
|
+
? 'beforeLogoutCallbacks'
|
|
2898
|
+
: T extends 'afterLogout'
|
|
2899
|
+
? 'afterLogoutCallbacks'
|
|
2900
|
+
: T extends 'beforeDelete'
|
|
2901
|
+
? 'beforeDeleteCallbacks'
|
|
2902
|
+
: T extends 'afterDelete'
|
|
2903
|
+
? 'afterDeleteCallbacks'
|
|
2904
|
+
: never;
|
|
1822
2905
|
if (!(prop in this)) {
|
|
1823
2906
|
throw new Error('Invalid event type.');
|
|
1824
2907
|
}
|
|
@@ -1829,33 +2912,53 @@ export default class User extends AbleObject<UserData> {
|
|
|
1829
2912
|
|
|
1830
2913
|
public static off<T extends EventType>(
|
|
1831
2914
|
event: T,
|
|
1832
|
-
callback: T extends '
|
|
1833
|
-
?
|
|
1834
|
-
: T extends '
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2915
|
+
callback: T extends 'checkUsername'
|
|
2916
|
+
? TilmeldCheckUsernameCallback
|
|
2917
|
+
: T extends 'beforeRegister'
|
|
2918
|
+
? TilmeldBeforeRegisterCallback
|
|
2919
|
+
: T extends 'afterRegister'
|
|
2920
|
+
? TilmeldAfterRegisterCallback
|
|
2921
|
+
: T extends 'beforeSave'
|
|
2922
|
+
? TilmeldBeforeSaveCallback
|
|
2923
|
+
: T extends 'afterSave'
|
|
2924
|
+
? TilmeldAfterSaveCallback
|
|
2925
|
+
: T extends 'beforeLogin'
|
|
2926
|
+
? TilmeldBeforeLoginCallback
|
|
2927
|
+
: T extends 'afterLogin'
|
|
2928
|
+
? TilmeldAfterLoginCallback
|
|
2929
|
+
: T extends 'beforeLogout'
|
|
2930
|
+
? TilmeldBeforeLogoutCallback
|
|
2931
|
+
: T extends 'afterLogout'
|
|
2932
|
+
? TilmeldAfterLogoutCallback
|
|
2933
|
+
: T extends 'beforeDelete'
|
|
2934
|
+
? TilmeldBeforeDeleteCallback
|
|
2935
|
+
: T extends 'afterDelete'
|
|
2936
|
+
? TilmeldAfterDeleteCallback
|
|
2937
|
+
: never,
|
|
1845
2938
|
) {
|
|
1846
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
1847
|
-
? '
|
|
1848
|
-
: T extends '
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2939
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2940
|
+
? 'checkUsernameCallbacks'
|
|
2941
|
+
: T extends 'beforeRegister'
|
|
2942
|
+
? 'beforeRegisterCallbacks'
|
|
2943
|
+
: T extends 'afterRegister'
|
|
2944
|
+
? 'afterRegisterCallbacks'
|
|
2945
|
+
: T extends 'beforeSave'
|
|
2946
|
+
? 'beforeSaveCallbacks'
|
|
2947
|
+
: T extends 'afterSave'
|
|
2948
|
+
? 'afterSaveCallbacks'
|
|
2949
|
+
: T extends 'beforeLogin'
|
|
2950
|
+
? 'beforeLoginCallbacks'
|
|
2951
|
+
: T extends 'afterLogin'
|
|
2952
|
+
? 'afterLoginCallbacks'
|
|
2953
|
+
: T extends 'beforeLogout'
|
|
2954
|
+
? 'beforeLogoutCallbacks'
|
|
2955
|
+
: T extends 'afterLogout'
|
|
2956
|
+
? 'afterLogoutCallbacks'
|
|
2957
|
+
: T extends 'beforeDelete'
|
|
2958
|
+
? 'beforeDeleteCallbacks'
|
|
2959
|
+
: T extends 'afterDelete'
|
|
2960
|
+
? 'afterDeleteCallbacks'
|
|
2961
|
+
: never;
|
|
1859
2962
|
if (!(prop in this)) {
|
|
1860
2963
|
return false;
|
|
1861
2964
|
}
|