@nymphjs/tilmeld 1.0.0-beta.6 → 1.0.0-beta.61
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 +284 -0
- package/README.md +14 -1
- package/dist/AbleObject.d.ts +15 -0
- package/dist/AbleObject.js +16 -1
- package/dist/AbleObject.js.map +1 -1
- package/dist/Group.d.ts +119 -8
- package/dist/Group.js +230 -97
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +3 -2
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +237 -9
- package/dist/Tilmeld.js +559 -195
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +333 -14
- package/dist/User.js +797 -204
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +3 -2
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +250 -3
- package/dist/conf/defaults.js +146 -98
- 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/errors/AccessControlError.d.ts +2 -1
- package/dist/errors/AccessControlError.js +3 -2
- package/dist/errors/AccessControlError.js.map +1 -1
- package/dist/errors/BadDataError.d.ts +2 -1
- package/dist/errors/BadDataError.js +3 -2
- package/dist/errors/BadDataError.js.map +1 -1
- package/dist/errors/BadEmailError.d.ts +2 -1
- package/dist/errors/BadEmailError.js +3 -2
- package/dist/errors/BadEmailError.js.map +1 -1
- package/dist/errors/BadUsernameError.d.ts +2 -1
- package/dist/errors/BadUsernameError.js +3 -2
- package/dist/errors/BadUsernameError.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +30 -26
- package/src/AbleObject.ts +2 -2
- package/src/Group.test.ts +3 -3
- package/src/Group.ts +194 -143
- package/src/Tilmeld.ts +384 -249
- package/src/User.test.ts +3 -3
- package/src/User.ts +876 -266
- package/src/conf/d.ts +26 -13
- package/src/conf/defaults.ts +151 -104
- 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/index.ts +2 -0
- package/src/tilmeldJoiProps.ts +9 -9
- package/tsconfig.json +1 -1
- package/typedoc.json +4 -0
package/dist/User.js
CHANGED
|
@@ -3,18 +3,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const guid_1 = require("@nymphjs/guid");
|
|
6
7
|
const strtotime_1 = __importDefault(require("locutus/php/datetime/strtotime"));
|
|
7
8
|
const enc_base64_1 = __importDefault(require("crypto-js/enc-base64"));
|
|
8
9
|
const sha256_1 = __importDefault(require("crypto-js/sha256"));
|
|
9
10
|
const md5_1 = __importDefault(require("crypto-js/md5"));
|
|
10
11
|
const lodash_1 = require("lodash");
|
|
11
|
-
const
|
|
12
|
+
const otpauth_1 = require("otpauth");
|
|
13
|
+
const qrcode_1 = require("qrcode");
|
|
14
|
+
const enforceTilmeld_1 = require("./enforceTilmeld");
|
|
12
15
|
const AbleObject_1 = __importDefault(require("./AbleObject"));
|
|
13
16
|
const errors_1 = require("./errors");
|
|
17
|
+
/**
|
|
18
|
+
* A user data model.
|
|
19
|
+
*
|
|
20
|
+
* Written by Hunter Perrin for SciActive.
|
|
21
|
+
*
|
|
22
|
+
* @author Hunter Perrin <hperrin@gmail.com>
|
|
23
|
+
* @copyright SciActive Inc
|
|
24
|
+
* @see http://nymph.io/
|
|
25
|
+
*/
|
|
14
26
|
class User extends AbleObject_1.default {
|
|
15
|
-
static async factory(guid) {
|
|
16
|
-
return (await super.factory(guid));
|
|
17
|
-
}
|
|
18
27
|
static async factoryUsername(username) {
|
|
19
28
|
const entity = new this();
|
|
20
29
|
if (username != null) {
|
|
@@ -30,32 +39,39 @@ class User extends AbleObject_1.default {
|
|
|
30
39
|
}
|
|
31
40
|
return entity;
|
|
32
41
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
constructor(guid) {
|
|
37
|
-
super(guid);
|
|
42
|
+
constructor() {
|
|
43
|
+
super();
|
|
38
44
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
39
45
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
40
46
|
this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
41
47
|
this.$allowlistTags = [];
|
|
48
|
+
/**
|
|
49
|
+
* This is explicitly used only during the registration proccess.
|
|
50
|
+
*/
|
|
42
51
|
this.$skipAcWhenSaving = false;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
52
|
+
// Defaults.
|
|
53
|
+
this.$data.enabled = true;
|
|
54
|
+
this.$data.abilities = [];
|
|
55
|
+
this.$data.groups = [];
|
|
56
|
+
this.$data.inheritAbilities = true;
|
|
57
|
+
this.$updateDataProtection();
|
|
50
58
|
}
|
|
51
59
|
static current(returnObjectIfNotExist) {
|
|
52
|
-
|
|
60
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
61
|
+
if (tilmeld.currentUser == null) {
|
|
53
62
|
return returnObjectIfNotExist ? this.factorySync() : null;
|
|
54
63
|
}
|
|
55
|
-
return
|
|
64
|
+
return tilmeld.currentUser;
|
|
56
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Send an account recovery link.
|
|
68
|
+
*
|
|
69
|
+
* @param data The input data from the client.
|
|
70
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
71
|
+
*/
|
|
57
72
|
static async sendRecovery(data) {
|
|
58
|
-
|
|
73
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
74
|
+
if (!tilmeld.config.pwRecovery) {
|
|
59
75
|
return {
|
|
60
76
|
result: false,
|
|
61
77
|
message: 'Account recovery is not allowed.',
|
|
@@ -63,9 +79,9 @@ class User extends AbleObject_1.default {
|
|
|
63
79
|
}
|
|
64
80
|
let user;
|
|
65
81
|
const options = {};
|
|
66
|
-
if (!
|
|
67
|
-
|
|
68
|
-
const getUser = await this.nymph.getEntity({ class:
|
|
82
|
+
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
83
|
+
// Create a username recovery email.
|
|
84
|
+
const getUser = await this.nymph.getEntity({ class: tilmeld.User, skipAc: true }, {
|
|
69
85
|
type: '&',
|
|
70
86
|
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
71
87
|
});
|
|
@@ -75,6 +91,7 @@ class User extends AbleObject_1.default {
|
|
|
75
91
|
message: 'Requested account is not accessible.',
|
|
76
92
|
};
|
|
77
93
|
}
|
|
94
|
+
// Send the recovery email.
|
|
78
95
|
user = getUser;
|
|
79
96
|
options.template = 'RecoverUsername';
|
|
80
97
|
options.message = {
|
|
@@ -86,18 +103,21 @@ class User extends AbleObject_1.default {
|
|
|
86
103
|
options.locals = {};
|
|
87
104
|
}
|
|
88
105
|
else if (data.recoveryType === 'password') {
|
|
89
|
-
|
|
106
|
+
// Create a password recovery email.
|
|
107
|
+
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
90
108
|
if (getUser.guid == null) {
|
|
91
109
|
return {
|
|
92
110
|
result: false,
|
|
93
111
|
message: 'Requested account is not accessible.',
|
|
94
112
|
};
|
|
95
113
|
}
|
|
114
|
+
// Create a unique secret.
|
|
96
115
|
getUser.recoverSecret = (0, guid_1.humanSecret)();
|
|
97
116
|
getUser.recoverSecretDate = Date.now();
|
|
98
117
|
if (!(await getUser.$saveSkipAC())) {
|
|
99
118
|
return { result: false, message: "Couldn't save recovery secret." };
|
|
100
119
|
}
|
|
120
|
+
// Send the recovery email.
|
|
101
121
|
user = getUser;
|
|
102
122
|
options.template = 'RecoverPassword';
|
|
103
123
|
options.message = {
|
|
@@ -108,13 +128,15 @@ class User extends AbleObject_1.default {
|
|
|
108
128
|
};
|
|
109
129
|
options.locals = {
|
|
110
130
|
recoverCode: getUser.recoverSecret,
|
|
111
|
-
timeLimit:
|
|
131
|
+
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
112
132
|
};
|
|
113
133
|
}
|
|
114
134
|
else {
|
|
115
135
|
return { result: false, message: 'Invalid recovery type.' };
|
|
116
136
|
}
|
|
117
|
-
|
|
137
|
+
this.nymph.config.debugLog('tilmeld', `User account recovery (${options.template}) request for "${user.username}".`);
|
|
138
|
+
// Send the email.
|
|
139
|
+
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
118
140
|
return {
|
|
119
141
|
result: true,
|
|
120
142
|
message: "We've sent an email to your registered address. Please " +
|
|
@@ -125,18 +147,25 @@ class User extends AbleObject_1.default {
|
|
|
125
147
|
return { result: false, message: "Couldn't send recovery email." };
|
|
126
148
|
}
|
|
127
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Recover account details.
|
|
152
|
+
*
|
|
153
|
+
* @param data The input data from the client.
|
|
154
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
155
|
+
*/
|
|
128
156
|
static async recover(data) {
|
|
129
|
-
|
|
157
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
158
|
+
if (!tilmeld.config.pwRecovery) {
|
|
130
159
|
return {
|
|
131
160
|
result: false,
|
|
132
161
|
message: 'Account recovery is not allowed.',
|
|
133
162
|
};
|
|
134
163
|
}
|
|
135
|
-
const user = await
|
|
164
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
136
165
|
if (user.guid == null ||
|
|
137
166
|
user.recoverSecret == null ||
|
|
138
167
|
data.secret !== user.recoverSecret ||
|
|
139
|
-
(0, strtotime_1.default)('+' +
|
|
168
|
+
(0, strtotime_1.default)('+' + tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) *
|
|
140
169
|
1000 <
|
|
141
170
|
Date.now()) {
|
|
142
171
|
return {
|
|
@@ -147,6 +176,7 @@ class User extends AbleObject_1.default {
|
|
|
147
176
|
if (!('password' in data) || !data.password.length) {
|
|
148
177
|
return { result: false, message: 'Password cannot be empty.' };
|
|
149
178
|
}
|
|
179
|
+
this.nymph.config.debugLog('tilmeld', `User account recovery for "${user.username}".`);
|
|
150
180
|
user.$password(data.password);
|
|
151
181
|
delete user.recoverSecret;
|
|
152
182
|
delete user.recoverSecretDate;
|
|
@@ -161,22 +191,24 @@ class User extends AbleObject_1.default {
|
|
|
161
191
|
}
|
|
162
192
|
}
|
|
163
193
|
static getClientConfig() {
|
|
194
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
164
195
|
return {
|
|
165
|
-
regFields:
|
|
166
|
-
userFields:
|
|
167
|
-
emailUsernames:
|
|
168
|
-
allowRegistration:
|
|
169
|
-
allowUsernameChange:
|
|
170
|
-
pwRecovery:
|
|
171
|
-
verifyEmail:
|
|
172
|
-
unverifiedAccess:
|
|
196
|
+
regFields: tilmeld.config.regFields,
|
|
197
|
+
userFields: tilmeld.config.userFields,
|
|
198
|
+
emailUsernames: tilmeld.config.emailUsernames,
|
|
199
|
+
allowRegistration: tilmeld.config.allowRegistration,
|
|
200
|
+
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
201
|
+
pwRecovery: tilmeld.config.pwRecovery,
|
|
202
|
+
verifyEmail: tilmeld.config.verifyEmail,
|
|
203
|
+
unverifiedAccess: tilmeld.config.unverifiedAccess,
|
|
173
204
|
};
|
|
174
205
|
}
|
|
175
206
|
static async loginUser(data) {
|
|
207
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
176
208
|
if (!('username' in data) || !data.username.length) {
|
|
177
209
|
return { result: false, message: 'Incorrect login/password.' };
|
|
178
210
|
}
|
|
179
|
-
const user = await
|
|
211
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
180
212
|
const result = await user.$login(data);
|
|
181
213
|
if (result.result) {
|
|
182
214
|
user.$updateDataProtection();
|
|
@@ -185,19 +217,33 @@ class User extends AbleObject_1.default {
|
|
|
185
217
|
return result;
|
|
186
218
|
}
|
|
187
219
|
async $login(data) {
|
|
188
|
-
const tilmeld = this
|
|
220
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
189
221
|
if (this.guid == null) {
|
|
190
222
|
return { result: false, message: 'Incorrect login/password.' };
|
|
191
223
|
}
|
|
192
224
|
if (!this.$data.enabled) {
|
|
193
225
|
return { result: false, message: 'This user is disabled.' };
|
|
194
226
|
}
|
|
195
|
-
if (this.$gatekeeper()) {
|
|
227
|
+
if (await this.$gatekeeper()) {
|
|
196
228
|
return { result: true, message: 'You are already logged in.' };
|
|
197
229
|
}
|
|
198
230
|
if (!this.$checkPassword(data.password)) {
|
|
199
231
|
return { result: false, message: 'Incorrect login/password.' };
|
|
200
232
|
}
|
|
233
|
+
if (this.$data.totpSecret != null) {
|
|
234
|
+
if (data.code == null ||
|
|
235
|
+
typeof data.code !== 'string' ||
|
|
236
|
+
data.code.length !== 6) {
|
|
237
|
+
return {
|
|
238
|
+
result: false,
|
|
239
|
+
needTOTP: true,
|
|
240
|
+
message: 'You need to provide a 2FA code.',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (!this.$checkTOTPCode(data.code)) {
|
|
244
|
+
return { result: false, message: 'Incorrect 2FA code.' };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
201
247
|
try {
|
|
202
248
|
for (let callback of this.constructor
|
|
203
249
|
.beforeLoginCallbacks) {
|
|
@@ -212,7 +258,67 @@ class User extends AbleObject_1.default {
|
|
|
212
258
|
message: e.message,
|
|
213
259
|
};
|
|
214
260
|
}
|
|
215
|
-
|
|
261
|
+
this.$nymph.config.debugLog('tilmeld', `User login for "${this.$data.username}".`);
|
|
262
|
+
// Authentication was successful, attempt to login.
|
|
263
|
+
if (!(await tilmeld.login(this, true))) {
|
|
264
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
265
|
+
}
|
|
266
|
+
for (let callback of this.constructor
|
|
267
|
+
.afterLoginCallbacks) {
|
|
268
|
+
if (callback) {
|
|
269
|
+
await callback(this);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Login was successful.
|
|
273
|
+
return { result: true, message: 'You are logged in.' };
|
|
274
|
+
}
|
|
275
|
+
async $switchUser(data) {
|
|
276
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
277
|
+
if (this.guid == null) {
|
|
278
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
279
|
+
}
|
|
280
|
+
let user = tilmeld.User.current();
|
|
281
|
+
if (!user || user.guid == null) {
|
|
282
|
+
return { result: false, message: "You're not logged in." };
|
|
283
|
+
}
|
|
284
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
285
|
+
if (isCurrentUser) {
|
|
286
|
+
return { result: false, message: "You can't switch to yourself." };
|
|
287
|
+
}
|
|
288
|
+
const toAbilities = this.$data.abilities || [];
|
|
289
|
+
const fromAbilities = user.abilities || [];
|
|
290
|
+
if (!fromAbilities.includes('tilmeld/switch') &&
|
|
291
|
+
!fromAbilities.includes('system/admin')) {
|
|
292
|
+
return {
|
|
293
|
+
result: false,
|
|
294
|
+
message: "You don't have permission to switch users.",
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (toAbilities.includes('system/admin') ||
|
|
298
|
+
toAbilities.includes('tilmeld/admin')) {
|
|
299
|
+
return { result: false, message: "You can't switch to an admin." };
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
for (let callback of this.constructor
|
|
303
|
+
.beforeLoginCallbacks) {
|
|
304
|
+
if (callback) {
|
|
305
|
+
await callback(this, {
|
|
306
|
+
username: this.$data.username || '',
|
|
307
|
+
password: '',
|
|
308
|
+
...(data || {}),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
return {
|
|
315
|
+
result: false,
|
|
316
|
+
message: e.message,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
this.$nymph.config.debugLog('tilmeld', `User login switch for "${user.username}" to "${this.$data.username}".`);
|
|
320
|
+
// Authentication was successful, attempt to login.
|
|
321
|
+
if (!(await tilmeld.loginSwitch(this, true))) {
|
|
216
322
|
return { result: false, message: 'Incorrect login/password.' };
|
|
217
323
|
}
|
|
218
324
|
for (let callback of this.constructor
|
|
@@ -221,10 +327,15 @@ class User extends AbleObject_1.default {
|
|
|
221
327
|
await callback(this);
|
|
222
328
|
}
|
|
223
329
|
}
|
|
330
|
+
// Login was successful.
|
|
224
331
|
return { result: true, message: 'You are logged in.' };
|
|
225
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Log a user out of the system.
|
|
335
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
336
|
+
*/
|
|
226
337
|
async $logout() {
|
|
227
|
-
const tilmeld = this
|
|
338
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
228
339
|
try {
|
|
229
340
|
for (let callback of this.constructor
|
|
230
341
|
.beforeLogoutCallbacks) {
|
|
@@ -239,7 +350,8 @@ class User extends AbleObject_1.default {
|
|
|
239
350
|
message: e.message,
|
|
240
351
|
};
|
|
241
352
|
}
|
|
242
|
-
tilmeld.
|
|
353
|
+
this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
|
|
354
|
+
await tilmeld.logout();
|
|
243
355
|
for (let callback of this.constructor
|
|
244
356
|
.afterLogoutCallbacks) {
|
|
245
357
|
if (callback) {
|
|
@@ -259,52 +371,87 @@ class User extends AbleObject_1.default {
|
|
|
259
371
|
(0, md5_1.default)(this.$data.email.trim().toLowerCase()).toString() +
|
|
260
372
|
'?d=identicon&s=40');
|
|
261
373
|
}
|
|
374
|
+
$getGid() {
|
|
375
|
+
if (this.$data.group) {
|
|
376
|
+
return this.$data.group.guid;
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
$getGids() {
|
|
381
|
+
if (this.$data.groups) {
|
|
382
|
+
return this.$data.groups
|
|
383
|
+
.map((group) => group.guid)
|
|
384
|
+
.filter((guid) => guid != null);
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get the user's group descendants.
|
|
390
|
+
*/
|
|
262
391
|
async $getDescendantGroups() {
|
|
263
392
|
if (this.$descendantGroups == null) {
|
|
264
393
|
this.$descendantGroups = [];
|
|
265
394
|
if (this.$data.group != null) {
|
|
395
|
+
await this.$data.group.$wake();
|
|
266
396
|
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
267
397
|
}
|
|
398
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
268
399
|
for (let curGroup of this.$data.groups ?? []) {
|
|
269
400
|
this.$descendantGroups = this.$descendantGroups?.concat(await curGroup.$getDescendants());
|
|
270
401
|
}
|
|
271
402
|
}
|
|
272
403
|
return this.$descendantGroups;
|
|
273
404
|
}
|
|
274
|
-
$getDescendantGroupsSync() {
|
|
275
|
-
if (this.$descendantGroups == null) {
|
|
276
|
-
this.$descendantGroups = [];
|
|
277
|
-
if (this.$data.group != null) {
|
|
278
|
-
this.$descendantGroups = this.$data.group.$getDescendantsSync();
|
|
279
|
-
}
|
|
280
|
-
for (let curGroup of this.$data.groups ?? []) {
|
|
281
|
-
this.$descendantGroups = this.$descendantGroups?.concat(curGroup.$getDescendantsSync());
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return this.$descendantGroups;
|
|
285
|
-
}
|
|
286
405
|
$jsonAcceptData(input, allowConflict = false) {
|
|
287
|
-
const tilmeld = this
|
|
288
|
-
this.$
|
|
406
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
407
|
+
this.$check();
|
|
408
|
+
if ('abilities' in input.data &&
|
|
409
|
+
input.data.abilities?.includes('system/admin') !==
|
|
410
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
411
|
+
(!tilmeld.currentUser ||
|
|
412
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
413
|
+
throw new errors_1.BadDataError("You don't have the authority to grant or revoke system/admin.");
|
|
414
|
+
}
|
|
289
415
|
if ('abilities' in input.data &&
|
|
290
|
-
input.data.abilities
|
|
291
|
-
|
|
292
|
-
tilmeld.
|
|
293
|
-
|
|
294
|
-
throw new errors_1.BadDataError("You don't have the authority to
|
|
416
|
+
input.data.abilities?.includes('tilmeld/admin') !==
|
|
417
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
418
|
+
(!tilmeld.currentUser ||
|
|
419
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
420
|
+
throw new errors_1.BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
|
|
421
|
+
}
|
|
422
|
+
if ('abilities' in input.data &&
|
|
423
|
+
input.data.abilities?.includes('tilmeld/switch') !==
|
|
424
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
425
|
+
(!tilmeld.currentUser ||
|
|
426
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
427
|
+
throw new errors_1.BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
|
|
295
428
|
}
|
|
296
429
|
this.$originalEmail = this.$data.email;
|
|
297
430
|
super.$jsonAcceptData(input, allowConflict);
|
|
298
431
|
}
|
|
299
432
|
$jsonAcceptPatch(patch, allowConflict = false) {
|
|
300
|
-
const tilmeld = this
|
|
301
|
-
this.$
|
|
433
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
434
|
+
this.$check();
|
|
435
|
+
if ('abilities' in patch.set &&
|
|
436
|
+
patch.set.abilities?.includes('system/admin') !==
|
|
437
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
438
|
+
(!tilmeld.currentUser ||
|
|
439
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
440
|
+
throw new errors_1.BadDataError("You don't have the authority to grant or revoke system/admin.");
|
|
441
|
+
}
|
|
302
442
|
if ('abilities' in patch.set &&
|
|
303
|
-
patch.set.abilities
|
|
304
|
-
|
|
305
|
-
tilmeld.
|
|
306
|
-
|
|
307
|
-
throw new errors_1.BadDataError("You don't have the authority to
|
|
443
|
+
patch.set.abilities?.includes('tilmeld/admin') !==
|
|
444
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
445
|
+
(!tilmeld.currentUser ||
|
|
446
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
447
|
+
throw new errors_1.BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
|
|
448
|
+
}
|
|
449
|
+
if ('abilities' in patch.set &&
|
|
450
|
+
patch.set.abilities?.includes('tilmeld/switch') !==
|
|
451
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
452
|
+
(!tilmeld.currentUser ||
|
|
453
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
454
|
+
throw new errors_1.BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
|
|
308
455
|
}
|
|
309
456
|
this.$originalEmail = this.$data.email;
|
|
310
457
|
super.$jsonAcceptPatch(patch, allowConflict);
|
|
@@ -313,8 +460,13 @@ class User extends AbleObject_1.default {
|
|
|
313
460
|
super.$putData(data, sdata);
|
|
314
461
|
this.$updateDataProtection();
|
|
315
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Update the data protection arrays for a user.
|
|
465
|
+
*
|
|
466
|
+
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
467
|
+
*/
|
|
316
468
|
$updateDataProtection(givenUser) {
|
|
317
|
-
const tilmeld = this
|
|
469
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
318
470
|
let user = givenUser ?? tilmeld.User.current();
|
|
319
471
|
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
320
472
|
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
@@ -323,75 +475,146 @@ class User extends AbleObject_1.default {
|
|
|
323
475
|
this.$privateData.push('username');
|
|
324
476
|
}
|
|
325
477
|
const isCurrentUser = user != null && user.$is(this);
|
|
478
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
479
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
326
480
|
const abilities = (isCurrentUser || user == null
|
|
327
481
|
? this.$data.abilities
|
|
328
482
|
: user?.abilities) ?? [];
|
|
329
483
|
const isNewUser = this.guid == null;
|
|
330
484
|
if (isCurrentUser) {
|
|
485
|
+
// Users can check to see what abilities they have.
|
|
331
486
|
this.$clientEnabledMethods.push('$gatekeeper');
|
|
332
487
|
this.$clientEnabledMethods.push('$changePassword');
|
|
488
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
333
489
|
this.$clientEnabledMethods.push('$logout');
|
|
334
490
|
this.$clientEnabledMethods.push('$sendEmailVerification');
|
|
491
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
492
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
493
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
494
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
335
495
|
}
|
|
336
496
|
if (user != null &&
|
|
337
|
-
(abilities.
|
|
338
|
-
abilities.
|
|
339
|
-
|
|
497
|
+
(abilities.includes('tilmeld/admin') ||
|
|
498
|
+
abilities.includes('system/admin'))) {
|
|
499
|
+
// Users who can edit other users can see most of their data.
|
|
500
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
501
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
502
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
340
503
|
this.$allowlistData = undefined;
|
|
341
504
|
}
|
|
342
505
|
else if (isCurrentUser || isNewUser) {
|
|
506
|
+
// Users can see their own data, and edit some of it.
|
|
343
507
|
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
344
508
|
this.$allowlistData.push('username');
|
|
345
509
|
}
|
|
346
510
|
this.$allowlistData.push('avatar');
|
|
347
|
-
if (tilmeld.config.userFields.
|
|
511
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
348
512
|
this.$allowlistData.push('nameFirst');
|
|
349
513
|
this.$allowlistData.push('nameMiddle');
|
|
350
514
|
this.$allowlistData.push('nameLast');
|
|
351
515
|
this.$allowlistData.push('name');
|
|
352
516
|
}
|
|
353
|
-
if (tilmeld.config.userFields.
|
|
517
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
354
518
|
this.$allowlistData.push('email');
|
|
355
519
|
}
|
|
356
|
-
if (tilmeld.config.userFields.
|
|
520
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
357
521
|
this.$allowlistData.push('phone');
|
|
358
522
|
}
|
|
359
523
|
this.$privateData = [
|
|
524
|
+
// The things that are commented are there to show what is _not_ private.
|
|
525
|
+
// 'abilities',
|
|
526
|
+
// 'username',
|
|
527
|
+
// 'nameFirst',
|
|
528
|
+
// 'nameMiddle',
|
|
529
|
+
// 'nameLast',
|
|
530
|
+
// 'name',
|
|
531
|
+
// 'email',
|
|
532
|
+
// 'avatar',
|
|
533
|
+
// 'phone',
|
|
534
|
+
// 'group',
|
|
535
|
+
// 'groups',
|
|
536
|
+
// 'inheritAbilities',
|
|
537
|
+
'enabled',
|
|
360
538
|
'secret',
|
|
539
|
+
'emailChangeDate',
|
|
361
540
|
'newEmailSecret',
|
|
362
|
-
'
|
|
541
|
+
// 'newEmailAddress',
|
|
363
542
|
'cancelEmailSecret',
|
|
364
|
-
'
|
|
543
|
+
'cancelEmailAddress',
|
|
365
544
|
'recoverSecret',
|
|
366
545
|
'recoverSecretDate',
|
|
367
|
-
'password',
|
|
368
546
|
'salt',
|
|
547
|
+
'password',
|
|
548
|
+
'passwordTemp',
|
|
549
|
+
// 'revokeTokenDate',
|
|
550
|
+
'totpSecret',
|
|
369
551
|
];
|
|
370
552
|
}
|
|
553
|
+
if (user != null &&
|
|
554
|
+
!isCurrentUser &&
|
|
555
|
+
(abilities.includes('tilmeld/switch') ||
|
|
556
|
+
abilities.includes('system/admin'))) {
|
|
557
|
+
// Users with this ability can switch to other users.
|
|
558
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
559
|
+
}
|
|
371
560
|
}
|
|
372
|
-
$
|
|
373
|
-
|
|
561
|
+
$updateGroupDataProtection() {
|
|
562
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
563
|
+
this.$data.group.$updateDataProtection();
|
|
564
|
+
}
|
|
565
|
+
for (let group of this.$data.groups ?? []) {
|
|
566
|
+
if (!group.$asleep()) {
|
|
567
|
+
group.$updateDataProtection();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Check to see if a user has an ability.
|
|
573
|
+
*
|
|
574
|
+
* This function will check both user and group abilities, if the user is
|
|
575
|
+
* marked to inherit the abilities of its group.
|
|
576
|
+
*
|
|
577
|
+
* If `ability` is undefined, it will check to see if the user is currently
|
|
578
|
+
* logged in.
|
|
579
|
+
*
|
|
580
|
+
* If the user has the "system/admin" ability, this function will return true.
|
|
581
|
+
*
|
|
582
|
+
* @param ability The ability.
|
|
583
|
+
* @returns True or false.
|
|
584
|
+
*/
|
|
585
|
+
async $gatekeeper(ability) {
|
|
586
|
+
await this.$wake();
|
|
587
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
374
588
|
if (ability == null) {
|
|
375
589
|
return tilmeld.User.current()?.$is(this) ?? false;
|
|
376
590
|
}
|
|
591
|
+
// Check the cache to see if we've already checked this user.
|
|
377
592
|
if (this.$gatekeeperCache == null) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
593
|
+
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
594
|
+
}
|
|
595
|
+
return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
596
|
+
!!this.$data.abilities?.includes('system/admin'));
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Build a gatekeeper cache object.
|
|
600
|
+
*/
|
|
601
|
+
async $getGatekeeperCache() {
|
|
602
|
+
await this.$wake();
|
|
603
|
+
let abilities = this.$data.abilities ?? [];
|
|
604
|
+
if (this.$data.inheritAbilities) {
|
|
605
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
606
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
607
|
+
if (curGroup.enabled) {
|
|
608
|
+
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
389
609
|
}
|
|
390
610
|
}
|
|
391
|
-
this.$
|
|
611
|
+
await this.$data.group?.$wake();
|
|
612
|
+
const group = this.$data.group;
|
|
613
|
+
if (group != null && group.cdate != null && group.enabled) {
|
|
614
|
+
abilities = abilities.concat(group.abilities ?? []);
|
|
615
|
+
}
|
|
392
616
|
}
|
|
393
|
-
return ((ability
|
|
394
|
-
!!this.$gatekeeperCache['system/admin']);
|
|
617
|
+
return Object.fromEntries(abilities.map((ability) => [ability, true]));
|
|
395
618
|
}
|
|
396
619
|
$clearCache() {
|
|
397
620
|
const ret = super.$clearCache();
|
|
@@ -399,8 +622,13 @@ class User extends AbleObject_1.default {
|
|
|
399
622
|
this.$gatekeeperCache = undefined;
|
|
400
623
|
return ret;
|
|
401
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Send the user email verification/change/cancellation links.
|
|
627
|
+
*
|
|
628
|
+
* @returns True on success, false on failure.
|
|
629
|
+
*/
|
|
402
630
|
async $sendEmailVerification() {
|
|
403
|
-
const tilmeld = this
|
|
631
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
404
632
|
if (this.guid == null) {
|
|
405
633
|
return false;
|
|
406
634
|
}
|
|
@@ -463,18 +691,33 @@ class User extends AbleObject_1.default {
|
|
|
463
691
|
}
|
|
464
692
|
return success;
|
|
465
693
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
694
|
+
/**
|
|
695
|
+
* Add the user to a (secondary) group.
|
|
696
|
+
*
|
|
697
|
+
* @param group The group.
|
|
698
|
+
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
699
|
+
*/
|
|
700
|
+
async $addGroup(group) {
|
|
701
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
702
|
+
let groups = this.$data.groups;
|
|
703
|
+
if (groups == null) {
|
|
704
|
+
groups = [];
|
|
705
|
+
}
|
|
706
|
+
if (!group.$inArray(groups)) {
|
|
707
|
+
groups.push(group);
|
|
708
|
+
this.$data.groups = groups;
|
|
709
|
+
return groups;
|
|
473
710
|
}
|
|
474
711
|
return true;
|
|
475
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Check the given password against the user's.
|
|
715
|
+
*
|
|
716
|
+
* @param password The password in question.
|
|
717
|
+
* @returns True if the passwords match, otherwise false.
|
|
718
|
+
*/
|
|
476
719
|
$checkPassword(password) {
|
|
477
|
-
const tilmeld = this
|
|
720
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
478
721
|
switch (tilmeld.config.pwMethod) {
|
|
479
722
|
case 'plain':
|
|
480
723
|
return this.$data.password === password;
|
|
@@ -486,10 +729,18 @@ class User extends AbleObject_1.default {
|
|
|
486
729
|
(0, sha256_1.default)(password + this.$data.salt).toString(enc_base64_1.default));
|
|
487
730
|
}
|
|
488
731
|
}
|
|
489
|
-
|
|
490
|
-
|
|
732
|
+
/**
|
|
733
|
+
* Remove the user from a (secondary) group.
|
|
734
|
+
*
|
|
735
|
+
* @param group The group.
|
|
736
|
+
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
737
|
+
*/
|
|
738
|
+
async $delGroup(group) {
|
|
739
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
740
|
+
let groups = this.$data.groups || [];
|
|
741
|
+
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
491
742
|
const newGroups = [];
|
|
492
|
-
for (let curGroup of
|
|
743
|
+
for (let curGroup of groups) {
|
|
493
744
|
if (!group.$is(curGroup)) {
|
|
494
745
|
newGroups.push(curGroup);
|
|
495
746
|
}
|
|
@@ -499,7 +750,13 @@ class User extends AbleObject_1.default {
|
|
|
499
750
|
}
|
|
500
751
|
return true;
|
|
501
752
|
}
|
|
502
|
-
|
|
753
|
+
/**
|
|
754
|
+
* Check whether the user is in a (primary or secondary) group.
|
|
755
|
+
*
|
|
756
|
+
* @param group The group, or the group's GUID.
|
|
757
|
+
* @returns True or false.
|
|
758
|
+
*/
|
|
759
|
+
async $inGroup(group) {
|
|
503
760
|
if (typeof group === 'string') {
|
|
504
761
|
if (this.$data.group?.guid === group) {
|
|
505
762
|
return true;
|
|
@@ -513,35 +770,66 @@ class User extends AbleObject_1.default {
|
|
|
513
770
|
}
|
|
514
771
|
return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
|
|
515
772
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
773
|
+
/**
|
|
774
|
+
* Check whether the user is a descendant of a group.
|
|
775
|
+
*
|
|
776
|
+
* @param group The group, or the group's GUID.
|
|
777
|
+
* @returns True or false.
|
|
778
|
+
*/
|
|
779
|
+
async $isDescendant(group) {
|
|
780
|
+
// Check to see if the user is in a descendant group of the given group.
|
|
781
|
+
await this.$data.group?.$wake();
|
|
782
|
+
if (this.$data.group != null &&
|
|
783
|
+
this.$data.group?.cdate != null &&
|
|
784
|
+
(await this.$data.group?.$isDescendant(group))) {
|
|
519
785
|
return true;
|
|
520
786
|
}
|
|
787
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
521
788
|
for (let curGroup of this.$data.groups ?? []) {
|
|
522
|
-
if (curGroup.$isDescendant(group)) {
|
|
789
|
+
if (await curGroup.$isDescendant(group)) {
|
|
523
790
|
return true;
|
|
524
791
|
}
|
|
525
792
|
}
|
|
526
793
|
return false;
|
|
527
794
|
}
|
|
795
|
+
/**
|
|
796
|
+
* A frontend accessible method to change the user's password.
|
|
797
|
+
*
|
|
798
|
+
* @param data The input data from the client.
|
|
799
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
800
|
+
*/
|
|
528
801
|
async $changePassword(data) {
|
|
802
|
+
this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
|
|
529
803
|
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
530
804
|
return { result: false, message: 'Please specify a password.' };
|
|
531
805
|
}
|
|
532
806
|
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
807
|
+
this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
|
|
533
808
|
return { result: false, message: 'Incorrect password.' };
|
|
534
809
|
}
|
|
535
810
|
this.$data.passwordTemp = data.newPassword;
|
|
811
|
+
if (data.revokeCurrentTokens) {
|
|
812
|
+
this.$data.revokeTokenDate = Date.now();
|
|
813
|
+
}
|
|
536
814
|
if (await this.$save()) {
|
|
815
|
+
if (data.revokeCurrentTokens) {
|
|
816
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
817
|
+
await tilmeld.login(this, true);
|
|
818
|
+
}
|
|
537
819
|
return { result: true, message: 'Your password has been changed.' };
|
|
538
820
|
}
|
|
539
821
|
else {
|
|
540
822
|
return { result: false, message: "Couldn't save new password." };
|
|
541
823
|
}
|
|
542
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Change the user's password.
|
|
827
|
+
*
|
|
828
|
+
* @param password The new password.
|
|
829
|
+
* @returns The resulting password or hash which is stored in the entity.
|
|
830
|
+
*/
|
|
543
831
|
$password(password) {
|
|
544
|
-
const tilmeld = this
|
|
832
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
545
833
|
switch (tilmeld.config.pwMethod) {
|
|
546
834
|
case 'plain':
|
|
547
835
|
delete this.$data.salt;
|
|
@@ -559,14 +847,194 @@ class User extends AbleObject_1.default {
|
|
|
559
847
|
}
|
|
560
848
|
return this.$data.password;
|
|
561
849
|
}
|
|
850
|
+
/**
|
|
851
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
852
|
+
*
|
|
853
|
+
* @param data The input data from the client.
|
|
854
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
855
|
+
*/
|
|
856
|
+
async $revokeCurrentTokens(data) {
|
|
857
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
858
|
+
return { result: false, message: 'Incorrect password.' };
|
|
859
|
+
}
|
|
860
|
+
this.$data.revokeTokenDate = Date.now();
|
|
861
|
+
if (await this.$save()) {
|
|
862
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
863
|
+
await tilmeld.login(this, true);
|
|
864
|
+
return {
|
|
865
|
+
result: true,
|
|
866
|
+
message: 'You have logged out of all other sessions.',
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async $hasTOTPSecret() {
|
|
874
|
+
return this.$data.totpSecret != null;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Check the given code against the user's TOTP secret.
|
|
878
|
+
*
|
|
879
|
+
* @param code The code in question.
|
|
880
|
+
* @returns True if the code is valid, otherwise false.
|
|
881
|
+
*/
|
|
882
|
+
$checkTOTPCode(code) {
|
|
883
|
+
if (this.$data.totpSecret == null) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
887
|
+
const totp = new otpauth_1.TOTP({
|
|
888
|
+
issuer: tilmeld.config.appName,
|
|
889
|
+
label: this.$data.username,
|
|
890
|
+
algorithm: 'SHA1',
|
|
891
|
+
digits: 6,
|
|
892
|
+
period: 30,
|
|
893
|
+
secret: this.$data.totpSecret,
|
|
894
|
+
});
|
|
895
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
899
|
+
*
|
|
900
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
901
|
+
*/
|
|
902
|
+
async $getNewTOTPSecret() {
|
|
903
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
904
|
+
if (this.$data.totpSecret != null) {
|
|
905
|
+
throw new errors_1.BadDataError('You already have a 2FA secret.');
|
|
906
|
+
}
|
|
907
|
+
const secret = new otpauth_1.Secret();
|
|
908
|
+
const totp = new otpauth_1.TOTP({
|
|
909
|
+
issuer: tilmeld.config.appName,
|
|
910
|
+
label: this.$data.username,
|
|
911
|
+
algorithm: 'SHA1',
|
|
912
|
+
digits: 6,
|
|
913
|
+
period: 30,
|
|
914
|
+
secret,
|
|
915
|
+
});
|
|
916
|
+
const uri = totp.toString();
|
|
917
|
+
const qrcode = await (0, qrcode_1.toDataURL)(uri);
|
|
918
|
+
return {
|
|
919
|
+
uri,
|
|
920
|
+
qrcode,
|
|
921
|
+
secret: secret.base32,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
926
|
+
*
|
|
927
|
+
* @param data The input data from the client.
|
|
928
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
929
|
+
*/
|
|
930
|
+
async $saveTOTPSecret(data) {
|
|
931
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
932
|
+
return { result: false, message: 'Incorrect password.' };
|
|
933
|
+
}
|
|
934
|
+
if (this.$data.totpSecret != null) {
|
|
935
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
936
|
+
}
|
|
937
|
+
if (data.secret == null ||
|
|
938
|
+
typeof data.secret !== 'string' ||
|
|
939
|
+
data.secret === '') {
|
|
940
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
941
|
+
}
|
|
942
|
+
if (data.code == null ||
|
|
943
|
+
typeof data.code !== 'string' ||
|
|
944
|
+
data.code.length !== 6) {
|
|
945
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
946
|
+
}
|
|
947
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
948
|
+
const totp = new otpauth_1.TOTP({
|
|
949
|
+
issuer: tilmeld.config.appName,
|
|
950
|
+
label: this.$data.username,
|
|
951
|
+
algorithm: 'SHA1',
|
|
952
|
+
digits: 6,
|
|
953
|
+
period: 30,
|
|
954
|
+
secret: data.secret,
|
|
955
|
+
});
|
|
956
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
957
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
958
|
+
}
|
|
959
|
+
this.$data.totpSecret = data.secret;
|
|
960
|
+
if (await this.$save()) {
|
|
961
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
969
|
+
* account.
|
|
970
|
+
*
|
|
971
|
+
* @param data The input data from the client.
|
|
972
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
973
|
+
*/
|
|
974
|
+
async $removeTOTPSecret(data) {
|
|
975
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
976
|
+
if (data) {
|
|
977
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
978
|
+
return { result: false, message: 'Incorrect password.' };
|
|
979
|
+
}
|
|
980
|
+
if (data.code == null ||
|
|
981
|
+
typeof data.code !== 'string' ||
|
|
982
|
+
data.code.length !== 6) {
|
|
983
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
984
|
+
}
|
|
985
|
+
if (this.$data.totpSecret == null) {
|
|
986
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
987
|
+
}
|
|
988
|
+
const totp = new otpauth_1.TOTP({
|
|
989
|
+
issuer: tilmeld.config.appName,
|
|
990
|
+
label: this.$data.username,
|
|
991
|
+
algorithm: 'SHA1',
|
|
992
|
+
digits: 6,
|
|
993
|
+
period: 30,
|
|
994
|
+
secret: this.$data.totpSecret,
|
|
995
|
+
});
|
|
996
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
997
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
else if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1001
|
+
return {
|
|
1002
|
+
result: false,
|
|
1003
|
+
message: 'You must provide your password and 2FA code.',
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
delete this.$data.totpSecret;
|
|
1007
|
+
if (await this.$save()) {
|
|
1008
|
+
return {
|
|
1009
|
+
result: true,
|
|
1010
|
+
message: 'Two factor secret has been removed.',
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Check that a username is valid.
|
|
1019
|
+
*
|
|
1020
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1021
|
+
*/
|
|
562
1022
|
async $checkUsername() {
|
|
563
|
-
const tilmeld = this
|
|
1023
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1024
|
+
this.$nymph.config.debugLog('tilmeld', `Username check "${this.$data.username}".`);
|
|
564
1025
|
if (!tilmeld.config.emailUsernames) {
|
|
565
1026
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
566
1027
|
return { result: false, message: 'Please specify a username.' };
|
|
567
1028
|
}
|
|
568
|
-
if (tilmeld.config.
|
|
569
|
-
|
|
1029
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1030
|
+
return {
|
|
1031
|
+
result: false,
|
|
1032
|
+
message: 'Usernames must be at least ' +
|
|
1033
|
+
tilmeld.config.minUsernameLength +
|
|
1034
|
+
' characters.',
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
570
1038
|
return {
|
|
571
1039
|
result: false,
|
|
572
1040
|
message: 'Usernames must not exceed ' +
|
|
@@ -600,6 +1068,17 @@ class User extends AbleObject_1.default {
|
|
|
600
1068
|
if (test != null) {
|
|
601
1069
|
return { result: false, message: 'That username is taken.' };
|
|
602
1070
|
}
|
|
1071
|
+
for (let callback of this.constructor
|
|
1072
|
+
.checkUsernameCallbacks) {
|
|
1073
|
+
if (callback) {
|
|
1074
|
+
const result = await callback(this, {
|
|
1075
|
+
username: this.$data.username,
|
|
1076
|
+
});
|
|
1077
|
+
if (!result.result) {
|
|
1078
|
+
return { message: 'Username is not available.', ...result };
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
603
1082
|
return {
|
|
604
1083
|
result: true,
|
|
605
1084
|
message: this.guid != null ? 'Username is valid.' : 'Username is available!',
|
|
@@ -610,8 +1089,15 @@ class User extends AbleObject_1.default {
|
|
|
610
1089
|
if (this.$data.username == null || !this.$data.username.length) {
|
|
611
1090
|
return { result: false, message: 'Please specify an email.' };
|
|
612
1091
|
}
|
|
613
|
-
if (tilmeld.config.
|
|
614
|
-
|
|
1092
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1093
|
+
return {
|
|
1094
|
+
result: false,
|
|
1095
|
+
message: 'Emails must be at least ' +
|
|
1096
|
+
tilmeld.config.minUsernameLength +
|
|
1097
|
+
' characters.',
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
615
1101
|
return {
|
|
616
1102
|
result: false,
|
|
617
1103
|
message: 'Emails must not exceed ' +
|
|
@@ -622,8 +1108,14 @@ class User extends AbleObject_1.default {
|
|
|
622
1108
|
return await this.$checkEmail();
|
|
623
1109
|
}
|
|
624
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Check that an email is unique.
|
|
1113
|
+
*
|
|
1114
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1115
|
+
*/
|
|
625
1116
|
async $checkEmail() {
|
|
626
|
-
const tilmeld = this
|
|
1117
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1118
|
+
this.$nymph.config.debugLog('tilmeld', `Email check "${this.$data.email}".`);
|
|
627
1119
|
if (this.$data.email == null || !this.$data.email.length) {
|
|
628
1120
|
if (tilmeld.config.verifyEmail) {
|
|
629
1121
|
return { result: false, message: 'Please specify an email.' };
|
|
@@ -657,8 +1149,14 @@ class User extends AbleObject_1.default {
|
|
|
657
1149
|
message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
|
|
658
1150
|
};
|
|
659
1151
|
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Check that a phone number is unique.
|
|
1154
|
+
*
|
|
1155
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1156
|
+
*/
|
|
660
1157
|
async $checkPhone() {
|
|
661
|
-
const tilmeld = this
|
|
1158
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1159
|
+
this.$nymph.config.debugLog('tilmeld', `Phone check "${this.$data.phone}".`);
|
|
662
1160
|
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
663
1161
|
return { result: false, message: 'Please specify a phone number.' };
|
|
664
1162
|
}
|
|
@@ -686,7 +1184,7 @@ class User extends AbleObject_1.default {
|
|
|
686
1184
|
};
|
|
687
1185
|
}
|
|
688
1186
|
async $register(data) {
|
|
689
|
-
let tilmeld = this
|
|
1187
|
+
let tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
690
1188
|
if (!tilmeld.config.allowRegistration) {
|
|
691
1189
|
return {
|
|
692
1190
|
result: false,
|
|
@@ -701,21 +1199,6 @@ class User extends AbleObject_1.default {
|
|
|
701
1199
|
message: 'This is already a registered user.',
|
|
702
1200
|
};
|
|
703
1201
|
}
|
|
704
|
-
try {
|
|
705
|
-
for (let callback of this.constructor
|
|
706
|
-
.beforeRegisterCallbacks) {
|
|
707
|
-
if (callback) {
|
|
708
|
-
await callback(this, data);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
catch (e) {
|
|
713
|
-
return {
|
|
714
|
-
result: false,
|
|
715
|
-
loggedin: false,
|
|
716
|
-
message: e.message,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
1202
|
if (!('password' in data) || !data.password.length) {
|
|
720
1203
|
return {
|
|
721
1204
|
result: false,
|
|
@@ -728,7 +1211,7 @@ class User extends AbleObject_1.default {
|
|
|
728
1211
|
return { ...unCheck, loggedin: false };
|
|
729
1212
|
}
|
|
730
1213
|
this.$password(data.password);
|
|
731
|
-
if (tilmeld.config.regFields.
|
|
1214
|
+
if (tilmeld.config.regFields.includes('name')) {
|
|
732
1215
|
this.$data.name =
|
|
733
1216
|
this.$data.nameFirst +
|
|
734
1217
|
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
@@ -740,19 +1223,45 @@ class User extends AbleObject_1.default {
|
|
|
740
1223
|
if (tilmeld.config.emailUsernames) {
|
|
741
1224
|
this.$data.email = this.$data.username;
|
|
742
1225
|
}
|
|
1226
|
+
// Start transaction.
|
|
743
1227
|
const transaction = 'tilmeld-register-' + (0, guid_1.nanoid)();
|
|
744
1228
|
const nymph = this.$nymph;
|
|
745
1229
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
746
1230
|
this.$nymph = tnymph;
|
|
747
|
-
tilmeld = this
|
|
1231
|
+
tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1232
|
+
let message = '';
|
|
1233
|
+
let loggedin = false;
|
|
748
1234
|
try {
|
|
1235
|
+
try {
|
|
1236
|
+
for (let callback of this.constructor
|
|
1237
|
+
.beforeRegisterCallbacks) {
|
|
1238
|
+
if (callback) {
|
|
1239
|
+
await callback(this, data);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
catch (e) {
|
|
1244
|
+
await tnymph.rollback(transaction);
|
|
1245
|
+
this.$nymph = nymph;
|
|
1246
|
+
return {
|
|
1247
|
+
result: false,
|
|
1248
|
+
loggedin: false,
|
|
1249
|
+
message: e.message,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
// Add primary group.
|
|
749
1253
|
let generatedPrimaryGroup = null;
|
|
750
1254
|
if (tilmeld.config.generatePrimary) {
|
|
1255
|
+
// Generate a new primary group for the user.
|
|
751
1256
|
generatedPrimaryGroup = await tilmeld.Group.factory();
|
|
752
1257
|
generatedPrimaryGroup.groupname = this.$data.username;
|
|
753
1258
|
generatedPrimaryGroup.avatar = this.$data.avatar;
|
|
754
|
-
|
|
755
|
-
|
|
1259
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
1260
|
+
generatedPrimaryGroup.name = this.$data.name;
|
|
1261
|
+
}
|
|
1262
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
1263
|
+
generatedPrimaryGroup.email = this.$data.email;
|
|
1264
|
+
}
|
|
756
1265
|
const parent = await tnymph.getEntity({ class: tilmeld.Group }, {
|
|
757
1266
|
type: '&',
|
|
758
1267
|
equal: ['defaultPrimary', true],
|
|
@@ -772,6 +1281,7 @@ class User extends AbleObject_1.default {
|
|
|
772
1281
|
this.$data.group = generatedPrimaryGroup;
|
|
773
1282
|
}
|
|
774
1283
|
else {
|
|
1284
|
+
// Add the default primary.
|
|
775
1285
|
const group = await tnymph.getEntity({ class: tilmeld.Group }, {
|
|
776
1286
|
type: '&',
|
|
777
1287
|
equal: ['defaultPrimary', true],
|
|
@@ -780,19 +1290,26 @@ class User extends AbleObject_1.default {
|
|
|
780
1290
|
this.$data.group = group;
|
|
781
1291
|
}
|
|
782
1292
|
}
|
|
783
|
-
|
|
1293
|
+
// Add secondary groups.
|
|
1294
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1295
|
+
tilmeld.config.verifyEmail &&
|
|
1296
|
+
tilmeld.config.unverifiedAccess) {
|
|
1297
|
+
// Add the default secondaries for unverified users.
|
|
784
1298
|
this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
|
|
785
1299
|
type: '&',
|
|
786
1300
|
equal: ['unverifiedSecondary', true],
|
|
787
1301
|
});
|
|
788
1302
|
}
|
|
789
1303
|
else {
|
|
1304
|
+
// Add the default secondaries.
|
|
790
1305
|
this.$data.groups = await tnymph.getEntities({ class: tilmeld.Group }, {
|
|
791
1306
|
type: '&',
|
|
792
1307
|
equal: ['defaultSecondary', true],
|
|
793
1308
|
});
|
|
794
1309
|
}
|
|
795
|
-
if (tilmeld.config.
|
|
1310
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1311
|
+
tilmeld.config.verifyEmail) {
|
|
1312
|
+
// The user will be enabled after verifying their e-mail address.
|
|
796
1313
|
if (!tilmeld.config.unverifiedAccess) {
|
|
797
1314
|
this.$data.enabled = false;
|
|
798
1315
|
}
|
|
@@ -800,6 +1317,8 @@ class User extends AbleObject_1.default {
|
|
|
800
1317
|
else {
|
|
801
1318
|
this.$data.enabled = true;
|
|
802
1319
|
}
|
|
1320
|
+
// If create_admin is true and there are no other users, grant
|
|
1321
|
+
// "system/admin".
|
|
803
1322
|
let madeAdmin = false;
|
|
804
1323
|
if (tilmeld.config.createAdmin) {
|
|
805
1324
|
const otherUsers = await tnymph.getEntities({
|
|
@@ -808,13 +1327,17 @@ class User extends AbleObject_1.default {
|
|
|
808
1327
|
limit: 1,
|
|
809
1328
|
return: 'guid',
|
|
810
1329
|
});
|
|
1330
|
+
// Make sure it's not just null, cause that means an error.
|
|
811
1331
|
if (!otherUsers.length) {
|
|
812
1332
|
this.$grant('system/admin');
|
|
813
1333
|
this.$data.enabled = true;
|
|
814
1334
|
madeAdmin = true;
|
|
815
1335
|
}
|
|
816
1336
|
}
|
|
1337
|
+
this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
|
|
817
1338
|
if (await this.$saveSkipAC()) {
|
|
1339
|
+
this.$nymph.config.debugLog('tilmeld', `New user registered "${this.$data.username}".`);
|
|
1340
|
+
// Send the new user registered email.
|
|
818
1341
|
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
819
1342
|
await tilmeld.config.sendEmail(tilmeld, {
|
|
820
1343
|
template: 'UserRegistered',
|
|
@@ -831,8 +1354,7 @@ class User extends AbleObject_1.default {
|
|
|
831
1354
|
},
|
|
832
1355
|
}, this);
|
|
833
1356
|
}
|
|
834
|
-
|
|
835
|
-
let loggedin = false;
|
|
1357
|
+
// Save the primary group.
|
|
836
1358
|
if (generatedPrimaryGroup != null) {
|
|
837
1359
|
generatedPrimaryGroup.user = this;
|
|
838
1360
|
if (!(await generatedPrimaryGroup.$saveSkipAC())) {
|
|
@@ -845,17 +1367,20 @@ class User extends AbleObject_1.default {
|
|
|
845
1367
|
};
|
|
846
1368
|
}
|
|
847
1369
|
}
|
|
1370
|
+
// Finish up.
|
|
848
1371
|
if (tilmeld.config.verifyEmail &&
|
|
849
1372
|
!tilmeld.config.unverifiedAccess &&
|
|
850
|
-
!madeAdmin
|
|
1373
|
+
!madeAdmin &&
|
|
1374
|
+
tilmeld.config.userFields.includes('email')) {
|
|
851
1375
|
message +=
|
|
852
1376
|
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
853
1377
|
'with a verification link for you to finish registration.';
|
|
854
1378
|
}
|
|
855
1379
|
else if (tilmeld.config.verifyEmail &&
|
|
856
1380
|
tilmeld.config.unverifiedAccess &&
|
|
857
|
-
!madeAdmin
|
|
858
|
-
|
|
1381
|
+
!madeAdmin &&
|
|
1382
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1383
|
+
if (!(await tilmeld.login(this, true))) {
|
|
859
1384
|
throw new Error('An error occurred trying to log you in.');
|
|
860
1385
|
}
|
|
861
1386
|
message +=
|
|
@@ -865,7 +1390,7 @@ class User extends AbleObject_1.default {
|
|
|
865
1390
|
loggedin = true;
|
|
866
1391
|
}
|
|
867
1392
|
else {
|
|
868
|
-
if (!tilmeld.login(this, true)) {
|
|
1393
|
+
if (!(await tilmeld.login(this, true))) {
|
|
869
1394
|
throw new Error('An error occurred trying to log you in.');
|
|
870
1395
|
}
|
|
871
1396
|
message += "You're now registered and logged in!";
|
|
@@ -880,21 +1405,9 @@ class User extends AbleObject_1.default {
|
|
|
880
1405
|
});
|
|
881
1406
|
}
|
|
882
1407
|
}
|
|
883
|
-
try {
|
|
884
|
-
await tnymph.commit(transaction);
|
|
885
|
-
this.$nymph = nymph;
|
|
886
|
-
this.$nymph.tilmeld?.fillSession(this);
|
|
887
|
-
}
|
|
888
|
-
catch (e) {
|
|
889
|
-
throw e;
|
|
890
|
-
}
|
|
891
|
-
return {
|
|
892
|
-
result: true,
|
|
893
|
-
loggedin,
|
|
894
|
-
message,
|
|
895
|
-
};
|
|
896
1408
|
}
|
|
897
1409
|
else {
|
|
1410
|
+
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}".`);
|
|
898
1411
|
await tnymph.rollback(transaction);
|
|
899
1412
|
this.$nymph = nymph;
|
|
900
1413
|
return {
|
|
@@ -905,22 +1418,37 @@ class User extends AbleObject_1.default {
|
|
|
905
1418
|
}
|
|
906
1419
|
}
|
|
907
1420
|
catch (e) {
|
|
1421
|
+
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
|
|
908
1422
|
await tnymph.rollback(transaction);
|
|
909
1423
|
this.$nymph = nymph;
|
|
910
1424
|
throw e;
|
|
911
1425
|
}
|
|
1426
|
+
try {
|
|
1427
|
+
await tnymph.commit(transaction);
|
|
1428
|
+
this.$nymph = nymph;
|
|
1429
|
+
await this.$nymph.tilmeld?.fillSession(this);
|
|
1430
|
+
}
|
|
1431
|
+
catch (e) {
|
|
1432
|
+
throw e;
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
result: true,
|
|
1436
|
+
loggedin,
|
|
1437
|
+
message,
|
|
1438
|
+
};
|
|
912
1439
|
}
|
|
913
1440
|
async $save() {
|
|
914
|
-
let tilmeld = this
|
|
1441
|
+
let tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
915
1442
|
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
916
1443
|
return false;
|
|
917
1444
|
}
|
|
918
1445
|
if (tilmeld.gatekeeper('tilmeld/admin') &&
|
|
919
1446
|
!tilmeld.gatekeeper('system/admin') &&
|
|
920
|
-
this.$
|
|
1447
|
+
this.$data.abilities?.includes('system/admin')) {
|
|
921
1448
|
throw new errors_1.BadDataError("You don't have the authority to modify system admins.");
|
|
922
1449
|
}
|
|
923
1450
|
let sendVerification = false;
|
|
1451
|
+
// Formatting.
|
|
924
1452
|
this.$data.username = this.$data.username.trim();
|
|
925
1453
|
if (tilmeld.config.emailUsernames) {
|
|
926
1454
|
this.$data.email = this.$data.username;
|
|
@@ -934,49 +1462,63 @@ class User extends AbleObject_1.default {
|
|
|
934
1462
|
(this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
|
|
935
1463
|
(this.$data.nameLast ? ' ' + this.$data.nameLast : '');
|
|
936
1464
|
this.$data.enabled = !!this.$data.enabled;
|
|
937
|
-
|
|
1465
|
+
// Clear empty values.
|
|
1466
|
+
if (this.$data.nameFirst === '' ||
|
|
1467
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
938
1468
|
delete this.$data.nameFirst;
|
|
939
1469
|
}
|
|
940
|
-
if (this.$data.nameMiddle === ''
|
|
1470
|
+
if (this.$data.nameMiddle === '' ||
|
|
1471
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
941
1472
|
delete this.$data.nameMiddle;
|
|
942
1473
|
}
|
|
943
|
-
if (this.$data.nameLast === ''
|
|
1474
|
+
if (this.$data.nameLast === '' ||
|
|
1475
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
944
1476
|
delete this.$data.nameLast;
|
|
945
1477
|
}
|
|
946
|
-
if (this.$data.name === '') {
|
|
1478
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
947
1479
|
delete this.$data.name;
|
|
948
1480
|
}
|
|
949
|
-
if (this.$data.email === ''
|
|
1481
|
+
if (this.$data.email === '' ||
|
|
1482
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
950
1483
|
delete this.$data.email;
|
|
951
1484
|
}
|
|
952
1485
|
if (this.$data.avatar === '') {
|
|
953
1486
|
delete this.$data.avatar;
|
|
954
1487
|
}
|
|
955
|
-
if (this.$data.phone === ''
|
|
1488
|
+
if (this.$data.phone === '' ||
|
|
1489
|
+
!tilmeld.config.userFields.includes('phone')) {
|
|
956
1490
|
delete this.$data.phone;
|
|
957
1491
|
}
|
|
958
|
-
if (this.$data.secret === ''
|
|
1492
|
+
if (this.$data.secret === '' ||
|
|
1493
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
959
1494
|
delete this.$data.secret;
|
|
960
1495
|
}
|
|
961
|
-
if (this.$data.emailChangeDate === 0
|
|
1496
|
+
if (this.$data.emailChangeDate === 0 ||
|
|
1497
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
962
1498
|
delete this.$data.emailChangeDate;
|
|
963
1499
|
}
|
|
964
|
-
if (this.$data.newEmailSecret === ''
|
|
1500
|
+
if (this.$data.newEmailSecret === '' ||
|
|
1501
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
965
1502
|
delete this.$data.newEmailSecret;
|
|
966
1503
|
}
|
|
967
|
-
if (this.$data.newEmailAddress === ''
|
|
1504
|
+
if (this.$data.newEmailAddress === '' ||
|
|
1505
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
968
1506
|
delete this.$data.newEmailAddress;
|
|
969
1507
|
}
|
|
970
|
-
if (this.$data.cancelEmailSecret === ''
|
|
1508
|
+
if (this.$data.cancelEmailSecret === '' ||
|
|
1509
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
971
1510
|
delete this.$data.cancelEmailSecret;
|
|
972
1511
|
}
|
|
973
|
-
if (this.$data.cancelEmailAddress === ''
|
|
1512
|
+
if (this.$data.cancelEmailAddress === '' ||
|
|
1513
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
974
1514
|
delete this.$data.cancelEmailAddress;
|
|
975
1515
|
}
|
|
976
|
-
if (this.$data.recoverSecret === ''
|
|
1516
|
+
if (this.$data.recoverSecret === '' ||
|
|
1517
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
977
1518
|
delete this.$data.recoverSecret;
|
|
978
1519
|
}
|
|
979
|
-
if (this.$data.recoverSecretDate === 0
|
|
1520
|
+
if (this.$data.recoverSecretDate === 0 ||
|
|
1521
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
980
1522
|
delete this.$data.recoverSecretDate;
|
|
981
1523
|
}
|
|
982
1524
|
if (this.$data.password === '') {
|
|
@@ -988,26 +1530,36 @@ class User extends AbleObject_1.default {
|
|
|
988
1530
|
if (this.$data.inheritAbilities === false) {
|
|
989
1531
|
delete this.$data.inheritAbilities;
|
|
990
1532
|
}
|
|
1533
|
+
// Verification.
|
|
991
1534
|
const unCheck = await this.$checkUsername();
|
|
992
1535
|
if (!unCheck.result) {
|
|
993
1536
|
throw new errors_1.BadUsernameError(unCheck.message);
|
|
994
1537
|
}
|
|
995
|
-
if (!tilmeld.config.emailUsernames
|
|
1538
|
+
if (!tilmeld.config.emailUsernames &&
|
|
1539
|
+
tilmeld.config.userFields.includes('email')) {
|
|
996
1540
|
const emCheck = await this.$checkEmail();
|
|
997
1541
|
if (!emCheck.result) {
|
|
998
1542
|
throw new errors_1.BadEmailError(emCheck.message);
|
|
999
1543
|
}
|
|
1000
1544
|
}
|
|
1001
|
-
|
|
1545
|
+
// Email changes.
|
|
1546
|
+
if (!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1547
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1548
|
+
// The user isn't an admin, so email address changes should contain some
|
|
1549
|
+
// security measures.
|
|
1002
1550
|
if (tilmeld.config.verifyEmail) {
|
|
1551
|
+
// The user needs to verify this new email address.
|
|
1003
1552
|
if (this.guid == null) {
|
|
1004
|
-
|
|
1553
|
+
// If this is the first user, they'll be made an admin, so they don't
|
|
1554
|
+
// need to verify.
|
|
1555
|
+
if (!this.$data.abilities?.includes('system/admin')) {
|
|
1005
1556
|
this.$data.secret = (0, guid_1.nanoid)();
|
|
1006
1557
|
sendVerification = true;
|
|
1007
1558
|
}
|
|
1008
1559
|
}
|
|
1009
1560
|
else if (this.$originalEmail != null &&
|
|
1010
1561
|
this.$originalEmail !== this.$data.email) {
|
|
1562
|
+
// The user already has an old email address.
|
|
1011
1563
|
if (tilmeld.config.emailRateLimit !== '' &&
|
|
1012
1564
|
this.$data.emailChangeDate != null &&
|
|
1013
1565
|
this.$data.emailChangeDate >=
|
|
@@ -1019,13 +1571,18 @@ class User extends AbleObject_1.default {
|
|
|
1019
1571
|
else {
|
|
1020
1572
|
if (this.$data.newEmailSecret == null &&
|
|
1021
1573
|
tilmeld.config.emailRateLimit !== '' &&
|
|
1574
|
+
// Make sure the user has at least the rate limit time to cancel
|
|
1575
|
+
// an email change.
|
|
1022
1576
|
(this.$data.emailChangeDate == null ||
|
|
1023
1577
|
this.$data.emailChangeDate <
|
|
1024
1578
|
(0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
|
|
1579
|
+
// Save the old email in case the cancel change link is clicked.
|
|
1025
1580
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1026
1581
|
this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
|
|
1027
1582
|
this.$data.emailChangeDate = Date.now();
|
|
1028
1583
|
}
|
|
1584
|
+
// Save the new address and reset to the old one (until they verify
|
|
1585
|
+
// it).
|
|
1029
1586
|
this.$data.newEmailAddress = this.$data.email;
|
|
1030
1587
|
this.$data.email = this.$originalEmail;
|
|
1031
1588
|
if (tilmeld.config.emailUsernames) {
|
|
@@ -1039,9 +1596,13 @@ class User extends AbleObject_1.default {
|
|
|
1039
1596
|
else if (this.guid != null &&
|
|
1040
1597
|
this.$originalEmail != null &&
|
|
1041
1598
|
this.$originalEmail !== this.$data.email &&
|
|
1599
|
+
// Make sure the user has at least the rate limit time to cancel an
|
|
1600
|
+
// email change.
|
|
1042
1601
|
(this.$data.emailChangeDate == null ||
|
|
1043
1602
|
this.$data.emailChangeDate <
|
|
1044
1603
|
(0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
|
|
1604
|
+
// The user doesn't need to verify their new email address, but should
|
|
1605
|
+
// be able to cancel the email change from their old address.
|
|
1045
1606
|
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1046
1607
|
this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
|
|
1047
1608
|
sendVerification = true;
|
|
@@ -1055,26 +1616,35 @@ class User extends AbleObject_1.default {
|
|
|
1055
1616
|
throw new errors_1.BadDataError('A password is required.');
|
|
1056
1617
|
}
|
|
1057
1618
|
try {
|
|
1058
|
-
tilmeld.config.validatorUser(this);
|
|
1619
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
1059
1620
|
}
|
|
1060
1621
|
catch (e) {
|
|
1061
1622
|
throw new errors_1.BadDataError(e?.message);
|
|
1062
1623
|
}
|
|
1624
|
+
// Start transaction.
|
|
1063
1625
|
const transaction = 'tilmeld-save-' + (0, guid_1.nanoid)();
|
|
1064
1626
|
const nymph = this.$nymph;
|
|
1065
1627
|
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1066
1628
|
this.$nymph = tnymph;
|
|
1067
|
-
tilmeld = this
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1629
|
+
tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1630
|
+
const group = this.$data.group;
|
|
1631
|
+
await group?.$wake();
|
|
1632
|
+
if (group != null && group.user != null && this.$is(group.user)) {
|
|
1071
1633
|
try {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1634
|
+
// Update the user's generated primary group.
|
|
1635
|
+
group.groupname = this.$data.username;
|
|
1636
|
+
group.avatar = this.$data.avatar;
|
|
1637
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
1638
|
+
group.email = this.$data.email;
|
|
1639
|
+
}
|
|
1640
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
1641
|
+
group.name = this.$data.name;
|
|
1642
|
+
}
|
|
1643
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
1644
|
+
group.phone = this.$data.phone;
|
|
1645
|
+
}
|
|
1646
|
+
await group.$saveSkipAC();
|
|
1647
|
+
this.$data.group = group;
|
|
1078
1648
|
}
|
|
1079
1649
|
catch (e) {
|
|
1080
1650
|
await tnymph.rollback(transaction);
|
|
@@ -1096,6 +1666,7 @@ class User extends AbleObject_1.default {
|
|
|
1096
1666
|
}
|
|
1097
1667
|
if (ret) {
|
|
1098
1668
|
if (sendVerification) {
|
|
1669
|
+
// The email has changed, so send a new verification email.
|
|
1099
1670
|
if (!(await this.$sendEmailVerification())) {
|
|
1100
1671
|
await tnymph.rollback(transaction);
|
|
1101
1672
|
this.guid = preGuid;
|
|
@@ -1108,8 +1679,10 @@ class User extends AbleObject_1.default {
|
|
|
1108
1679
|
this.$descendantGroups = undefined;
|
|
1109
1680
|
this.$gatekeeperCache = undefined;
|
|
1110
1681
|
await tnymph.commit(transaction);
|
|
1111
|
-
|
|
1112
|
-
|
|
1682
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(nymph);
|
|
1683
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
1684
|
+
// Update the user in the session cache.
|
|
1685
|
+
await tilmeld.fillSession(this);
|
|
1113
1686
|
}
|
|
1114
1687
|
}
|
|
1115
1688
|
else {
|
|
@@ -1118,6 +1691,9 @@ class User extends AbleObject_1.default {
|
|
|
1118
1691
|
this.$nymph = nymph;
|
|
1119
1692
|
return ret;
|
|
1120
1693
|
}
|
|
1694
|
+
/**
|
|
1695
|
+
* This should *never* be accessible on the client.
|
|
1696
|
+
*/
|
|
1121
1697
|
async $saveSkipAC() {
|
|
1122
1698
|
this.$skipAcWhenSaving = true;
|
|
1123
1699
|
return await this.$save();
|
|
@@ -1130,12 +1706,12 @@ class User extends AbleObject_1.default {
|
|
|
1130
1706
|
return false;
|
|
1131
1707
|
}
|
|
1132
1708
|
async $delete() {
|
|
1133
|
-
const tilmeld = this
|
|
1709
|
+
const tilmeld = (0, enforceTilmeld_1.enforceTilmeld)(this);
|
|
1134
1710
|
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1135
1711
|
throw new errors_1.BadDataError("You don't have the authority to delete users.");
|
|
1136
1712
|
}
|
|
1137
1713
|
if (!tilmeld.gatekeeper('system/admin') &&
|
|
1138
|
-
this.$
|
|
1714
|
+
this.$data.abilities?.includes('system/admin')) {
|
|
1139
1715
|
throw new errors_1.BadDataError("You don't have the authority to delete system admins.");
|
|
1140
1716
|
}
|
|
1141
1717
|
if (tilmeld.User.current(true).$is(this)) {
|
|
@@ -1148,6 +1724,7 @@ class User extends AbleObject_1.default {
|
|
|
1148
1724
|
if (!(prop in this)) {
|
|
1149
1725
|
throw new Error('Invalid event type.');
|
|
1150
1726
|
}
|
|
1727
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1151
1728
|
this[prop].push(callback);
|
|
1152
1729
|
return () => this.off(event, callback);
|
|
1153
1730
|
}
|
|
@@ -1156,16 +1733,21 @@ class User extends AbleObject_1.default {
|
|
|
1156
1733
|
if (!(prop in this)) {
|
|
1157
1734
|
return false;
|
|
1158
1735
|
}
|
|
1736
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1159
1737
|
const i = this[prop].indexOf(callback);
|
|
1160
1738
|
if (i > -1) {
|
|
1739
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1161
1740
|
this[prop].splice(i, 1);
|
|
1162
1741
|
}
|
|
1163
1742
|
return true;
|
|
1164
1743
|
}
|
|
1165
1744
|
}
|
|
1166
|
-
|
|
1745
|
+
/**
|
|
1746
|
+
* The instance of Tilmeld to use for queries.
|
|
1747
|
+
*/
|
|
1167
1748
|
User.ETYPE = 'tilmeld_user';
|
|
1168
1749
|
User.class = 'User';
|
|
1750
|
+
User.checkUsernameCallbacks = [];
|
|
1169
1751
|
User.beforeRegisterCallbacks = [];
|
|
1170
1752
|
User.afterRegisterCallbacks = [];
|
|
1171
1753
|
User.beforeLoginCallbacks = [];
|
|
@@ -1180,23 +1762,33 @@ User.DEFAULT_CLIENT_ENABLED_METHODS = [
|
|
|
1180
1762
|
'$register',
|
|
1181
1763
|
];
|
|
1182
1764
|
User.DEFAULT_PRIVATE_DATA = [
|
|
1765
|
+
// The things that are commented are there to show what is _not_ private.
|
|
1766
|
+
'abilities',
|
|
1767
|
+
// 'username',
|
|
1768
|
+
// 'nameFirst',
|
|
1769
|
+
// 'nameMiddle',
|
|
1770
|
+
// 'nameLast',
|
|
1771
|
+
// 'name',
|
|
1183
1772
|
'email',
|
|
1184
|
-
'
|
|
1773
|
+
// 'avatar',
|
|
1185
1774
|
'phone',
|
|
1186
1775
|
'group',
|
|
1187
1776
|
'groups',
|
|
1188
|
-
'abilities',
|
|
1189
1777
|
'inheritAbilities',
|
|
1190
|
-
'
|
|
1191
|
-
'recoverSecretDate',
|
|
1192
|
-
'password',
|
|
1193
|
-
'salt',
|
|
1778
|
+
'enabled',
|
|
1194
1779
|
'secret',
|
|
1195
|
-
'
|
|
1780
|
+
'emailChangeDate',
|
|
1196
1781
|
'newEmailSecret',
|
|
1197
|
-
'
|
|
1782
|
+
'newEmailAddress',
|
|
1198
1783
|
'cancelEmailSecret',
|
|
1199
|
-
'
|
|
1784
|
+
'cancelEmailAddress',
|
|
1785
|
+
'recoverSecret',
|
|
1786
|
+
'recoverSecretDate',
|
|
1787
|
+
'salt',
|
|
1788
|
+
'password',
|
|
1789
|
+
'passwordTemp',
|
|
1790
|
+
'revokeTokenDate',
|
|
1791
|
+
'totpSecret',
|
|
1200
1792
|
];
|
|
1201
1793
|
User.DEFAULT_ALLOWLIST_DATA = [];
|
|
1202
1794
|
User.clientEnabledStaticMethods = [
|
|
@@ -1207,4 +1799,5 @@ User.clientEnabledStaticMethods = [
|
|
|
1207
1799
|
'getClientConfig',
|
|
1208
1800
|
];
|
|
1209
1801
|
User.searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
1802
|
+
exports.default = User;
|
|
1210
1803
|
//# sourceMappingURL=User.js.map
|