@nymphjs/tilmeld 1.0.0-beta.4 → 1.0.0-beta.41
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 +189 -0
- package/README.md +13 -0
- package/dist/Group.d.ts +4 -7
- package/dist/Group.js +107 -83
- package/dist/Group.js.map +1 -1
- package/dist/Tilmeld.d.ts +10 -9
- package/dist/Tilmeld.js +226 -175
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +66 -13
- package/dist/User.js +421 -128
- package/dist/User.js.map +1 -1
- package/dist/conf/d.d.ts +3 -1
- package/dist/conf/defaults.js +15 -4
- package/dist/conf/defaults.js.map +1 -1
- package/dist/enforceTilmeld.d.ts +3 -0
- package/dist/enforceTilmeld.js +22 -0
- package/dist/enforceTilmeld.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +24 -21
- package/src/Group.ts +140 -102
- package/src/Tilmeld.ts +312 -221
- package/src/User.ts +600 -145
- package/src/conf/d.ts +16 -7
- package/src/conf/defaults.ts +17 -4
- package/src/enforceTilmeld.ts +20 -0
- package/src/index.ts +2 -0
- package/typedoc.json +4 -0
package/src/User.ts
CHANGED
|
@@ -5,15 +5,17 @@ import {
|
|
|
5
5
|
Selector,
|
|
6
6
|
SerializedEntityData,
|
|
7
7
|
} from '@nymphjs/nymph';
|
|
8
|
+
import { humanSecret, nanoid } from '@nymphjs/guid';
|
|
8
9
|
import { EmailOptions } from 'email-templates';
|
|
9
10
|
import strtotime from 'locutus/php/datetime/strtotime';
|
|
10
11
|
import Base64 from 'crypto-js/enc-base64';
|
|
11
12
|
import sha256 from 'crypto-js/sha256';
|
|
12
13
|
import md5 from 'crypto-js/md5';
|
|
13
14
|
import { difference } from 'lodash';
|
|
14
|
-
import {
|
|
15
|
+
import { TOTP, Secret } from 'otpauth';
|
|
16
|
+
import { toDataURL } from 'qrcode';
|
|
15
17
|
|
|
16
|
-
import
|
|
18
|
+
import { enforceTilmeld } from './enforceTilmeld';
|
|
17
19
|
import AbleObject from './AbleObject';
|
|
18
20
|
import Group, { GroupData } from './Group';
|
|
19
21
|
import {
|
|
@@ -24,12 +26,22 @@ import {
|
|
|
24
26
|
} from './errors';
|
|
25
27
|
|
|
26
28
|
export type EventType =
|
|
29
|
+
| 'checkUsername'
|
|
27
30
|
| 'beforeRegister'
|
|
28
31
|
| 'afterRegister'
|
|
29
32
|
| 'beforeLogin'
|
|
30
33
|
| 'afterLogin'
|
|
31
34
|
| 'beforeLogout'
|
|
32
35
|
| 'afterLogout';
|
|
36
|
+
/**
|
|
37
|
+
* This is run when the user has entered an otherwise valid username into the
|
|
38
|
+
* signup form. It should return a result, which when false will stop the
|
|
39
|
+
* process and return the included message, disallowing the username.
|
|
40
|
+
*/
|
|
41
|
+
export type TilmeldCheckUsernameCallback = (
|
|
42
|
+
user: User & UserData,
|
|
43
|
+
data: { username: string }
|
|
44
|
+
) => Promise<{ result: boolean; message?: string }>;
|
|
33
45
|
/**
|
|
34
46
|
* Theses are run before the user data checks, so the only checks before are
|
|
35
47
|
* whether registration is allowed and whether the user is already registered.
|
|
@@ -135,7 +147,8 @@ export type UserData = {
|
|
|
135
147
|
*/
|
|
136
148
|
newEmailSecret?: string;
|
|
137
149
|
/**
|
|
138
|
-
*
|
|
150
|
+
* If the user has changed their email address, this is the new one, awaiting
|
|
151
|
+
* verification.
|
|
139
152
|
*/
|
|
140
153
|
newEmailAddress?: string;
|
|
141
154
|
/**
|
|
@@ -167,6 +180,15 @@ export type UserData = {
|
|
|
167
180
|
* database.
|
|
168
181
|
*/
|
|
169
182
|
passwordTemp?: string;
|
|
183
|
+
/**
|
|
184
|
+
* If set, this timestamp is the cutoff point for JWT issue dates. Any token
|
|
185
|
+
* issued before this date will not authenticate the user.
|
|
186
|
+
*/
|
|
187
|
+
revokeTokenDate?: number;
|
|
188
|
+
/**
|
|
189
|
+
* Two factor auth secret.
|
|
190
|
+
*/
|
|
191
|
+
totpSecret?: string;
|
|
170
192
|
};
|
|
171
193
|
|
|
172
194
|
/**
|
|
@@ -182,9 +204,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
182
204
|
/**
|
|
183
205
|
* The instance of Tilmeld to use for queries.
|
|
184
206
|
*/
|
|
185
|
-
static tilmeld: Tilmeld;
|
|
186
207
|
static ETYPE = 'tilmeld_user';
|
|
187
208
|
static class = 'User';
|
|
209
|
+
private static checkUsernameCallbacks: TilmeldCheckUsernameCallback[] = [];
|
|
188
210
|
private static beforeRegisterCallbacks: TilmeldBeforeRegisterCallback[] = [];
|
|
189
211
|
private static afterRegisterCallbacks: TilmeldAfterRegisterCallback[] = [];
|
|
190
212
|
private static beforeLoginCallbacks: TilmeldBeforeLoginCallback[] = [];
|
|
@@ -200,23 +222,33 @@ export default class User extends AbleObject<UserData> {
|
|
|
200
222
|
'$register',
|
|
201
223
|
];
|
|
202
224
|
private static DEFAULT_PRIVATE_DATA = [
|
|
225
|
+
// The things that are commented are there to show what is _not_ private.
|
|
226
|
+
'abilities',
|
|
227
|
+
// 'username',
|
|
228
|
+
// 'nameFirst',
|
|
229
|
+
// 'nameMiddle',
|
|
230
|
+
// 'nameLast',
|
|
231
|
+
// 'name',
|
|
203
232
|
'email',
|
|
204
|
-
'
|
|
233
|
+
// 'avatar',
|
|
205
234
|
'phone',
|
|
206
235
|
'group',
|
|
207
236
|
'groups',
|
|
208
|
-
'abilities',
|
|
209
237
|
'inheritAbilities',
|
|
210
|
-
'
|
|
211
|
-
'recoverSecretDate',
|
|
212
|
-
'password',
|
|
213
|
-
'salt',
|
|
238
|
+
'enabled',
|
|
214
239
|
'secret',
|
|
215
|
-
'
|
|
240
|
+
'emailChangeDate',
|
|
216
241
|
'newEmailSecret',
|
|
217
|
-
'
|
|
242
|
+
'newEmailAddress',
|
|
218
243
|
'cancelEmailSecret',
|
|
219
|
-
'
|
|
244
|
+
'cancelEmailAddress',
|
|
245
|
+
'recoverSecret',
|
|
246
|
+
'recoverSecretDate',
|
|
247
|
+
'salt',
|
|
248
|
+
'password',
|
|
249
|
+
'passwordTemp',
|
|
250
|
+
'revokeTokenDate',
|
|
251
|
+
'totpSecret',
|
|
220
252
|
];
|
|
221
253
|
private static DEFAULT_ALLOWLIST_DATA: string[] = [];
|
|
222
254
|
|
|
@@ -280,21 +312,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
280
312
|
return entity;
|
|
281
313
|
}
|
|
282
314
|
|
|
283
|
-
static factorySync(
|
|
284
|
-
return super.factorySync(
|
|
315
|
+
static factorySync(): User & UserData {
|
|
316
|
+
return super.factorySync() as User & UserData;
|
|
285
317
|
}
|
|
286
318
|
|
|
287
|
-
constructor(
|
|
288
|
-
super(
|
|
319
|
+
constructor() {
|
|
320
|
+
super();
|
|
289
321
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
this.$updateDataProtection();
|
|
297
|
-
}
|
|
322
|
+
// Defaults.
|
|
323
|
+
this.$data.enabled = true;
|
|
324
|
+
this.$data.abilities = [];
|
|
325
|
+
this.$data.groups = [];
|
|
326
|
+
this.$data.inheritAbilities = true;
|
|
327
|
+
this.$updateDataProtection();
|
|
298
328
|
}
|
|
299
329
|
|
|
300
330
|
public static current(returnObjectIfNotExist: true): User & UserData;
|
|
@@ -304,10 +334,11 @@ export default class User extends AbleObject<UserData> {
|
|
|
304
334
|
public static current(
|
|
305
335
|
returnObjectIfNotExist?: boolean
|
|
306
336
|
): (User & UserData) | null {
|
|
307
|
-
|
|
337
|
+
const tilmeld = enforceTilmeld(this);
|
|
338
|
+
if (tilmeld.currentUser == null) {
|
|
308
339
|
return returnObjectIfNotExist ? this.factorySync() : null;
|
|
309
340
|
}
|
|
310
|
-
return
|
|
341
|
+
return tilmeld.currentUser;
|
|
311
342
|
}
|
|
312
343
|
|
|
313
344
|
/**
|
|
@@ -320,7 +351,8 @@ export default class User extends AbleObject<UserData> {
|
|
|
320
351
|
recoveryType: 'username' | 'password';
|
|
321
352
|
account: string;
|
|
322
353
|
}): Promise<{ result: boolean; message: string }> {
|
|
323
|
-
|
|
354
|
+
const tilmeld = enforceTilmeld(this);
|
|
355
|
+
if (!tilmeld.config.pwRecovery) {
|
|
324
356
|
return {
|
|
325
357
|
result: false,
|
|
326
358
|
message: 'Account recovery is not allowed.',
|
|
@@ -330,14 +362,11 @@ export default class User extends AbleObject<UserData> {
|
|
|
330
362
|
let user: User & UserData;
|
|
331
363
|
const options: EmailOptions = {};
|
|
332
364
|
|
|
333
|
-
if (
|
|
334
|
-
!this.tilmeld.config.emailUsernames &&
|
|
335
|
-
data.recoveryType === 'username'
|
|
336
|
-
) {
|
|
365
|
+
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
337
366
|
// Create a username recovery email.
|
|
338
367
|
|
|
339
368
|
const getUser = await this.nymph.getEntity(
|
|
340
|
-
{ class:
|
|
369
|
+
{ class: tilmeld.User, skipAc: true },
|
|
341
370
|
{
|
|
342
371
|
type: '&',
|
|
343
372
|
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
@@ -364,7 +393,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
364
393
|
} else if (data.recoveryType === 'password') {
|
|
365
394
|
// Create a password recovery email.
|
|
366
395
|
|
|
367
|
-
const getUser = await
|
|
396
|
+
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
368
397
|
|
|
369
398
|
if (getUser.guid == null) {
|
|
370
399
|
return {
|
|
@@ -391,14 +420,14 @@ export default class User extends AbleObject<UserData> {
|
|
|
391
420
|
};
|
|
392
421
|
options.locals = {
|
|
393
422
|
recoverCode: getUser.recoverSecret,
|
|
394
|
-
timeLimit:
|
|
423
|
+
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
395
424
|
};
|
|
396
425
|
} else {
|
|
397
426
|
return { result: false, message: 'Invalid recovery type.' };
|
|
398
427
|
}
|
|
399
428
|
|
|
400
429
|
// Send the email.
|
|
401
|
-
if (await
|
|
430
|
+
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
402
431
|
return {
|
|
403
432
|
result: true,
|
|
404
433
|
message:
|
|
@@ -421,21 +450,22 @@ export default class User extends AbleObject<UserData> {
|
|
|
421
450
|
secret: string;
|
|
422
451
|
password: string;
|
|
423
452
|
}): Promise<{ result: boolean; message: string }> {
|
|
424
|
-
|
|
453
|
+
const tilmeld = enforceTilmeld(this);
|
|
454
|
+
if (!tilmeld.config.pwRecovery) {
|
|
425
455
|
return {
|
|
426
456
|
result: false,
|
|
427
457
|
message: 'Account recovery is not allowed.',
|
|
428
458
|
};
|
|
429
459
|
}
|
|
430
460
|
|
|
431
|
-
const user = await
|
|
461
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
432
462
|
|
|
433
463
|
if (
|
|
434
464
|
user.guid == null ||
|
|
435
465
|
user.recoverSecret == null ||
|
|
436
466
|
data.secret !== user.recoverSecret ||
|
|
437
467
|
strtotime(
|
|
438
|
-
'+' +
|
|
468
|
+
'+' + tilmeld.config.pwRecoveryTimeLimit,
|
|
439
469
|
Math.floor((user.recoverSecretDate ?? 0) / 1000)
|
|
440
470
|
) *
|
|
441
471
|
1000 <
|
|
@@ -466,27 +496,30 @@ export default class User extends AbleObject<UserData> {
|
|
|
466
496
|
}
|
|
467
497
|
|
|
468
498
|
public static getClientConfig() {
|
|
499
|
+
const tilmeld = enforceTilmeld(this);
|
|
469
500
|
return {
|
|
470
|
-
regFields:
|
|
471
|
-
userFields:
|
|
472
|
-
emailUsernames:
|
|
473
|
-
allowRegistration:
|
|
474
|
-
allowUsernameChange:
|
|
475
|
-
pwRecovery:
|
|
476
|
-
verifyEmail:
|
|
477
|
-
unverifiedAccess:
|
|
501
|
+
regFields: tilmeld.config.regFields,
|
|
502
|
+
userFields: tilmeld.config.userFields,
|
|
503
|
+
emailUsernames: tilmeld.config.emailUsernames,
|
|
504
|
+
allowRegistration: tilmeld.config.allowRegistration,
|
|
505
|
+
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
506
|
+
pwRecovery: tilmeld.config.pwRecovery,
|
|
507
|
+
verifyEmail: tilmeld.config.verifyEmail,
|
|
508
|
+
unverifiedAccess: tilmeld.config.unverifiedAccess,
|
|
478
509
|
};
|
|
479
510
|
}
|
|
480
511
|
|
|
481
512
|
public static async loginUser(data: {
|
|
482
513
|
username: string;
|
|
483
514
|
password: string;
|
|
515
|
+
code?: string;
|
|
484
516
|
additionalData?: { [k: string]: any };
|
|
485
517
|
}) {
|
|
518
|
+
const tilmeld = enforceTilmeld(this);
|
|
486
519
|
if (!('username' in data) || !data.username.length) {
|
|
487
520
|
return { result: false, message: 'Incorrect login/password.' };
|
|
488
521
|
}
|
|
489
|
-
const user = await
|
|
522
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
490
523
|
const result: { result: boolean; message: string; user?: User & UserData } =
|
|
491
524
|
await user.$login(data);
|
|
492
525
|
if (result.result) {
|
|
@@ -499,22 +532,41 @@ export default class User extends AbleObject<UserData> {
|
|
|
499
532
|
public async $login(data: {
|
|
500
533
|
username: string;
|
|
501
534
|
password: string;
|
|
535
|
+
code?: string;
|
|
502
536
|
additionalData?: { [k: string]: any };
|
|
503
537
|
}) {
|
|
504
|
-
const tilmeld = this
|
|
538
|
+
const tilmeld = enforceTilmeld(this);
|
|
505
539
|
if (this.guid == null) {
|
|
506
540
|
return { result: false, message: 'Incorrect login/password.' };
|
|
507
541
|
}
|
|
508
542
|
if (!this.$data.enabled) {
|
|
509
543
|
return { result: false, message: 'This user is disabled.' };
|
|
510
544
|
}
|
|
511
|
-
if (this.$gatekeeper()) {
|
|
545
|
+
if (await this.$gatekeeper()) {
|
|
512
546
|
return { result: true, message: 'You are already logged in.' };
|
|
513
547
|
}
|
|
514
548
|
if (!this.$checkPassword(data.password)) {
|
|
515
549
|
return { result: false, message: 'Incorrect login/password.' };
|
|
516
550
|
}
|
|
517
551
|
|
|
552
|
+
if (this.$data.totpSecret != null) {
|
|
553
|
+
if (
|
|
554
|
+
data.code == null ||
|
|
555
|
+
typeof data.code !== 'string' ||
|
|
556
|
+
data.code.length !== 6
|
|
557
|
+
) {
|
|
558
|
+
return {
|
|
559
|
+
result: false,
|
|
560
|
+
needTOTP: true,
|
|
561
|
+
message: 'You need to provide a 2FA code.',
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (!this.$checkTOTPCode(data.code)) {
|
|
566
|
+
return { result: false, message: 'Incorrect 2FA code.' };
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
518
570
|
try {
|
|
519
571
|
for (let callback of (this.constructor as typeof User)
|
|
520
572
|
.beforeLoginCallbacks) {
|
|
@@ -545,12 +597,84 @@ export default class User extends AbleObject<UserData> {
|
|
|
545
597
|
return { result: true, message: 'You are logged in.' };
|
|
546
598
|
}
|
|
547
599
|
|
|
600
|
+
public async $switchUser(data?: { additionalData?: { [k: string]: any } }) {
|
|
601
|
+
const tilmeld = enforceTilmeld(this);
|
|
602
|
+
if (this.guid == null) {
|
|
603
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
let user = tilmeld.User.current();
|
|
607
|
+
|
|
608
|
+
if (!user || user.guid == null) {
|
|
609
|
+
return { result: false, message: "You're not logged in." };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
613
|
+
|
|
614
|
+
if (isCurrentUser) {
|
|
615
|
+
return { result: false, message: "You can't switch to yourself." };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const toAbilities = this.$data.abilities || [];
|
|
619
|
+
const fromAbilities = user.abilities || [];
|
|
620
|
+
|
|
621
|
+
if (
|
|
622
|
+
fromAbilities.indexOf('tilmeld/switch') === -1 &&
|
|
623
|
+
fromAbilities.indexOf('system/admin') === -1
|
|
624
|
+
) {
|
|
625
|
+
return {
|
|
626
|
+
result: false,
|
|
627
|
+
message: "You don't have permission to switch users.",
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (
|
|
632
|
+
toAbilities.indexOf('system/admin') !== -1 ||
|
|
633
|
+
toAbilities.indexOf('tilmeld/admin') !== -1
|
|
634
|
+
) {
|
|
635
|
+
return { result: false, message: "You can't switch to an admin." };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
for (let callback of (this.constructor as typeof User)
|
|
640
|
+
.beforeLoginCallbacks) {
|
|
641
|
+
if (callback) {
|
|
642
|
+
await callback(this, {
|
|
643
|
+
username: this.$data.username || '',
|
|
644
|
+
password: '',
|
|
645
|
+
...(data || {}),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} catch (e: any) {
|
|
650
|
+
return {
|
|
651
|
+
result: false,
|
|
652
|
+
message: e.message,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Authentication was successful, attempt to login.
|
|
657
|
+
if (!tilmeld.loginSwitch(this, true)) {
|
|
658
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for (let callback of (this.constructor as typeof User)
|
|
662
|
+
.afterLoginCallbacks) {
|
|
663
|
+
if (callback) {
|
|
664
|
+
await callback(this);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Login was successful.
|
|
669
|
+
return { result: true, message: 'You are logged in.' };
|
|
670
|
+
}
|
|
671
|
+
|
|
548
672
|
/**
|
|
549
673
|
* Log a user out of the system.
|
|
550
674
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
551
675
|
*/
|
|
552
676
|
public async $logout() {
|
|
553
|
-
const tilmeld = this
|
|
677
|
+
const tilmeld = enforceTilmeld(this);
|
|
554
678
|
|
|
555
679
|
try {
|
|
556
680
|
for (let callback of (this.constructor as typeof User)
|
|
@@ -566,7 +690,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
566
690
|
};
|
|
567
691
|
}
|
|
568
692
|
|
|
569
|
-
tilmeld.logout();
|
|
693
|
+
await tilmeld.logout();
|
|
570
694
|
|
|
571
695
|
for (let callback of (this.constructor as typeof User)
|
|
572
696
|
.afterLogoutCallbacks) {
|
|
@@ -592,6 +716,22 @@ export default class User extends AbleObject<UserData> {
|
|
|
592
716
|
);
|
|
593
717
|
}
|
|
594
718
|
|
|
719
|
+
public $getGid() {
|
|
720
|
+
if (this.$data.group) {
|
|
721
|
+
return this.$data.group.guid;
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
public $getGids() {
|
|
727
|
+
if (this.$data.groups) {
|
|
728
|
+
return this.$data.groups
|
|
729
|
+
.map((group) => group.guid)
|
|
730
|
+
.filter((guid) => guid != null) as string[];
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
|
|
595
735
|
/**
|
|
596
736
|
* Get the user's group descendants.
|
|
597
737
|
*/
|
|
@@ -599,8 +739,10 @@ export default class User extends AbleObject<UserData> {
|
|
|
599
739
|
if (this.$descendantGroups == null) {
|
|
600
740
|
this.$descendantGroups = [];
|
|
601
741
|
if (this.$data.group != null) {
|
|
742
|
+
await this.$data.group.$wake();
|
|
602
743
|
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
603
744
|
}
|
|
745
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
604
746
|
for (let curGroup of this.$data.groups ?? []) {
|
|
605
747
|
this.$descendantGroups = this.$descendantGroups?.concat(
|
|
606
748
|
await curGroup.$getDescendants()
|
|
@@ -610,36 +752,43 @@ export default class User extends AbleObject<UserData> {
|
|
|
610
752
|
return this.$descendantGroups;
|
|
611
753
|
}
|
|
612
754
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
this.$
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
755
|
+
public $jsonAcceptData(input: EntityJson, allowConflict = false) {
|
|
756
|
+
const tilmeld = enforceTilmeld(this);
|
|
757
|
+
this.$check();
|
|
758
|
+
|
|
759
|
+
if (
|
|
760
|
+
'abilities' in input.data &&
|
|
761
|
+
input.data.abilities?.includes('system/admin') !==
|
|
762
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
763
|
+
(!tilmeld.currentUser ||
|
|
764
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
765
|
+
) {
|
|
766
|
+
throw new BadDataError(
|
|
767
|
+
"You don't have the authority to grant or revoke system/admin."
|
|
768
|
+
);
|
|
627
769
|
}
|
|
628
|
-
return this.$descendantGroups;
|
|
629
|
-
}
|
|
630
770
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
771
|
+
if (
|
|
772
|
+
'abilities' in input.data &&
|
|
773
|
+
input.data.abilities?.includes('tilmeld/admin') !==
|
|
774
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
775
|
+
(!tilmeld.currentUser ||
|
|
776
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
777
|
+
) {
|
|
778
|
+
throw new BadDataError(
|
|
779
|
+
"You don't have the authority to grant or revoke tilmeld/admin."
|
|
780
|
+
);
|
|
781
|
+
}
|
|
634
782
|
|
|
635
783
|
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
!tilmeld.
|
|
784
|
+
'abilities' in input.data &&
|
|
785
|
+
input.data.abilities?.includes('tilmeld/switch') !==
|
|
786
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
787
|
+
(!tilmeld.currentUser ||
|
|
788
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
640
789
|
) {
|
|
641
790
|
throw new BadDataError(
|
|
642
|
-
"You don't have the authority to
|
|
791
|
+
"You don't have the authority to grant or revoke tilmeld/switch."
|
|
643
792
|
);
|
|
644
793
|
}
|
|
645
794
|
|
|
@@ -648,17 +797,42 @@ export default class User extends AbleObject<UserData> {
|
|
|
648
797
|
}
|
|
649
798
|
|
|
650
799
|
public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
|
|
651
|
-
const tilmeld = this
|
|
652
|
-
this.$
|
|
800
|
+
const tilmeld = enforceTilmeld(this);
|
|
801
|
+
this.$check();
|
|
653
802
|
|
|
654
803
|
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
!tilmeld.
|
|
804
|
+
'abilities' in patch.set &&
|
|
805
|
+
patch.set.abilities?.includes('system/admin') !==
|
|
806
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
807
|
+
(!tilmeld.currentUser ||
|
|
808
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
659
809
|
) {
|
|
660
810
|
throw new BadDataError(
|
|
661
|
-
"You don't have the authority to
|
|
811
|
+
"You don't have the authority to grant or revoke system/admin."
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (
|
|
816
|
+
'abilities' in patch.set &&
|
|
817
|
+
patch.set.abilities?.includes('tilmeld/admin') !==
|
|
818
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
819
|
+
(!tilmeld.currentUser ||
|
|
820
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
821
|
+
) {
|
|
822
|
+
throw new BadDataError(
|
|
823
|
+
"You don't have the authority to grant or revoke tilmeld/admin."
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (
|
|
828
|
+
'abilities' in patch.set &&
|
|
829
|
+
patch.set.abilities?.includes('tilmeld/switch') !==
|
|
830
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
831
|
+
(!tilmeld.currentUser ||
|
|
832
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
833
|
+
) {
|
|
834
|
+
throw new BadDataError(
|
|
835
|
+
"You don't have the authority to grant or revoke tilmeld/switch."
|
|
662
836
|
);
|
|
663
837
|
}
|
|
664
838
|
|
|
@@ -677,7 +851,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
677
851
|
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
678
852
|
*/
|
|
679
853
|
public $updateDataProtection(givenUser?: User & UserData) {
|
|
680
|
-
const tilmeld = this
|
|
854
|
+
const tilmeld = enforceTilmeld(this);
|
|
681
855
|
let user = givenUser ?? tilmeld.User.current();
|
|
682
856
|
|
|
683
857
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
@@ -701,8 +875,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
701
875
|
// Users can check to see what abilities they have.
|
|
702
876
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
703
877
|
this.$clientEnabledMethods.push('$changePassword');
|
|
878
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
704
879
|
this.$clientEnabledMethods.push('$logout');
|
|
705
880
|
this.$clientEnabledMethods.push('$sendEmailVerification');
|
|
881
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
882
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
883
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
884
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
706
885
|
}
|
|
707
886
|
|
|
708
887
|
if (
|
|
@@ -711,7 +890,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
711
890
|
abilities.indexOf('system/admin') !== -1)
|
|
712
891
|
) {
|
|
713
892
|
// Users who can edit other users can see most of their data.
|
|
714
|
-
this.$privateData = ['password', 'salt'];
|
|
893
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
715
894
|
this.$allowlistData = undefined;
|
|
716
895
|
} else if (isCurrentUser || isNewUser) {
|
|
717
896
|
// Users can see their own data, and edit some of it.
|
|
@@ -732,17 +911,56 @@ export default class User extends AbleObject<UserData> {
|
|
|
732
911
|
this.$allowlistData.push('phone');
|
|
733
912
|
}
|
|
734
913
|
this.$privateData = [
|
|
914
|
+
// The things that are commented are there to show what is _not_ private.
|
|
915
|
+
// 'abilities',
|
|
916
|
+
// 'username',
|
|
917
|
+
// 'nameFirst',
|
|
918
|
+
// 'nameMiddle',
|
|
919
|
+
// 'nameLast',
|
|
920
|
+
// 'name',
|
|
921
|
+
// 'email',
|
|
922
|
+
// 'avatar',
|
|
923
|
+
// 'phone',
|
|
924
|
+
// 'group',
|
|
925
|
+
// 'groups',
|
|
926
|
+
// 'inheritAbilities',
|
|
927
|
+
'enabled',
|
|
735
928
|
'secret',
|
|
929
|
+
'emailChangeDate',
|
|
736
930
|
'newEmailSecret',
|
|
737
|
-
'
|
|
931
|
+
// 'newEmailAddress',
|
|
738
932
|
'cancelEmailSecret',
|
|
739
|
-
'
|
|
933
|
+
'cancelEmailAddress',
|
|
740
934
|
'recoverSecret',
|
|
741
935
|
'recoverSecretDate',
|
|
742
|
-
'password',
|
|
743
936
|
'salt',
|
|
937
|
+
'password',
|
|
938
|
+
'passwordTemp',
|
|
939
|
+
// 'revokeTokenDate',
|
|
940
|
+
'totpSecret',
|
|
744
941
|
];
|
|
745
942
|
}
|
|
943
|
+
|
|
944
|
+
if (
|
|
945
|
+
user != null &&
|
|
946
|
+
!isCurrentUser &&
|
|
947
|
+
(abilities.indexOf('tilmeld/switch') !== -1 ||
|
|
948
|
+
abilities.indexOf('system/admin') !== -1)
|
|
949
|
+
) {
|
|
950
|
+
// Users with this ability can switch to other users.
|
|
951
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
public $updateGroupDataProtection() {
|
|
956
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
957
|
+
this.$data.group.$updateDataProtection();
|
|
958
|
+
}
|
|
959
|
+
for (let group of this.$data.groups ?? []) {
|
|
960
|
+
if (!group.$asleep()) {
|
|
961
|
+
group.$updateDataProtection();
|
|
962
|
+
}
|
|
963
|
+
}
|
|
746
964
|
}
|
|
747
965
|
|
|
748
966
|
/**
|
|
@@ -759,8 +977,8 @@ export default class User extends AbleObject<UserData> {
|
|
|
759
977
|
* @param ability The ability.
|
|
760
978
|
* @returns True or false.
|
|
761
979
|
*/
|
|
762
|
-
public $gatekeeper(ability?: string): boolean {
|
|
763
|
-
const tilmeld = this
|
|
980
|
+
public async $gatekeeper(ability?: string): Promise<boolean> {
|
|
981
|
+
const tilmeld = enforceTilmeld(this);
|
|
764
982
|
if (ability == null) {
|
|
765
983
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
766
984
|
}
|
|
@@ -768,17 +986,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
768
986
|
if (this.$gatekeeperCache == null) {
|
|
769
987
|
let abilities = this.$data.abilities ?? [];
|
|
770
988
|
if (this.$data.inheritAbilities) {
|
|
989
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
771
990
|
for (let curGroup of this.$data.groups ?? []) {
|
|
772
991
|
if (curGroup.enabled) {
|
|
773
992
|
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
774
993
|
}
|
|
775
994
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
) {
|
|
781
|
-
abilities = abilities.concat(this.$data.group.abilities ?? []);
|
|
995
|
+
await this.$data.group?.$wake();
|
|
996
|
+
const group = this.$data.group;
|
|
997
|
+
if (group != null && group.cdate != null && group.enabled) {
|
|
998
|
+
abilities = abilities.concat(group.abilities ?? []);
|
|
782
999
|
}
|
|
783
1000
|
}
|
|
784
1001
|
this.$gatekeeperCache = Object.fromEntries(
|
|
@@ -787,7 +1004,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
787
1004
|
}
|
|
788
1005
|
return (
|
|
789
1006
|
(ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
790
|
-
!!this.$
|
|
1007
|
+
!!this.$data.abilities?.includes('system/admin')
|
|
791
1008
|
);
|
|
792
1009
|
}
|
|
793
1010
|
|
|
@@ -804,7 +1021,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
804
1021
|
* @returns True on success, false on failure.
|
|
805
1022
|
*/
|
|
806
1023
|
public async $sendEmailVerification() {
|
|
807
|
-
const tilmeld = this
|
|
1024
|
+
const tilmeld = enforceTilmeld(this);
|
|
808
1025
|
if (this.guid == null) {
|
|
809
1026
|
return false;
|
|
810
1027
|
}
|
|
@@ -899,13 +1116,16 @@ export default class User extends AbleObject<UserData> {
|
|
|
899
1116
|
* @param group The group.
|
|
900
1117
|
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
901
1118
|
*/
|
|
902
|
-
public $addGroup(group: Group & GroupData) {
|
|
903
|
-
|
|
904
|
-
|
|
1119
|
+
public async $addGroup(group: Group & GroupData) {
|
|
1120
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1121
|
+
let groups = this.$data.groups;
|
|
1122
|
+
if (groups == null) {
|
|
1123
|
+
groups = [];
|
|
905
1124
|
}
|
|
906
|
-
if (!group.$inArray(
|
|
907
|
-
|
|
908
|
-
|
|
1125
|
+
if (!group.$inArray(groups)) {
|
|
1126
|
+
groups.push(group);
|
|
1127
|
+
this.$data.groups = groups;
|
|
1128
|
+
return groups;
|
|
909
1129
|
}
|
|
910
1130
|
return true;
|
|
911
1131
|
}
|
|
@@ -917,7 +1137,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
917
1137
|
* @returns True if the passwords match, otherwise false.
|
|
918
1138
|
*/
|
|
919
1139
|
public $checkPassword(password: string) {
|
|
920
|
-
const tilmeld = this
|
|
1140
|
+
const tilmeld = enforceTilmeld(this);
|
|
921
1141
|
switch (tilmeld.config.pwMethod) {
|
|
922
1142
|
case 'plain':
|
|
923
1143
|
return this.$data.password === password;
|
|
@@ -938,10 +1158,12 @@ export default class User extends AbleObject<UserData> {
|
|
|
938
1158
|
* @param group The group.
|
|
939
1159
|
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
940
1160
|
*/
|
|
941
|
-
public $delGroup(group: Group & GroupData) {
|
|
942
|
-
|
|
1161
|
+
public async $delGroup(group: Group & GroupData) {
|
|
1162
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1163
|
+
let groups = this.$data.groups || [];
|
|
1164
|
+
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
943
1165
|
const newGroups: (Group & GroupData)[] = [];
|
|
944
|
-
for (let curGroup of
|
|
1166
|
+
for (let curGroup of groups) {
|
|
945
1167
|
if (!group.$is(curGroup)) {
|
|
946
1168
|
newGroups.push(curGroup);
|
|
947
1169
|
}
|
|
@@ -958,7 +1180,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
958
1180
|
* @param group The group, or the group's GUID.
|
|
959
1181
|
* @returns True or false.
|
|
960
1182
|
*/
|
|
961
|
-
public $inGroup(group: (Group & GroupData) | string) {
|
|
1183
|
+
public async $inGroup(group: (Group & GroupData) | string) {
|
|
962
1184
|
if (typeof group === 'string') {
|
|
963
1185
|
if (this.$data.group?.guid === group) {
|
|
964
1186
|
return true;
|
|
@@ -981,16 +1203,19 @@ export default class User extends AbleObject<UserData> {
|
|
|
981
1203
|
* @param group The group, or the group's GUID.
|
|
982
1204
|
* @returns True or false.
|
|
983
1205
|
*/
|
|
984
|
-
public $isDescendant(group: (Group & GroupData) | string) {
|
|
1206
|
+
public async $isDescendant(group: (Group & GroupData) | string) {
|
|
985
1207
|
// Check to see if the user is in a descendant group of the given group.
|
|
1208
|
+
await this.$data.group?.$wake();
|
|
986
1209
|
if (
|
|
1210
|
+
this.$data.group != null &&
|
|
987
1211
|
this.$data.group?.cdate != null &&
|
|
988
|
-
this.$data.group
|
|
1212
|
+
(await this.$data.group?.$isDescendant(group))
|
|
989
1213
|
) {
|
|
990
1214
|
return true;
|
|
991
1215
|
}
|
|
1216
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
992
1217
|
for (let curGroup of this.$data.groups ?? []) {
|
|
993
|
-
if (curGroup.$isDescendant(group)) {
|
|
1218
|
+
if (await curGroup.$isDescendant(group)) {
|
|
994
1219
|
return true;
|
|
995
1220
|
}
|
|
996
1221
|
}
|
|
@@ -1006,6 +1231,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1006
1231
|
public async $changePassword(data: {
|
|
1007
1232
|
newPassword: string;
|
|
1008
1233
|
currentPassword: string;
|
|
1234
|
+
revokeCurrentTokens?: boolean;
|
|
1009
1235
|
}): Promise<{ result: boolean; message: string }> {
|
|
1010
1236
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
1011
1237
|
return { result: false, message: 'Please specify a password.' };
|
|
@@ -1014,7 +1240,14 @@ export default class User extends AbleObject<UserData> {
|
|
|
1014
1240
|
return { result: false, message: 'Incorrect password.' };
|
|
1015
1241
|
}
|
|
1016
1242
|
this.$data.passwordTemp = data.newPassword;
|
|
1243
|
+
if (data.revokeCurrentTokens) {
|
|
1244
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1245
|
+
}
|
|
1017
1246
|
if (await this.$save()) {
|
|
1247
|
+
if (data.revokeCurrentTokens) {
|
|
1248
|
+
const tilmeld = enforceTilmeld(this);
|
|
1249
|
+
tilmeld.login(this, true);
|
|
1250
|
+
}
|
|
1018
1251
|
return { result: true, message: 'Your password has been changed.' };
|
|
1019
1252
|
} else {
|
|
1020
1253
|
return { result: false, message: "Couldn't save new password." };
|
|
@@ -1028,7 +1261,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1028
1261
|
* @returns The resulting password or hash which is stored in the entity.
|
|
1029
1262
|
*/
|
|
1030
1263
|
public $password(password: string) {
|
|
1031
|
-
const tilmeld = this
|
|
1264
|
+
const tilmeld = enforceTilmeld(this);
|
|
1032
1265
|
switch (tilmeld.config.pwMethod) {
|
|
1033
1266
|
case 'plain':
|
|
1034
1267
|
delete this.$data.salt;
|
|
@@ -1049,13 +1282,216 @@ export default class User extends AbleObject<UserData> {
|
|
|
1049
1282
|
return this.$data.password;
|
|
1050
1283
|
}
|
|
1051
1284
|
|
|
1285
|
+
/**
|
|
1286
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
1287
|
+
*
|
|
1288
|
+
* @param data The input data from the client.
|
|
1289
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1290
|
+
*/
|
|
1291
|
+
public async $revokeCurrentTokens(data: {
|
|
1292
|
+
password: string;
|
|
1293
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1294
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1295
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1299
|
+
|
|
1300
|
+
if (await this.$save()) {
|
|
1301
|
+
const tilmeld = enforceTilmeld(this);
|
|
1302
|
+
tilmeld.login(this, true);
|
|
1303
|
+
return {
|
|
1304
|
+
result: true,
|
|
1305
|
+
message: 'You have logged out of all other sessions.',
|
|
1306
|
+
};
|
|
1307
|
+
} else {
|
|
1308
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
public async $hasTOTPSecret() {
|
|
1313
|
+
return this.$data.totpSecret != null;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Check the given code against the user's TOTP secret.
|
|
1318
|
+
*
|
|
1319
|
+
* @param code The code in question.
|
|
1320
|
+
* @returns True if the code is valid, otherwise false.
|
|
1321
|
+
*/
|
|
1322
|
+
public $checkTOTPCode(code: string) {
|
|
1323
|
+
if (this.$data.totpSecret == null) {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
const tilmeld = enforceTilmeld(this);
|
|
1328
|
+
|
|
1329
|
+
const totp = new TOTP({
|
|
1330
|
+
issuer: tilmeld.config.appName,
|
|
1331
|
+
label: this.$data.username,
|
|
1332
|
+
algorithm: 'SHA1',
|
|
1333
|
+
digits: 6,
|
|
1334
|
+
period: 30,
|
|
1335
|
+
secret: this.$data.totpSecret,
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
1343
|
+
*
|
|
1344
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
1345
|
+
*/
|
|
1346
|
+
public async $getNewTOTPSecret() {
|
|
1347
|
+
const tilmeld = enforceTilmeld(this);
|
|
1348
|
+
|
|
1349
|
+
if (this.$data.totpSecret != null) {
|
|
1350
|
+
throw new BadDataError('You already have a 2FA secret.');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const secret = new Secret();
|
|
1354
|
+
|
|
1355
|
+
const totp = new TOTP({
|
|
1356
|
+
issuer: tilmeld.config.appName,
|
|
1357
|
+
label: this.$data.username,
|
|
1358
|
+
algorithm: 'SHA1',
|
|
1359
|
+
digits: 6,
|
|
1360
|
+
period: 30,
|
|
1361
|
+
secret,
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
const uri = totp.toString();
|
|
1365
|
+
|
|
1366
|
+
const qrcode = await toDataURL(uri);
|
|
1367
|
+
|
|
1368
|
+
return {
|
|
1369
|
+
uri,
|
|
1370
|
+
qrcode,
|
|
1371
|
+
secret: secret.base32,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1377
|
+
*
|
|
1378
|
+
* @param data The input data from the client.
|
|
1379
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1380
|
+
*/
|
|
1381
|
+
public async $saveTOTPSecret(data: {
|
|
1382
|
+
password: string;
|
|
1383
|
+
secret: string;
|
|
1384
|
+
code: string;
|
|
1385
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1386
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1387
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (this.$data.totpSecret != null) {
|
|
1391
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (
|
|
1395
|
+
data.secret == null ||
|
|
1396
|
+
typeof data.secret !== 'string' ||
|
|
1397
|
+
data.secret === ''
|
|
1398
|
+
) {
|
|
1399
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (
|
|
1403
|
+
data.code == null ||
|
|
1404
|
+
typeof data.code !== 'string' ||
|
|
1405
|
+
data.code.length !== 6
|
|
1406
|
+
) {
|
|
1407
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const tilmeld = enforceTilmeld(this);
|
|
1411
|
+
|
|
1412
|
+
const totp = new TOTP({
|
|
1413
|
+
issuer: tilmeld.config.appName,
|
|
1414
|
+
label: this.$data.username,
|
|
1415
|
+
algorithm: 'SHA1',
|
|
1416
|
+
digits: 6,
|
|
1417
|
+
period: 30,
|
|
1418
|
+
secret: data.secret,
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1422
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
this.$data.totpSecret = data.secret;
|
|
1426
|
+
|
|
1427
|
+
if (await this.$save()) {
|
|
1428
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1429
|
+
} else {
|
|
1430
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1436
|
+
* account.
|
|
1437
|
+
*
|
|
1438
|
+
* @param data The input data from the client.
|
|
1439
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1440
|
+
*/
|
|
1441
|
+
public async $removeTOTPSecret(data: {
|
|
1442
|
+
password: string;
|
|
1443
|
+
code: string;
|
|
1444
|
+
}): Promise<{ result: boolean; message: string }> {
|
|
1445
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1446
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
if (this.$data.totpSecret == null) {
|
|
1450
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if (
|
|
1454
|
+
data.code == null ||
|
|
1455
|
+
typeof data.code !== 'string' ||
|
|
1456
|
+
data.code.length !== 6
|
|
1457
|
+
) {
|
|
1458
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const tilmeld = enforceTilmeld(this);
|
|
1462
|
+
|
|
1463
|
+
const totp = new TOTP({
|
|
1464
|
+
issuer: tilmeld.config.appName,
|
|
1465
|
+
label: this.$data.username,
|
|
1466
|
+
algorithm: 'SHA1',
|
|
1467
|
+
digits: 6,
|
|
1468
|
+
period: 30,
|
|
1469
|
+
secret: this.$data.totpSecret,
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1473
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
delete this.$data.totpSecret;
|
|
1477
|
+
|
|
1478
|
+
if (await this.$save()) {
|
|
1479
|
+
return {
|
|
1480
|
+
result: true,
|
|
1481
|
+
message: 'Your two factor secret has been removed.',
|
|
1482
|
+
};
|
|
1483
|
+
} else {
|
|
1484
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1052
1488
|
/**
|
|
1053
1489
|
* Check that a username is valid.
|
|
1054
1490
|
*
|
|
1055
1491
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1056
1492
|
*/
|
|
1057
1493
|
public async $checkUsername(): Promise<{ result: boolean; message: string }> {
|
|
1058
|
-
const tilmeld = this
|
|
1494
|
+
const tilmeld = enforceTilmeld(this);
|
|
1059
1495
|
if (!tilmeld.config.emailUsernames) {
|
|
1060
1496
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
1061
1497
|
return { result: false, message: 'Please specify a username.' };
|
|
@@ -1109,6 +1545,18 @@ export default class User extends AbleObject<UserData> {
|
|
|
1109
1545
|
return { result: false, message: 'That username is taken.' };
|
|
1110
1546
|
}
|
|
1111
1547
|
|
|
1548
|
+
for (let callback of (this.constructor as typeof User)
|
|
1549
|
+
.checkUsernameCallbacks) {
|
|
1550
|
+
if (callback) {
|
|
1551
|
+
const result = await callback(this, {
|
|
1552
|
+
username: this.$data.username,
|
|
1553
|
+
});
|
|
1554
|
+
if (!result.result) {
|
|
1555
|
+
return { message: 'Username is not available.', ...result };
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1112
1560
|
return {
|
|
1113
1561
|
result: true,
|
|
1114
1562
|
message:
|
|
@@ -1142,7 +1590,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1142
1590
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1143
1591
|
*/
|
|
1144
1592
|
public async $checkEmail(): Promise<{ result: boolean; message: string }> {
|
|
1145
|
-
const tilmeld = this
|
|
1593
|
+
const tilmeld = enforceTilmeld(this);
|
|
1146
1594
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
1147
1595
|
if (tilmeld.config.verifyEmail) {
|
|
1148
1596
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -1187,7 +1635,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1187
1635
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1188
1636
|
*/
|
|
1189
1637
|
public async $checkPhone(): Promise<{ result: boolean; message: string }> {
|
|
1190
|
-
const tilmeld = this
|
|
1638
|
+
const tilmeld = enforceTilmeld(this);
|
|
1191
1639
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
1192
1640
|
return { result: false, message: 'Please specify a phone number.' };
|
|
1193
1641
|
}
|
|
@@ -1226,7 +1674,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1226
1674
|
password: string;
|
|
1227
1675
|
additionalData?: { [k: string]: any };
|
|
1228
1676
|
}): Promise<{ result: boolean; loggedin: boolean; message: string }> {
|
|
1229
|
-
let tilmeld = this
|
|
1677
|
+
let tilmeld = enforceTilmeld(this);
|
|
1230
1678
|
if (!tilmeld.config.allowRegistration) {
|
|
1231
1679
|
return {
|
|
1232
1680
|
result: false,
|
|
@@ -1288,7 +1736,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1288
1736
|
const nymph = this.$nymph;
|
|
1289
1737
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1290
1738
|
this.$nymph = tnymph;
|
|
1291
|
-
tilmeld = this
|
|
1739
|
+
tilmeld = enforceTilmeld(this);
|
|
1292
1740
|
|
|
1293
1741
|
try {
|
|
1294
1742
|
// Add primary group.
|
|
@@ -1493,15 +1941,15 @@ export default class User extends AbleObject<UserData> {
|
|
|
1493
1941
|
}
|
|
1494
1942
|
|
|
1495
1943
|
public async $save() {
|
|
1496
|
-
let tilmeld = this
|
|
1944
|
+
let tilmeld = enforceTilmeld(this);
|
|
1497
1945
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
1498
1946
|
return false;
|
|
1499
1947
|
}
|
|
1500
1948
|
|
|
1501
1949
|
if (
|
|
1502
|
-
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1503
|
-
!tilmeld.gatekeeper('system/admin') &&
|
|
1504
|
-
this.$
|
|
1950
|
+
(await tilmeld.gatekeeper('tilmeld/admin')) &&
|
|
1951
|
+
!(await tilmeld.gatekeeper('system/admin')) &&
|
|
1952
|
+
this.$data.abilities?.includes('system/admin')
|
|
1505
1953
|
) {
|
|
1506
1954
|
throw new BadDataError(
|
|
1507
1955
|
"You don't have the authority to modify system admins."
|
|
@@ -1594,7 +2042,7 @@ export default class User extends AbleObject<UserData> {
|
|
|
1594
2042
|
}
|
|
1595
2043
|
|
|
1596
2044
|
// Email changes.
|
|
1597
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2045
|
+
if (!(await tilmeld.gatekeeper('tilmeld/admin'))) {
|
|
1598
2046
|
// The user isn't an admin, so email address changes should contain some
|
|
1599
2047
|
// security measures.
|
|
1600
2048
|
if (tilmeld.config.verifyEmail) {
|
|
@@ -1691,21 +2139,20 @@ export default class User extends AbleObject<UserData> {
|
|
|
1691
2139
|
const nymph = this.$nymph;
|
|
1692
2140
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1693
2141
|
this.$nymph = tnymph;
|
|
1694
|
-
tilmeld = this
|
|
2142
|
+
tilmeld = enforceTilmeld(this);
|
|
1695
2143
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
this.$is(this.$data.group.user)
|
|
1700
|
-
) {
|
|
2144
|
+
const group = this.$data.group;
|
|
2145
|
+
await group?.$wake();
|
|
2146
|
+
if (group != null && group.user != null && this.$is(group.user)) {
|
|
1701
2147
|
try {
|
|
1702
2148
|
// Update the user's generated primary group.
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
await
|
|
2149
|
+
group.groupname = this.$data.username;
|
|
2150
|
+
group.avatar = this.$data.avatar;
|
|
2151
|
+
group.email = this.$data.email;
|
|
2152
|
+
group.name = this.$data.name;
|
|
2153
|
+
group.phone = this.$data.phone;
|
|
2154
|
+
await group.$saveSkipAC();
|
|
2155
|
+
this.$data.group = group;
|
|
1709
2156
|
} catch (e: any) {
|
|
1710
2157
|
await tnymph.rollback(transaction);
|
|
1711
2158
|
this.$nymph = nymph;
|
|
@@ -1742,9 +2189,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1742
2189
|
this.$gatekeeperCache = undefined;
|
|
1743
2190
|
await tnymph.commit(transaction);
|
|
1744
2191
|
|
|
1745
|
-
if ((nymph
|
|
2192
|
+
if (enforceTilmeld(nymph).User.current(true).$is(this)) {
|
|
1746
2193
|
// Update the user in the session cache.
|
|
1747
|
-
(nymph
|
|
2194
|
+
enforceTilmeld(nymph).fillSession(this);
|
|
1748
2195
|
}
|
|
1749
2196
|
} else {
|
|
1750
2197
|
await tnymph.rollback(transaction);
|
|
@@ -1770,13 +2217,13 @@ export default class User extends AbleObject<UserData> {
|
|
|
1770
2217
|
}
|
|
1771
2218
|
|
|
1772
2219
|
public async $delete() {
|
|
1773
|
-
const tilmeld = this
|
|
1774
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2220
|
+
const tilmeld = enforceTilmeld(this);
|
|
2221
|
+
if (!(await tilmeld.gatekeeper('tilmeld/admin'))) {
|
|
1775
2222
|
throw new BadDataError("You don't have the authority to delete users.");
|
|
1776
2223
|
}
|
|
1777
2224
|
if (
|
|
1778
|
-
!tilmeld.gatekeeper('system/admin') &&
|
|
1779
|
-
this.$
|
|
2225
|
+
!(await tilmeld.gatekeeper('system/admin')) &&
|
|
2226
|
+
this.$data.abilities?.includes('system/admin')
|
|
1780
2227
|
) {
|
|
1781
2228
|
throw new BadDataError(
|
|
1782
2229
|
"You don't have the authority to delete system admins."
|
|
@@ -1790,7 +2237,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1790
2237
|
|
|
1791
2238
|
public static on<T extends EventType>(
|
|
1792
2239
|
event: T,
|
|
1793
|
-
callback: T extends '
|
|
2240
|
+
callback: T extends 'checkUsername'
|
|
2241
|
+
? TilmeldCheckUsernameCallback
|
|
2242
|
+
: T extends 'beforeRegister'
|
|
1794
2243
|
? TilmeldBeforeRegisterCallback
|
|
1795
2244
|
: T extends 'afterRegister'
|
|
1796
2245
|
? TilmeldAfterRegisterCallback
|
|
@@ -1804,7 +2253,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1804
2253
|
? TilmeldAfterLogoutCallback
|
|
1805
2254
|
: never
|
|
1806
2255
|
) {
|
|
1807
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
2256
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2257
|
+
? 'checkUsernameCallbacks'
|
|
2258
|
+
: T extends 'beforeRegister'
|
|
1808
2259
|
? 'beforeRegisterCallbacks'
|
|
1809
2260
|
: T extends 'afterRegister'
|
|
1810
2261
|
? 'afterRegisterCallbacks'
|
|
@@ -1827,7 +2278,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1827
2278
|
|
|
1828
2279
|
public static off<T extends EventType>(
|
|
1829
2280
|
event: T,
|
|
1830
|
-
callback: T extends '
|
|
2281
|
+
callback: T extends 'checkUsername'
|
|
2282
|
+
? TilmeldCheckUsernameCallback
|
|
2283
|
+
: T extends 'beforeRegister'
|
|
1831
2284
|
? TilmeldBeforeRegisterCallback
|
|
1832
2285
|
: T extends 'afterRegister'
|
|
1833
2286
|
? TilmeldAfterRegisterCallback
|
|
@@ -1841,7 +2294,9 @@ export default class User extends AbleObject<UserData> {
|
|
|
1841
2294
|
? TilmeldAfterLogoutCallback
|
|
1842
2295
|
: never
|
|
1843
2296
|
) {
|
|
1844
|
-
const prop = (event + 'Callbacks') as T extends '
|
|
2297
|
+
const prop = (event + 'Callbacks') as T extends 'checkUsername'
|
|
2298
|
+
? 'checkUsernameCallbacks'
|
|
2299
|
+
: T extends 'beforeRegister'
|
|
1845
2300
|
? 'beforeRegisterCallbacks'
|
|
1846
2301
|
: T extends 'afterRegister'
|
|
1847
2302
|
? 'afterRegisterCallbacks'
|