@nymphjs/tilmeld 1.0.0-beta.121 → 1.0.0-beta.123
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 +22 -0
- package/README.md +6 -3
- package/dist/Group.d.ts +9 -9
- package/dist/Group.js +790 -793
- package/dist/Group.js.map +1 -1
- package/dist/Group.testArtifacts.js +3 -3
- package/dist/Group.testArtifacts.js.map +1 -1
- package/dist/Tilmeld.js +6 -15
- package/dist/Tilmeld.js.map +1 -1
- package/dist/User.d.ts +8 -9
- package/dist/User.js +1838 -1957
- package/dist/User.js.map +1 -1
- package/dist/User.testArtifacts.js +7 -7
- package/dist/User.testArtifacts.js.map +1 -1
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +3 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/requireAbility.d.ts +10 -0
- package/dist/decorators/requireAbility.js +21 -0
- package/dist/decorators/requireAbility.js.map +1 -0
- package/dist/decorators/requireAuthentication.d.ts +8 -0
- package/dist/decorators/requireAuthentication.js +16 -0
- package/dist/decorators/requireAuthentication.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/testArtifacts.d.ts +5 -1
- package/dist/testArtifacts.js +211 -67
- package/dist/testArtifacts.js.map +1 -1
- package/package.json +13 -7
- package/src/Group.testArtifacts.ts +3 -3
- package/src/Group.ts +108 -158
- package/src/Tilmeld.ts +6 -35
- package/src/User.testArtifacts.ts +7 -7
- package/src/User.ts +364 -515
- package/src/decorators/index.ts +2 -0
- package/src/decorators/requireAbility.ts +26 -0
- package/src/decorators/requireAuthentication.ts +19 -0
- package/src/index.ts +1 -0
- package/src/testArtifacts.ts +165 -46
package/dist/User.js
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
1
35
|
import crypto from 'node:crypto';
|
|
36
|
+
import { transaction, transactional, } from '@nymphjs/nymph';
|
|
2
37
|
import { humanSecret, nanoid } from '@nymphjs/guid';
|
|
3
38
|
import { strtotime } from 'locutus/php/datetime/index';
|
|
4
39
|
import { difference, xor } from 'lodash-es';
|
|
@@ -9,116 +44,51 @@ import { enforceTilmeld } from './enforceTilmeld.js';
|
|
|
9
44
|
import AbleObject from './AbleObject.js';
|
|
10
45
|
import GroupClass from './Group.js';
|
|
11
46
|
import { AccessControlError, BadDataError, BadEmailError, BadUsernameError, EmailChangeRateLimitExceededError, } from './errors/index.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// 'username',
|
|
49
|
-
// 'nameFirst',
|
|
50
|
-
// 'nameMiddle',
|
|
51
|
-
// 'nameLast',
|
|
52
|
-
// 'name',
|
|
53
|
-
'email',
|
|
54
|
-
// 'avatar',
|
|
55
|
-
'phone',
|
|
56
|
-
'group',
|
|
57
|
-
'groups',
|
|
58
|
-
'inheritAbilities',
|
|
59
|
-
'enabled',
|
|
60
|
-
'secret',
|
|
61
|
-
'emailChangeDate',
|
|
62
|
-
'newEmailSecret',
|
|
63
|
-
'newEmailAddress',
|
|
64
|
-
'cancelEmailSecret',
|
|
65
|
-
'cancelEmailAddress',
|
|
66
|
-
'recoverSecret',
|
|
67
|
-
'recoverSecretDate',
|
|
68
|
-
'salt',
|
|
69
|
-
'password',
|
|
70
|
-
'passwordTemp',
|
|
71
|
-
'revokeTokenDate',
|
|
72
|
-
'totpSecret',
|
|
73
|
-
];
|
|
74
|
-
static DEFAULT_ALLOWLIST_DATA = [];
|
|
75
|
-
$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
76
|
-
static clientEnabledStaticMethods = [
|
|
77
|
-
'current',
|
|
78
|
-
'loginUser',
|
|
79
|
-
'sendRecovery',
|
|
80
|
-
'recover',
|
|
81
|
-
'getClientConfig',
|
|
82
|
-
'getDomainUsers',
|
|
83
|
-
];
|
|
84
|
-
$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
85
|
-
static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
86
|
-
$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
87
|
-
$allowlistTags = [];
|
|
88
|
-
/**
|
|
89
|
-
* Gatekeeper ability cache.
|
|
90
|
-
*
|
|
91
|
-
* Gatekeeper will cache the user's abilities that it calculates, so it can
|
|
92
|
-
* check faster if that user has been checked before.
|
|
93
|
-
*/
|
|
94
|
-
$gatekeeperCache;
|
|
95
|
-
/**
|
|
96
|
-
* This should only be used by the backend.
|
|
97
|
-
*/
|
|
98
|
-
$skipAcWhenSaving = false;
|
|
99
|
-
/**
|
|
100
|
-
* This should only be used by the backend.
|
|
101
|
-
*/
|
|
102
|
-
$skipAcWhenDeleting = false;
|
|
103
|
-
/**
|
|
104
|
-
* The user's group descendants.
|
|
105
|
-
*/
|
|
106
|
-
$descendantGroups;
|
|
107
|
-
/**
|
|
108
|
-
* Used to save the current email address to send verification if it changes
|
|
109
|
-
* from the frontend.
|
|
110
|
-
*
|
|
111
|
-
* If you are changing a user's email address and want to bypass email
|
|
112
|
-
* verification, don't set this.
|
|
113
|
-
*/
|
|
114
|
-
$originalEmail;
|
|
115
|
-
/**
|
|
116
|
-
* Used to save the current username for domain admin permissions.
|
|
117
|
-
*/
|
|
118
|
-
$originalUsername;
|
|
119
|
-
static getFTSText(name, value) {
|
|
120
|
-
if ([
|
|
47
|
+
let User = (() => {
|
|
48
|
+
let _classSuper = AbleObject;
|
|
49
|
+
let _instanceExtraInitializers = [];
|
|
50
|
+
let _$delete_decorators;
|
|
51
|
+
return class User extends _classSuper {
|
|
52
|
+
static {
|
|
53
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
54
|
+
_$delete_decorators = [transactional];
|
|
55
|
+
__esDecorate(this, null, _$delete_decorators, { kind: "method", name: "$delete", static: false, private: false, access: { has: obj => "$delete" in obj, get: obj => obj.$delete }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
56
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The instance of Tilmeld to use for queries.
|
|
60
|
+
*/
|
|
61
|
+
static ETYPE = 'tilmeld_user';
|
|
62
|
+
static class = 'User';
|
|
63
|
+
static checkUsernameCallbacks = [];
|
|
64
|
+
static beforeRegisterCallbacks = [];
|
|
65
|
+
static afterRegisterCallbacks = [];
|
|
66
|
+
static beforeSaveCallbacks = [];
|
|
67
|
+
static afterSaveCallbacks = [];
|
|
68
|
+
static beforeLoginCallbacks = [];
|
|
69
|
+
static afterLoginCallbacks = [];
|
|
70
|
+
static beforeLogoutCallbacks = [];
|
|
71
|
+
static afterLogoutCallbacks = [];
|
|
72
|
+
static beforeDeleteCallbacks = [];
|
|
73
|
+
static afterDeleteCallbacks = [];
|
|
74
|
+
static DEFAULT_CLIENT_ENABLED_METHODS = [
|
|
75
|
+
'$checkUsername',
|
|
76
|
+
'$checkEmail',
|
|
77
|
+
'$checkPhone',
|
|
78
|
+
'$getAvatar',
|
|
79
|
+
'$register',
|
|
80
|
+
];
|
|
81
|
+
static DEFAULT_PRIVATE_DATA = [
|
|
82
|
+
// The things that are commented are there to show what is _not_ private.
|
|
121
83
|
'abilities',
|
|
84
|
+
// 'username',
|
|
85
|
+
// 'nameFirst',
|
|
86
|
+
// 'nameMiddle',
|
|
87
|
+
// 'nameLast',
|
|
88
|
+
// 'name',
|
|
89
|
+
'email',
|
|
90
|
+
// 'avatar',
|
|
91
|
+
'phone',
|
|
122
92
|
'group',
|
|
123
93
|
'groups',
|
|
124
94
|
'inheritAbilities',
|
|
@@ -136,1994 +106,1914 @@ export default class User extends AbleObject {
|
|
|
136
106
|
'passwordTemp',
|
|
137
107
|
'revokeTokenDate',
|
|
138
108
|
'totpSecret',
|
|
139
|
-
]
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
109
|
+
];
|
|
110
|
+
static DEFAULT_ALLOWLIST_DATA = [];
|
|
111
|
+
$clientEnabledMethods = (__runInitializers(this, _instanceExtraInitializers), [...User.DEFAULT_CLIENT_ENABLED_METHODS]);
|
|
112
|
+
static clientEnabledStaticMethods = [
|
|
113
|
+
'current',
|
|
114
|
+
'loginUser',
|
|
115
|
+
'sendRecovery',
|
|
116
|
+
'recover',
|
|
117
|
+
'getClientConfig',
|
|
118
|
+
'getDomainUsers',
|
|
119
|
+
];
|
|
120
|
+
$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
121
|
+
static searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
122
|
+
$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
123
|
+
$allowlistTags = [];
|
|
124
|
+
/**
|
|
125
|
+
* Gatekeeper ability cache.
|
|
126
|
+
*
|
|
127
|
+
* Gatekeeper will cache the user's abilities that it calculates, so it can
|
|
128
|
+
* check faster if that user has been checked before.
|
|
129
|
+
*/
|
|
130
|
+
$gatekeeperCache;
|
|
131
|
+
/**
|
|
132
|
+
* This should only be used by the backend.
|
|
133
|
+
*/
|
|
134
|
+
$skipAcWhenSaving = false;
|
|
135
|
+
/**
|
|
136
|
+
* This should only be used by the backend.
|
|
137
|
+
*/
|
|
138
|
+
$skipAcWhenDeleting = false;
|
|
139
|
+
/**
|
|
140
|
+
* The user's group descendants.
|
|
141
|
+
*/
|
|
142
|
+
$descendantGroups;
|
|
143
|
+
/**
|
|
144
|
+
* Used to save the current email address to send verification if it changes
|
|
145
|
+
* from the frontend.
|
|
146
|
+
*
|
|
147
|
+
* If you are changing a user's email address and want to bypass email
|
|
148
|
+
* verification, don't set this.
|
|
149
|
+
*/
|
|
150
|
+
$originalEmail;
|
|
151
|
+
/**
|
|
152
|
+
* Used to save the current username for domain admin permissions.
|
|
153
|
+
*/
|
|
154
|
+
$originalUsername;
|
|
155
|
+
static getFTSText(name, value) {
|
|
156
|
+
if ([
|
|
157
|
+
'abilities',
|
|
158
|
+
'group',
|
|
159
|
+
'groups',
|
|
160
|
+
'inheritAbilities',
|
|
161
|
+
'enabled',
|
|
162
|
+
'secret',
|
|
163
|
+
'emailChangeDate',
|
|
164
|
+
'newEmailSecret',
|
|
165
|
+
'newEmailAddress',
|
|
166
|
+
'cancelEmailSecret',
|
|
167
|
+
'cancelEmailAddress',
|
|
168
|
+
'recoverSecret',
|
|
169
|
+
'recoverSecretDate',
|
|
170
|
+
'salt',
|
|
171
|
+
'password',
|
|
172
|
+
'passwordTemp',
|
|
173
|
+
'revokeTokenDate',
|
|
174
|
+
'totpSecret',
|
|
175
|
+
].indexOf(name) !== -1) {
|
|
176
|
+
return null;
|
|
155
177
|
}
|
|
178
|
+
return super.getFTSText(name, value);
|
|
156
179
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
* @param data The input data from the client.
|
|
170
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
171
|
-
*/
|
|
172
|
-
static async sendRecovery(data) {
|
|
173
|
-
const tilmeld = enforceTilmeld(this);
|
|
174
|
-
if (!tilmeld.config.pwRecovery) {
|
|
175
|
-
return {
|
|
176
|
-
result: false,
|
|
177
|
-
message: 'Account recovery is not allowed.',
|
|
178
|
-
};
|
|
180
|
+
static async factoryUsername(username) {
|
|
181
|
+
const entity = new this();
|
|
182
|
+
if (username != null) {
|
|
183
|
+
const entity = await this.nymph.getEntity({ class: this }, {
|
|
184
|
+
type: '&',
|
|
185
|
+
ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
186
|
+
});
|
|
187
|
+
if (entity != null) {
|
|
188
|
+
return entity;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return entity;
|
|
179
192
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
193
|
+
static current(returnObjectIfNotExist) {
|
|
194
|
+
const tilmeld = enforceTilmeld(this);
|
|
195
|
+
if (tilmeld.currentUser == null) {
|
|
196
|
+
return returnObjectIfNotExist ? this.factorySync() : null;
|
|
197
|
+
}
|
|
198
|
+
return tilmeld.currentUser;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Send an account recovery link.
|
|
202
|
+
*
|
|
203
|
+
* @param data The input data from the client.
|
|
204
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
205
|
+
*/
|
|
206
|
+
static async sendRecovery(data) {
|
|
207
|
+
const tilmeld = enforceTilmeld(this);
|
|
208
|
+
if (!tilmeld.config.pwRecovery) {
|
|
209
|
+
return { result: false, message: 'Account recovery is not allowed.' };
|
|
210
|
+
}
|
|
211
|
+
let user;
|
|
212
|
+
const options = {};
|
|
213
|
+
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
214
|
+
// Create a username recovery email.
|
|
215
|
+
const getUser = await this.nymph.getEntity({ class: tilmeld.User, skipAc: true }, {
|
|
216
|
+
type: '&',
|
|
217
|
+
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
218
|
+
});
|
|
219
|
+
if (getUser == null) {
|
|
220
|
+
return {
|
|
221
|
+
result: false,
|
|
222
|
+
message: 'Requested account is not accessible.',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Send the recovery email.
|
|
226
|
+
user = getUser;
|
|
227
|
+
options.template = 'RecoverUsername';
|
|
228
|
+
options.message = {
|
|
229
|
+
to: { name: user.name ?? '', address: user.email ?? '' },
|
|
230
|
+
};
|
|
231
|
+
options.locals = {};
|
|
232
|
+
}
|
|
233
|
+
else if (data.recoveryType === 'password') {
|
|
234
|
+
// Create a password recovery email.
|
|
235
|
+
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
236
|
+
if (getUser.guid == null) {
|
|
237
|
+
return {
|
|
238
|
+
result: false,
|
|
239
|
+
message: 'Requested account is not accessible.',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Create a unique secret.
|
|
243
|
+
getUser.recoverSecret = humanSecret();
|
|
244
|
+
getUser.recoverSecretDate = Date.now();
|
|
245
|
+
try {
|
|
246
|
+
await getUser.$saveSkipAC();
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return { result: false, message: "Couldn't save recovery secret." };
|
|
250
|
+
}
|
|
251
|
+
// Send the recovery email.
|
|
252
|
+
user = getUser;
|
|
253
|
+
options.template = 'RecoverPassword';
|
|
254
|
+
options.message = {
|
|
255
|
+
to: { name: user.name ?? '', address: user.email ?? '' },
|
|
256
|
+
};
|
|
257
|
+
options.locals = {
|
|
258
|
+
recoverCode: getUser.recoverSecret,
|
|
259
|
+
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
return { result: false, message: 'Invalid recovery type.' };
|
|
264
|
+
}
|
|
265
|
+
this.nymph.config.debugLog('tilmeld', `User account recovery (${options.template}) request for "${user.username}".`);
|
|
266
|
+
// Send the email.
|
|
267
|
+
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
189
268
|
return {
|
|
190
|
-
result:
|
|
191
|
-
message: '
|
|
269
|
+
result: true,
|
|
270
|
+
message: "We've sent an email to your registered address. Please " +
|
|
271
|
+
'check your email to continue with account recovery.',
|
|
192
272
|
};
|
|
193
273
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
options.message = {
|
|
198
|
-
to: {
|
|
199
|
-
name: user.name ?? '',
|
|
200
|
-
address: user.email ?? '',
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
options.locals = {};
|
|
274
|
+
else {
|
|
275
|
+
return { result: false, message: "Couldn't send recovery email." };
|
|
276
|
+
}
|
|
204
277
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Recover account details.
|
|
280
|
+
*
|
|
281
|
+
* @param data The input data from the client.
|
|
282
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
283
|
+
*/
|
|
284
|
+
static async recover(data) {
|
|
285
|
+
const tilmeld = enforceTilmeld(this);
|
|
286
|
+
if (!tilmeld.config.pwRecovery) {
|
|
287
|
+
return { result: false, message: 'Account recovery is not allowed.' };
|
|
288
|
+
}
|
|
289
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
290
|
+
if (user.guid == null ||
|
|
291
|
+
user.recoverSecret == null ||
|
|
292
|
+
data.secret !== user.recoverSecret ||
|
|
293
|
+
(strtotime('+' + tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) || 0) *
|
|
294
|
+
1000 <
|
|
295
|
+
Date.now()) {
|
|
296
|
+
return { result: false, message: 'The secret code does not match.' };
|
|
297
|
+
}
|
|
298
|
+
if (!('password' in data) || !data.password.length) {
|
|
299
|
+
return { result: false, message: 'Password cannot be empty.' };
|
|
300
|
+
}
|
|
301
|
+
this.nymph.config.debugLog('tilmeld', `User account recovery for "${user.username}".`);
|
|
302
|
+
user.$password(data.password);
|
|
303
|
+
delete user.recoverSecret;
|
|
304
|
+
delete user.recoverSecretDate;
|
|
305
|
+
try {
|
|
306
|
+
await user.$saveSkipAC();
|
|
209
307
|
return {
|
|
210
|
-
result:
|
|
211
|
-
message: '
|
|
308
|
+
result: true,
|
|
309
|
+
message: 'Your password has been reset. You can now log in using your new password.',
|
|
212
310
|
};
|
|
213
311
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (!(await getUser.$saveSkipAC())) {
|
|
218
|
-
return { result: false, message: "Couldn't save recovery secret." };
|
|
219
|
-
}
|
|
220
|
-
// Send the recovery email.
|
|
221
|
-
user = getUser;
|
|
222
|
-
options.template = 'RecoverPassword';
|
|
223
|
-
options.message = {
|
|
224
|
-
to: {
|
|
225
|
-
name: user.name ?? '',
|
|
226
|
-
address: user.email ?? '',
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
options.locals = {
|
|
230
|
-
recoverCode: getUser.recoverSecret,
|
|
231
|
-
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
return { result: false, message: 'Invalid recovery type.' };
|
|
236
|
-
}
|
|
237
|
-
this.nymph.config.debugLog('tilmeld', `User account recovery (${options.template}) request for "${user.username}".`);
|
|
238
|
-
// Send the email.
|
|
239
|
-
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
240
|
-
return {
|
|
241
|
-
result: true,
|
|
242
|
-
message: "We've sent an email to your registered address. Please " +
|
|
243
|
-
'check your email to continue with account recovery.',
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
return { result: false, message: "Couldn't send recovery email." };
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Recover account details.
|
|
252
|
-
*
|
|
253
|
-
* @param data The input data from the client.
|
|
254
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
255
|
-
*/
|
|
256
|
-
static async recover(data) {
|
|
257
|
-
const tilmeld = enforceTilmeld(this);
|
|
258
|
-
if (!tilmeld.config.pwRecovery) {
|
|
259
|
-
return {
|
|
260
|
-
result: false,
|
|
261
|
-
message: 'Account recovery is not allowed.',
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
const user = await tilmeld.User.factoryUsername(data.username);
|
|
265
|
-
if (user.guid == null ||
|
|
266
|
-
user.recoverSecret == null ||
|
|
267
|
-
data.secret !== user.recoverSecret ||
|
|
268
|
-
(strtotime('+' + tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) || 0) *
|
|
269
|
-
1000 <
|
|
270
|
-
Date.now()) {
|
|
271
|
-
return {
|
|
272
|
-
result: false,
|
|
273
|
-
message: 'The secret code does not match.',
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
if (!('password' in data) || !data.password.length) {
|
|
277
|
-
return { result: false, message: 'Password cannot be empty.' };
|
|
312
|
+
catch (e) {
|
|
313
|
+
return { result: false, message: 'Error saving new password.' };
|
|
314
|
+
}
|
|
278
315
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
delete user.recoverSecret;
|
|
282
|
-
delete user.recoverSecretDate;
|
|
283
|
-
if (await user.$saveSkipAC()) {
|
|
316
|
+
static getClientConfig() {
|
|
317
|
+
const tilmeld = enforceTilmeld(this);
|
|
284
318
|
return {
|
|
285
|
-
|
|
286
|
-
|
|
319
|
+
regFields: tilmeld.config.regFields,
|
|
320
|
+
userFields: tilmeld.config.userFields,
|
|
321
|
+
emailUsernames: tilmeld.config.emailUsernames,
|
|
322
|
+
domainSupport: tilmeld.config.domainSupport,
|
|
323
|
+
allowRegistration: tilmeld.config.allowRegistration,
|
|
324
|
+
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
325
|
+
pwRecovery: tilmeld.config.pwRecovery,
|
|
326
|
+
verifyEmail: tilmeld.config.verifyEmail,
|
|
327
|
+
unverifiedAccess: tilmeld.config.unverifiedAccess,
|
|
287
328
|
};
|
|
288
329
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
allowUsernameChange: tilmeld.config.allowUsernameChange,
|
|
302
|
-
pwRecovery: tilmeld.config.pwRecovery,
|
|
303
|
-
verifyEmail: tilmeld.config.verifyEmail,
|
|
304
|
-
unverifiedAccess: tilmeld.config.unverifiedAccess,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
static async loginUser(data) {
|
|
308
|
-
const tilmeld = enforceTilmeld(this);
|
|
309
|
-
if (!('username' in data) || !data.username.length) {
|
|
310
|
-
return { result: false, message: 'Incorrect login/password.' };
|
|
311
|
-
}
|
|
312
|
-
const user = await tilmeld.User.factoryUsername(data.username);
|
|
313
|
-
const result = await user.$login(data);
|
|
314
|
-
if (result.result) {
|
|
315
|
-
user.$updateDataProtection();
|
|
316
|
-
result.user = user;
|
|
317
|
-
}
|
|
318
|
-
return result;
|
|
319
|
-
}
|
|
320
|
-
/**
|
|
321
|
-
* Get the users in a domain. This is only accessible to the domain's admins.
|
|
322
|
-
*/
|
|
323
|
-
static async getDomainUsers(domain, options) {
|
|
324
|
-
const tilmeld = enforceTilmeld(this);
|
|
325
|
-
if (!tilmeld.config.domainSupport) {
|
|
326
|
-
throw new AccessControlError('Domain support is not enabled.');
|
|
330
|
+
static async loginUser(data) {
|
|
331
|
+
const tilmeld = enforceTilmeld(this);
|
|
332
|
+
if (!('username' in data) || !data.username.length) {
|
|
333
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
334
|
+
}
|
|
335
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
336
|
+
const result = await user.$login(data);
|
|
337
|
+
if (result.result) {
|
|
338
|
+
user.$updateDataProtection();
|
|
339
|
+
result.user = user;
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
327
342
|
}
|
|
328
|
-
|
|
329
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Get the users in a domain. This is only accessible to the domain's admins.
|
|
345
|
+
*/
|
|
346
|
+
static async getDomainUsers(domain, options) {
|
|
347
|
+
const tilmeld = enforceTilmeld(this);
|
|
348
|
+
if (!tilmeld.config.domainSupport) {
|
|
349
|
+
throw new AccessControlError('Domain support is not enabled.');
|
|
350
|
+
}
|
|
351
|
+
if (!domain) {
|
|
352
|
+
throw new BadDataError('Domain is required.');
|
|
353
|
+
}
|
|
354
|
+
domain = domain.toLowerCase();
|
|
355
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
356
|
+
throw new BadDataError(tilmeld.config.validDomainRegexNotice);
|
|
357
|
+
}
|
|
358
|
+
if (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
359
|
+
throw new AccessControlError('You do not have the authority to manage this domain.');
|
|
360
|
+
}
|
|
361
|
+
return await this.nymph.getEntities({
|
|
362
|
+
class: this.nymph.getEntityClass(User),
|
|
363
|
+
...(options?.limit != null ? { limit: options.limit } : {}),
|
|
364
|
+
...(options?.offset != null ? { offset: options.offset } : {}),
|
|
365
|
+
...(options?.sort != null ? { sort: options.sort } : {}),
|
|
366
|
+
...(options?.reverse != null ? { reverse: options.reverse } : {}),
|
|
367
|
+
}, {
|
|
368
|
+
type: '&',
|
|
369
|
+
like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
|
|
370
|
+
});
|
|
330
371
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
372
|
+
constructor() {
|
|
373
|
+
super();
|
|
374
|
+
// Defaults.
|
|
375
|
+
this.$data.enabled = true;
|
|
376
|
+
this.$data.abilities = [];
|
|
377
|
+
this.$data.groups = [];
|
|
378
|
+
this.$data.inheritAbilities = true;
|
|
379
|
+
this.$updateDataProtection();
|
|
334
380
|
}
|
|
335
|
-
|
|
336
|
-
|
|
381
|
+
async $getUniques() {
|
|
382
|
+
const tilmeld = enforceTilmeld(this);
|
|
383
|
+
const uniques = [`u:${this.$data.username}`];
|
|
384
|
+
if (!tilmeld.config.emailUsernames &&
|
|
385
|
+
tilmeld.config.userFields.includes('email')) {
|
|
386
|
+
uniques.push(`e:${this.$data.email}`);
|
|
387
|
+
if (this.$data.newEmailAddress != null) {
|
|
388
|
+
uniques.push(`e:${this.$data.newEmailAddress}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return uniques;
|
|
337
392
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
offset: options.offset,
|
|
348
|
-
}
|
|
349
|
-
: {}),
|
|
350
|
-
...(options?.sort != null
|
|
351
|
-
? {
|
|
352
|
-
sort: options.sort,
|
|
353
|
-
}
|
|
354
|
-
: {}),
|
|
355
|
-
...(options?.reverse != null
|
|
356
|
-
? {
|
|
357
|
-
reverse: options.reverse,
|
|
358
|
-
}
|
|
359
|
-
: {}),
|
|
360
|
-
}, {
|
|
361
|
-
type: '&',
|
|
362
|
-
like: ['username', `%@${domain.replace(/([\\%_])/g, (s) => `\\${s}`)}`],
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
constructor() {
|
|
366
|
-
super();
|
|
367
|
-
// Defaults.
|
|
368
|
-
this.$data.enabled = true;
|
|
369
|
-
this.$data.abilities = [];
|
|
370
|
-
this.$data.groups = [];
|
|
371
|
-
this.$data.inheritAbilities = true;
|
|
372
|
-
this.$updateDataProtection();
|
|
373
|
-
}
|
|
374
|
-
$setNymph(nymph) {
|
|
375
|
-
this.$nymph = nymph;
|
|
376
|
-
if (!this.$asleep()) {
|
|
377
|
-
if (this.$data.group && this.$data.group.$nymph !== nymph) {
|
|
378
|
-
this.$data.group.$setNymph(nymph);
|
|
393
|
+
async $login(data) {
|
|
394
|
+
const tilmeld = enforceTilmeld(this);
|
|
395
|
+
if (this.guid == null) {
|
|
396
|
+
await tilmeld.config.failedLoginAttempt(null, data);
|
|
397
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
398
|
+
}
|
|
399
|
+
if (!this.$checkPassword(data.password)) {
|
|
400
|
+
await tilmeld.config.failedLoginAttempt(this, data);
|
|
401
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
379
402
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
403
|
+
if (!this.$data.enabled) {
|
|
404
|
+
return { result: false, message: 'Your account is disabled.' };
|
|
405
|
+
}
|
|
406
|
+
if (await this.$gatekeeper()) {
|
|
407
|
+
return { result: true, message: 'You are already logged in.' };
|
|
408
|
+
}
|
|
409
|
+
if (this.$data.totpSecret != null) {
|
|
410
|
+
if (data.code == null ||
|
|
411
|
+
typeof data.code !== 'string' ||
|
|
412
|
+
data.code.length !== 6) {
|
|
413
|
+
return {
|
|
414
|
+
result: false,
|
|
415
|
+
needTOTP: true,
|
|
416
|
+
message: 'You need to provide a 2FA code.',
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (!this.$checkTOTPCode(data.code)) {
|
|
420
|
+
await tilmeld.config.failedLoginAttempt(this, data);
|
|
421
|
+
return { result: false, message: 'Incorrect 2FA code.' };
|
|
383
422
|
}
|
|
384
423
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
uniques.push(`e:${this.$data.email}`);
|
|
393
|
-
if (this.$data.newEmailAddress != null) {
|
|
394
|
-
uniques.push(`e:${this.$data.newEmailAddress}`);
|
|
424
|
+
try {
|
|
425
|
+
for (let callback of this.constructor
|
|
426
|
+
.beforeLoginCallbacks) {
|
|
427
|
+
if (callback) {
|
|
428
|
+
await callback(this, data);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
395
431
|
}
|
|
432
|
+
catch (e) {
|
|
433
|
+
return { result: false, message: e.message };
|
|
434
|
+
}
|
|
435
|
+
this.$nymph.config.debugLog('tilmeld', `User login for "${this.$data.username}".`);
|
|
436
|
+
// Authentication was successful, attempt to login.
|
|
437
|
+
if (!(await tilmeld.login(this, true))) {
|
|
438
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
439
|
+
}
|
|
440
|
+
for (let callback of this.constructor
|
|
441
|
+
.afterLoginCallbacks) {
|
|
442
|
+
if (callback) {
|
|
443
|
+
await callback(this);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Login was successful.
|
|
447
|
+
return { result: true, message: 'You are logged in.' };
|
|
396
448
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
if (this.$data.totpSecret != null) {
|
|
416
|
-
if (data.code == null ||
|
|
417
|
-
typeof data.code !== 'string' ||
|
|
418
|
-
data.code.length !== 6) {
|
|
449
|
+
async $switchUser(data) {
|
|
450
|
+
const tilmeld = enforceTilmeld(this);
|
|
451
|
+
if (this.guid == null) {
|
|
452
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
453
|
+
}
|
|
454
|
+
let user = tilmeld.User.current();
|
|
455
|
+
if (!user || user.guid == null) {
|
|
456
|
+
return { result: false, message: "You're not logged in." };
|
|
457
|
+
}
|
|
458
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
459
|
+
if (isCurrentUser) {
|
|
460
|
+
return { result: false, message: "You can't switch to yourself." };
|
|
461
|
+
}
|
|
462
|
+
const toAbilities = this.$data.abilities || [];
|
|
463
|
+
const fromAbilities = user.abilities || [];
|
|
464
|
+
if (!fromAbilities.includes('tilmeld/switch') &&
|
|
465
|
+
!fromAbilities.includes('system/admin')) {
|
|
419
466
|
return {
|
|
420
467
|
result: false,
|
|
421
|
-
|
|
422
|
-
message: 'You need to provide a 2FA code.',
|
|
468
|
+
message: "You don't have permission to switch users.",
|
|
423
469
|
};
|
|
424
470
|
}
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
return { result: false, message: '
|
|
471
|
+
if (toAbilities.includes('system/admin') ||
|
|
472
|
+
toAbilities.includes('tilmeld/admin')) {
|
|
473
|
+
return { result: false, message: "You can't switch to an admin." };
|
|
428
474
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
475
|
+
try {
|
|
476
|
+
for (let callback of this.constructor
|
|
477
|
+
.beforeLoginCallbacks) {
|
|
478
|
+
if (callback) {
|
|
479
|
+
await callback(this, {
|
|
480
|
+
username: this.$data.username || '',
|
|
481
|
+
password: '',
|
|
482
|
+
...(data || {}),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
435
485
|
}
|
|
436
486
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
this.$nymph.config.debugLog('tilmeld', `User login for "${this.$data.username}".`);
|
|
445
|
-
// Authentication was successful, attempt to login.
|
|
446
|
-
if (!(await tilmeld.login(this, true))) {
|
|
447
|
-
return { result: false, message: 'Incorrect login/password.' };
|
|
448
|
-
}
|
|
449
|
-
for (let callback of this.constructor
|
|
450
|
-
.afterLoginCallbacks) {
|
|
451
|
-
if (callback) {
|
|
452
|
-
await callback(this);
|
|
487
|
+
catch (e) {
|
|
488
|
+
return { result: false, message: e.message };
|
|
489
|
+
}
|
|
490
|
+
this.$nymph.config.debugLog('tilmeld', `User login switch for "${user.username}" to "${this.$data.username}".`);
|
|
491
|
+
// Authentication was successful, attempt to login.
|
|
492
|
+
if (!(await tilmeld.loginSwitch(this, true))) {
|
|
493
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
453
494
|
}
|
|
454
|
-
}
|
|
455
|
-
// Login was successful.
|
|
456
|
-
return { result: true, message: 'You are logged in.' };
|
|
457
|
-
}
|
|
458
|
-
async $switchUser(data) {
|
|
459
|
-
const tilmeld = enforceTilmeld(this);
|
|
460
|
-
if (this.guid == null) {
|
|
461
|
-
return { result: false, message: 'Incorrect login/password.' };
|
|
462
|
-
}
|
|
463
|
-
let user = tilmeld.User.current();
|
|
464
|
-
if (!user || user.guid == null) {
|
|
465
|
-
return { result: false, message: "You're not logged in." };
|
|
466
|
-
}
|
|
467
|
-
const isCurrentUser = user != null && user.$is(this);
|
|
468
|
-
if (isCurrentUser) {
|
|
469
|
-
return { result: false, message: "You can't switch to yourself." };
|
|
470
|
-
}
|
|
471
|
-
const toAbilities = this.$data.abilities || [];
|
|
472
|
-
const fromAbilities = user.abilities || [];
|
|
473
|
-
if (!fromAbilities.includes('tilmeld/switch') &&
|
|
474
|
-
!fromAbilities.includes('system/admin')) {
|
|
475
|
-
return {
|
|
476
|
-
result: false,
|
|
477
|
-
message: "You don't have permission to switch users.",
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
if (toAbilities.includes('system/admin') ||
|
|
481
|
-
toAbilities.includes('tilmeld/admin')) {
|
|
482
|
-
return { result: false, message: "You can't switch to an admin." };
|
|
483
|
-
}
|
|
484
|
-
try {
|
|
485
495
|
for (let callback of this.constructor
|
|
486
|
-
.
|
|
496
|
+
.afterLoginCallbacks) {
|
|
487
497
|
if (callback) {
|
|
488
|
-
await callback(this
|
|
489
|
-
username: this.$data.username || '',
|
|
490
|
-
password: '',
|
|
491
|
-
...(data || {}),
|
|
492
|
-
});
|
|
498
|
+
await callback(this);
|
|
493
499
|
}
|
|
494
500
|
}
|
|
501
|
+
// Login was successful.
|
|
502
|
+
return { result: true, message: 'You are logged in.' };
|
|
495
503
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (callback) {
|
|
510
|
-
await callback(this);
|
|
504
|
+
/**
|
|
505
|
+
* Log a user out of the system.
|
|
506
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
507
|
+
*/
|
|
508
|
+
async $logout() {
|
|
509
|
+
const tilmeld = enforceTilmeld(this);
|
|
510
|
+
try {
|
|
511
|
+
for (let callback of this.constructor
|
|
512
|
+
.beforeLogoutCallbacks) {
|
|
513
|
+
if (callback) {
|
|
514
|
+
await callback(this);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
511
517
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
* Log a user out of the system.
|
|
518
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
519
|
-
*/
|
|
520
|
-
async $logout() {
|
|
521
|
-
const tilmeld = enforceTilmeld(this);
|
|
522
|
-
try {
|
|
518
|
+
catch (e) {
|
|
519
|
+
return { result: false, message: e.message };
|
|
520
|
+
}
|
|
521
|
+
this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
|
|
522
|
+
await tilmeld.logout();
|
|
523
523
|
for (let callback of this.constructor
|
|
524
|
-
.
|
|
524
|
+
.afterLogoutCallbacks) {
|
|
525
525
|
if (callback) {
|
|
526
526
|
await callback(this);
|
|
527
527
|
}
|
|
528
528
|
}
|
|
529
|
+
return { result: true, message: 'You have been logged out.' };
|
|
529
530
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
message: e.message,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
|
|
537
|
-
await tilmeld.logout();
|
|
538
|
-
for (let callback of this.constructor
|
|
539
|
-
.afterLogoutCallbacks) {
|
|
540
|
-
if (callback) {
|
|
541
|
-
await callback(this);
|
|
531
|
+
$getAvatar() {
|
|
532
|
+
if (this.$data.avatar != null && this.$data.avatar !== '') {
|
|
533
|
+
return this.$data.avatar;
|
|
542
534
|
}
|
|
535
|
+
if (this.$data.email == null || this.$data.email === '') {
|
|
536
|
+
return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
|
|
537
|
+
}
|
|
538
|
+
return ('https://secure.gravatar.com/avatar/' +
|
|
539
|
+
crypto
|
|
540
|
+
.createHash('sha256')
|
|
541
|
+
.update(this.$data.email.trim().toLowerCase())
|
|
542
|
+
.digest('hex')
|
|
543
|
+
.toLowerCase() +
|
|
544
|
+
'?d=identicon&s=40');
|
|
543
545
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return
|
|
549
|
-
}
|
|
550
|
-
if (this.$data.email == null || this.$data.email === '') {
|
|
551
|
-
return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
|
|
546
|
+
$getGid() {
|
|
547
|
+
if (this.$data.group) {
|
|
548
|
+
return this.$data.group.guid;
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
552
551
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
$getGid() {
|
|
562
|
-
if (this.$data.group) {
|
|
563
|
-
return this.$data.group.guid;
|
|
552
|
+
$getGids() {
|
|
553
|
+
if (this.$data.groups) {
|
|
554
|
+
return this.$data.groups
|
|
555
|
+
.map((group) => group.guid)
|
|
556
|
+
.filter((guid) => guid != null);
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
564
559
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
.
|
|
560
|
+
/**
|
|
561
|
+
* Get the user's group descendants.
|
|
562
|
+
*/
|
|
563
|
+
async $getDescendantGroups() {
|
|
564
|
+
if (this.$descendantGroups == null) {
|
|
565
|
+
this.$descendantGroups = [];
|
|
566
|
+
if (this.$data.group != null) {
|
|
567
|
+
await this.$data.group.$wake();
|
|
568
|
+
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
569
|
+
}
|
|
570
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
571
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
572
|
+
this.$descendantGroups = this.$descendantGroups?.concat(await curGroup.$getDescendants());
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return this.$descendantGroups;
|
|
572
576
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
577
|
+
$jsonAcceptData(input, allowConflict = false) {
|
|
578
|
+
const tilmeld = enforceTilmeld(this);
|
|
579
|
+
this.$check();
|
|
580
|
+
if ('abilities' in input.data) {
|
|
581
|
+
this.$checkIncomingAbilities(input.data.abilities);
|
|
582
|
+
}
|
|
583
|
+
// If the user is a new domain user, the data protection needs to be updated
|
|
584
|
+
// before the data is accepted, or else some things won't be accepted
|
|
585
|
+
// correctly (enabled, passwordTemp).
|
|
586
|
+
let reloadDataProtection = false;
|
|
587
|
+
if (tilmeld.config.domainSupport) {
|
|
588
|
+
let [_username, domain] = splitn(input.data.username ?? '', '@', 2);
|
|
589
|
+
// Lowercase the domain part.
|
|
590
|
+
if (domain) {
|
|
591
|
+
domain = domain.toLowerCase();
|
|
592
|
+
}
|
|
593
|
+
const user = tilmeld.User.current();
|
|
594
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
595
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
596
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
597
|
+
const abilities = (isCurrentUser || user == null
|
|
598
|
+
? this.$data.abilities
|
|
599
|
+
: user?.abilities) ?? [];
|
|
600
|
+
const isNewUser = input.guid == null;
|
|
601
|
+
const isDomainUser = !isCurrentUser &&
|
|
602
|
+
user != null &&
|
|
603
|
+
domain &&
|
|
604
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
605
|
+
reloadDataProtection = !!(isNewUser && isDomainUser);
|
|
584
606
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
607
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
608
|
+
if (reloadDataProtection) {
|
|
609
|
+
// Load it again after data protection is run.
|
|
610
|
+
this.$updateDataProtection();
|
|
611
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
588
612
|
}
|
|
589
613
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
614
|
+
$jsonAcceptPatch(patch, allowConflict = false) {
|
|
615
|
+
this.$check();
|
|
616
|
+
if ('abilities' in patch.set) {
|
|
617
|
+
this.$checkIncomingAbilities(patch.set.abilities);
|
|
618
|
+
}
|
|
619
|
+
super.$jsonAcceptPatch(patch, allowConflict);
|
|
620
|
+
}
|
|
621
|
+
$checkIncomingAbilities(incoming) {
|
|
622
|
+
const tilmeld = enforceTilmeld(this);
|
|
623
|
+
if (!Array.isArray(incoming) ||
|
|
624
|
+
incoming.find((ability) => typeof ability !== 'string')) {
|
|
625
|
+
throw new BadDataError('Abilities must be an array of strings.');
|
|
626
|
+
}
|
|
627
|
+
if (incoming.includes('system/admin') !==
|
|
628
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
629
|
+
(!tilmeld.currentUser ||
|
|
630
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
631
|
+
throw new BadDataError("You don't have the authority to grant or revoke system/admin.");
|
|
632
|
+
}
|
|
633
|
+
if (incoming.includes('tilmeld/admin') !==
|
|
634
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
635
|
+
(!tilmeld.currentUser ||
|
|
636
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
637
|
+
throw new BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
|
|
638
|
+
}
|
|
639
|
+
if (incoming.includes('tilmeld/switch') !==
|
|
640
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
641
|
+
(!tilmeld.currentUser ||
|
|
642
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
643
|
+
throw new BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
|
|
644
|
+
}
|
|
645
|
+
if (xor(incoming.filter((ability) => ability.startsWith('tilmeld/')), this.$data.abilities?.filter((ability) => ability.startsWith('tilmeld/')) ?? []).length &&
|
|
646
|
+
(!tilmeld.currentUser ||
|
|
647
|
+
(!tilmeld.currentUser.abilities?.includes('system/admin') &&
|
|
648
|
+
!tilmeld.currentUser.abilities?.includes('tilmeld/admin')))) {
|
|
649
|
+
throw new BadDataError("You don't have the authority to grant or revoke Tilmeld abilities.");
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
$putData(data, sdata, source) {
|
|
653
|
+
super.$putData(data, sdata, source);
|
|
654
|
+
// TODO: Fix these being added by the drivers.
|
|
655
|
+
// @ts-ignore
|
|
656
|
+
delete this.$data.acRead;
|
|
657
|
+
// @ts-ignore
|
|
658
|
+
delete this.$data.acWrite;
|
|
659
|
+
// @ts-ignore
|
|
660
|
+
delete this.$data.acFull;
|
|
661
|
+
if (source === 'server') {
|
|
662
|
+
this.$originalEmail = this.$data.email;
|
|
663
|
+
this.$originalUsername = this.$data.username;
|
|
664
|
+
}
|
|
665
|
+
this.$updateDataProtection();
|
|
597
666
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
667
|
+
/**
|
|
668
|
+
* Update the data protection arrays for a user.
|
|
669
|
+
*
|
|
670
|
+
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
671
|
+
*/
|
|
672
|
+
$updateDataProtection(givenUser) {
|
|
673
|
+
const tilmeld = enforceTilmeld(this);
|
|
674
|
+
let user = givenUser ?? tilmeld.User.current();
|
|
675
|
+
let [_username, domain] = tilmeld.config.domainSupport
|
|
676
|
+
? splitn(this.$originalUsername ?? this.$data.username ?? '', '@', 2)
|
|
677
|
+
: [this.$originalUsername ?? this.$data.username, null];
|
|
604
678
|
// Lowercase the domain part.
|
|
605
679
|
if (domain) {
|
|
606
680
|
domain = domain.toLowerCase();
|
|
607
681
|
}
|
|
608
|
-
|
|
682
|
+
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
683
|
+
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
684
|
+
this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
685
|
+
if (tilmeld.config.emailUsernames) {
|
|
686
|
+
this.$privateData.push('username');
|
|
687
|
+
}
|
|
609
688
|
const isCurrentUser = user != null && user.$is(this);
|
|
610
689
|
// While loading, accessing abilities on the current user will infinitely
|
|
611
690
|
// loop, but if this is the current user, we can use its abilities.
|
|
612
691
|
const abilities = (isCurrentUser || user == null
|
|
613
692
|
? this.$data.abilities
|
|
614
693
|
: user?.abilities) ?? [];
|
|
615
|
-
const isNewUser =
|
|
616
|
-
const isDomainUser =
|
|
694
|
+
const isNewUser = this.guid == null;
|
|
695
|
+
const isDomainUser = tilmeld.config.domainSupport &&
|
|
696
|
+
!isCurrentUser &&
|
|
617
697
|
user != null &&
|
|
618
698
|
domain &&
|
|
619
699
|
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
700
|
+
if (isCurrentUser) {
|
|
701
|
+
// Users can check to see what abilities they have.
|
|
702
|
+
this.$clientEnabledMethods.push('$gatekeeper');
|
|
703
|
+
this.$clientEnabledMethods.push('$changePassword');
|
|
704
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
705
|
+
this.$clientEnabledMethods.push('$logout');
|
|
706
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
707
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
708
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
709
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
710
|
+
}
|
|
711
|
+
if (isDomainUser) {
|
|
712
|
+
// Domain admins can check to see what abilities their users have.
|
|
713
|
+
this.$clientEnabledMethods.push('$gatekeeper');
|
|
714
|
+
this.$clientEnabledMethods.push('$changePassword');
|
|
715
|
+
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
716
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
717
|
+
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
718
|
+
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
719
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
720
|
+
}
|
|
721
|
+
if (user != null &&
|
|
722
|
+
(abilities.includes('tilmeld/admin') ||
|
|
723
|
+
abilities.includes('system/admin'))) {
|
|
724
|
+
// Users who can edit other users can see most of their data.
|
|
725
|
+
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
726
|
+
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
727
|
+
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
728
|
+
this.$allowlistData = undefined;
|
|
729
|
+
}
|
|
730
|
+
else if (isDomainUser) {
|
|
731
|
+
// Domain admins can see their users' data, and edit some of it.
|
|
732
|
+
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
733
|
+
this.$allowlistData.push('username');
|
|
734
|
+
}
|
|
735
|
+
this.$allowlistData.push('avatar');
|
|
736
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
737
|
+
this.$allowlistData.push('nameFirst');
|
|
738
|
+
this.$allowlistData.push('nameMiddle');
|
|
739
|
+
this.$allowlistData.push('nameLast');
|
|
740
|
+
this.$allowlistData.push('name');
|
|
741
|
+
}
|
|
742
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
743
|
+
this.$allowlistData.push('email');
|
|
744
|
+
}
|
|
745
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
746
|
+
this.$allowlistData.push('phone');
|
|
747
|
+
}
|
|
748
|
+
this.$allowlistData.push('enabled');
|
|
749
|
+
this.$allowlistData.push('passwordTemp');
|
|
750
|
+
this.$privateData = [
|
|
751
|
+
// The things commented are there to show what is _not_ private.
|
|
752
|
+
// 'abilities',
|
|
753
|
+
// 'username',
|
|
754
|
+
// 'nameFirst',
|
|
755
|
+
// 'nameMiddle',
|
|
756
|
+
// 'nameLast',
|
|
757
|
+
// 'name',
|
|
758
|
+
// 'email',
|
|
759
|
+
// 'avatar',
|
|
760
|
+
// 'phone',
|
|
761
|
+
// 'group',
|
|
762
|
+
// 'groups',
|
|
763
|
+
// 'inheritAbilities',
|
|
764
|
+
// 'enabled',
|
|
765
|
+
'secret',
|
|
766
|
+
'emailChangeDate',
|
|
767
|
+
'newEmailSecret',
|
|
768
|
+
// 'newEmailAddress',
|
|
769
|
+
'cancelEmailSecret',
|
|
770
|
+
'cancelEmailAddress',
|
|
771
|
+
'recoverSecret',
|
|
772
|
+
'recoverSecretDate',
|
|
773
|
+
'salt',
|
|
774
|
+
'password',
|
|
775
|
+
// 'passwordTemp',
|
|
776
|
+
// 'revokeTokenDate',
|
|
777
|
+
'totpSecret',
|
|
778
|
+
];
|
|
779
|
+
}
|
|
780
|
+
else if (isCurrentUser || isNewUser) {
|
|
781
|
+
// Users can see their own data, and edit some of it.
|
|
782
|
+
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
783
|
+
this.$allowlistData.push('username');
|
|
784
|
+
}
|
|
785
|
+
this.$allowlistData.push('avatar');
|
|
786
|
+
if (tilmeld.config.userFields.includes('name')) {
|
|
787
|
+
this.$allowlistData.push('nameFirst');
|
|
788
|
+
this.$allowlistData.push('nameMiddle');
|
|
789
|
+
this.$allowlistData.push('nameLast');
|
|
790
|
+
this.$allowlistData.push('name');
|
|
791
|
+
}
|
|
792
|
+
if (tilmeld.config.userFields.includes('email')) {
|
|
793
|
+
this.$allowlistData.push('email');
|
|
794
|
+
}
|
|
795
|
+
if (tilmeld.config.userFields.includes('phone')) {
|
|
796
|
+
this.$allowlistData.push('phone');
|
|
797
|
+
}
|
|
798
|
+
this.$privateData = [
|
|
799
|
+
// The things commented are there to show what is _not_ private.
|
|
800
|
+
// 'abilities',
|
|
801
|
+
// 'username',
|
|
802
|
+
// 'nameFirst',
|
|
803
|
+
// 'nameMiddle',
|
|
804
|
+
// 'nameLast',
|
|
805
|
+
// 'name',
|
|
806
|
+
// 'email',
|
|
807
|
+
// 'avatar',
|
|
808
|
+
// 'phone',
|
|
809
|
+
// 'group',
|
|
810
|
+
// 'groups',
|
|
811
|
+
// 'inheritAbilities',
|
|
812
|
+
'enabled',
|
|
813
|
+
'secret',
|
|
814
|
+
'emailChangeDate',
|
|
815
|
+
'newEmailSecret',
|
|
816
|
+
// 'newEmailAddress',
|
|
817
|
+
'cancelEmailSecret',
|
|
818
|
+
'cancelEmailAddress',
|
|
819
|
+
'recoverSecret',
|
|
820
|
+
'recoverSecretDate',
|
|
821
|
+
'salt',
|
|
822
|
+
'password',
|
|
823
|
+
'passwordTemp',
|
|
824
|
+
// 'revokeTokenDate',
|
|
825
|
+
'totpSecret',
|
|
826
|
+
];
|
|
827
|
+
}
|
|
828
|
+
if (user != null &&
|
|
829
|
+
!isCurrentUser &&
|
|
830
|
+
(abilities.includes('tilmeld/switch') ||
|
|
831
|
+
abilities.includes('system/admin'))) {
|
|
832
|
+
// Users with this ability can switch to other users.
|
|
833
|
+
this.$clientEnabledMethods.push('$switchUser');
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
$updateGroupDataProtection() {
|
|
837
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
838
|
+
this.$data.group.$updateDataProtection();
|
|
839
|
+
}
|
|
840
|
+
for (let group of this.$data.groups ?? []) {
|
|
841
|
+
if (!group.$asleep()) {
|
|
842
|
+
group.$updateDataProtection();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
641
845
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
846
|
+
/**
|
|
847
|
+
* Check to see if a user has an ability.
|
|
848
|
+
*
|
|
849
|
+
* This function will check both user and group abilities, if the user is
|
|
850
|
+
* marked to inherit the abilities of its group.
|
|
851
|
+
*
|
|
852
|
+
* If `ability` is undefined, it will check to see if the user is currently
|
|
853
|
+
* logged in.
|
|
854
|
+
*
|
|
855
|
+
* If the user has the "system/admin" ability, this function will return true.
|
|
856
|
+
*
|
|
857
|
+
* @param ability The ability.
|
|
858
|
+
* @returns True or false.
|
|
859
|
+
*/
|
|
860
|
+
async $gatekeeper(ability) {
|
|
861
|
+
await this.$wake();
|
|
862
|
+
const tilmeld = enforceTilmeld(this);
|
|
863
|
+
if (ability == null) {
|
|
864
|
+
return tilmeld.User.current()?.$is(this) ?? false;
|
|
865
|
+
}
|
|
866
|
+
// Check the cache to see if we've already checked this user.
|
|
867
|
+
if (this.$gatekeeperCache == null) {
|
|
868
|
+
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
869
|
+
}
|
|
870
|
+
return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
871
|
+
!!this.$data.abilities?.includes('system/admin'));
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Build a gatekeeper cache object.
|
|
875
|
+
*/
|
|
876
|
+
async $getGatekeeperCache() {
|
|
877
|
+
await this.$wake();
|
|
878
|
+
let abilities = this.$data.abilities ?? [];
|
|
879
|
+
if (this.$data.inheritAbilities) {
|
|
880
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
881
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
882
|
+
if (curGroup.enabled) {
|
|
883
|
+
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
await this.$data.group?.$wake();
|
|
887
|
+
const group = this.$data.group;
|
|
888
|
+
if (group != null && group.cdate != null && group.enabled) {
|
|
889
|
+
abilities = abilities.concat(group.abilities ?? []);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return Object.fromEntries(abilities.map((ability) => [ability, true]));
|
|
647
893
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
894
|
+
$clearCache() {
|
|
895
|
+
const ret = super.$clearCache();
|
|
896
|
+
this.$descendantGroups = undefined;
|
|
897
|
+
this.$gatekeeperCache = undefined;
|
|
898
|
+
return ret;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Send the user email verification/change/cancellation links.
|
|
902
|
+
*
|
|
903
|
+
* @returns True on success, false on failure.
|
|
904
|
+
*/
|
|
905
|
+
async $sendEmailVerification() {
|
|
906
|
+
const tilmeld = enforceTilmeld(this);
|
|
907
|
+
if (this.guid == null) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
let success = true;
|
|
911
|
+
const verifyUrl = `${tilmeld.config.appUrl.replace(/\/$/, () => '')}${tilmeld.config.setupPath}/verify`;
|
|
912
|
+
if (this.$data.secret != null) {
|
|
913
|
+
const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.secret)}`;
|
|
914
|
+
success =
|
|
915
|
+
success &&
|
|
916
|
+
(await tilmeld.config.sendEmail(tilmeld, {
|
|
917
|
+
template: 'VerifyEmail',
|
|
918
|
+
message: {
|
|
919
|
+
to: {
|
|
920
|
+
name: this.$data.name ?? '',
|
|
921
|
+
address: this.$data.email ?? '',
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
locals: { verifyLink: link },
|
|
925
|
+
}, this));
|
|
926
|
+
}
|
|
927
|
+
if (this.$data.newEmailSecret != null) {
|
|
928
|
+
const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
|
|
929
|
+
success =
|
|
930
|
+
success &&
|
|
931
|
+
(await tilmeld.config.sendEmail(tilmeld, {
|
|
932
|
+
template: 'VerifyEmailChange',
|
|
933
|
+
message: {
|
|
934
|
+
to: {
|
|
935
|
+
name: this.$data.name ?? '',
|
|
936
|
+
address: this.$data.newEmailAddress ?? '',
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
locals: {
|
|
940
|
+
verifyLink: link,
|
|
941
|
+
oldEmail: this.$data.cancelEmailAddress,
|
|
942
|
+
newEmail: this.$data.newEmailAddress,
|
|
943
|
+
},
|
|
944
|
+
}, this));
|
|
945
|
+
}
|
|
946
|
+
if (this.$data.cancelEmailSecret != null) {
|
|
947
|
+
const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
|
|
948
|
+
success =
|
|
949
|
+
success &&
|
|
950
|
+
(await tilmeld.config.sendEmail(tilmeld, {
|
|
951
|
+
template: 'CancelEmailChange',
|
|
952
|
+
message: {
|
|
953
|
+
to: {
|
|
954
|
+
name: this.$data.name ?? '',
|
|
955
|
+
address: this.$data.cancelEmailAddress ?? '',
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
locals: {
|
|
959
|
+
cancelLink: link,
|
|
960
|
+
oldEmail: this.$data.cancelEmailAddress,
|
|
961
|
+
newEmail: this.$data.newEmailAddress ?? this.$data.email,
|
|
962
|
+
},
|
|
963
|
+
}, this));
|
|
964
|
+
}
|
|
965
|
+
return success;
|
|
653
966
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
967
|
+
/**
|
|
968
|
+
* Add the user to a (secondary) group.
|
|
969
|
+
*
|
|
970
|
+
* @param group The group.
|
|
971
|
+
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
972
|
+
*/
|
|
973
|
+
async $addGroup(group) {
|
|
974
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
975
|
+
let groups = this.$data.groups;
|
|
976
|
+
if (groups == null) {
|
|
977
|
+
groups = [];
|
|
978
|
+
}
|
|
979
|
+
if (!group.$inArray(groups)) {
|
|
980
|
+
groups.push(group);
|
|
981
|
+
this.$data.groups = groups;
|
|
982
|
+
return groups;
|
|
983
|
+
}
|
|
984
|
+
return true;
|
|
659
985
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
let [_username, domain] = tilmeld.config.domainSupport
|
|
691
|
-
? splitn(this.$originalUsername ?? this.$data.username ?? '', '@', 2)
|
|
692
|
-
: [this.$originalUsername ?? this.$data.username, null];
|
|
693
|
-
// Lowercase the domain part.
|
|
694
|
-
if (domain) {
|
|
695
|
-
domain = domain.toLowerCase();
|
|
696
|
-
}
|
|
697
|
-
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
698
|
-
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
699
|
-
this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
700
|
-
if (tilmeld.config.emailUsernames) {
|
|
701
|
-
this.$privateData.push('username');
|
|
702
|
-
}
|
|
703
|
-
const isCurrentUser = user != null && user.$is(this);
|
|
704
|
-
// While loading, accessing abilities on the current user will infinitely
|
|
705
|
-
// loop, but if this is the current user, we can use its abilities.
|
|
706
|
-
const abilities = (isCurrentUser || user == null
|
|
707
|
-
? this.$data.abilities
|
|
708
|
-
: user?.abilities) ?? [];
|
|
709
|
-
const isNewUser = this.guid == null;
|
|
710
|
-
const isDomainUser = tilmeld.config.domainSupport &&
|
|
711
|
-
!isCurrentUser &&
|
|
712
|
-
user != null &&
|
|
713
|
-
domain &&
|
|
714
|
-
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
715
|
-
if (isCurrentUser) {
|
|
716
|
-
// Users can check to see what abilities they have.
|
|
717
|
-
this.$clientEnabledMethods.push('$gatekeeper');
|
|
718
|
-
this.$clientEnabledMethods.push('$changePassword');
|
|
719
|
-
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
720
|
-
this.$clientEnabledMethods.push('$logout');
|
|
721
|
-
this.$clientEnabledMethods.push('$sendEmailVerification');
|
|
722
|
-
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
723
|
-
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
724
|
-
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
725
|
-
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
726
|
-
}
|
|
727
|
-
if (isDomainUser) {
|
|
728
|
-
// Domain admins can check to see what abilities their users have.
|
|
729
|
-
this.$clientEnabledMethods.push('$gatekeeper');
|
|
730
|
-
this.$clientEnabledMethods.push('$changePassword');
|
|
731
|
-
this.$clientEnabledMethods.push('$revokeCurrentTokens');
|
|
732
|
-
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
733
|
-
this.$clientEnabledMethods.push('$getNewTOTPSecret');
|
|
734
|
-
this.$clientEnabledMethods.push('$saveTOTPSecret');
|
|
735
|
-
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
736
|
-
}
|
|
737
|
-
if (user != null &&
|
|
738
|
-
(abilities.includes('tilmeld/admin') ||
|
|
739
|
-
abilities.includes('system/admin'))) {
|
|
740
|
-
// Users who can edit other users can see most of their data.
|
|
741
|
-
this.$privateData = ['password', 'salt', 'totpSecret'];
|
|
742
|
-
this.$clientEnabledMethods.push('$hasTOTPSecret');
|
|
743
|
-
this.$clientEnabledMethods.push('$removeTOTPSecret');
|
|
744
|
-
this.$allowlistData = undefined;
|
|
745
|
-
}
|
|
746
|
-
else if (isDomainUser) {
|
|
747
|
-
// Domain admins can see their users' data, and edit some of it.
|
|
748
|
-
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
749
|
-
this.$allowlistData.push('username');
|
|
750
|
-
}
|
|
751
|
-
this.$allowlistData.push('avatar');
|
|
752
|
-
if (tilmeld.config.userFields.includes('name')) {
|
|
753
|
-
this.$allowlistData.push('nameFirst');
|
|
754
|
-
this.$allowlistData.push('nameMiddle');
|
|
755
|
-
this.$allowlistData.push('nameLast');
|
|
756
|
-
this.$allowlistData.push('name');
|
|
757
|
-
}
|
|
758
|
-
if (tilmeld.config.userFields.includes('email')) {
|
|
759
|
-
this.$allowlistData.push('email');
|
|
760
|
-
}
|
|
761
|
-
if (tilmeld.config.userFields.includes('phone')) {
|
|
762
|
-
this.$allowlistData.push('phone');
|
|
763
|
-
}
|
|
764
|
-
this.$allowlistData.push('enabled');
|
|
765
|
-
this.$allowlistData.push('passwordTemp');
|
|
766
|
-
this.$privateData = [
|
|
767
|
-
// The things commented are there to show what is _not_ private.
|
|
768
|
-
// 'abilities',
|
|
769
|
-
// 'username',
|
|
770
|
-
// 'nameFirst',
|
|
771
|
-
// 'nameMiddle',
|
|
772
|
-
// 'nameLast',
|
|
773
|
-
// 'name',
|
|
774
|
-
// 'email',
|
|
775
|
-
// 'avatar',
|
|
776
|
-
// 'phone',
|
|
777
|
-
// 'group',
|
|
778
|
-
// 'groups',
|
|
779
|
-
// 'inheritAbilities',
|
|
780
|
-
// 'enabled',
|
|
781
|
-
'secret',
|
|
782
|
-
'emailChangeDate',
|
|
783
|
-
'newEmailSecret',
|
|
784
|
-
// 'newEmailAddress',
|
|
785
|
-
'cancelEmailSecret',
|
|
786
|
-
'cancelEmailAddress',
|
|
787
|
-
'recoverSecret',
|
|
788
|
-
'recoverSecretDate',
|
|
789
|
-
'salt',
|
|
790
|
-
'password',
|
|
791
|
-
// 'passwordTemp',
|
|
792
|
-
// 'revokeTokenDate',
|
|
793
|
-
'totpSecret',
|
|
794
|
-
];
|
|
795
|
-
}
|
|
796
|
-
else if (isCurrentUser || isNewUser) {
|
|
797
|
-
// Users can see their own data, and edit some of it.
|
|
798
|
-
if (tilmeld.config.allowUsernameChange || isNewUser) {
|
|
799
|
-
this.$allowlistData.push('username');
|
|
800
|
-
}
|
|
801
|
-
this.$allowlistData.push('avatar');
|
|
802
|
-
if (tilmeld.config.userFields.includes('name')) {
|
|
803
|
-
this.$allowlistData.push('nameFirst');
|
|
804
|
-
this.$allowlistData.push('nameMiddle');
|
|
805
|
-
this.$allowlistData.push('nameLast');
|
|
806
|
-
this.$allowlistData.push('name');
|
|
807
|
-
}
|
|
808
|
-
if (tilmeld.config.userFields.includes('email')) {
|
|
809
|
-
this.$allowlistData.push('email');
|
|
810
|
-
}
|
|
811
|
-
if (tilmeld.config.userFields.includes('phone')) {
|
|
812
|
-
this.$allowlistData.push('phone');
|
|
813
|
-
}
|
|
814
|
-
this.$privateData = [
|
|
815
|
-
// The things commented are there to show what is _not_ private.
|
|
816
|
-
// 'abilities',
|
|
817
|
-
// 'username',
|
|
818
|
-
// 'nameFirst',
|
|
819
|
-
// 'nameMiddle',
|
|
820
|
-
// 'nameLast',
|
|
821
|
-
// 'name',
|
|
822
|
-
// 'email',
|
|
823
|
-
// 'avatar',
|
|
824
|
-
// 'phone',
|
|
825
|
-
// 'group',
|
|
826
|
-
// 'groups',
|
|
827
|
-
// 'inheritAbilities',
|
|
828
|
-
'enabled',
|
|
829
|
-
'secret',
|
|
830
|
-
'emailChangeDate',
|
|
831
|
-
'newEmailSecret',
|
|
832
|
-
// 'newEmailAddress',
|
|
833
|
-
'cancelEmailSecret',
|
|
834
|
-
'cancelEmailAddress',
|
|
835
|
-
'recoverSecret',
|
|
836
|
-
'recoverSecretDate',
|
|
837
|
-
'salt',
|
|
838
|
-
'password',
|
|
839
|
-
'passwordTemp',
|
|
840
|
-
// 'revokeTokenDate',
|
|
841
|
-
'totpSecret',
|
|
842
|
-
];
|
|
843
|
-
}
|
|
844
|
-
if (user != null &&
|
|
845
|
-
!isCurrentUser &&
|
|
846
|
-
(abilities.includes('tilmeld/switch') ||
|
|
847
|
-
abilities.includes('system/admin'))) {
|
|
848
|
-
// Users with this ability can switch to other users.
|
|
849
|
-
this.$clientEnabledMethods.push('$switchUser');
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
$updateGroupDataProtection() {
|
|
853
|
-
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
854
|
-
this.$data.group.$updateDataProtection();
|
|
855
|
-
}
|
|
856
|
-
for (let group of this.$data.groups ?? []) {
|
|
857
|
-
if (!group.$asleep()) {
|
|
858
|
-
group.$updateDataProtection();
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
/**
|
|
863
|
-
* Check to see if a user has an ability.
|
|
864
|
-
*
|
|
865
|
-
* This function will check both user and group abilities, if the user is
|
|
866
|
-
* marked to inherit the abilities of its group.
|
|
867
|
-
*
|
|
868
|
-
* If `ability` is undefined, it will check to see if the user is currently
|
|
869
|
-
* logged in.
|
|
870
|
-
*
|
|
871
|
-
* If the user has the "system/admin" ability, this function will return true.
|
|
872
|
-
*
|
|
873
|
-
* @param ability The ability.
|
|
874
|
-
* @returns True or false.
|
|
875
|
-
*/
|
|
876
|
-
async $gatekeeper(ability) {
|
|
877
|
-
await this.$wake();
|
|
878
|
-
const tilmeld = enforceTilmeld(this);
|
|
879
|
-
if (ability == null) {
|
|
880
|
-
return tilmeld.User.current()?.$is(this) ?? false;
|
|
881
|
-
}
|
|
882
|
-
// Check the cache to see if we've already checked this user.
|
|
883
|
-
if (this.$gatekeeperCache == null) {
|
|
884
|
-
this.$gatekeeperCache = await this.$getGatekeeperCache();
|
|
885
|
-
}
|
|
886
|
-
return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
887
|
-
!!this.$data.abilities?.includes('system/admin'));
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* Build a gatekeeper cache object.
|
|
891
|
-
*/
|
|
892
|
-
async $getGatekeeperCache() {
|
|
893
|
-
await this.$wake();
|
|
894
|
-
let abilities = this.$data.abilities ?? [];
|
|
895
|
-
if (this.$data.inheritAbilities) {
|
|
986
|
+
/**
|
|
987
|
+
* Check the given password against the user's.
|
|
988
|
+
*
|
|
989
|
+
* @param password The password in question.
|
|
990
|
+
* @returns True if the passwords match, otherwise false.
|
|
991
|
+
*/
|
|
992
|
+
$checkPassword(password) {
|
|
993
|
+
const tilmeld = enforceTilmeld(this);
|
|
994
|
+
switch (tilmeld.config.pwMethod) {
|
|
995
|
+
case 'plain':
|
|
996
|
+
return this.$data.password === password;
|
|
997
|
+
case 'digest':
|
|
998
|
+
return (this.$data.password ===
|
|
999
|
+
crypto.createHash('sha256').update(password).digest('base64'));
|
|
1000
|
+
case 'salt':
|
|
1001
|
+
default:
|
|
1002
|
+
return (this.$data.password ===
|
|
1003
|
+
crypto
|
|
1004
|
+
.createHash('sha256')
|
|
1005
|
+
.update(password + this.$data.salt)
|
|
1006
|
+
.digest('base64'));
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Remove the user from a (secondary) group.
|
|
1011
|
+
*
|
|
1012
|
+
* @param group The group.
|
|
1013
|
+
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
1014
|
+
*/
|
|
1015
|
+
async $delGroup(group) {
|
|
896
1016
|
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1017
|
+
let groups = this.$data.groups || [];
|
|
1018
|
+
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
1019
|
+
const newGroups = [];
|
|
1020
|
+
for (let curGroup of groups) {
|
|
1021
|
+
if (!group.$is(curGroup)) {
|
|
1022
|
+
newGroups.push(curGroup);
|
|
1023
|
+
}
|
|
900
1024
|
}
|
|
1025
|
+
this.$data.groups = newGroups;
|
|
1026
|
+
return this.$data.groups;
|
|
901
1027
|
}
|
|
902
|
-
|
|
903
|
-
const group = this.$data.group;
|
|
904
|
-
if (group != null && group.cdate != null && group.enabled) {
|
|
905
|
-
abilities = abilities.concat(group.abilities ?? []);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
return Object.fromEntries(abilities.map((ability) => [ability, true]));
|
|
909
|
-
}
|
|
910
|
-
$clearCache() {
|
|
911
|
-
const ret = super.$clearCache();
|
|
912
|
-
this.$descendantGroups = undefined;
|
|
913
|
-
this.$gatekeeperCache = undefined;
|
|
914
|
-
return ret;
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* Send the user email verification/change/cancellation links.
|
|
918
|
-
*
|
|
919
|
-
* @returns True on success, false on failure.
|
|
920
|
-
*/
|
|
921
|
-
async $sendEmailVerification() {
|
|
922
|
-
const tilmeld = enforceTilmeld(this);
|
|
923
|
-
if (this.guid == null) {
|
|
924
|
-
return false;
|
|
925
|
-
}
|
|
926
|
-
let success = true;
|
|
927
|
-
const verifyUrl = `${tilmeld.config.appUrl.replace(/\/$/, () => '')}${tilmeld.config.setupPath}/verify`;
|
|
928
|
-
if (this.$data.secret != null) {
|
|
929
|
-
const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.secret)}`;
|
|
930
|
-
success =
|
|
931
|
-
success &&
|
|
932
|
-
(await tilmeld.config.sendEmail(tilmeld, {
|
|
933
|
-
template: 'VerifyEmail',
|
|
934
|
-
message: {
|
|
935
|
-
to: {
|
|
936
|
-
name: this.$data.name ?? '',
|
|
937
|
-
address: this.$data.email ?? '',
|
|
938
|
-
},
|
|
939
|
-
},
|
|
940
|
-
locals: {
|
|
941
|
-
verifyLink: link,
|
|
942
|
-
},
|
|
943
|
-
}, this));
|
|
944
|
-
}
|
|
945
|
-
if (this.$data.newEmailSecret != null) {
|
|
946
|
-
const link = `${verifyUrl}?action=verifychange&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.newEmailSecret)}`;
|
|
947
|
-
success =
|
|
948
|
-
success &&
|
|
949
|
-
(await tilmeld.config.sendEmail(tilmeld, {
|
|
950
|
-
template: 'VerifyEmailChange',
|
|
951
|
-
message: {
|
|
952
|
-
to: {
|
|
953
|
-
name: this.$data.name ?? '',
|
|
954
|
-
address: this.$data.newEmailAddress ?? '',
|
|
955
|
-
},
|
|
956
|
-
},
|
|
957
|
-
locals: {
|
|
958
|
-
verifyLink: link,
|
|
959
|
-
oldEmail: this.$data.cancelEmailAddress,
|
|
960
|
-
newEmail: this.$data.newEmailAddress,
|
|
961
|
-
},
|
|
962
|
-
}, this));
|
|
963
|
-
}
|
|
964
|
-
if (this.$data.cancelEmailSecret != null) {
|
|
965
|
-
const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
|
|
966
|
-
success =
|
|
967
|
-
success &&
|
|
968
|
-
(await tilmeld.config.sendEmail(tilmeld, {
|
|
969
|
-
template: 'CancelEmailChange',
|
|
970
|
-
message: {
|
|
971
|
-
to: {
|
|
972
|
-
name: this.$data.name ?? '',
|
|
973
|
-
address: this.$data.cancelEmailAddress ?? '',
|
|
974
|
-
},
|
|
975
|
-
},
|
|
976
|
-
locals: {
|
|
977
|
-
cancelLink: link,
|
|
978
|
-
oldEmail: this.$data.cancelEmailAddress,
|
|
979
|
-
newEmail: this.$data.newEmailAddress ?? this.$data.email,
|
|
980
|
-
},
|
|
981
|
-
}, this));
|
|
982
|
-
}
|
|
983
|
-
return success;
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Add the user to a (secondary) group.
|
|
987
|
-
*
|
|
988
|
-
* @param group The group.
|
|
989
|
-
* @returns True if the user is already in the group. The resulting array of groups if the user was not.
|
|
990
|
-
*/
|
|
991
|
-
async $addGroup(group) {
|
|
992
|
-
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
993
|
-
let groups = this.$data.groups;
|
|
994
|
-
if (groups == null) {
|
|
995
|
-
groups = [];
|
|
996
|
-
}
|
|
997
|
-
if (!group.$inArray(groups)) {
|
|
998
|
-
groups.push(group);
|
|
999
|
-
this.$data.groups = groups;
|
|
1000
|
-
return groups;
|
|
1001
|
-
}
|
|
1002
|
-
return true;
|
|
1003
|
-
}
|
|
1004
|
-
/**
|
|
1005
|
-
* Check the given password against the user's.
|
|
1006
|
-
*
|
|
1007
|
-
* @param password The password in question.
|
|
1008
|
-
* @returns True if the passwords match, otherwise false.
|
|
1009
|
-
*/
|
|
1010
|
-
$checkPassword(password) {
|
|
1011
|
-
const tilmeld = enforceTilmeld(this);
|
|
1012
|
-
switch (tilmeld.config.pwMethod) {
|
|
1013
|
-
case 'plain':
|
|
1014
|
-
return this.$data.password === password;
|
|
1015
|
-
case 'digest':
|
|
1016
|
-
return (this.$data.password ===
|
|
1017
|
-
crypto.createHash('sha256').update(password).digest('base64'));
|
|
1018
|
-
case 'salt':
|
|
1019
|
-
default:
|
|
1020
|
-
return (this.$data.password ===
|
|
1021
|
-
crypto
|
|
1022
|
-
.createHash('sha256')
|
|
1023
|
-
.update(password + this.$data.salt)
|
|
1024
|
-
.digest('base64'));
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
/**
|
|
1028
|
-
* Remove the user from a (secondary) group.
|
|
1029
|
-
*
|
|
1030
|
-
* @param group The group.
|
|
1031
|
-
* @returns True if the user wasn't in the group. The resulting array of groups if the user was.
|
|
1032
|
-
*/
|
|
1033
|
-
async $delGroup(group) {
|
|
1034
|
-
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1035
|
-
let groups = this.$data.groups || [];
|
|
1036
|
-
if (this.$data.groups != null && group.$inArray(groups)) {
|
|
1037
|
-
const newGroups = [];
|
|
1038
|
-
for (let curGroup of groups) {
|
|
1039
|
-
if (!group.$is(curGroup)) {
|
|
1040
|
-
newGroups.push(curGroup);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
this.$data.groups = newGroups;
|
|
1044
|
-
return this.$data.groups;
|
|
1028
|
+
return true;
|
|
1045
1029
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1030
|
+
/**
|
|
1031
|
+
* Check whether the user is in a (primary or secondary) group.
|
|
1032
|
+
*
|
|
1033
|
+
* @param group The group, or the group's GUID.
|
|
1034
|
+
* @returns True or false.
|
|
1035
|
+
*/
|
|
1036
|
+
async $inGroup(group) {
|
|
1037
|
+
if (typeof group === 'string') {
|
|
1038
|
+
if (this.$data.group?.guid === group) {
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
for (let curGroup of this.$data.groups ?? []) {
|
|
1042
|
+
if (curGroup.guid === group) {
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
return (group.$is(this.$data.group) || group.$inArray(this.$data.groups ?? []));
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Check whether the user is a descendant of a group.
|
|
1052
|
+
*
|
|
1053
|
+
* @param group The group, or the group's GUID.
|
|
1054
|
+
* @returns True or false.
|
|
1055
|
+
*/
|
|
1056
|
+
async $isDescendant(group) {
|
|
1057
|
+
// Check to see if the user is in a descendant group of the given group.
|
|
1058
|
+
await this.$data.group?.$wake();
|
|
1059
|
+
if (this.$data.group != null &&
|
|
1060
|
+
this.$data.group?.cdate != null &&
|
|
1061
|
+
(await this.$data.group?.$isDescendant(group))) {
|
|
1057
1062
|
return true;
|
|
1058
1063
|
}
|
|
1064
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
1059
1065
|
for (let curGroup of this.$data.groups ?? []) {
|
|
1060
|
-
if (curGroup
|
|
1066
|
+
if (await curGroup.$isDescendant(group)) {
|
|
1061
1067
|
return true;
|
|
1062
1068
|
}
|
|
1063
1069
|
}
|
|
1064
1070
|
return false;
|
|
1065
1071
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1072
|
+
/**
|
|
1073
|
+
* A frontend accessible method to change the user's password.
|
|
1074
|
+
*
|
|
1075
|
+
* @param data The input data from the client.
|
|
1076
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1077
|
+
*/
|
|
1078
|
+
async $changePassword(data) {
|
|
1079
|
+
this.$nymph.config.debugLog('tilmeld', `Password change request for "${this.$data.username}".`);
|
|
1080
|
+
if (!('newPassword' in data) || !data.newPassword.length) {
|
|
1081
|
+
return { result: false, message: 'Please specify a password.' };
|
|
1082
|
+
}
|
|
1083
|
+
if (!this.$checkPassword(data.currentPassword ?? '')) {
|
|
1084
|
+
this.$nymph.config.debugLog('tilmeld', `Incorrect password for change request for "${this.$data.username}".`);
|
|
1085
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1086
|
+
}
|
|
1087
|
+
this.$data.passwordTemp = data.newPassword;
|
|
1088
|
+
if (data.revokeCurrentTokens) {
|
|
1089
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
await this.$save();
|
|
1093
|
+
if (data.revokeCurrentTokens) {
|
|
1094
|
+
const tilmeld = enforceTilmeld(this);
|
|
1095
|
+
await tilmeld.login(this, true);
|
|
1096
|
+
}
|
|
1097
|
+
return { result: true, message: 'Your password has been changed.' };
|
|
1098
|
+
}
|
|
1099
|
+
catch (e) {
|
|
1100
|
+
return { result: false, message: "Couldn't save new password." };
|
|
1086
1101
|
}
|
|
1087
1102
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1103
|
+
/**
|
|
1104
|
+
* Change the user's password.
|
|
1105
|
+
*
|
|
1106
|
+
* @param password The new password.
|
|
1107
|
+
* @returns The resulting password or hash which is stored in the entity.
|
|
1108
|
+
*/
|
|
1109
|
+
$password(password) {
|
|
1110
|
+
const tilmeld = enforceTilmeld(this);
|
|
1111
|
+
switch (tilmeld.config.pwMethod) {
|
|
1112
|
+
case 'plain':
|
|
1113
|
+
delete this.$data.salt;
|
|
1114
|
+
this.$data.password = password;
|
|
1115
|
+
break;
|
|
1116
|
+
case 'digest':
|
|
1117
|
+
delete this.$data.salt;
|
|
1118
|
+
this.$data.password = crypto
|
|
1119
|
+
.createHash('sha256')
|
|
1120
|
+
.update(password)
|
|
1121
|
+
.digest('base64');
|
|
1122
|
+
break;
|
|
1123
|
+
case 'salt':
|
|
1124
|
+
default:
|
|
1125
|
+
this.$data.salt = nanoid();
|
|
1126
|
+
this.$data.password = crypto
|
|
1127
|
+
.createHash('sha256')
|
|
1128
|
+
.update(password + this.$data.salt)
|
|
1129
|
+
.digest('base64');
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
return this.$data.password;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* A frontend accessible method to revoke all currently issued tokens.
|
|
1136
|
+
*
|
|
1137
|
+
* @param data The input data from the client.
|
|
1138
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1139
|
+
*/
|
|
1140
|
+
async $revokeCurrentTokens(data) {
|
|
1141
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1142
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1143
|
+
}
|
|
1107
1144
|
this.$data.revokeTokenDate = Date.now();
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
if (data.revokeCurrentTokens) {
|
|
1145
|
+
try {
|
|
1146
|
+
await this.$save();
|
|
1111
1147
|
const tilmeld = enforceTilmeld(this);
|
|
1112
1148
|
await tilmeld.login(this, true);
|
|
1149
|
+
return {
|
|
1150
|
+
result: true,
|
|
1151
|
+
message: 'You have logged out of all other sessions.',
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
catch (e) {
|
|
1155
|
+
return { result: false, message: "Couldn't save revocation date." };
|
|
1113
1156
|
}
|
|
1114
|
-
return { result: true, message: 'Your password has been changed.' };
|
|
1115
|
-
}
|
|
1116
|
-
else {
|
|
1117
|
-
return { result: false, message: "Couldn't save new password." };
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
/**
|
|
1121
|
-
* Change the user's password.
|
|
1122
|
-
*
|
|
1123
|
-
* @param password The new password.
|
|
1124
|
-
* @returns The resulting password or hash which is stored in the entity.
|
|
1125
|
-
*/
|
|
1126
|
-
$password(password) {
|
|
1127
|
-
const tilmeld = enforceTilmeld(this);
|
|
1128
|
-
switch (tilmeld.config.pwMethod) {
|
|
1129
|
-
case 'plain':
|
|
1130
|
-
delete this.$data.salt;
|
|
1131
|
-
this.$data.password = password;
|
|
1132
|
-
break;
|
|
1133
|
-
case 'digest':
|
|
1134
|
-
delete this.$data.salt;
|
|
1135
|
-
this.$data.password = crypto
|
|
1136
|
-
.createHash('sha256')
|
|
1137
|
-
.update(password)
|
|
1138
|
-
.digest('base64');
|
|
1139
|
-
break;
|
|
1140
|
-
case 'salt':
|
|
1141
|
-
default:
|
|
1142
|
-
this.$data.salt = nanoid();
|
|
1143
|
-
this.$data.password = crypto
|
|
1144
|
-
.createHash('sha256')
|
|
1145
|
-
.update(password + this.$data.salt)
|
|
1146
|
-
.digest('base64');
|
|
1147
|
-
break;
|
|
1148
1157
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
/**
|
|
1152
|
-
* A frontend accessible method to revoke all currently issued tokens.
|
|
1153
|
-
*
|
|
1154
|
-
* @param data The input data from the client.
|
|
1155
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1156
|
-
*/
|
|
1157
|
-
async $revokeCurrentTokens(data) {
|
|
1158
|
-
if (!this.$checkPassword(data.password ?? '')) {
|
|
1159
|
-
return { result: false, message: 'Incorrect password.' };
|
|
1158
|
+
$hasTOTPSecret() {
|
|
1159
|
+
return this.$data.totpSecret != null;
|
|
1160
1160
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1161
|
+
/**
|
|
1162
|
+
* Check the given code against the user's TOTP secret.
|
|
1163
|
+
*
|
|
1164
|
+
* @param code The code in question.
|
|
1165
|
+
* @returns True if the code is valid, otherwise false.
|
|
1166
|
+
*/
|
|
1167
|
+
$checkTOTPCode(code) {
|
|
1168
|
+
if (this.$data.totpSecret == null) {
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1163
1171
|
const tilmeld = enforceTilmeld(this);
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const tilmeld = enforceTilmeld(this);
|
|
1205
|
-
if (this.$data.totpSecret != null) {
|
|
1206
|
-
throw new BadDataError('You already have a 2FA secret.');
|
|
1207
|
-
}
|
|
1208
|
-
const secret = new Secret();
|
|
1209
|
-
const totp = new TOTP({
|
|
1210
|
-
issuer: tilmeld.config.appName,
|
|
1211
|
-
label: this.$data.username,
|
|
1212
|
-
algorithm: 'SHA1',
|
|
1213
|
-
digits: 6,
|
|
1214
|
-
period: 30,
|
|
1215
|
-
secret,
|
|
1216
|
-
});
|
|
1217
|
-
const uri = totp.toString();
|
|
1218
|
-
const qrcode = await toDataURL(uri);
|
|
1219
|
-
return {
|
|
1220
|
-
uri,
|
|
1221
|
-
qrcode,
|
|
1222
|
-
secret: secret.base32,
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
/**
|
|
1226
|
-
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1227
|
-
*
|
|
1228
|
-
* @param data The input data from the client.
|
|
1229
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1230
|
-
*/
|
|
1231
|
-
async $saveTOTPSecret(data) {
|
|
1232
|
-
if (!this.$checkPassword(data.password ?? '')) {
|
|
1233
|
-
return { result: false, message: 'Incorrect password.' };
|
|
1234
|
-
}
|
|
1235
|
-
if (this.$data.totpSecret != null) {
|
|
1236
|
-
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1237
|
-
}
|
|
1238
|
-
if (data.secret == null ||
|
|
1239
|
-
typeof data.secret !== 'string' ||
|
|
1240
|
-
data.secret === '') {
|
|
1241
|
-
return { result: false, message: '2FA secret is invalid.' };
|
|
1242
|
-
}
|
|
1243
|
-
if (data.code == null ||
|
|
1244
|
-
typeof data.code !== 'string' ||
|
|
1245
|
-
data.code.length !== 6) {
|
|
1246
|
-
return { result: false, message: '2FA code is invalid.' };
|
|
1247
|
-
}
|
|
1248
|
-
const tilmeld = enforceTilmeld(this);
|
|
1249
|
-
const totp = new TOTP({
|
|
1250
|
-
issuer: tilmeld.config.appName,
|
|
1251
|
-
label: this.$data.username,
|
|
1252
|
-
algorithm: 'SHA1',
|
|
1253
|
-
digits: 6,
|
|
1254
|
-
period: 30,
|
|
1255
|
-
secret: data.secret,
|
|
1256
|
-
});
|
|
1257
|
-
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1258
|
-
return { result: false, message: '2FA code is incorrect.' };
|
|
1259
|
-
}
|
|
1260
|
-
this.$data.totpSecret = data.secret;
|
|
1261
|
-
if (await this.$save()) {
|
|
1262
|
-
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1263
|
-
}
|
|
1264
|
-
else {
|
|
1265
|
-
return { result: false, message: "Couldn't save two factor secret." };
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
/**
|
|
1269
|
-
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1270
|
-
* account.
|
|
1271
|
-
*
|
|
1272
|
-
* @param data The input data from the client.
|
|
1273
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1274
|
-
*/
|
|
1275
|
-
async $removeTOTPSecret(data) {
|
|
1276
|
-
const tilmeld = enforceTilmeld(this);
|
|
1277
|
-
if (data) {
|
|
1172
|
+
const totp = new TOTP({
|
|
1173
|
+
issuer: tilmeld.config.appName,
|
|
1174
|
+
label: this.$data.username,
|
|
1175
|
+
algorithm: 'SHA1',
|
|
1176
|
+
digits: 6,
|
|
1177
|
+
period: 30,
|
|
1178
|
+
secret: this.$data.totpSecret,
|
|
1179
|
+
});
|
|
1180
|
+
return totp.validate({ token: code, window: 2 }) != null;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* A frontend accessible method to generate a new TOTP secret.
|
|
1184
|
+
*
|
|
1185
|
+
* @returns An object with 'uri', 'qrcode', and 'secret'.
|
|
1186
|
+
*/
|
|
1187
|
+
async $getNewTOTPSecret() {
|
|
1188
|
+
const tilmeld = enforceTilmeld(this);
|
|
1189
|
+
if (this.$data.totpSecret != null) {
|
|
1190
|
+
throw new BadDataError('You already have a 2FA secret.');
|
|
1191
|
+
}
|
|
1192
|
+
const secret = new Secret();
|
|
1193
|
+
const totp = new TOTP({
|
|
1194
|
+
issuer: tilmeld.config.appName,
|
|
1195
|
+
label: this.$data.username,
|
|
1196
|
+
algorithm: 'SHA1',
|
|
1197
|
+
digits: 6,
|
|
1198
|
+
period: 30,
|
|
1199
|
+
secret,
|
|
1200
|
+
});
|
|
1201
|
+
const uri = totp.toString();
|
|
1202
|
+
const qrcode = await toDataURL(uri);
|
|
1203
|
+
return { uri, qrcode, secret: secret.base32 };
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* A frontend accessible method to save a TOTP secret into the user's account.
|
|
1207
|
+
*
|
|
1208
|
+
* @param data The input data from the client.
|
|
1209
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1210
|
+
*/
|
|
1211
|
+
async $saveTOTPSecret(data) {
|
|
1278
1212
|
if (!this.$checkPassword(data.password ?? '')) {
|
|
1279
1213
|
return { result: false, message: 'Incorrect password.' };
|
|
1280
1214
|
}
|
|
1215
|
+
if (this.$data.totpSecret != null) {
|
|
1216
|
+
return { result: false, message: 'You already have a 2FA secret.' };
|
|
1217
|
+
}
|
|
1218
|
+
if (data.secret == null ||
|
|
1219
|
+
typeof data.secret !== 'string' ||
|
|
1220
|
+
data.secret === '') {
|
|
1221
|
+
return { result: false, message: '2FA secret is invalid.' };
|
|
1222
|
+
}
|
|
1281
1223
|
if (data.code == null ||
|
|
1282
1224
|
typeof data.code !== 'string' ||
|
|
1283
1225
|
data.code.length !== 6) {
|
|
1284
1226
|
return { result: false, message: '2FA code is invalid.' };
|
|
1285
1227
|
}
|
|
1286
|
-
|
|
1287
|
-
return { result: false, message: "You don't have a 2FA secret." };
|
|
1288
|
-
}
|
|
1228
|
+
const tilmeld = enforceTilmeld(this);
|
|
1289
1229
|
const totp = new TOTP({
|
|
1290
1230
|
issuer: tilmeld.config.appName,
|
|
1291
1231
|
label: this.$data.username,
|
|
1292
1232
|
algorithm: 'SHA1',
|
|
1293
1233
|
digits: 6,
|
|
1294
1234
|
period: 30,
|
|
1295
|
-
secret:
|
|
1235
|
+
secret: data.secret,
|
|
1296
1236
|
});
|
|
1297
1237
|
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1298
1238
|
return { result: false, message: '2FA code is incorrect.' };
|
|
1299
1239
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
result:
|
|
1304
|
-
message: 'You must provide your password and 2FA code.',
|
|
1305
|
-
};
|
|
1306
|
-
}
|
|
1307
|
-
delete this.$data.totpSecret;
|
|
1308
|
-
if (await this.$save()) {
|
|
1309
|
-
return {
|
|
1310
|
-
result: true,
|
|
1311
|
-
message: 'Two factor secret has been removed.',
|
|
1312
|
-
};
|
|
1313
|
-
}
|
|
1314
|
-
else {
|
|
1315
|
-
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
/**
|
|
1319
|
-
* Check that a username is valid.
|
|
1320
|
-
*
|
|
1321
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1322
|
-
*/
|
|
1323
|
-
async $checkUsername() {
|
|
1324
|
-
const tilmeld = enforceTilmeld(this);
|
|
1325
|
-
this.$nymph.config.debugInfo('tilmeld', `Username check "${this.$data.username}".`);
|
|
1326
|
-
if (!tilmeld.config.emailUsernames) {
|
|
1327
|
-
let [username, domain] = tilmeld.config.domainSupport
|
|
1328
|
-
? splitn(this.$data.username ?? '', '@', 2)
|
|
1329
|
-
: [this.$data.username, null];
|
|
1330
|
-
// Lowercase the domain part.
|
|
1331
|
-
if (domain) {
|
|
1332
|
-
domain = domain.toLowerCase();
|
|
1333
|
-
this.$data.username = `${username}@${domain}`;
|
|
1240
|
+
this.$data.totpSecret = data.secret;
|
|
1241
|
+
try {
|
|
1242
|
+
await this.$save();
|
|
1243
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1334
1244
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
username == null ||
|
|
1338
|
-
!username.length) {
|
|
1339
|
-
return { result: false, message: 'Please specify a username.' };
|
|
1245
|
+
catch (e) {
|
|
1246
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1340
1247
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* A frontend accessible method to remove the TOTP secret from the user's
|
|
1251
|
+
* account.
|
|
1252
|
+
*
|
|
1253
|
+
* @param data The input data from the client.
|
|
1254
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1255
|
+
*/
|
|
1256
|
+
async $removeTOTPSecret(data) {
|
|
1257
|
+
const tilmeld = enforceTilmeld(this);
|
|
1258
|
+
if (data) {
|
|
1259
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1260
|
+
return { result: false, message: 'Incorrect password.' };
|
|
1261
|
+
}
|
|
1262
|
+
if (data.code == null ||
|
|
1263
|
+
typeof data.code !== 'string' ||
|
|
1264
|
+
data.code.length !== 6) {
|
|
1265
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1266
|
+
}
|
|
1267
|
+
if (this.$data.totpSecret == null) {
|
|
1268
|
+
return { result: false, message: "You don't have a 2FA secret." };
|
|
1269
|
+
}
|
|
1270
|
+
const totp = new TOTP({
|
|
1271
|
+
issuer: tilmeld.config.appName,
|
|
1272
|
+
label: this.$data.username,
|
|
1273
|
+
algorithm: 'SHA1',
|
|
1274
|
+
digits: 6,
|
|
1275
|
+
period: 30,
|
|
1276
|
+
secret: this.$data.totpSecret,
|
|
1277
|
+
});
|
|
1278
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1279
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1280
|
+
}
|
|
1348
1281
|
}
|
|
1349
|
-
if (
|
|
1282
|
+
else if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1350
1283
|
return {
|
|
1351
1284
|
result: false,
|
|
1352
|
-
message: '
|
|
1353
|
-
tilmeld.config.maxUsernameLength +
|
|
1354
|
-
' characters.',
|
|
1285
|
+
message: 'You must provide your password and 2FA code.',
|
|
1355
1286
|
};
|
|
1356
1287
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1288
|
+
delete this.$data.totpSecret;
|
|
1289
|
+
try {
|
|
1290
|
+
await this.$save();
|
|
1291
|
+
return { result: true, message: 'Two factor secret has been removed.' };
|
|
1292
|
+
}
|
|
1293
|
+
catch (e) {
|
|
1294
|
+
return { result: false, message: "Couldn't remove two factor secret." };
|
|
1363
1295
|
}
|
|
1364
|
-
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Check that a username is valid.
|
|
1299
|
+
*
|
|
1300
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1301
|
+
*/
|
|
1302
|
+
async $checkUsername() {
|
|
1303
|
+
const tilmeld = enforceTilmeld(this);
|
|
1304
|
+
this.$nymph.config.debugInfo('tilmeld', `Username check "${this.$data.username}".`);
|
|
1305
|
+
if (!tilmeld.config.emailUsernames) {
|
|
1306
|
+
let [username, domain] = tilmeld.config.domainSupport
|
|
1307
|
+
? splitn(this.$data.username ?? '', '@', 2)
|
|
1308
|
+
: [this.$data.username, null];
|
|
1309
|
+
// Lowercase the domain part.
|
|
1310
|
+
if (domain) {
|
|
1311
|
+
domain = domain.toLowerCase();
|
|
1312
|
+
this.$data.username = `${username}@${domain}`;
|
|
1313
|
+
}
|
|
1314
|
+
if (this.$data.username == null ||
|
|
1315
|
+
!this.$data.username.length ||
|
|
1316
|
+
username == null ||
|
|
1317
|
+
!username.length) {
|
|
1318
|
+
return { result: false, message: 'Please specify a username.' };
|
|
1319
|
+
}
|
|
1320
|
+
if (username.length < tilmeld.config.minUsernameLength) {
|
|
1321
|
+
return {
|
|
1322
|
+
result: false,
|
|
1323
|
+
message: 'Usernames must be at least ' +
|
|
1324
|
+
tilmeld.config.minUsernameLength +
|
|
1325
|
+
' characters.',
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (username.length > tilmeld.config.maxUsernameLength) {
|
|
1329
|
+
return {
|
|
1330
|
+
result: false,
|
|
1331
|
+
message: 'Usernames must not exceed ' +
|
|
1332
|
+
tilmeld.config.maxUsernameLength +
|
|
1333
|
+
' characters.',
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
if (difference(username.split(''), tilmeld.config.validChars.split(''))
|
|
1337
|
+
.length) {
|
|
1338
|
+
return { result: false, message: tilmeld.config.validCharsNotice };
|
|
1339
|
+
}
|
|
1340
|
+
if (!tilmeld.config.validRegex.test(username)) {
|
|
1341
|
+
return { result: false, message: tilmeld.config.validRegexNotice };
|
|
1342
|
+
}
|
|
1343
|
+
if (domain != null) {
|
|
1344
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
1345
|
+
return {
|
|
1346
|
+
result: false,
|
|
1347
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
if (!this.$skipAcWhenSaving &&
|
|
1351
|
+
!this.$is(tilmeld.currentUser) &&
|
|
1352
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
1353
|
+
return {
|
|
1354
|
+
result: false,
|
|
1355
|
+
message: "You don't have the authority to manage users on this domain.",
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const selector = {
|
|
1360
|
+
type: '&',
|
|
1361
|
+
ilike: [
|
|
1362
|
+
'username',
|
|
1363
|
+
this.$data.username.replace(/([\\%_])/g, (s) => `\\${s}`),
|
|
1364
|
+
],
|
|
1365
|
+
};
|
|
1366
|
+
if (this.guid != null) {
|
|
1367
|
+
selector['!guid'] = this.guid;
|
|
1368
|
+
}
|
|
1369
|
+
const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
|
|
1370
|
+
if (test != null) {
|
|
1371
|
+
return { result: false, message: 'That username is taken.' };
|
|
1372
|
+
}
|
|
1373
|
+
for (let callback of this.constructor
|
|
1374
|
+
.checkUsernameCallbacks) {
|
|
1375
|
+
if (callback) {
|
|
1376
|
+
const result = await callback(this, {
|
|
1377
|
+
username: this.$data.username,
|
|
1378
|
+
});
|
|
1379
|
+
if (!result.result) {
|
|
1380
|
+
return { message: 'Username is not available.', ...result };
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1365
1384
|
return {
|
|
1366
|
-
result:
|
|
1367
|
-
message:
|
|
1385
|
+
result: true,
|
|
1386
|
+
message: this.guid != null ? 'Username is valid.' : 'Username is available!',
|
|
1368
1387
|
};
|
|
1369
1388
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
1389
|
+
else {
|
|
1390
|
+
this.$data.email = this.$data.username;
|
|
1391
|
+
if (this.$data.username == null || !this.$data.username.length) {
|
|
1392
|
+
return { result: false, message: 'Please specify an email.' };
|
|
1393
|
+
}
|
|
1394
|
+
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1372
1395
|
return {
|
|
1373
1396
|
result: false,
|
|
1374
|
-
message:
|
|
1397
|
+
message: 'Emails must be at least ' +
|
|
1398
|
+
tilmeld.config.minUsernameLength +
|
|
1399
|
+
' characters.',
|
|
1375
1400
|
};
|
|
1376
1401
|
}
|
|
1377
|
-
if (
|
|
1378
|
-
!this.$is(tilmeld.currentUser) &&
|
|
1379
|
-
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
1402
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1380
1403
|
return {
|
|
1381
1404
|
result: false,
|
|
1382
|
-
message:
|
|
1405
|
+
message: 'Emails must not exceed ' +
|
|
1406
|
+
tilmeld.config.maxUsernameLength +
|
|
1407
|
+
' characters.',
|
|
1383
1408
|
};
|
|
1384
1409
|
}
|
|
1410
|
+
return await this.$checkEmail();
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Check that an email is unique.
|
|
1415
|
+
*
|
|
1416
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1417
|
+
*/
|
|
1418
|
+
async $checkEmail() {
|
|
1419
|
+
const tilmeld = enforceTilmeld(this);
|
|
1420
|
+
this.$nymph.config.debugInfo('tilmeld', `Email check "${this.$data.email}".`);
|
|
1421
|
+
if (this.$data.email == null || !this.$data.email.length) {
|
|
1422
|
+
if (tilmeld.config.verifyEmail) {
|
|
1423
|
+
return { result: false, message: 'Please specify an email.' };
|
|
1424
|
+
}
|
|
1425
|
+
else {
|
|
1426
|
+
return { result: true, message: '' };
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
if (!tilmeld.config.validEmailRegex.test(this.$data.email)) {
|
|
1430
|
+
return { result: false, message: tilmeld.config.validEmailRegexNotice };
|
|
1385
1431
|
}
|
|
1386
1432
|
const selector = {
|
|
1387
1433
|
type: '&',
|
|
1388
|
-
ilike: [
|
|
1389
|
-
'username',
|
|
1390
|
-
this.$data.username.replace(/([\\%_])/g, (s) => `\\${s}`),
|
|
1391
|
-
],
|
|
1434
|
+
ilike: ['email', this.$data.email.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
1392
1435
|
};
|
|
1393
1436
|
if (this.guid != null) {
|
|
1394
1437
|
selector['!guid'] = this.guid;
|
|
1395
1438
|
}
|
|
1396
1439
|
const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
|
|
1397
1440
|
if (test != null) {
|
|
1398
|
-
return { result: false, message: 'That username is taken.' };
|
|
1399
|
-
}
|
|
1400
|
-
for (let callback of this.constructor
|
|
1401
|
-
.checkUsernameCallbacks) {
|
|
1402
|
-
if (callback) {
|
|
1403
|
-
const result = await callback(this, {
|
|
1404
|
-
username: this.$data.username,
|
|
1405
|
-
});
|
|
1406
|
-
if (!result.result) {
|
|
1407
|
-
return { message: 'Username is not available.', ...result };
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
return {
|
|
1412
|
-
result: true,
|
|
1413
|
-
message: this.guid != null ? 'Username is valid.' : 'Username is available!',
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
else {
|
|
1417
|
-
this.$data.email = this.$data.username;
|
|
1418
|
-
if (this.$data.username == null || !this.$data.username.length) {
|
|
1419
|
-
return { result: false, message: 'Please specify an email.' };
|
|
1420
|
-
}
|
|
1421
|
-
if (this.$data.username.length < tilmeld.config.minUsernameLength) {
|
|
1422
|
-
return {
|
|
1423
|
-
result: false,
|
|
1424
|
-
message: 'Emails must be at least ' +
|
|
1425
|
-
tilmeld.config.minUsernameLength +
|
|
1426
|
-
' characters.',
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1430
1441
|
return {
|
|
1431
1442
|
result: false,
|
|
1432
|
-
message: '
|
|
1433
|
-
tilmeld.config.maxUsernameLength +
|
|
1434
|
-
' characters.',
|
|
1443
|
+
message: 'That email address is already registered.',
|
|
1435
1444
|
};
|
|
1436
1445
|
}
|
|
1437
|
-
return await this.$checkEmail();
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
/**
|
|
1441
|
-
* Check that an email is unique.
|
|
1442
|
-
*
|
|
1443
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1444
|
-
*/
|
|
1445
|
-
async $checkEmail() {
|
|
1446
|
-
const tilmeld = enforceTilmeld(this);
|
|
1447
|
-
this.$nymph.config.debugInfo('tilmeld', `Email check "${this.$data.email}".`);
|
|
1448
|
-
if (this.$data.email == null || !this.$data.email.length) {
|
|
1449
|
-
if (tilmeld.config.verifyEmail) {
|
|
1450
|
-
return { result: false, message: 'Please specify an email.' };
|
|
1451
|
-
}
|
|
1452
|
-
else {
|
|
1453
|
-
return { result: true, message: '' };
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
if (!tilmeld.config.validEmailRegex.test(this.$data.email)) {
|
|
1457
|
-
return {
|
|
1458
|
-
result: false,
|
|
1459
|
-
message: tilmeld.config.validEmailRegexNotice,
|
|
1460
|
-
};
|
|
1461
|
-
}
|
|
1462
|
-
const selector = {
|
|
1463
|
-
type: '&',
|
|
1464
|
-
ilike: ['email', this.$data.email.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
1465
|
-
};
|
|
1466
|
-
if (this.guid != null) {
|
|
1467
|
-
selector['!guid'] = this.guid;
|
|
1468
|
-
}
|
|
1469
|
-
const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
|
|
1470
|
-
if (test != null) {
|
|
1471
|
-
return {
|
|
1472
|
-
result: false,
|
|
1473
|
-
message: 'That email address is already registered.',
|
|
1474
|
-
};
|
|
1475
|
-
}
|
|
1476
|
-
return {
|
|
1477
|
-
result: true,
|
|
1478
|
-
message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
|
|
1479
|
-
};
|
|
1480
|
-
}
|
|
1481
|
-
/**
|
|
1482
|
-
* Check that a phone number is unique.
|
|
1483
|
-
*
|
|
1484
|
-
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1485
|
-
*/
|
|
1486
|
-
async $checkPhone() {
|
|
1487
|
-
const tilmeld = enforceTilmeld(this);
|
|
1488
|
-
this.$nymph.config.debugInfo('tilmeld', `Phone check "${this.$data.phone}".`);
|
|
1489
|
-
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
1490
|
-
return { result: false, message: 'Please specify a phone number.' };
|
|
1491
|
-
}
|
|
1492
|
-
const stripToDigits = this.$data.phone.replace(/\D/g, () => '');
|
|
1493
|
-
if (!/\d{10}/.test(stripToDigits)) {
|
|
1494
|
-
return {
|
|
1495
|
-
result: false,
|
|
1496
|
-
message: 'Phone must contain at least 10 digits, but formatting does not matter.',
|
|
1497
|
-
};
|
|
1498
|
-
}
|
|
1499
|
-
const selector = {
|
|
1500
|
-
type: '&',
|
|
1501
|
-
equal: ['phone', stripToDigits],
|
|
1502
|
-
};
|
|
1503
|
-
if (this.guid != null) {
|
|
1504
|
-
selector['!guid'] = this.guid;
|
|
1505
|
-
}
|
|
1506
|
-
const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
|
|
1507
|
-
if (test != null) {
|
|
1508
|
-
return { result: false, message: 'Phone number is in use.' };
|
|
1509
|
-
}
|
|
1510
|
-
return {
|
|
1511
|
-
result: true,
|
|
1512
|
-
message: this.guid != null ? 'Phone number is valid.' : 'Phone number is valid!',
|
|
1513
|
-
};
|
|
1514
|
-
}
|
|
1515
|
-
async $register(data) {
|
|
1516
|
-
let tilmeld = enforceTilmeld(this);
|
|
1517
|
-
if (!tilmeld.config.allowRegistration) {
|
|
1518
1446
|
return {
|
|
1519
|
-
result:
|
|
1520
|
-
|
|
1521
|
-
message: 'Registration is not allowed.',
|
|
1447
|
+
result: true,
|
|
1448
|
+
message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
|
|
1522
1449
|
};
|
|
1523
1450
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1451
|
+
/**
|
|
1452
|
+
* Check that a phone number is unique.
|
|
1453
|
+
*
|
|
1454
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
1455
|
+
*/
|
|
1456
|
+
async $checkPhone() {
|
|
1457
|
+
const tilmeld = enforceTilmeld(this);
|
|
1458
|
+
this.$nymph.config.debugInfo('tilmeld', `Phone check "${this.$data.phone}".`);
|
|
1459
|
+
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
1460
|
+
return { result: false, message: 'Please specify a phone number.' };
|
|
1461
|
+
}
|
|
1462
|
+
const stripToDigits = this.$data.phone.replace(/\D/g, () => '');
|
|
1463
|
+
if (!/\d{10}/.test(stripToDigits)) {
|
|
1464
|
+
return {
|
|
1465
|
+
result: false,
|
|
1466
|
+
message: 'Phone must contain at least 10 digits, but formatting does not matter.',
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
const selector = {
|
|
1470
|
+
type: '&',
|
|
1471
|
+
equal: ['phone', stripToDigits],
|
|
1529
1472
|
};
|
|
1530
|
-
|
|
1531
|
-
|
|
1473
|
+
if (this.guid != null) {
|
|
1474
|
+
selector['!guid'] = this.guid;
|
|
1475
|
+
}
|
|
1476
|
+
const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
|
|
1477
|
+
if (test != null) {
|
|
1478
|
+
return { result: false, message: 'Phone number is in use.' };
|
|
1479
|
+
}
|
|
1532
1480
|
return {
|
|
1533
|
-
result:
|
|
1534
|
-
|
|
1535
|
-
message: 'Password is a required field.',
|
|
1481
|
+
result: true,
|
|
1482
|
+
message: this.guid != null ? 'Phone number is valid.' : 'Phone number is valid!',
|
|
1536
1483
|
};
|
|
1537
1484
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
1547
|
-
(this.$data.nameLast == null ? '' : ' ' + this.$data.nameLast);
|
|
1548
|
-
if (this.$data.name === '') {
|
|
1549
|
-
this.$data.name = this.$data.username;
|
|
1485
|
+
async $register(data) {
|
|
1486
|
+
let tilmeld = enforceTilmeld(this);
|
|
1487
|
+
if (!tilmeld.config.allowRegistration) {
|
|
1488
|
+
return {
|
|
1489
|
+
result: false,
|
|
1490
|
+
loggedin: false,
|
|
1491
|
+
message: 'Registration is not allowed.',
|
|
1492
|
+
};
|
|
1550
1493
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
const nymph = this.$nymph;
|
|
1558
|
-
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1559
|
-
this.$setNymph(tnymph);
|
|
1560
|
-
tilmeld = enforceTilmeld(this);
|
|
1561
|
-
let message = '';
|
|
1562
|
-
let loggedin = false;
|
|
1563
|
-
try {
|
|
1564
|
-
try {
|
|
1565
|
-
for (let callback of this.constructor
|
|
1566
|
-
.beforeRegisterCallbacks) {
|
|
1567
|
-
if (callback) {
|
|
1568
|
-
await callback(this, data);
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1494
|
+
if (this.guid != null) {
|
|
1495
|
+
return {
|
|
1496
|
+
result: false,
|
|
1497
|
+
loggedin: false,
|
|
1498
|
+
message: 'This is already a registered user.',
|
|
1499
|
+
};
|
|
1571
1500
|
}
|
|
1572
|
-
|
|
1573
|
-
await tnymph.rollback(transaction);
|
|
1574
|
-
this.$setNymph(nymph);
|
|
1501
|
+
if (!('password' in data) || !data.password.length) {
|
|
1575
1502
|
return {
|
|
1576
1503
|
result: false,
|
|
1577
1504
|
loggedin: false,
|
|
1578
|
-
message:
|
|
1505
|
+
message: 'Password is a required field.',
|
|
1579
1506
|
};
|
|
1580
1507
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1508
|
+
const unCheck = await this.$checkUsername();
|
|
1509
|
+
if (!unCheck.result) {
|
|
1510
|
+
return { ...unCheck, loggedin: false };
|
|
1511
|
+
}
|
|
1512
|
+
this.$password(data.password);
|
|
1513
|
+
if (tilmeld.config.regFields.includes('name')) {
|
|
1514
|
+
this.$data.name =
|
|
1515
|
+
this.$data.nameFirst +
|
|
1516
|
+
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
1517
|
+
(this.$data.nameLast == null ? '' : ' ' + this.$data.nameLast);
|
|
1518
|
+
if (this.$data.name === '') {
|
|
1519
|
+
this.$data.name = this.$data.username;
|
|
1586
1520
|
}
|
|
1587
1521
|
}
|
|
1588
|
-
|
|
1589
|
-
this.$data.
|
|
1590
|
-
}
|
|
1591
|
-
// If createAdmin is true and there are no other users, grant
|
|
1592
|
-
// "system/admin".
|
|
1593
|
-
let madeAdmin = false;
|
|
1594
|
-
if (tilmeld.config.createAdmin) {
|
|
1595
|
-
const otherUsers = await tnymph.getEntities({
|
|
1596
|
-
class: tilmeld.User,
|
|
1597
|
-
skipAc: true,
|
|
1598
|
-
limit: 1,
|
|
1599
|
-
return: 'guid',
|
|
1600
|
-
});
|
|
1601
|
-
// Make sure it's not just null, cause that means an error.
|
|
1602
|
-
if (otherUsers == null) {
|
|
1603
|
-
await tnymph.rollback(transaction);
|
|
1604
|
-
this.$setNymph(nymph);
|
|
1605
|
-
return {
|
|
1606
|
-
result: false,
|
|
1607
|
-
loggedin: false,
|
|
1608
|
-
message: 'An error occurred.',
|
|
1609
|
-
};
|
|
1610
|
-
}
|
|
1611
|
-
if (!otherUsers.length) {
|
|
1612
|
-
this.$grant('system/admin');
|
|
1613
|
-
this.$data.enabled = true;
|
|
1614
|
-
madeAdmin = true;
|
|
1615
|
-
}
|
|
1522
|
+
if (tilmeld.config.emailUsernames) {
|
|
1523
|
+
this.$data.email = this.$data.username;
|
|
1616
1524
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1525
|
+
try {
|
|
1526
|
+
const result = await transaction(this.$nymph, 'tilmeld-register-' + nanoid(), async (nymph) => {
|
|
1527
|
+
this.$setNymph(nymph);
|
|
1528
|
+
const tilmeld = enforceTilmeld(this);
|
|
1529
|
+
let message = '';
|
|
1530
|
+
let loggedin = false;
|
|
1531
|
+
for (let callback of this.constructor
|
|
1532
|
+
.beforeRegisterCallbacks) {
|
|
1533
|
+
if (callback) {
|
|
1534
|
+
await callback(this, data);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1538
|
+
tilmeld.config.verifyEmail) {
|
|
1539
|
+
// The user will be enabled after verifying their e-mail address.
|
|
1540
|
+
if (!tilmeld.config.unverifiedAccess) {
|
|
1541
|
+
this.$data.enabled = false;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
this.$data.enabled = true;
|
|
1546
|
+
}
|
|
1547
|
+
// If createAdmin is true and there are no other users, grant
|
|
1548
|
+
// "system/admin".
|
|
1549
|
+
let madeAdmin = false;
|
|
1550
|
+
if (tilmeld.config.createAdmin) {
|
|
1551
|
+
const otherUsers = await this.$nymph.getEntities({
|
|
1552
|
+
class: tilmeld.User,
|
|
1553
|
+
skipAc: true,
|
|
1554
|
+
limit: 1,
|
|
1555
|
+
return: 'guid',
|
|
1556
|
+
});
|
|
1557
|
+
// Make sure it's not just null, cause that means an error.
|
|
1558
|
+
if (otherUsers == null) {
|
|
1559
|
+
throw new Error('An error occurred.');
|
|
1560
|
+
}
|
|
1561
|
+
if (!otherUsers.length) {
|
|
1562
|
+
this.$grant('system/admin');
|
|
1563
|
+
this.$data.enabled = true;
|
|
1564
|
+
madeAdmin = true;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
|
|
1568
|
+
await this.$saveSkipAC();
|
|
1569
|
+
// Finish up.
|
|
1570
|
+
if (tilmeld.config.verifyEmail &&
|
|
1571
|
+
!tilmeld.config.unverifiedAccess &&
|
|
1572
|
+
!madeAdmin &&
|
|
1573
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1574
|
+
message +=
|
|
1575
|
+
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
1576
|
+
'with a verification link for you to finish registration.';
|
|
1577
|
+
}
|
|
1578
|
+
else if (tilmeld.config.verifyEmail &&
|
|
1579
|
+
tilmeld.config.unverifiedAccess &&
|
|
1580
|
+
!madeAdmin &&
|
|
1581
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1582
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1583
|
+
throw new Error('An error occurred trying to log you in.');
|
|
1584
|
+
}
|
|
1585
|
+
message +=
|
|
1586
|
+
"You're now logged in! An email has been sent to " +
|
|
1587
|
+
`${this.$data.email} with a verification link for you to finish ` +
|
|
1588
|
+
'registration.';
|
|
1589
|
+
loggedin = true;
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1593
|
+
throw new Error('An error occurred trying to log you in.');
|
|
1594
|
+
}
|
|
1595
|
+
message += "You're now registered and logged in!";
|
|
1596
|
+
loggedin = true;
|
|
1597
|
+
}
|
|
1598
|
+
for (let callback of this.constructor
|
|
1599
|
+
.afterRegisterCallbacks) {
|
|
1600
|
+
if (callback) {
|
|
1601
|
+
await callback(this, { loggedin, message });
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return { result: true, loggedin, message };
|
|
1605
|
+
}, async (nymph) => {
|
|
1606
|
+
this.$setNymph(nymph);
|
|
1607
|
+
});
|
|
1620
1608
|
// Send the new user registered email.
|
|
1621
1609
|
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
to: tilmeld.config.userRegisteredRecipient,
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
},
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
// Finish up.
|
|
1638
|
-
if (tilmeld.config.verifyEmail &&
|
|
1639
|
-
!tilmeld.config.unverifiedAccess &&
|
|
1640
|
-
!madeAdmin &&
|
|
1641
|
-
tilmeld.config.userFields.includes('email')) {
|
|
1642
|
-
message +=
|
|
1643
|
-
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
1644
|
-
'with a verification link for you to finish registration.';
|
|
1645
|
-
}
|
|
1646
|
-
else if (tilmeld.config.verifyEmail &&
|
|
1647
|
-
tilmeld.config.unverifiedAccess &&
|
|
1648
|
-
!madeAdmin &&
|
|
1649
|
-
tilmeld.config.userFields.includes('email')) {
|
|
1650
|
-
if (!(await tilmeld.login(this, true))) {
|
|
1651
|
-
throw new Error('An error occurred trying to log you in.');
|
|
1610
|
+
try {
|
|
1611
|
+
if (!(await tilmeld.config.sendEmail(tilmeld, {
|
|
1612
|
+
template: 'UserRegistered',
|
|
1613
|
+
message: { to: tilmeld.config.userRegisteredRecipient },
|
|
1614
|
+
locals: {
|
|
1615
|
+
userUsername: this.$data.username,
|
|
1616
|
+
userName: this.$data.name,
|
|
1617
|
+
userFirstName: this.$data.nameFirst,
|
|
1618
|
+
userLastName: this.$data.nameLast,
|
|
1619
|
+
userEmail: this.$data.email,
|
|
1620
|
+
userPhone: this.$data.phone,
|
|
1621
|
+
},
|
|
1622
|
+
}, this))) {
|
|
1623
|
+
throw new Error('Failed to send.');
|
|
1624
|
+
}
|
|
1652
1625
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
`${this.$data.email} with a verification link for you to finish ` +
|
|
1656
|
-
'registration.';
|
|
1657
|
-
loggedin = true;
|
|
1658
|
-
}
|
|
1659
|
-
else {
|
|
1660
|
-
if (!(await tilmeld.login(this, true))) {
|
|
1661
|
-
throw new Error('An error occurred trying to log you in.');
|
|
1626
|
+
catch (e) {
|
|
1627
|
+
this.$nymph.config.debugError('tilmeld', `Couldn't send new user registered email for "${this.$data.username}": ${e.message}`);
|
|
1662
1628
|
}
|
|
1663
|
-
message += "You're now registered and logged in!";
|
|
1664
|
-
loggedin = true;
|
|
1665
1629
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1630
|
+
if (result.loggedin) {
|
|
1631
|
+
try {
|
|
1632
|
+
await this.$nymph.tilmeld?.fillSession(this);
|
|
1633
|
+
}
|
|
1634
|
+
catch (e) {
|
|
1635
|
+
return {
|
|
1636
|
+
result: true,
|
|
1637
|
+
loggedin: false,
|
|
1638
|
+
message: `You are registered, but an error occurred logging you in: ${e.message}`,
|
|
1639
|
+
};
|
|
1673
1640
|
}
|
|
1674
1641
|
}
|
|
1642
|
+
return result;
|
|
1675
1643
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
await tnymph.rollback(transaction);
|
|
1679
|
-
this.$setNymph(nymph);
|
|
1680
|
-
return {
|
|
1681
|
-
result: false,
|
|
1682
|
-
loggedin: false,
|
|
1683
|
-
message: 'Error registering user.',
|
|
1684
|
-
};
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
catch (e) {
|
|
1688
|
-
this.$nymph.config.debugError('tilmeld', `Error registering new user "${this.$data.username}": ${e}`);
|
|
1689
|
-
await tnymph.rollback(transaction);
|
|
1690
|
-
this.$setNymph(nymph);
|
|
1691
|
-
throw e;
|
|
1692
|
-
}
|
|
1693
|
-
try {
|
|
1694
|
-
await tnymph.commit(transaction);
|
|
1695
|
-
this.$setNymph(nymph);
|
|
1696
|
-
await this.$nymph.tilmeld?.fillSession(this);
|
|
1697
|
-
}
|
|
1698
|
-
catch (e) {
|
|
1699
|
-
throw e;
|
|
1700
|
-
}
|
|
1701
|
-
return {
|
|
1702
|
-
result: true,
|
|
1703
|
-
loggedin,
|
|
1704
|
-
message,
|
|
1705
|
-
};
|
|
1706
|
-
}
|
|
1707
|
-
async $save() {
|
|
1708
|
-
let tilmeld = enforceTilmeld(this);
|
|
1709
|
-
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
1710
|
-
return false;
|
|
1711
|
-
}
|
|
1712
|
-
if (!this.$skipAcWhenSaving &&
|
|
1713
|
-
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1714
|
-
!tilmeld.gatekeeper('system/admin') &&
|
|
1715
|
-
this.$data.abilities?.includes('system/admin')) {
|
|
1716
|
-
throw new AccessControlError("You don't have the authority to modify system admins.");
|
|
1717
|
-
}
|
|
1718
|
-
// Lowercase the domain part.
|
|
1719
|
-
if (tilmeld.config.domainSupport) {
|
|
1720
|
-
const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
1721
|
-
if (domain) {
|
|
1722
|
-
this.$data.username = `${username}@${domain.toLowerCase()}`;
|
|
1644
|
+
catch (e) {
|
|
1645
|
+
return { result: false, loggedin: false, message: e.message };
|
|
1723
1646
|
}
|
|
1724
1647
|
}
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
((originalDomain == null && domain != null) ||
|
|
1730
|
-
(domain == null && originalDomain != null))) {
|
|
1731
|
-
throw new AccessControlError("Users can't switch to or from having a domain once created.");
|
|
1648
|
+
async $save() {
|
|
1649
|
+
let tilmeld = enforceTilmeld(this);
|
|
1650
|
+
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
1651
|
+
throw new BadUsernameError('Username must not be empty.');
|
|
1732
1652
|
}
|
|
1733
1653
|
if (!this.$skipAcWhenSaving &&
|
|
1734
|
-
|
|
1735
|
-
!tilmeld.gatekeeper('system/admin')
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1654
|
+
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1655
|
+
!tilmeld.gatekeeper('system/admin') &&
|
|
1656
|
+
this.$data.abilities?.includes('system/admin')) {
|
|
1657
|
+
throw new AccessControlError("You don't have the authority to modify system admins.");
|
|
1658
|
+
}
|
|
1659
|
+
// Lowercase the domain part.
|
|
1660
|
+
if (tilmeld.config.domainSupport) {
|
|
1661
|
+
const [username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
1662
|
+
if (domain) {
|
|
1663
|
+
this.$data.username = `${username}@${domain.toLowerCase()}`;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (tilmeld.config.domainSupport) {
|
|
1667
|
+
const [_username, domain] = splitn(this.$data.username ?? '', '@', 2);
|
|
1668
|
+
const [_originalUsername, originalDomain] = splitn(this.$originalUsername ?? '', '@', 2);
|
|
1669
|
+
if (this.guid != null &&
|
|
1670
|
+
((originalDomain == null && domain != null) ||
|
|
1671
|
+
(domain == null && originalDomain != null))) {
|
|
1672
|
+
throw new AccessControlError("Users can't switch to or from having a domain once created.");
|
|
1740
1673
|
}
|
|
1741
|
-
if (
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1674
|
+
if (!this.$skipAcWhenSaving &&
|
|
1675
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1676
|
+
!tilmeld.gatekeeper('system/admin')) {
|
|
1677
|
+
if (domain != null &&
|
|
1678
|
+
!this.$is(tilmeld.currentUser) &&
|
|
1679
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
1680
|
+
throw new AccessControlError("You don't have the authority to modify users on this domain.");
|
|
1681
|
+
}
|
|
1682
|
+
if (originalDomain != null &&
|
|
1683
|
+
originalDomain !== domain &&
|
|
1684
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
1685
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))) {
|
|
1686
|
+
throw new AccessControlError("You don't have the authority to modify users on this domain.");
|
|
1687
|
+
}
|
|
1746
1688
|
}
|
|
1747
1689
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
this.$data.email = this.$data.username;
|
|
1754
|
-
}
|
|
1755
|
-
this.$data.nameFirst = (this.$data.nameFirst ?? '').trim();
|
|
1756
|
-
this.$data.nameMiddle = (this.$data.nameMiddle ?? '').trim();
|
|
1757
|
-
this.$data.nameLast = (this.$data.nameLast ?? '').trim();
|
|
1758
|
-
this.$data.phone = (this.$data.phone ?? '').trim();
|
|
1759
|
-
this.$data.name =
|
|
1760
|
-
this.$data.nameFirst +
|
|
1761
|
-
(this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
|
|
1762
|
-
(this.$data.nameLast ? ' ' + this.$data.nameLast : '');
|
|
1763
|
-
this.$data.enabled = !!this.$data.enabled;
|
|
1764
|
-
// Clear empty values.
|
|
1765
|
-
if (this.$data.nameFirst === '' ||
|
|
1766
|
-
!tilmeld.config.userFields.includes('name')) {
|
|
1767
|
-
delete this.$data.nameFirst;
|
|
1768
|
-
}
|
|
1769
|
-
if (this.$data.nameMiddle === '' ||
|
|
1770
|
-
!tilmeld.config.userFields.includes('name')) {
|
|
1771
|
-
delete this.$data.nameMiddle;
|
|
1772
|
-
}
|
|
1773
|
-
if (this.$data.nameLast === '' ||
|
|
1774
|
-
!tilmeld.config.userFields.includes('name')) {
|
|
1775
|
-
delete this.$data.nameLast;
|
|
1776
|
-
}
|
|
1777
|
-
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
1778
|
-
delete this.$data.name;
|
|
1779
|
-
}
|
|
1780
|
-
if (this.$data.email === '' ||
|
|
1781
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1782
|
-
delete this.$data.email;
|
|
1783
|
-
}
|
|
1784
|
-
if (this.$data.avatar === '') {
|
|
1785
|
-
delete this.$data.avatar;
|
|
1786
|
-
}
|
|
1787
|
-
if (this.$data.phone === '' ||
|
|
1788
|
-
!tilmeld.config.userFields.includes('phone')) {
|
|
1789
|
-
delete this.$data.phone;
|
|
1790
|
-
}
|
|
1791
|
-
if (this.$data.secret === '' ||
|
|
1792
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1793
|
-
delete this.$data.secret;
|
|
1794
|
-
}
|
|
1795
|
-
if (this.$data.emailChangeDate === 0 ||
|
|
1796
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1797
|
-
delete this.$data.emailChangeDate;
|
|
1798
|
-
}
|
|
1799
|
-
if (this.$data.newEmailSecret === '' ||
|
|
1800
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1801
|
-
delete this.$data.newEmailSecret;
|
|
1802
|
-
}
|
|
1803
|
-
if (this.$data.newEmailAddress === '' ||
|
|
1804
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1805
|
-
delete this.$data.newEmailAddress;
|
|
1806
|
-
}
|
|
1807
|
-
if (this.$data.cancelEmailSecret === '' ||
|
|
1808
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1809
|
-
delete this.$data.cancelEmailSecret;
|
|
1810
|
-
}
|
|
1811
|
-
if (this.$data.cancelEmailAddress === '' ||
|
|
1812
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1813
|
-
delete this.$data.cancelEmailAddress;
|
|
1814
|
-
}
|
|
1815
|
-
if (this.$data.recoverSecret === '' ||
|
|
1816
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1817
|
-
delete this.$data.recoverSecret;
|
|
1818
|
-
}
|
|
1819
|
-
if (this.$data.recoverSecretDate === 0 ||
|
|
1820
|
-
!tilmeld.config.userFields.includes('email')) {
|
|
1821
|
-
delete this.$data.recoverSecretDate;
|
|
1822
|
-
}
|
|
1823
|
-
if (this.$data.password === '') {
|
|
1824
|
-
delete this.$data.password;
|
|
1825
|
-
}
|
|
1826
|
-
if (this.$data.passwordTemp === '') {
|
|
1827
|
-
delete this.$data.passwordTemp;
|
|
1828
|
-
}
|
|
1829
|
-
if (this.$data.inheritAbilities === false) {
|
|
1830
|
-
delete this.$data.inheritAbilities;
|
|
1831
|
-
}
|
|
1832
|
-
// Verification.
|
|
1833
|
-
const unCheck = await this.$checkUsername();
|
|
1834
|
-
if (!unCheck.result) {
|
|
1835
|
-
throw new BadUsernameError(unCheck.message);
|
|
1836
|
-
}
|
|
1837
|
-
if (!tilmeld.config.emailUsernames &&
|
|
1838
|
-
tilmeld.config.userFields.includes('email')) {
|
|
1839
|
-
const emCheck = await this.$checkEmail();
|
|
1840
|
-
if (!emCheck.result) {
|
|
1841
|
-
throw new BadEmailError(emCheck.message);
|
|
1690
|
+
let sendVerification = false;
|
|
1691
|
+
// Formatting.
|
|
1692
|
+
this.$data.username = this.$data.username.trim();
|
|
1693
|
+
if (tilmeld.config.emailUsernames) {
|
|
1694
|
+
this.$data.email = this.$data.username;
|
|
1842
1695
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1696
|
+
this.$data.nameFirst = (this.$data.nameFirst ?? '').trim();
|
|
1697
|
+
this.$data.nameMiddle = (this.$data.nameMiddle ?? '').trim();
|
|
1698
|
+
this.$data.nameLast = (this.$data.nameLast ?? '').trim();
|
|
1699
|
+
this.$data.phone = (this.$data.phone ?? '').trim();
|
|
1700
|
+
this.$data.name =
|
|
1701
|
+
this.$data.nameFirst +
|
|
1702
|
+
(this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
|
|
1703
|
+
(this.$data.nameLast ? ' ' + this.$data.nameLast : '');
|
|
1704
|
+
this.$data.enabled = !!this.$data.enabled;
|
|
1705
|
+
// Clear empty values.
|
|
1706
|
+
if (this.$data.nameFirst === '' ||
|
|
1707
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
1708
|
+
delete this.$data.nameFirst;
|
|
1709
|
+
}
|
|
1710
|
+
if (this.$data.nameMiddle === '' ||
|
|
1711
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
1712
|
+
delete this.$data.nameMiddle;
|
|
1713
|
+
}
|
|
1714
|
+
if (this.$data.nameLast === '' ||
|
|
1715
|
+
!tilmeld.config.userFields.includes('name')) {
|
|
1716
|
+
delete this.$data.nameLast;
|
|
1717
|
+
}
|
|
1718
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
1719
|
+
delete this.$data.name;
|
|
1720
|
+
}
|
|
1721
|
+
if (this.$data.email === '' ||
|
|
1722
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1723
|
+
delete this.$data.email;
|
|
1724
|
+
}
|
|
1725
|
+
if (this.$data.avatar === '') {
|
|
1726
|
+
delete this.$data.avatar;
|
|
1727
|
+
}
|
|
1728
|
+
if (this.$data.phone === '' ||
|
|
1729
|
+
!tilmeld.config.userFields.includes('phone')) {
|
|
1730
|
+
delete this.$data.phone;
|
|
1731
|
+
}
|
|
1732
|
+
if (this.$data.secret === '' ||
|
|
1733
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1734
|
+
delete this.$data.secret;
|
|
1735
|
+
}
|
|
1736
|
+
if (this.$data.emailChangeDate === 0 ||
|
|
1737
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1738
|
+
delete this.$data.emailChangeDate;
|
|
1739
|
+
}
|
|
1740
|
+
if (this.$data.newEmailSecret === '' ||
|
|
1741
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1742
|
+
delete this.$data.newEmailSecret;
|
|
1743
|
+
}
|
|
1744
|
+
if (this.$data.newEmailAddress === '' ||
|
|
1745
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1746
|
+
delete this.$data.newEmailAddress;
|
|
1747
|
+
}
|
|
1748
|
+
if (this.$data.cancelEmailSecret === '' ||
|
|
1749
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1750
|
+
delete this.$data.cancelEmailSecret;
|
|
1751
|
+
}
|
|
1752
|
+
if (this.$data.cancelEmailAddress === '' ||
|
|
1753
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1754
|
+
delete this.$data.cancelEmailAddress;
|
|
1755
|
+
}
|
|
1756
|
+
if (this.$data.recoverSecret === '' ||
|
|
1757
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1758
|
+
delete this.$data.recoverSecret;
|
|
1759
|
+
}
|
|
1760
|
+
if (this.$data.recoverSecretDate === 0 ||
|
|
1761
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
1762
|
+
delete this.$data.recoverSecretDate;
|
|
1763
|
+
}
|
|
1764
|
+
if (this.$data.password === '') {
|
|
1765
|
+
delete this.$data.password;
|
|
1766
|
+
}
|
|
1767
|
+
if (this.$data.passwordTemp === '') {
|
|
1768
|
+
delete this.$data.passwordTemp;
|
|
1769
|
+
}
|
|
1770
|
+
if (this.$data.inheritAbilities === false) {
|
|
1771
|
+
delete this.$data.inheritAbilities;
|
|
1772
|
+
}
|
|
1773
|
+
// Verification.
|
|
1774
|
+
const unCheck = await this.$checkUsername();
|
|
1775
|
+
if (!unCheck.result) {
|
|
1776
|
+
throw new BadUsernameError(unCheck.message);
|
|
1777
|
+
}
|
|
1778
|
+
if (!tilmeld.config.emailUsernames &&
|
|
1779
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1780
|
+
const emCheck = await this.$checkEmail();
|
|
1781
|
+
if (!emCheck.result) {
|
|
1782
|
+
throw new BadEmailError(emCheck.message);
|
|
1858
1783
|
}
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1784
|
+
}
|
|
1785
|
+
// Email changes.
|
|
1786
|
+
if (!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
1787
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1788
|
+
// The user isn't an admin, so email address changes should contain some
|
|
1789
|
+
// security measures.
|
|
1790
|
+
if (tilmeld.config.verifyEmail) {
|
|
1791
|
+
// The user needs to verify this new email address.
|
|
1792
|
+
if (this.guid == null) {
|
|
1793
|
+
// If this is the first user, they'll be made an admin, so they don't
|
|
1794
|
+
// need to verify.
|
|
1795
|
+
if (!this.$data.abilities?.includes('system/admin')) {
|
|
1796
|
+
this.$data.secret = nanoid();
|
|
1797
|
+
sendVerification = true;
|
|
1798
|
+
}
|
|
1869
1799
|
}
|
|
1870
|
-
else
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
this.$data.cancelEmailSecret = nanoid();
|
|
1881
|
-
this.$data.emailChangeDate = Date.now();
|
|
1800
|
+
else if (this.$originalEmail != null &&
|
|
1801
|
+
this.$originalEmail !== this.$data.email) {
|
|
1802
|
+
// The user already has an old email address.
|
|
1803
|
+
if (tilmeld.config.emailRateLimit !== '' &&
|
|
1804
|
+
this.$data.emailChangeDate != null &&
|
|
1805
|
+
this.$data.emailChangeDate >=
|
|
1806
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000) {
|
|
1807
|
+
throw new EmailChangeRateLimitExceededError('You already changed your email address recently. Please wait until ' +
|
|
1808
|
+
new Date((strtotime('+' + tilmeld.config.emailRateLimit, Math.floor(this.$data.emailChangeDate / 1000)) || 0) * 1000).toString() +
|
|
1809
|
+
' to change your email address again.');
|
|
1882
1810
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1811
|
+
else {
|
|
1812
|
+
if (this.$data.newEmailSecret == null &&
|
|
1813
|
+
tilmeld.config.emailRateLimit !== '' &&
|
|
1814
|
+
// Make sure the user has at least the rate limit time to cancel
|
|
1815
|
+
// an email change.
|
|
1816
|
+
(this.$data.emailChangeDate == null ||
|
|
1817
|
+
this.$data.emailChangeDate <
|
|
1818
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)) {
|
|
1819
|
+
// Save the old email in case the cancel change link is clicked.
|
|
1820
|
+
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1821
|
+
this.$data.cancelEmailSecret = nanoid();
|
|
1822
|
+
this.$data.emailChangeDate = Date.now();
|
|
1823
|
+
}
|
|
1824
|
+
// Save the new address and reset to the old one (until they verify
|
|
1825
|
+
// it).
|
|
1826
|
+
this.$data.newEmailAddress = this.$data.email;
|
|
1827
|
+
this.$data.email = this.$originalEmail;
|
|
1828
|
+
if (tilmeld.config.emailUsernames) {
|
|
1829
|
+
this.$data.username = this.$data.email;
|
|
1830
|
+
}
|
|
1831
|
+
this.$data.newEmailSecret = nanoid();
|
|
1832
|
+
sendVerification = true;
|
|
1889
1833
|
}
|
|
1890
|
-
this.$data.newEmailSecret = nanoid();
|
|
1891
|
-
sendVerification = true;
|
|
1892
1834
|
}
|
|
1893
1835
|
}
|
|
1836
|
+
else if (this.guid != null &&
|
|
1837
|
+
this.$originalEmail != null &&
|
|
1838
|
+
this.$originalEmail !== this.$data.email &&
|
|
1839
|
+
// Make sure the user has at least the rate limit time to cancel an
|
|
1840
|
+
// email change.
|
|
1841
|
+
(this.$data.emailChangeDate == null ||
|
|
1842
|
+
this.$data.emailChangeDate <
|
|
1843
|
+
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)) {
|
|
1844
|
+
// The user doesn't need to verify their new email address, but should
|
|
1845
|
+
// be able to cancel the email change from their old address.
|
|
1846
|
+
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1847
|
+
this.$data.cancelEmailSecret = nanoid();
|
|
1848
|
+
sendVerification = true;
|
|
1849
|
+
}
|
|
1894
1850
|
}
|
|
1895
|
-
|
|
1896
|
-
this.$
|
|
1897
|
-
this.$originalEmail !== this.$data.email &&
|
|
1898
|
-
// Make sure the user has at least the rate limit time to cancel an
|
|
1899
|
-
// email change.
|
|
1900
|
-
(this.$data.emailChangeDate == null ||
|
|
1901
|
-
this.$data.emailChangeDate <
|
|
1902
|
-
(strtotime('-' + tilmeld.config.emailRateLimit) || 0) * 1000)) {
|
|
1903
|
-
// The user doesn't need to verify their new email address, but should
|
|
1904
|
-
// be able to cancel the email change from their old address.
|
|
1905
|
-
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1906
|
-
this.$data.cancelEmailSecret = nanoid();
|
|
1907
|
-
sendVerification = true;
|
|
1851
|
+
if (this.$data.passwordTemp != null) {
|
|
1852
|
+
this.$password(this.$data.passwordTemp);
|
|
1908
1853
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
}
|
|
1913
|
-
delete this.$data.passwordTemp;
|
|
1914
|
-
if (this.$data.password == null) {
|
|
1915
|
-
throw new BadDataError('A password is required.');
|
|
1916
|
-
}
|
|
1917
|
-
// Start transaction.
|
|
1918
|
-
const transaction = 'tilmeld-save-user-' + (this.guid || nanoid());
|
|
1919
|
-
const nymph = this.$nymph;
|
|
1920
|
-
const tnymph = await this.$nymph.startTransaction(transaction);
|
|
1921
|
-
this.$setNymph(tnymph);
|
|
1922
|
-
tilmeld = enforceTilmeld(this);
|
|
1923
|
-
const Group = tnymph.getEntityClass(GroupClass);
|
|
1924
|
-
try {
|
|
1925
|
-
let group = this.$data.group;
|
|
1926
|
-
if (group != null) {
|
|
1927
|
-
// Make sure to get fresh group data.
|
|
1928
|
-
if (group.$asleep()) {
|
|
1929
|
-
await group.$wake();
|
|
1930
|
-
}
|
|
1931
|
-
else {
|
|
1932
|
-
if (!(await group.$refresh())) {
|
|
1933
|
-
throw new Error("Couldn't retrieve primary group info.");
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1854
|
+
delete this.$data.passwordTemp;
|
|
1855
|
+
if (this.$data.password == null) {
|
|
1856
|
+
throw new BadDataError('A password is required.');
|
|
1936
1857
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1858
|
+
let preGuid = this.guid;
|
|
1859
|
+
let preCdate = this.cdate;
|
|
1860
|
+
let preMdate = this.mdate;
|
|
1861
|
+
return await transaction(this.$nymph, 'tilmeld-save-user-' + (this.guid || nanoid()), async (nymph) => {
|
|
1862
|
+
this.$setNymph(nymph);
|
|
1863
|
+
const tilmeld = enforceTilmeld(this);
|
|
1864
|
+
const Group = this.$nymph.getEntityClass(GroupClass);
|
|
1865
|
+
let group = this.$data.group;
|
|
1866
|
+
if (group != null) {
|
|
1867
|
+
// Make sure to get fresh group data.
|
|
1868
|
+
if (group.$asleep()) {
|
|
1869
|
+
await group.$wake();
|
|
1948
1870
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
type: '&',
|
|
1954
|
-
equal: ['defaultPrimary', true],
|
|
1955
|
-
});
|
|
1956
|
-
if (group != null) {
|
|
1957
|
-
this.$data.group = group;
|
|
1871
|
+
else {
|
|
1872
|
+
if (!(await group.$refresh())) {
|
|
1873
|
+
throw new Error("Couldn't retrieve primary group info.");
|
|
1874
|
+
}
|
|
1958
1875
|
}
|
|
1959
1876
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
if (tilmeld.config.userFields.includes('name') &&
|
|
1978
|
-
group.name !== this.$data.name) {
|
|
1979
|
-
group.name = this.$data.name;
|
|
1980
|
-
changed = true;
|
|
1877
|
+
if (group == null && this.guid == null) {
|
|
1878
|
+
if (tilmeld.config.generatePrimary) {
|
|
1879
|
+
// Generate a new primary group for the user.
|
|
1880
|
+
group = await Group.factory();
|
|
1881
|
+
group.user = this;
|
|
1882
|
+
const parent = await this.$nymph.getEntity({ class: Group }, { type: '&', equal: ['defaultPrimary', true] });
|
|
1883
|
+
if (parent != null) {
|
|
1884
|
+
group.parent = parent;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
else {
|
|
1888
|
+
// Add the default primary.
|
|
1889
|
+
const group = await this.$nymph.getEntity({ class: Group }, { type: '&', equal: ['defaultPrimary', true] });
|
|
1890
|
+
if (group != null) {
|
|
1891
|
+
this.$data.group = group;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1981
1894
|
}
|
|
1982
|
-
if (
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1895
|
+
if (group != null && group.user != null && this.$is(group.user)) {
|
|
1896
|
+
// Update the user's generated primary group.
|
|
1897
|
+
let changed = false;
|
|
1898
|
+
if (group.groupname !== this.$data.username) {
|
|
1899
|
+
group.groupname = this.$data.username;
|
|
1900
|
+
changed = true;
|
|
1901
|
+
}
|
|
1902
|
+
if (group.avatar !== this.$data.avatar) {
|
|
1903
|
+
group.avatar = this.$data.avatar;
|
|
1904
|
+
changed = true;
|
|
1905
|
+
}
|
|
1906
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1907
|
+
group.email !== this.$data.email) {
|
|
1908
|
+
group.email = this.$data.email;
|
|
1909
|
+
changed = true;
|
|
1910
|
+
}
|
|
1911
|
+
if (tilmeld.config.userFields.includes('name') &&
|
|
1912
|
+
group.name !== this.$data.name) {
|
|
1913
|
+
group.name = this.$data.name;
|
|
1914
|
+
changed = true;
|
|
1915
|
+
}
|
|
1916
|
+
if (tilmeld.config.userFields.includes('phone') &&
|
|
1917
|
+
group.phone !== this.$data.phone) {
|
|
1918
|
+
group.phone = this.$data.phone;
|
|
1919
|
+
changed = true;
|
|
1920
|
+
}
|
|
1921
|
+
if (changed || group.guid == null) {
|
|
1922
|
+
await group.$saveSkipAC();
|
|
1923
|
+
}
|
|
1924
|
+
this.$data.group = group;
|
|
1986
1925
|
}
|
|
1987
|
-
if (
|
|
1988
|
-
|
|
1989
|
-
|
|
1926
|
+
if (this.$data.groups == null || this.$data.groups.length === 0) {
|
|
1927
|
+
// Add secondary groups.
|
|
1928
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1929
|
+
tilmeld.config.verifyEmail &&
|
|
1930
|
+
tilmeld.config.unverifiedAccess &&
|
|
1931
|
+
this.$data.secret != null) {
|
|
1932
|
+
// Add the default secondaries for unverified users.
|
|
1933
|
+
this.$data.groups = await this.$nymph.getEntities({ class: Group }, { type: '&', equal: ['unverifiedSecondary', true] });
|
|
1934
|
+
}
|
|
1935
|
+
else {
|
|
1936
|
+
// Add the default secondaries.
|
|
1937
|
+
this.$data.groups = await this.$nymph.getEntities({ class: Group }, { type: '&', equal: ['defaultSecondary', true] });
|
|
1990
1938
|
}
|
|
1991
1939
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
if (this.$data.groups == null || this.$data.groups.length === 0) {
|
|
1995
|
-
// Add secondary groups.
|
|
1996
|
-
if (tilmeld.config.userFields.includes('email') &&
|
|
1997
|
-
tilmeld.config.verifyEmail &&
|
|
1998
|
-
tilmeld.config.unverifiedAccess &&
|
|
1999
|
-
this.$data.secret != null) {
|
|
2000
|
-
// Add the default secondaries for unverified users.
|
|
2001
|
-
this.$data.groups = await tnymph.getEntities({ class: Group }, {
|
|
2002
|
-
type: '&',
|
|
2003
|
-
equal: ['unverifiedSecondary', true],
|
|
2004
|
-
});
|
|
1940
|
+
try {
|
|
1941
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
2005
1942
|
}
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
this.$data.groups = await tnymph.getEntities({ class: Group }, {
|
|
2009
|
-
type: '&',
|
|
2010
|
-
equal: ['defaultSecondary', true],
|
|
2011
|
-
});
|
|
1943
|
+
catch (e) {
|
|
1944
|
+
throw new BadDataError(e?.message);
|
|
2012
1945
|
}
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
throw new BadDataError(e?.message);
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
catch (e) {
|
|
2022
|
-
await tnymph.rollback(transaction);
|
|
2023
|
-
this.$setNymph(nymph);
|
|
2024
|
-
throw e;
|
|
2025
|
-
}
|
|
2026
|
-
let ret = false;
|
|
2027
|
-
let preGuid = this.guid;
|
|
2028
|
-
let preCdate = this.cdate;
|
|
2029
|
-
let preMdate = this.mdate;
|
|
2030
|
-
try {
|
|
2031
|
-
for (let callback of this.constructor
|
|
2032
|
-
.beforeSaveCallbacks) {
|
|
2033
|
-
if (callback) {
|
|
2034
|
-
await callback(this);
|
|
1946
|
+
for (let callback of this.constructor
|
|
1947
|
+
.beforeSaveCallbacks) {
|
|
1948
|
+
if (callback) {
|
|
1949
|
+
await callback(this);
|
|
1950
|
+
}
|
|
2035
1951
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
this.cdate = preCdate;
|
|
2043
|
-
this.mdate = preMdate;
|
|
2044
|
-
this.$setNymph(nymph);
|
|
2045
|
-
throw e;
|
|
2046
|
-
}
|
|
2047
|
-
if (ret) {
|
|
2048
|
-
if (sendVerification) {
|
|
2049
|
-
// The email has changed, so send a new verification email.
|
|
2050
|
-
if (!(await this.$sendEmailVerification())) {
|
|
2051
|
-
await tnymph.rollback(transaction);
|
|
2052
|
-
this.guid = preGuid;
|
|
2053
|
-
this.cdate = preCdate;
|
|
2054
|
-
this.mdate = preMdate;
|
|
2055
|
-
this.$setNymph(nymph);
|
|
2056
|
-
throw new Error("Couldn't send verification email.");
|
|
1952
|
+
await super.$save();
|
|
1953
|
+
if (sendVerification) {
|
|
1954
|
+
// The email has changed, so send a new verification email.
|
|
1955
|
+
if (!(await this.$sendEmailVerification())) {
|
|
1956
|
+
throw new Error("Couldn't send verification email.");
|
|
1957
|
+
}
|
|
2057
1958
|
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
this.$gatekeeperCache = undefined;
|
|
2061
|
-
try {
|
|
1959
|
+
this.$descendantGroups = undefined;
|
|
1960
|
+
this.$gatekeeperCache = undefined;
|
|
2062
1961
|
for (let callback of this.constructor
|
|
2063
1962
|
.afterSaveCallbacks) {
|
|
2064
1963
|
if (callback) {
|
|
2065
1964
|
await callback(this);
|
|
2066
1965
|
}
|
|
2067
1966
|
}
|
|
2068
|
-
}
|
|
2069
|
-
catch (e) {
|
|
2070
|
-
await tnymph.rollback(transaction);
|
|
2071
|
-
this.guid = preGuid;
|
|
2072
|
-
this.cdate = preCdate;
|
|
2073
|
-
this.mdate = preMdate;
|
|
1967
|
+
}, async (nymph, committed) => {
|
|
2074
1968
|
this.$setNymph(nymph);
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
1969
|
+
if (committed) {
|
|
1970
|
+
this.$originalEmail = this.$data.email;
|
|
1971
|
+
this.$originalUsername = this.$data.username;
|
|
1972
|
+
try {
|
|
1973
|
+
const tilmeld = enforceTilmeld(nymph);
|
|
1974
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
1975
|
+
// Update the user in the session cache.
|
|
1976
|
+
await tilmeld.fillSession(this);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
catch (e) {
|
|
1980
|
+
// Ignore.
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
else {
|
|
1984
|
+
this.guid = preGuid;
|
|
1985
|
+
this.cdate = preCdate;
|
|
1986
|
+
this.mdate = preMdate;
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
2078
1989
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
1990
|
+
/**
|
|
1991
|
+
* This should *never* be accessible on the client.
|
|
1992
|
+
*/
|
|
1993
|
+
async $saveSkipAC() {
|
|
1994
|
+
this.$skipAcWhenSaving = true;
|
|
1995
|
+
await this.$save();
|
|
2081
1996
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
const tilmeld = enforceTilmeld(nymph);
|
|
2087
|
-
if (tilmeld.User.current(true).$is(this)) {
|
|
2088
|
-
// Update the user in the session cache.
|
|
2089
|
-
await tilmeld.fillSession(this);
|
|
1997
|
+
$tilmeldSaveSkipAC() {
|
|
1998
|
+
if (this.$skipAcWhenSaving) {
|
|
1999
|
+
this.$skipAcWhenSaving = false;
|
|
2000
|
+
return true;
|
|
2090
2001
|
}
|
|
2002
|
+
return false;
|
|
2091
2003
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
}
|
|
2106
|
-
return false;
|
|
2107
|
-
}
|
|
2108
|
-
async $delete() {
|
|
2109
|
-
const tilmeld = enforceTilmeld(this);
|
|
2110
|
-
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2111
|
-
throw new AccessControlError("You don't have the authority to delete users.");
|
|
2112
|
-
}
|
|
2113
|
-
if (!this.$skipAcWhenDeleting &&
|
|
2114
|
-
!tilmeld.gatekeeper('system/admin') &&
|
|
2115
|
-
this.$data.abilities?.includes('system/admin')) {
|
|
2116
|
-
throw new AccessControlError("You don't have the authority to delete system admins.");
|
|
2117
|
-
}
|
|
2118
|
-
if (this.$data.username !== this.$originalUsername) {
|
|
2119
|
-
throw new AccessControlError("You can't delete a user while switching their username.");
|
|
2120
|
-
}
|
|
2121
|
-
const transaction = 'tilmeld-delete-user-' + this.guid;
|
|
2122
|
-
const nymph = this.$nymph;
|
|
2123
|
-
const tnymph = await nymph.startTransaction(transaction);
|
|
2124
|
-
this.$setNymph(tnymph);
|
|
2125
|
-
let success = false;
|
|
2126
|
-
try {
|
|
2004
|
+
async $delete() {
|
|
2005
|
+
const tilmeld = enforceTilmeld(this);
|
|
2006
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
2007
|
+
throw new AccessControlError("You don't have the authority to delete users.");
|
|
2008
|
+
}
|
|
2009
|
+
if (!this.$skipAcWhenDeleting &&
|
|
2010
|
+
!tilmeld.gatekeeper('system/admin') &&
|
|
2011
|
+
this.$data.abilities?.includes('system/admin')) {
|
|
2012
|
+
throw new AccessControlError("You don't have the authority to delete system admins.");
|
|
2013
|
+
}
|
|
2014
|
+
if (this.$data.username !== this.$originalUsername) {
|
|
2015
|
+
throw new AccessControlError("You can't delete a user while switching their username.");
|
|
2016
|
+
}
|
|
2127
2017
|
if (!(await this.$refresh())) {
|
|
2128
2018
|
throw new BadDataError('User could not be refreshed.');
|
|
2129
2019
|
}
|
|
@@ -2150,80 +2040,71 @@ export default class User extends AbleObject {
|
|
|
2150
2040
|
// Read the skip ac value here, because super.$delete changes it.
|
|
2151
2041
|
const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
|
|
2152
2042
|
// Delete the user.
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2043
|
+
await super.$delete();
|
|
2044
|
+
// Delete the generated group.
|
|
2045
|
+
if ($skipAcWhenDeleting) {
|
|
2046
|
+
await this.$data.group.$deleteSkipAC();
|
|
2047
|
+
}
|
|
2048
|
+
else {
|
|
2049
|
+
await this.$data.group.$delete();
|
|
2159
2050
|
}
|
|
2160
2051
|
}
|
|
2161
2052
|
else {
|
|
2162
|
-
|
|
2053
|
+
await super.$delete();
|
|
2163
2054
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
throw e;
|
|
2169
|
-
}
|
|
2170
|
-
if (success) {
|
|
2171
|
-
try {
|
|
2172
|
-
for (let callback of this.constructor
|
|
2173
|
-
.afterDeleteCallbacks) {
|
|
2174
|
-
if (callback) {
|
|
2175
|
-
await callback(this);
|
|
2176
|
-
}
|
|
2055
|
+
for (let callback of this.constructor
|
|
2056
|
+
.afterDeleteCallbacks) {
|
|
2057
|
+
if (callback) {
|
|
2058
|
+
await callback(this);
|
|
2177
2059
|
}
|
|
2178
2060
|
}
|
|
2179
|
-
catch (e) {
|
|
2180
|
-
await tnymph.rollback(transaction);
|
|
2181
|
-
this.$setNymph(nymph);
|
|
2182
|
-
throw e;
|
|
2183
|
-
}
|
|
2184
|
-
success = await tnymph.commit(transaction);
|
|
2185
|
-
}
|
|
2186
|
-
else {
|
|
2187
|
-
await tnymph.rollback(transaction);
|
|
2188
2061
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
async $deleteSkipAC() {
|
|
2196
|
-
this.$skipAcWhenDeleting = true;
|
|
2197
|
-
return await this.$delete();
|
|
2198
|
-
}
|
|
2199
|
-
$tilmeldDeleteSkipAC() {
|
|
2200
|
-
if (this.$skipAcWhenDeleting) {
|
|
2201
|
-
this.$skipAcWhenDeleting = false;
|
|
2202
|
-
return true;
|
|
2203
|
-
}
|
|
2204
|
-
return false;
|
|
2205
|
-
}
|
|
2206
|
-
static on(event, callback) {
|
|
2207
|
-
const prop = (event + 'Callbacks');
|
|
2208
|
-
if (!(prop in this)) {
|
|
2209
|
-
throw new Error('Invalid event type.');
|
|
2062
|
+
/*
|
|
2063
|
+
* This should *never* be accessible on the client.
|
|
2064
|
+
*/
|
|
2065
|
+
async $deleteSkipAC() {
|
|
2066
|
+
this.$skipAcWhenDeleting = true;
|
|
2067
|
+
await this.$delete();
|
|
2210
2068
|
}
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
const prop = (event + 'Callbacks');
|
|
2217
|
-
if (!(prop in this)) {
|
|
2069
|
+
$tilmeldDeleteSkipAC() {
|
|
2070
|
+
if (this.$skipAcWhenDeleting) {
|
|
2071
|
+
this.$skipAcWhenDeleting = false;
|
|
2072
|
+
return true;
|
|
2073
|
+
}
|
|
2218
2074
|
return false;
|
|
2219
2075
|
}
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2076
|
+
static on(event, callback) {
|
|
2077
|
+
const prop = (event + 'Callbacks');
|
|
2078
|
+
if (!(prop in this)) {
|
|
2079
|
+
throw new Error('Invalid event type.');
|
|
2080
|
+
}
|
|
2223
2081
|
// @ts-ignore: The callback should always be the right type here.
|
|
2224
|
-
this[prop].
|
|
2082
|
+
this[prop].push(callback);
|
|
2083
|
+
return () => this.off(event, callback);
|
|
2225
2084
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2085
|
+
static off(event, callback) {
|
|
2086
|
+
const prop = (event + 'Callbacks');
|
|
2087
|
+
if (!(prop in this)) {
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
2091
|
+
const i = this[prop].indexOf(callback);
|
|
2092
|
+
if (i > -1) {
|
|
2093
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
2094
|
+
this[prop].splice(i, 1);
|
|
2095
|
+
}
|
|
2096
|
+
return true;
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
})();
|
|
2100
|
+
/**
|
|
2101
|
+
* A user data model.
|
|
2102
|
+
*
|
|
2103
|
+
* Written by Hunter Perrin for SciActive.
|
|
2104
|
+
*
|
|
2105
|
+
* @author Hunter Perrin <hperrin@port87.com>
|
|
2106
|
+
* @copyright SciActive Inc
|
|
2107
|
+
* @see http://nymph.io/
|
|
2108
|
+
*/
|
|
2109
|
+
export default User;
|
|
2229
2110
|
//# sourceMappingURL=User.js.map
|