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