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