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