@nymphjs/tilmeld 1.0.0-beta.13 → 1.0.0-beta.130
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 +598 -0
- package/README.md +21 -5
- package/dist/AbleObject.d.ts +15 -0
- package/dist/AbleObject.js +18 -6
- package/dist/AbleObject.js.map +1 -1
- package/dist/Group.d.ts +186 -19
- package/dist/Group.js +817 -440
- package/dist/Group.js.map +1 -1
- package/dist/Group.testArtifacts.d.ts +3 -0
- package/dist/Group.testArtifacts.js +120 -0
- package/dist/Group.testArtifacts.js.map +1 -0
- package/dist/Tilmeld.d.ts +241 -12
- package/dist/Tilmeld.js +725 -286
- package/dist/Tilmeld.js.map +1 -1
- package/dist/Tilmeld.types.d.ts +5 -5
- package/dist/Tilmeld.types.js +1 -2
- package/dist/User.d.ts +399 -26
- package/dist/User.js +1955 -1055
- package/dist/User.js.map +1 -1
- package/dist/User.testArtifacts.d.ts +3 -0
- package/dist/User.testArtifacts.js +175 -0
- package/dist/User.testArtifacts.js.map +1 -0
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +196 -141
- package/dist/conf/defaults.js.map +1 -1
- package/dist/conf/index.d.ts +355 -2
- package/dist/conf/index.js +2 -8
- package/dist/conf/index.js.map +1 -1
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.js +3 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/requireAbility.d.ts +10 -0
- package/dist/decorators/requireAbility.js +21 -0
- package/dist/decorators/requireAbility.js.map +1 -0
- package/dist/decorators/requireAuthentication.d.ts +8 -0
- package/dist/decorators/requireAuthentication.js +16 -0
- package/dist/decorators/requireAuthentication.js.map +1 -0
- package/dist/enforceTilmeld.d.ts +3 -0
- package/dist/enforceTilmeld.js +18 -0
- package/dist/enforceTilmeld.js.map +1 -0
- package/dist/errors/AccessControlError.d.ts +2 -1
- package/dist/errors/AccessControlError.js +3 -6
- package/dist/errors/AccessControlError.js.map +1 -1
- package/dist/errors/BadDataError.d.ts +2 -1
- package/dist/errors/BadDataError.js +3 -6
- package/dist/errors/BadDataError.js.map +1 -1
- package/dist/errors/BadEmailError.d.ts +2 -1
- package/dist/errors/BadEmailError.js +3 -6
- package/dist/errors/BadEmailError.js.map +1 -1
- package/dist/errors/BadUsernameError.d.ts +2 -1
- package/dist/errors/BadUsernameError.js +3 -6
- package/dist/errors/BadUsernameError.js.map +1 -1
- package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js +1 -5
- package/dist/errors/CouldNotChangeDefaultPrimaryGroupError.js.map +1 -1
- package/dist/errors/EmailChangeRateLimitExceededError.js +1 -5
- package/dist/errors/EmailChangeRateLimitExceededError.js.map +1 -1
- package/dist/errors/index.d.ts +6 -6
- package/dist/errors/index.js +6 -22
- package/dist/errors/index.js.map +1 -1
- package/dist/index.d.ts +12 -10
- package/dist/index.js +17 -35
- package/dist/index.js.map +1 -1
- package/dist/testArtifacts.d.ts +23 -0
- package/dist/testArtifacts.js +1317 -0
- package/dist/testArtifacts.js.map +1 -0
- package/dist/tilmeldJoiProps.d.ts +3 -3
- package/dist/tilmeldJoiProps.js +13 -18
- package/dist/tilmeldJoiProps.js.map +1 -1
- package/jest.config.js +31 -5
- package/package.json +49 -36
- package/src/AbleObject.ts +2 -2
- package/src/Group.testArtifacts.ts +159 -0
- package/src/Group.ts +647 -238
- package/src/Tilmeld.ts +550 -315
- package/src/Tilmeld.types.ts +5 -5
- package/src/User.testArtifacts.ts +226 -0
- package/src/User.ts +1657 -634
- package/src/conf/defaults.ts +172 -109
- package/src/conf/index.ts +370 -2
- package/src/decorators/index.ts +2 -0
- package/src/decorators/requireAbility.ts +26 -0
- package/src/decorators/requireAuthentication.ts +19 -0
- package/src/enforceTilmeld.ts +20 -0
- package/src/errors/AccessControlError.ts +4 -2
- package/src/errors/BadDataError.ts +4 -2
- package/src/errors/BadEmailError.ts +4 -2
- package/src/errors/BadUsernameError.ts +4 -2
- package/src/errors/index.ts +6 -6
- package/src/index.ts +13 -10
- package/src/testArtifacts.ts +2098 -0
- package/src/tilmeldJoiProps.ts +8 -23
- package/tsconfig.json +6 -3
- package/typedoc.json +4 -0
- package/dist/Group.test.d.ts +0 -1
- package/dist/Group.test.js +0 -48
- package/dist/Group.test.js.map +0 -1
- package/dist/User.test.d.ts +0 -1
- package/dist/User.test.js +0 -51
- package/dist/User.test.js.map +0 -1
- package/dist/conf/d.d.ts +0 -55
- package/dist/conf/d.js +0 -3
- package/dist/conf/d.js.map +0 -1
- package/src/Group.test.ts +0 -58
- package/src/User.test.ts +0 -61
- package/src/conf/d.ts +0 -303
package/dist/User.js
CHANGED
|
@@ -1,1210 +1,2110 @@
|
|
|
1
|
-
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const strtotime_1 = __importDefault(require("locutus/php/datetime/strtotime"));
|
|
7
|
-
const enc_base64_1 = __importDefault(require("crypto-js/enc-base64"));
|
|
8
|
-
const sha256_1 = __importDefault(require("crypto-js/sha256"));
|
|
9
|
-
const md5_1 = __importDefault(require("crypto-js/md5"));
|
|
10
|
-
const lodash_1 = require("lodash");
|
|
11
|
-
const guid_1 = require("@nymphjs/guid");
|
|
12
|
-
const AbleObject_1 = __importDefault(require("./AbleObject"));
|
|
13
|
-
const errors_1 = require("./errors");
|
|
14
|
-
class User extends AbleObject_1.default {
|
|
15
|
-
static async factory(guid) {
|
|
16
|
-
return (await super.factory(guid));
|
|
17
|
-
}
|
|
18
|
-
static async factoryUsername(username) {
|
|
19
|
-
const entity = new this();
|
|
20
|
-
if (username != null) {
|
|
21
|
-
const entity = await this.nymph.getEntity({
|
|
22
|
-
class: this,
|
|
23
|
-
}, {
|
|
24
|
-
type: '&',
|
|
25
|
-
ilike: ['username', username.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
26
|
-
});
|
|
27
|
-
if (entity != null) {
|
|
28
|
-
return entity;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return entity;
|
|
32
|
-
}
|
|
33
|
-
static factorySync(guid) {
|
|
34
|
-
return super.factorySync(guid);
|
|
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);
|
|
35
5
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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] = _;
|
|
49
30
|
}
|
|
50
31
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import crypto from 'node:crypto';
|
|
36
|
+
import { transaction, transactional, } from '@nymphjs/nymph';
|
|
37
|
+
import { humanSecret, nanoid } from '@nymphjs/guid';
|
|
38
|
+
import { strtotime } from 'locutus/php/datetime/index';
|
|
39
|
+
import { difference, xor } from 'lodash-es';
|
|
40
|
+
import { TOTP, Secret } from 'otpauth';
|
|
41
|
+
import { toDataURL } from 'qrcode';
|
|
42
|
+
import { splitn } from '@sciactive/splitn';
|
|
43
|
+
import { enforceTilmeld } from './enforceTilmeld.js';
|
|
44
|
+
import AbleObject from './AbleObject.js';
|
|
45
|
+
import GroupClass from './Group.js';
|
|
46
|
+
import { AccessControlError, BadDataError, BadEmailError, BadUsernameError, EmailChangeRateLimitExceededError, } from './errors/index.js';
|
|
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.
|
|
83
|
+
'abilities',
|
|
84
|
+
// 'username',
|
|
85
|
+
// 'nameFirst',
|
|
86
|
+
// 'nameMiddle',
|
|
87
|
+
// 'nameLast',
|
|
88
|
+
// 'name',
|
|
89
|
+
'email',
|
|
90
|
+
// 'avatar',
|
|
91
|
+
'phone',
|
|
92
|
+
'group',
|
|
93
|
+
'groups',
|
|
94
|
+
'inheritAbilities',
|
|
95
|
+
'enabled',
|
|
96
|
+
'secret',
|
|
97
|
+
'emailChangeDate',
|
|
98
|
+
'newEmailSecret',
|
|
99
|
+
'newEmailAddress',
|
|
100
|
+
'cancelEmailSecret',
|
|
101
|
+
'cancelEmailAddress',
|
|
102
|
+
'recoverSecret',
|
|
103
|
+
'recoverSecretDate',
|
|
104
|
+
'salt',
|
|
105
|
+
'password',
|
|
106
|
+
'passwordTemp',
|
|
107
|
+
'revokeTokenDate',
|
|
108
|
+
'totpSecret',
|
|
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;
|
|
177
|
+
}
|
|
178
|
+
return super.getFTSText(name, value);
|
|
54
179
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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;
|
|
63
192
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
193
|
+
static current(returnObjectIfNotExist) {
|
|
194
|
+
const tilmeld = enforceTilmeld(this);
|
|
195
|
+
if (tilmeld.currentUser == null) {
|
|
196
|
+
return returnObjectIfNotExist ? this.factorySync() : null;
|
|
197
|
+
}
|
|
198
|
+
return tilmeld.currentUser;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Send an account recovery link.
|
|
202
|
+
*
|
|
203
|
+
* @param data The input data from the client.
|
|
204
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
205
|
+
*/
|
|
206
|
+
static async sendRecovery(data) {
|
|
207
|
+
const tilmeld = enforceTilmeld(this);
|
|
208
|
+
if (!tilmeld.config.pwRecovery) {
|
|
209
|
+
return { result: false, message: 'Account recovery is not allowed.' };
|
|
210
|
+
}
|
|
211
|
+
let user;
|
|
212
|
+
const options = {};
|
|
213
|
+
if (!tilmeld.config.emailUsernames && data.recoveryType === 'username') {
|
|
214
|
+
// Create a username recovery email.
|
|
215
|
+
const getUser = await this.nymph.getEntity({ class: tilmeld.User, skipAc: true }, {
|
|
216
|
+
type: '&',
|
|
217
|
+
ilike: ['email', data.account.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
218
|
+
});
|
|
219
|
+
if (getUser == null) {
|
|
220
|
+
return {
|
|
221
|
+
result: false,
|
|
222
|
+
message: 'Requested account is not accessible.',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
// Send the recovery email.
|
|
226
|
+
user = getUser;
|
|
227
|
+
options.template = 'RecoverUsername';
|
|
228
|
+
options.message = {
|
|
229
|
+
to: { name: user.name ?? '', address: user.email ?? '' },
|
|
230
|
+
};
|
|
231
|
+
options.locals = {};
|
|
232
|
+
}
|
|
233
|
+
else if (data.recoveryType === 'password') {
|
|
234
|
+
// Create a password recovery email.
|
|
235
|
+
const getUser = await tilmeld.User.factoryUsername(data.account);
|
|
236
|
+
if (getUser.guid == null) {
|
|
237
|
+
return {
|
|
238
|
+
result: false,
|
|
239
|
+
message: 'Requested account is not accessible.',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Create a unique secret.
|
|
243
|
+
getUser.recoverSecret = humanSecret();
|
|
244
|
+
getUser.recoverSecretDate = Date.now();
|
|
245
|
+
try {
|
|
246
|
+
await getUser.$saveSkipAC();
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return { result: false, message: "Couldn't save recovery secret." };
|
|
250
|
+
}
|
|
251
|
+
// Send the recovery email.
|
|
252
|
+
user = getUser;
|
|
253
|
+
options.template = 'RecoverPassword';
|
|
254
|
+
options.message = {
|
|
255
|
+
to: { name: user.name ?? '', address: user.email ?? '' },
|
|
256
|
+
};
|
|
257
|
+
options.locals = {
|
|
258
|
+
recoverCode: getUser.recoverSecret,
|
|
259
|
+
timeLimit: tilmeld.config.pwRecoveryTimeLimit,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
return { result: false, message: 'Invalid recovery type.' };
|
|
264
|
+
}
|
|
265
|
+
this.nymph.config.debugLog('tilmeld', `User account recovery (${options.template}) request for "${user.username}".`);
|
|
266
|
+
// Send the email.
|
|
267
|
+
if (await tilmeld.config.sendEmail(tilmeld, options, user)) {
|
|
73
268
|
return {
|
|
74
|
-
result:
|
|
75
|
-
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.',
|
|
76
272
|
};
|
|
77
273
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
to: {
|
|
82
|
-
name: user.name ?? '',
|
|
83
|
-
address: user.email ?? '',
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
options.locals = {};
|
|
274
|
+
else {
|
|
275
|
+
return { result: false, message: "Couldn't send recovery email." };
|
|
276
|
+
}
|
|
87
277
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Recover account details.
|
|
280
|
+
*
|
|
281
|
+
* @param data The input data from the client.
|
|
282
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
283
|
+
*/
|
|
284
|
+
static async recover(data) {
|
|
285
|
+
const tilmeld = enforceTilmeld(this);
|
|
286
|
+
if (!tilmeld.config.pwRecovery) {
|
|
287
|
+
return { result: false, message: 'Account recovery is not allowed.' };
|
|
288
|
+
}
|
|
289
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
290
|
+
if (user.guid == null ||
|
|
291
|
+
user.recoverSecret == null ||
|
|
292
|
+
data.secret !== user.recoverSecret ||
|
|
293
|
+
(strtotime('+' + tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) || 0) *
|
|
294
|
+
1000 <
|
|
295
|
+
Date.now()) {
|
|
296
|
+
return { result: false, message: 'The secret code does not match.' };
|
|
297
|
+
}
|
|
298
|
+
if (!('password' in data) || !data.password.length) {
|
|
299
|
+
return { result: false, message: 'Password cannot be empty.' };
|
|
300
|
+
}
|
|
301
|
+
this.nymph.config.debugLog('tilmeld', `User account recovery for "${user.username}".`);
|
|
302
|
+
user.$password(data.password);
|
|
303
|
+
delete user.recoverSecret;
|
|
304
|
+
delete user.recoverSecretDate;
|
|
305
|
+
try {
|
|
306
|
+
await user.$saveSkipAC();
|
|
91
307
|
return {
|
|
92
|
-
result:
|
|
93
|
-
message: '
|
|
308
|
+
result: true,
|
|
309
|
+
message: 'Your password has been reset. You can now log in using your new password.',
|
|
94
310
|
};
|
|
95
311
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return { result: false, message: "Couldn't save recovery secret." };
|
|
100
|
-
}
|
|
101
|
-
user = getUser;
|
|
102
|
-
options.template = 'RecoverPassword';
|
|
103
|
-
options.message = {
|
|
104
|
-
to: {
|
|
105
|
-
name: user.name ?? '',
|
|
106
|
-
address: user.email ?? '',
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
options.locals = {
|
|
110
|
-
recoverCode: getUser.recoverSecret,
|
|
111
|
-
timeLimit: this.tilmeld.config.pwRecoveryTimeLimit,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
return { result: false, message: 'Invalid recovery type.' };
|
|
116
|
-
}
|
|
117
|
-
if (await this.tilmeld.config.sendEmail(this.tilmeld, options, user)) {
|
|
118
|
-
return {
|
|
119
|
-
result: true,
|
|
120
|
-
message: "We've sent an email to your registered address. Please " +
|
|
121
|
-
'check your email to continue with account recovery.',
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
return { result: false, message: "Couldn't send recovery email." };
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
static async recover(data) {
|
|
129
|
-
if (!this.tilmeld.config.pwRecovery) {
|
|
130
|
-
return {
|
|
131
|
-
result: false,
|
|
132
|
-
message: 'Account recovery is not allowed.',
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
const user = await this.tilmeld.User.factoryUsername(data.username);
|
|
136
|
-
if (user.guid == null ||
|
|
137
|
-
user.recoverSecret == null ||
|
|
138
|
-
data.secret !== user.recoverSecret ||
|
|
139
|
-
(0, strtotime_1.default)('+' + this.tilmeld.config.pwRecoveryTimeLimit, Math.floor((user.recoverSecretDate ?? 0) / 1000)) *
|
|
140
|
-
1000 <
|
|
141
|
-
Date.now()) {
|
|
142
|
-
return {
|
|
143
|
-
result: false,
|
|
144
|
-
message: 'The secret code does not match.',
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
if (!('password' in data) || !data.password.length) {
|
|
148
|
-
return { result: false, message: 'Password cannot be empty.' };
|
|
312
|
+
catch (e) {
|
|
313
|
+
return { result: false, message: 'Error saving new password.' };
|
|
314
|
+
}
|
|
149
315
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
delete user.recoverSecretDate;
|
|
153
|
-
if (await user.$saveSkipAC()) {
|
|
316
|
+
static getClientConfig() {
|
|
317
|
+
const tilmeld = enforceTilmeld(this);
|
|
154
318
|
return {
|
|
155
|
-
|
|
156
|
-
|
|
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,
|
|
157
328
|
};
|
|
158
329
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
330
|
+
static async loginUser(data) {
|
|
331
|
+
const tilmeld = enforceTilmeld(this);
|
|
332
|
+
if (!('username' in data) || !data.username.length) {
|
|
333
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
334
|
+
}
|
|
335
|
+
const user = await tilmeld.User.factoryUsername(data.username);
|
|
336
|
+
const result = await user.$login(data);
|
|
337
|
+
if (result.result) {
|
|
338
|
+
user.$updateDataProtection();
|
|
339
|
+
result.user = user;
|
|
340
|
+
}
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
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
|
+
});
|
|
194
371
|
}
|
|
195
|
-
|
|
196
|
-
|
|
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();
|
|
197
380
|
}
|
|
198
|
-
|
|
199
|
-
|
|
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;
|
|
200
392
|
}
|
|
201
|
-
|
|
393
|
+
async $login(data) {
|
|
394
|
+
const tilmeld = enforceTilmeld(this);
|
|
395
|
+
if (this.guid == null) {
|
|
396
|
+
await tilmeld.config.failedLoginAttempt(null, data);
|
|
397
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
398
|
+
}
|
|
399
|
+
if (!this.$checkPassword(data.password)) {
|
|
400
|
+
await tilmeld.config.failedLoginAttempt(this, data);
|
|
401
|
+
return { result: false, message: 'Incorrect login/password.' };
|
|
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.' };
|
|
439
|
+
}
|
|
202
440
|
for (let callback of this.constructor
|
|
203
|
-
.
|
|
441
|
+
.afterLoginCallbacks) {
|
|
204
442
|
if (callback) {
|
|
205
|
-
await callback(this
|
|
443
|
+
await callback(this);
|
|
206
444
|
}
|
|
207
445
|
}
|
|
446
|
+
// Login was successful.
|
|
447
|
+
return { result: true, message: 'You are logged in.' };
|
|
208
448
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
message:
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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.' };
|
|
222
494
|
}
|
|
223
|
-
}
|
|
224
|
-
return { result: true, message: 'You are logged in.' };
|
|
225
|
-
}
|
|
226
|
-
async $logout() {
|
|
227
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
228
|
-
try {
|
|
229
495
|
for (let callback of this.constructor
|
|
230
|
-
.
|
|
496
|
+
.afterLoginCallbacks) {
|
|
231
497
|
if (callback) {
|
|
232
498
|
await callback(this);
|
|
233
499
|
}
|
|
234
500
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
501
|
+
// Login was successful.
|
|
502
|
+
return { result: true, message: 'You are logged in.' };
|
|
503
|
+
}
|
|
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
|
+
}
|
|
247
517
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
$getAvatar() {
|
|
252
|
-
if (this.$data.avatar != null && this.$data.avatar !== '') {
|
|
253
|
-
return this.$data.avatar;
|
|
254
|
-
}
|
|
255
|
-
if (this.$data.email == null || this.$data.email === '') {
|
|
256
|
-
return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
|
|
257
|
-
}
|
|
258
|
-
return ('https://secure.gravatar.com/avatar/' +
|
|
259
|
-
(0, md5_1.default)(this.$data.email.trim().toLowerCase()).toString() +
|
|
260
|
-
'?d=identicon&s=40');
|
|
261
|
-
}
|
|
262
|
-
async $getDescendantGroups() {
|
|
263
|
-
if (this.$descendantGroups == null) {
|
|
264
|
-
this.$descendantGroups = [];
|
|
265
|
-
if (this.$data.group != null) {
|
|
266
|
-
this.$descendantGroups = await this.$data.group.$getDescendants();
|
|
518
|
+
catch (e) {
|
|
519
|
+
return { result: false, message: e.message };
|
|
267
520
|
}
|
|
268
|
-
|
|
269
|
-
|
|
521
|
+
this.$nymph.config.debugLog('tilmeld', `User logout for "${this.$data.username}".`);
|
|
522
|
+
await tilmeld.logout();
|
|
523
|
+
for (let callback of this.constructor
|
|
524
|
+
.afterLogoutCallbacks) {
|
|
525
|
+
if (callback) {
|
|
526
|
+
await callback(this);
|
|
527
|
+
}
|
|
270
528
|
}
|
|
529
|
+
return { result: true, message: 'You have been logged out.' };
|
|
271
530
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (this.$descendantGroups == null) {
|
|
276
|
-
this.$descendantGroups = [];
|
|
277
|
-
if (this.$data.group != null) {
|
|
278
|
-
this.$descendantGroups = this.$data.group.$getDescendantsSync();
|
|
531
|
+
$getAvatar() {
|
|
532
|
+
if (this.$data.avatar != null && this.$data.avatar !== '') {
|
|
533
|
+
return this.$data.avatar;
|
|
279
534
|
}
|
|
280
|
-
|
|
281
|
-
|
|
535
|
+
if (this.$data.email == null || this.$data.email === '') {
|
|
536
|
+
return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
|
|
282
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');
|
|
545
|
+
}
|
|
546
|
+
$getGid() {
|
|
547
|
+
if (this.$data.group) {
|
|
548
|
+
return this.$data.group.guid;
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
283
551
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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;
|
|
559
|
+
}
|
|
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;
|
|
295
576
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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);
|
|
606
|
+
}
|
|
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);
|
|
612
|
+
}
|
|
308
613
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
$updateDataProtection(givenUser) {
|
|
317
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
318
|
-
let user = givenUser ?? tilmeld.User.current();
|
|
319
|
-
this.$clientEnabledMethods = [...User.DEFAULT_CLIENT_ENABLED_METHODS];
|
|
320
|
-
this.$privateData = [...User.DEFAULT_PRIVATE_DATA];
|
|
321
|
-
this.$allowlistData = [...User.DEFAULT_ALLOWLIST_DATA];
|
|
322
|
-
if (tilmeld.config.emailUsernames) {
|
|
323
|
-
this.$privateData.push('username');
|
|
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);
|
|
324
620
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
}
|
|
335
651
|
}
|
|
336
|
-
|
|
337
|
-
(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
this.$
|
|
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();
|
|
341
666
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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];
|
|
678
|
+
// Lowercase the domain part.
|
|
679
|
+
if (domain) {
|
|
680
|
+
domain = domain.toLowerCase();
|
|
345
681
|
}
|
|
346
|
-
this.$
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
this.$
|
|
351
|
-
this.$allowlistData.push('name');
|
|
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');
|
|
352
687
|
}
|
|
353
|
-
|
|
354
|
-
|
|
688
|
+
const isCurrentUser = user != null && user.$is(this);
|
|
689
|
+
// While loading, accessing abilities on the current user will infinitely
|
|
690
|
+
// loop, but if this is the current user, we can use its abilities.
|
|
691
|
+
const abilities = (isCurrentUser || user == null
|
|
692
|
+
? this.$data.abilities
|
|
693
|
+
: user?.abilities) ?? [];
|
|
694
|
+
const isNewUser = this.guid == null;
|
|
695
|
+
const isDomainUser = tilmeld.config.domainSupport &&
|
|
696
|
+
!isCurrentUser &&
|
|
697
|
+
user != null &&
|
|
698
|
+
domain &&
|
|
699
|
+
abilities.includes(`tilmeld/domain/${domain}/admin`);
|
|
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');
|
|
355
710
|
}
|
|
356
|
-
if (
|
|
357
|
-
|
|
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');
|
|
358
834
|
}
|
|
359
|
-
this.$privateData = [
|
|
360
|
-
'secret',
|
|
361
|
-
'newEmailSecret',
|
|
362
|
-
'cancelEmailAddress',
|
|
363
|
-
'cancelEmailSecret',
|
|
364
|
-
'emailChangeDate',
|
|
365
|
-
'recoverSecret',
|
|
366
|
-
'recoverSecretDate',
|
|
367
|
-
'password',
|
|
368
|
-
'salt',
|
|
369
|
-
];
|
|
370
835
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
836
|
+
$updateGroupDataProtection() {
|
|
837
|
+
if (this.$data.group != null && !this.$data.group.$asleep()) {
|
|
838
|
+
this.$data.group.$updateDataProtection();
|
|
839
|
+
}
|
|
840
|
+
for (let group of this.$data.groups ?? []) {
|
|
841
|
+
if (!group.$asleep()) {
|
|
842
|
+
group.$updateDataProtection();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
376
845
|
}
|
|
377
|
-
|
|
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();
|
|
378
878
|
let abilities = this.$data.abilities ?? [];
|
|
379
879
|
if (this.$data.inheritAbilities) {
|
|
880
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
380
881
|
for (let curGroup of this.$data.groups ?? []) {
|
|
381
882
|
if (curGroup.enabled) {
|
|
382
883
|
abilities = abilities.concat(curGroup.abilities ?? []);
|
|
383
884
|
}
|
|
384
885
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
abilities = abilities.concat(
|
|
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 ?? []);
|
|
389
890
|
}
|
|
390
891
|
}
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
return ((ability in this.$gatekeeperCache && !!this.$gatekeeperCache[ability]) ||
|
|
394
|
-
!!this.$gatekeeperCache['system/admin']);
|
|
395
|
-
}
|
|
396
|
-
$clearCache() {
|
|
397
|
-
const ret = super.$clearCache();
|
|
398
|
-
this.$descendantGroups = undefined;
|
|
399
|
-
this.$gatekeeperCache = undefined;
|
|
400
|
-
return ret;
|
|
401
|
-
}
|
|
402
|
-
async $sendEmailVerification() {
|
|
403
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
404
|
-
if (this.guid == null) {
|
|
405
|
-
return false;
|
|
892
|
+
return Object.fromEntries(abilities.map((ability) => [ability, true]));
|
|
406
893
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
894
|
+
$clearCache() {
|
|
895
|
+
const ret = super.$clearCache();
|
|
896
|
+
this.$descendantGroups = undefined;
|
|
897
|
+
this.$gatekeeperCache = undefined;
|
|
898
|
+
return ret;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Send the user email verification/change/cancellation links.
|
|
902
|
+
*
|
|
903
|
+
* @returns True on success, false on failure.
|
|
904
|
+
*/
|
|
905
|
+
async $sendEmailVerification() {
|
|
906
|
+
const tilmeld = enforceTilmeld(this);
|
|
907
|
+
if (this.guid == null) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
let success = true;
|
|
911
|
+
const verifyUrl = `${tilmeld.config.appUrl.replace(/\/$/, () => '')}${tilmeld.config.setupPath}/verify`;
|
|
912
|
+
if (this.$data.secret != null) {
|
|
913
|
+
const link = `${verifyUrl}?action=verify&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.secret)}`;
|
|
914
|
+
success =
|
|
915
|
+
success &&
|
|
916
|
+
(await tilmeld.config.sendEmail(tilmeld, {
|
|
917
|
+
template: 'VerifyEmail',
|
|
918
|
+
message: {
|
|
919
|
+
to: {
|
|
920
|
+
name: this.$data.name ?? '',
|
|
921
|
+
address: this.$data.email ?? '',
|
|
922
|
+
},
|
|
419
923
|
},
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
name: this.$data.name ?? '',
|
|
435
|
-
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
|
+
},
|
|
436
938
|
},
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
newEmail: this.$data.newEmailAddress,
|
|
442
|
-
},
|
|
443
|
-
}, this));
|
|
444
|
-
}
|
|
445
|
-
if (this.$data.cancelEmailSecret != null) {
|
|
446
|
-
const link = `${verifyUrl}?action=cancelchange&id=${encodeURIComponent(this.guid)}&secret=${encodeURIComponent(this.$data.cancelEmailSecret)}`;
|
|
447
|
-
success =
|
|
448
|
-
success &&
|
|
449
|
-
(await tilmeld.config.sendEmail(tilmeld, {
|
|
450
|
-
template: 'CancelEmailChange',
|
|
451
|
-
message: {
|
|
452
|
-
to: {
|
|
453
|
-
name: this.$data.name ?? '',
|
|
454
|
-
address: this.$data.cancelEmailAddress ?? '',
|
|
939
|
+
locals: {
|
|
940
|
+
verifyLink: link,
|
|
941
|
+
oldEmail: this.$data.cancelEmailAddress,
|
|
942
|
+
newEmail: this.$data.newEmailAddress,
|
|
455
943
|
},
|
|
456
|
-
},
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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;
|
|
966
|
+
}
|
|
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;
|
|
473
985
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
+
}
|
|
487
1008
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
+
}
|
|
495
1024
|
}
|
|
1025
|
+
this.$data.groups = newGroups;
|
|
1026
|
+
return this.$data.groups;
|
|
496
1027
|
}
|
|
497
|
-
|
|
498
|
-
return this.$data.groups;
|
|
1028
|
+
return true;
|
|
499
1029
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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))) {
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
await Promise.all(this.$data.groups?.map((e) => e.$wake()) || []);
|
|
507
1065
|
for (let curGroup of this.$data.groups ?? []) {
|
|
508
|
-
if (curGroup
|
|
1066
|
+
if (await curGroup.$isDescendant(group)) {
|
|
509
1067
|
return true;
|
|
510
1068
|
}
|
|
511
1069
|
}
|
|
512
1070
|
return false;
|
|
513
1071
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
return
|
|
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." };
|
|
524
1101
|
}
|
|
525
1102
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
+
}
|
|
1144
|
+
this.$data.revokeTokenDate = Date.now();
|
|
1145
|
+
try {
|
|
1146
|
+
await this.$save();
|
|
1147
|
+
const tilmeld = enforceTilmeld(this);
|
|
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." };
|
|
1156
|
+
}
|
|
541
1157
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
1158
|
+
$hasTOTPSecret() {
|
|
1159
|
+
return this.$data.totpSecret != null;
|
|
1160
|
+
}
|
|
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
|
+
}
|
|
1171
|
+
const tilmeld = enforceTilmeld(this);
|
|
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) {
|
|
1212
|
+
if (!this.$checkPassword(data.password ?? '')) {
|
|
1213
|
+
return { result: false, message: 'Incorrect password.' };
|
|
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
|
+
}
|
|
1223
|
+
if (data.code == null ||
|
|
1224
|
+
typeof data.code !== 'string' ||
|
|
1225
|
+
data.code.length !== 6) {
|
|
1226
|
+
return { result: false, message: '2FA code is invalid.' };
|
|
1227
|
+
}
|
|
1228
|
+
const tilmeld = enforceTilmeld(this);
|
|
1229
|
+
const totp = new TOTP({
|
|
1230
|
+
issuer: tilmeld.config.appName,
|
|
1231
|
+
label: this.$data.username,
|
|
1232
|
+
algorithm: 'SHA1',
|
|
1233
|
+
digits: 6,
|
|
1234
|
+
period: 30,
|
|
1235
|
+
secret: data.secret,
|
|
1236
|
+
});
|
|
1237
|
+
if (totp.validate({ token: data.code, window: 2 }) == null) {
|
|
1238
|
+
return { result: false, message: '2FA code is incorrect.' };
|
|
1239
|
+
}
|
|
1240
|
+
this.$data.totpSecret = data.secret;
|
|
1241
|
+
try {
|
|
1242
|
+
await this.$save();
|
|
1243
|
+
return { result: true, message: 'Your two factor secret is now set.' };
|
|
1244
|
+
}
|
|
1245
|
+
catch (e) {
|
|
1246
|
+
return { result: false, message: "Couldn't save two factor secret." };
|
|
1247
|
+
}
|
|
559
1248
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
else if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
570
1283
|
return {
|
|
571
1284
|
result: false,
|
|
572
|
-
message: '
|
|
573
|
-
tilmeld.config.maxUsernameLength +
|
|
574
|
-
' characters.',
|
|
1285
|
+
message: 'You must provide your password and 2FA code.',
|
|
575
1286
|
};
|
|
576
1287
|
}
|
|
577
|
-
|
|
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." };
|
|
1295
|
+
}
|
|
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
|
+
}
|
|
1384
|
+
return {
|
|
1385
|
+
result: true,
|
|
1386
|
+
message: this.guid != null ? 'Username is valid.' : 'Username is available!',
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
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) {
|
|
1395
|
+
return {
|
|
1396
|
+
result: false,
|
|
1397
|
+
message: 'Emails must be at least ' +
|
|
1398
|
+
tilmeld.config.minUsernameLength +
|
|
1399
|
+
' characters.',
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
if (this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1403
|
+
return {
|
|
1404
|
+
result: false,
|
|
1405
|
+
message: 'Emails must not exceed ' +
|
|
1406
|
+
tilmeld.config.maxUsernameLength +
|
|
1407
|
+
' characters.',
|
|
1408
|
+
};
|
|
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) {
|
|
578
1441
|
return {
|
|
579
1442
|
result: false,
|
|
580
|
-
message:
|
|
1443
|
+
message: 'That email address is already registered.',
|
|
581
1444
|
};
|
|
582
1445
|
}
|
|
583
|
-
|
|
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)) {
|
|
584
1464
|
return {
|
|
585
1465
|
result: false,
|
|
586
|
-
message:
|
|
1466
|
+
message: 'Phone must contain at least 10 digits, but formatting does not matter.',
|
|
587
1467
|
};
|
|
588
1468
|
}
|
|
589
1469
|
const selector = {
|
|
590
1470
|
type: '&',
|
|
591
|
-
|
|
592
|
-
'username',
|
|
593
|
-
this.$data.username.replace(/([\\%_])/g, (s) => `\\${s}`),
|
|
594
|
-
],
|
|
1471
|
+
equal: ['phone', stripToDigits],
|
|
595
1472
|
};
|
|
596
1473
|
if (this.guid != null) {
|
|
597
1474
|
selector['!guid'] = this.guid;
|
|
598
1475
|
}
|
|
599
1476
|
const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
|
|
600
1477
|
if (test != null) {
|
|
601
|
-
return { result: false, message: '
|
|
1478
|
+
return { result: false, message: 'Phone number is in use.' };
|
|
602
1479
|
}
|
|
603
1480
|
return {
|
|
604
1481
|
result: true,
|
|
605
|
-
message: this.guid != null ? '
|
|
1482
|
+
message: this.guid != null ? 'Phone number is valid.' : 'Phone number is valid!',
|
|
606
1483
|
};
|
|
607
1484
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if (
|
|
611
|
-
return {
|
|
1485
|
+
async $register(data) {
|
|
1486
|
+
let tilmeld = enforceTilmeld(this);
|
|
1487
|
+
if (!tilmeld.config.allowRegistration) {
|
|
1488
|
+
return {
|
|
1489
|
+
result: false,
|
|
1490
|
+
loggedin: false,
|
|
1491
|
+
message: 'Registration is not allowed.',
|
|
1492
|
+
};
|
|
612
1493
|
}
|
|
613
|
-
if (
|
|
614
|
-
this.$data.username.length > tilmeld.config.maxUsernameLength) {
|
|
1494
|
+
if (this.guid != null) {
|
|
615
1495
|
return {
|
|
616
1496
|
result: false,
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
' characters.',
|
|
1497
|
+
loggedin: false,
|
|
1498
|
+
message: 'This is already a registered user.',
|
|
620
1499
|
};
|
|
621
1500
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (tilmeld.config.verifyEmail) {
|
|
629
|
-
return { result: false, message: 'Please specify an email.' };
|
|
1501
|
+
if (!('password' in data) || !data.password.length) {
|
|
1502
|
+
return {
|
|
1503
|
+
result: false,
|
|
1504
|
+
loggedin: false,
|
|
1505
|
+
message: 'Password is a required field.',
|
|
1506
|
+
};
|
|
630
1507
|
}
|
|
631
|
-
|
|
632
|
-
|
|
1508
|
+
const unCheck = await this.$checkUsername();
|
|
1509
|
+
if (!unCheck.result) {
|
|
1510
|
+
return { ...unCheck, loggedin: false };
|
|
633
1511
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
type: '&',
|
|
643
|
-
ilike: ['email', this.$data.email.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
644
|
-
};
|
|
645
|
-
if (this.guid != null) {
|
|
646
|
-
selector['!guid'] = this.guid;
|
|
647
|
-
}
|
|
648
|
-
const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
|
|
649
|
-
if (test != null) {
|
|
650
|
-
return {
|
|
651
|
-
result: false,
|
|
652
|
-
message: 'That email address is already registered.',
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
return {
|
|
656
|
-
result: true,
|
|
657
|
-
message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
async $checkPhone() {
|
|
661
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
662
|
-
if (this.$data.phone == null || !this.$data.phone.length) {
|
|
663
|
-
return { result: false, message: 'Please specify a phone number.' };
|
|
664
|
-
}
|
|
665
|
-
const stripToDigits = this.$data.phone.replace(/\D/g, () => '');
|
|
666
|
-
if (!/\d{10}/.test(stripToDigits)) {
|
|
667
|
-
return {
|
|
668
|
-
result: false,
|
|
669
|
-
message: 'Phone must contain at least 10 digits, but formatting does not matter.',
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
const selector = {
|
|
673
|
-
type: '&',
|
|
674
|
-
equal: ['phone', stripToDigits],
|
|
675
|
-
};
|
|
676
|
-
if (this.guid != null) {
|
|
677
|
-
selector['!guid'] = this.guid;
|
|
678
|
-
}
|
|
679
|
-
const test = await this.$nymph.getEntity({ class: tilmeld.User, skipAc: true }, selector);
|
|
680
|
-
if (test != null) {
|
|
681
|
-
return { result: false, message: 'Phone number is in use.' };
|
|
682
|
-
}
|
|
683
|
-
return {
|
|
684
|
-
result: true,
|
|
685
|
-
message: this.guid != null ? 'Phone number is valid.' : 'Phone number is valid!',
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
async $register(data) {
|
|
689
|
-
let tilmeld = this.$nymph.tilmeld;
|
|
690
|
-
if (!tilmeld.config.allowRegistration) {
|
|
691
|
-
return {
|
|
692
|
-
result: false,
|
|
693
|
-
loggedin: false,
|
|
694
|
-
message: 'Registration is not allowed.',
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
if (this.guid != null) {
|
|
698
|
-
return {
|
|
699
|
-
result: false,
|
|
700
|
-
loggedin: false,
|
|
701
|
-
message: 'This is already a registered user.',
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
try {
|
|
705
|
-
for (let callback of this.constructor
|
|
706
|
-
.beforeRegisterCallbacks) {
|
|
707
|
-
if (callback) {
|
|
708
|
-
await callback(this, data);
|
|
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;
|
|
709
1520
|
}
|
|
710
1521
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return {
|
|
714
|
-
result: false,
|
|
715
|
-
loggedin: false,
|
|
716
|
-
message: e.message,
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
if (!('password' in data) || !data.password.length) {
|
|
720
|
-
return {
|
|
721
|
-
result: false,
|
|
722
|
-
loggedin: false,
|
|
723
|
-
message: 'Password is a required field.',
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
const unCheck = await this.$checkUsername();
|
|
727
|
-
if (!unCheck.result) {
|
|
728
|
-
return { ...unCheck, loggedin: false };
|
|
729
|
-
}
|
|
730
|
-
this.$password(data.password);
|
|
731
|
-
if (tilmeld.config.regFields.indexOf('name') !== -1) {
|
|
732
|
-
this.$data.name =
|
|
733
|
-
this.$data.nameFirst +
|
|
734
|
-
(this.$data.nameMiddle == null ? '' : ' ' + this.$data.nameMiddle) +
|
|
735
|
-
(this.$data.nameLast == null ? '' : ' ' + this.$data.nameLast);
|
|
736
|
-
if (this.$data.name === '') {
|
|
737
|
-
this.$data.name = this.$data.username;
|
|
1522
|
+
if (tilmeld.config.emailUsernames) {
|
|
1523
|
+
this.$data.email = this.$data.username;
|
|
738
1524
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1525
|
+
try {
|
|
1526
|
+
const result = await transaction(this.$nymph, 'tilmeld-register-' + nanoid(), async (nymph) => {
|
|
1527
|
+
this.$setNymph(nymph);
|
|
1528
|
+
const tilmeld = enforceTilmeld(this);
|
|
1529
|
+
let message = '';
|
|
1530
|
+
let loggedin = false;
|
|
1531
|
+
for (let callback of this.constructor
|
|
1532
|
+
.beforeRegisterCallbacks) {
|
|
1533
|
+
if (callback) {
|
|
1534
|
+
await callback(this, data);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
if (tilmeld.config.userFields.includes('email') &&
|
|
1538
|
+
tilmeld.config.verifyEmail) {
|
|
1539
|
+
// The user will be enabled after verifying their e-mail address.
|
|
1540
|
+
if (!tilmeld.config.unverifiedAccess) {
|
|
1541
|
+
this.$data.enabled = false;
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
else {
|
|
1545
|
+
this.$data.enabled = true;
|
|
1546
|
+
}
|
|
1547
|
+
// If createAdmin is true and there are no other users, grant
|
|
1548
|
+
// "system/admin".
|
|
1549
|
+
let madeAdmin = false;
|
|
1550
|
+
if (tilmeld.config.createAdmin) {
|
|
1551
|
+
const otherUsers = await this.$nymph.getEntities({
|
|
1552
|
+
class: tilmeld.User,
|
|
1553
|
+
skipAc: true,
|
|
1554
|
+
limit: 1,
|
|
1555
|
+
return: 'guid',
|
|
1556
|
+
});
|
|
1557
|
+
// Make sure it's not just null, cause that means an error.
|
|
1558
|
+
if (otherUsers == null) {
|
|
1559
|
+
throw new Error('An error occurred.');
|
|
1560
|
+
}
|
|
1561
|
+
if (!otherUsers.length) {
|
|
1562
|
+
this.$grant('system/admin');
|
|
1563
|
+
this.$data.enabled = true;
|
|
1564
|
+
madeAdmin = true;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
this.$nymph.config.debugLog('tilmeld', `Registering new user "${this.$data.username}".`);
|
|
1568
|
+
await this.$saveSkipAC();
|
|
1569
|
+
// Finish up.
|
|
1570
|
+
if (tilmeld.config.verifyEmail &&
|
|
1571
|
+
!tilmeld.config.unverifiedAccess &&
|
|
1572
|
+
!madeAdmin &&
|
|
1573
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1574
|
+
message +=
|
|
1575
|
+
`Almost there. An email has been sent to ${this.$data.email} ` +
|
|
1576
|
+
'with a verification link for you to finish registration.';
|
|
1577
|
+
}
|
|
1578
|
+
else if (tilmeld.config.verifyEmail &&
|
|
1579
|
+
tilmeld.config.unverifiedAccess &&
|
|
1580
|
+
!madeAdmin &&
|
|
1581
|
+
tilmeld.config.userFields.includes('email')) {
|
|
1582
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1583
|
+
throw new Error('An error occurred trying to log you in.');
|
|
1584
|
+
}
|
|
1585
|
+
message +=
|
|
1586
|
+
"You're now logged in! An email has been sent to " +
|
|
1587
|
+
`${this.$data.email} with a verification link for you to finish ` +
|
|
1588
|
+
'registration.';
|
|
1589
|
+
loggedin = true;
|
|
1590
|
+
}
|
|
1591
|
+
else {
|
|
1592
|
+
if (!(await tilmeld.login(this, true))) {
|
|
1593
|
+
throw new Error('An error occurred trying to log you in.');
|
|
1594
|
+
}
|
|
1595
|
+
message += "You're now registered and logged in!";
|
|
1596
|
+
loggedin = true;
|
|
1597
|
+
}
|
|
1598
|
+
for (let callback of this.constructor
|
|
1599
|
+
.afterRegisterCallbacks) {
|
|
1600
|
+
if (callback) {
|
|
1601
|
+
await callback(this, { loggedin, message });
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return { result: true, loggedin, message };
|
|
1605
|
+
}, async (nymph) => {
|
|
1606
|
+
this.$setNymph(nymph);
|
|
759
1607
|
});
|
|
760
|
-
|
|
761
|
-
|
|
1608
|
+
// Send the new user registered email.
|
|
1609
|
+
if (tilmeld.config.userRegisteredRecipient != null) {
|
|
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
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
catch (e) {
|
|
1627
|
+
this.$nymph.config.debugError('tilmeld', `Couldn't send new user registered email for "${this.$data.username}": ${e.message}`);
|
|
1628
|
+
}
|
|
762
1629
|
}
|
|
763
|
-
if (
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
+
};
|
|
1640
|
+
}
|
|
771
1641
|
}
|
|
772
|
-
|
|
1642
|
+
return result;
|
|
773
1643
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1644
|
+
catch (e) {
|
|
1645
|
+
return { result: false, loggedin: false, message: e.message };
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
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.');
|
|
1652
|
+
}
|
|
1653
|
+
if (!this.$skipAcWhenSaving &&
|
|
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()}`;
|
|
781
1664
|
}
|
|
782
1665
|
}
|
|
783
|
-
if (tilmeld.config.
|
|
784
|
-
this.$data.
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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.");
|
|
1673
|
+
}
|
|
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
|
+
}
|
|
1688
|
+
}
|
|
788
1689
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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;
|
|
1695
|
+
}
|
|
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;
|
|
794
1739
|
}
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
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);
|
|
798
1783
|
}
|
|
799
1784
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
+
}
|
|
1799
|
+
}
|
|
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.');
|
|
1810
|
+
}
|
|
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;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
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;
|
|
815
1849
|
}
|
|
816
1850
|
}
|
|
817
|
-
if (
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
return {
|
|
842
|
-
result: false,
|
|
843
|
-
loggedin: false,
|
|
844
|
-
message: 'Your primary group could not be assigned. Please try again later.',
|
|
845
|
-
};
|
|
1851
|
+
if (this.$data.passwordTemp != null) {
|
|
1852
|
+
this.$password(this.$data.passwordTemp);
|
|
1853
|
+
}
|
|
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
|
+
}
|
|
846
1875
|
}
|
|
847
1876
|
}
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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
|
+
}
|
|
860
1893
|
}
|
|
861
|
-
message +=
|
|
862
|
-
"You're now logged in! An email has been sent to " +
|
|
863
|
-
`${this.$data.email} with a verification link for you to finish ` +
|
|
864
|
-
'registration.';
|
|
865
|
-
loggedin = true;
|
|
866
1894
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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;
|
|
870
1910
|
}
|
|
871
|
-
|
|
872
|
-
|
|
1911
|
+
if (tilmeld.config.userFields.includes('name') &&
|
|
1912
|
+
group.name !== this.$data.name) {
|
|
1913
|
+
group.name = this.$data.name;
|
|
1914
|
+
changed = true;
|
|
1915
|
+
}
|
|
1916
|
+
if (tilmeld.config.userFields.includes('phone') &&
|
|
1917
|
+
group.phone !== this.$data.phone) {
|
|
1918
|
+
group.phone = this.$data.phone;
|
|
1919
|
+
changed = true;
|
|
1920
|
+
}
|
|
1921
|
+
if (changed || group.guid == null) {
|
|
1922
|
+
await group.$saveSkipAC();
|
|
1923
|
+
}
|
|
1924
|
+
this.$data.group = group;
|
|
873
1925
|
}
|
|
874
|
-
|
|
875
|
-
.
|
|
876
|
-
if (
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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] });
|
|
881
1938
|
}
|
|
882
1939
|
}
|
|
883
1940
|
try {
|
|
884
|
-
|
|
885
|
-
this.$nymph = nymph;
|
|
886
|
-
this.$nymph.tilmeld?.fillSession(this);
|
|
1941
|
+
tilmeld.config.validatorUser(tilmeld, this);
|
|
887
1942
|
}
|
|
888
1943
|
catch (e) {
|
|
889
|
-
throw e;
|
|
1944
|
+
throw new BadDataError(e?.message);
|
|
890
1945
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
else {
|
|
898
|
-
await tnymph.rollback(transaction);
|
|
899
|
-
this.$nymph = nymph;
|
|
900
|
-
return {
|
|
901
|
-
result: false,
|
|
902
|
-
loggedin: false,
|
|
903
|
-
message: 'Error registering user.',
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
catch (e) {
|
|
908
|
-
await tnymph.rollback(transaction);
|
|
909
|
-
this.$nymph = nymph;
|
|
910
|
-
throw e;
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
async $save() {
|
|
914
|
-
let tilmeld = this.$nymph.tilmeld;
|
|
915
|
-
if (this.$data.username == null || !this.$data.username.trim().length) {
|
|
916
|
-
return false;
|
|
917
|
-
}
|
|
918
|
-
if (tilmeld.gatekeeper('tilmeld/admin') &&
|
|
919
|
-
!tilmeld.gatekeeper('system/admin') &&
|
|
920
|
-
this.$gatekeeper('system/admin')) {
|
|
921
|
-
throw new errors_1.BadDataError("You don't have the authority to modify system admins.");
|
|
922
|
-
}
|
|
923
|
-
let sendVerification = false;
|
|
924
|
-
this.$data.username = this.$data.username.trim();
|
|
925
|
-
if (tilmeld.config.emailUsernames) {
|
|
926
|
-
this.$data.email = this.$data.username;
|
|
927
|
-
}
|
|
928
|
-
this.$data.nameFirst = (this.$data.nameFirst ?? '').trim();
|
|
929
|
-
this.$data.nameMiddle = (this.$data.nameMiddle ?? '').trim();
|
|
930
|
-
this.$data.nameLast = (this.$data.nameLast ?? '').trim();
|
|
931
|
-
this.$data.phone = (this.$data.phone ?? '').trim();
|
|
932
|
-
this.$data.name =
|
|
933
|
-
this.$data.nameFirst +
|
|
934
|
-
(this.$data.nameMiddle ? ' ' + this.$data.nameMiddle : '') +
|
|
935
|
-
(this.$data.nameLast ? ' ' + this.$data.nameLast : '');
|
|
936
|
-
this.$data.enabled = !!this.$data.enabled;
|
|
937
|
-
if (this.$data.nameFirst === '') {
|
|
938
|
-
delete this.$data.nameFirst;
|
|
939
|
-
}
|
|
940
|
-
if (this.$data.nameMiddle === '') {
|
|
941
|
-
delete this.$data.nameMiddle;
|
|
942
|
-
}
|
|
943
|
-
if (this.$data.nameLast === '') {
|
|
944
|
-
delete this.$data.nameLast;
|
|
945
|
-
}
|
|
946
|
-
if (this.$data.name === '') {
|
|
947
|
-
delete this.$data.name;
|
|
948
|
-
}
|
|
949
|
-
if (this.$data.email === '') {
|
|
950
|
-
delete this.$data.email;
|
|
951
|
-
}
|
|
952
|
-
if (this.$data.avatar === '') {
|
|
953
|
-
delete this.$data.avatar;
|
|
954
|
-
}
|
|
955
|
-
if (this.$data.phone === '') {
|
|
956
|
-
delete this.$data.phone;
|
|
957
|
-
}
|
|
958
|
-
if (this.$data.secret === '') {
|
|
959
|
-
delete this.$data.secret;
|
|
960
|
-
}
|
|
961
|
-
if (this.$data.emailChangeDate === 0) {
|
|
962
|
-
delete this.$data.emailChangeDate;
|
|
963
|
-
}
|
|
964
|
-
if (this.$data.newEmailSecret === '') {
|
|
965
|
-
delete this.$data.newEmailSecret;
|
|
966
|
-
}
|
|
967
|
-
if (this.$data.newEmailAddress === '') {
|
|
968
|
-
delete this.$data.newEmailAddress;
|
|
969
|
-
}
|
|
970
|
-
if (this.$data.cancelEmailSecret === '') {
|
|
971
|
-
delete this.$data.cancelEmailSecret;
|
|
972
|
-
}
|
|
973
|
-
if (this.$data.cancelEmailAddress === '') {
|
|
974
|
-
delete this.$data.cancelEmailAddress;
|
|
975
|
-
}
|
|
976
|
-
if (this.$data.recoverSecret === '') {
|
|
977
|
-
delete this.$data.recoverSecret;
|
|
978
|
-
}
|
|
979
|
-
if (this.$data.recoverSecretDate === 0) {
|
|
980
|
-
delete this.$data.recoverSecretDate;
|
|
981
|
-
}
|
|
982
|
-
if (this.$data.password === '') {
|
|
983
|
-
delete this.$data.password;
|
|
984
|
-
}
|
|
985
|
-
if (this.$data.passwordTemp === '') {
|
|
986
|
-
delete this.$data.passwordTemp;
|
|
987
|
-
}
|
|
988
|
-
if (this.$data.inheritAbilities === false) {
|
|
989
|
-
delete this.$data.inheritAbilities;
|
|
990
|
-
}
|
|
991
|
-
const unCheck = await this.$checkUsername();
|
|
992
|
-
if (!unCheck.result) {
|
|
993
|
-
throw new errors_1.BadUsernameError(unCheck.message);
|
|
994
|
-
}
|
|
995
|
-
if (!tilmeld.config.emailUsernames) {
|
|
996
|
-
const emCheck = await this.$checkEmail();
|
|
997
|
-
if (!emCheck.result) {
|
|
998
|
-
throw new errors_1.BadEmailError(emCheck.message);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1002
|
-
if (tilmeld.config.verifyEmail) {
|
|
1003
|
-
if (this.guid == null) {
|
|
1004
|
-
if ((this.$data.abilities?.indexOf('system/admin') ?? -1) === -1) {
|
|
1005
|
-
this.$data.secret = (0, guid_1.nanoid)();
|
|
1006
|
-
sendVerification = true;
|
|
1946
|
+
for (let callback of this.constructor
|
|
1947
|
+
.beforeSaveCallbacks) {
|
|
1948
|
+
if (callback) {
|
|
1949
|
+
await callback(this);
|
|
1007
1950
|
}
|
|
1008
1951
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
(0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000) {
|
|
1015
|
-
throw new errors_1.EmailChangeRateLimitExceededError('You already changed your email address recently. Please wait until ' +
|
|
1016
|
-
new Date((0, strtotime_1.default)('+' + tilmeld.config.emailRateLimit, Math.floor(this.$data.emailChangeDate / 1000)) * 1000).toString() +
|
|
1017
|
-
' to change your email address again.');
|
|
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.");
|
|
1018
1957
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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);
|
|
1033
1977
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1978
|
+
}
|
|
1979
|
+
catch (e) {
|
|
1980
|
+
// Ignore.
|
|
1036
1981
|
}
|
|
1037
1982
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
(0, strtotime_1.default)('-' + tilmeld.config.emailRateLimit) * 1000)) {
|
|
1045
|
-
this.$data.cancelEmailAddress = this.$originalEmail;
|
|
1046
|
-
this.$data.cancelEmailSecret = (0, guid_1.nanoid)();
|
|
1047
|
-
sendVerification = true;
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
if (this.$data.passwordTemp != null) {
|
|
1051
|
-
this.$password(this.$data.passwordTemp);
|
|
1052
|
-
}
|
|
1053
|
-
delete this.$data.passwordTemp;
|
|
1054
|
-
if (this.$data.password == null) {
|
|
1055
|
-
throw new errors_1.BadDataError('A password is required.');
|
|
1983
|
+
else {
|
|
1984
|
+
this.guid = preGuid;
|
|
1985
|
+
this.cdate = preCdate;
|
|
1986
|
+
this.mdate = preMdate;
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1056
1989
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1990
|
+
/**
|
|
1991
|
+
* This should *never* be accessible on the client.
|
|
1992
|
+
*/
|
|
1993
|
+
async $saveSkipAC() {
|
|
1994
|
+
this.$skipAcWhenSaving = true;
|
|
1995
|
+
await this.$save();
|
|
1059
1996
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1997
|
+
$tilmeldSaveSkipAC() {
|
|
1998
|
+
if (this.$skipAcWhenSaving) {
|
|
1999
|
+
this.$skipAcWhenSaving = false;
|
|
2000
|
+
return true;
|
|
2001
|
+
}
|
|
2002
|
+
return false;
|
|
1062
2003
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
tilmeld = this.$nymph.tilmeld;
|
|
1068
|
-
if (this.$data.group != null &&
|
|
1069
|
-
this.$data.group.user != null &&
|
|
1070
|
-
this.$is(this.$data.group.user)) {
|
|
1071
|
-
try {
|
|
1072
|
-
this.$data.group.groupname = this.$data.username;
|
|
1073
|
-
this.$data.group.avatar = this.$data.avatar;
|
|
1074
|
-
this.$data.group.email = this.$data.email;
|
|
1075
|
-
this.$data.group.name = this.$data.name;
|
|
1076
|
-
this.$data.group.phone = this.$data.phone;
|
|
1077
|
-
await this.$data.group.$saveSkipAC();
|
|
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.");
|
|
1078
2008
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
this.$
|
|
1082
|
-
throw
|
|
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.");
|
|
1083
2013
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
if (sendVerification) {
|
|
1099
|
-
if (!(await this.$sendEmailVerification())) {
|
|
1100
|
-
await tnymph.rollback(transaction);
|
|
1101
|
-
this.guid = preGuid;
|
|
1102
|
-
this.cdate = preCdate;
|
|
1103
|
-
this.mdate = preMdate;
|
|
1104
|
-
this.$nymph = nymph;
|
|
1105
|
-
throw new Error("Couldn't send verification email.");
|
|
2014
|
+
if (this.$data.username !== this.$originalUsername) {
|
|
2015
|
+
throw new AccessControlError("You can't delete a user while switching their username.");
|
|
2016
|
+
}
|
|
2017
|
+
if (!(await this.$refresh())) {
|
|
2018
|
+
throw new BadDataError('User could not be refreshed.');
|
|
2019
|
+
}
|
|
2020
|
+
if (this.$data.group != null) {
|
|
2021
|
+
if (this.$data.group.$asleep()) {
|
|
2022
|
+
await this.$data.group.$wake();
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
if (!(await this.$data.group.$refresh())) {
|
|
2026
|
+
throw new BadDataError('Group could not be refreshed.');
|
|
2027
|
+
}
|
|
1106
2028
|
}
|
|
1107
2029
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
2030
|
+
for (let callback of this.constructor
|
|
2031
|
+
.beforeDeleteCallbacks) {
|
|
2032
|
+
if (callback) {
|
|
2033
|
+
await callback(this);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (tilmeld.User.current(true).$is(this)) {
|
|
2037
|
+
await this.$logout();
|
|
2038
|
+
}
|
|
2039
|
+
if (this.$data.group != null && this.$is(this.$data.group.user)) {
|
|
2040
|
+
// Read the skip ac value here, because super.$delete changes it.
|
|
2041
|
+
const $skipAcWhenDeleting = this.$skipAcWhenDeleting;
|
|
2042
|
+
// Delete the user.
|
|
2043
|
+
await super.$delete();
|
|
2044
|
+
// Delete the generated group.
|
|
2045
|
+
if ($skipAcWhenDeleting) {
|
|
2046
|
+
await this.$data.group.$deleteSkipAC();
|
|
2047
|
+
}
|
|
2048
|
+
else {
|
|
2049
|
+
await this.$data.group.$delete();
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
else {
|
|
2053
|
+
await super.$delete();
|
|
2054
|
+
}
|
|
2055
|
+
for (let callback of this.constructor
|
|
2056
|
+
.afterDeleteCallbacks) {
|
|
2057
|
+
if (callback) {
|
|
2058
|
+
await callback(this);
|
|
2059
|
+
}
|
|
1113
2060
|
}
|
|
1114
2061
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
async $saveSkipAC() {
|
|
1122
|
-
this.$skipAcWhenSaving = true;
|
|
1123
|
-
return await this.$save();
|
|
1124
|
-
}
|
|
1125
|
-
$tilmeldSaveSkipAC() {
|
|
1126
|
-
if (this.$skipAcWhenSaving) {
|
|
1127
|
-
this.$skipAcWhenSaving = false;
|
|
1128
|
-
return true;
|
|
1129
|
-
}
|
|
1130
|
-
return false;
|
|
1131
|
-
}
|
|
1132
|
-
async $delete() {
|
|
1133
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
1134
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1135
|
-
throw new errors_1.BadDataError("You don't have the authority to delete users.");
|
|
1136
|
-
}
|
|
1137
|
-
if (!tilmeld.gatekeeper('system/admin') &&
|
|
1138
|
-
this.$gatekeeper('system/admin')) {
|
|
1139
|
-
throw new errors_1.BadDataError("You don't have the authority to delete system admins.");
|
|
1140
|
-
}
|
|
1141
|
-
if (tilmeld.User.current(true).$is(this)) {
|
|
1142
|
-
await this.$logout();
|
|
1143
|
-
}
|
|
1144
|
-
return await super.$delete();
|
|
1145
|
-
}
|
|
1146
|
-
static on(event, callback) {
|
|
1147
|
-
const prop = (event + 'Callbacks');
|
|
1148
|
-
if (!(prop in this)) {
|
|
1149
|
-
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();
|
|
1150
2068
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
if (!(prop in this)) {
|
|
2069
|
+
$tilmeldDeleteSkipAC() {
|
|
2070
|
+
if (this.$skipAcWhenDeleting) {
|
|
2071
|
+
this.$skipAcWhenDeleting = false;
|
|
2072
|
+
return true;
|
|
2073
|
+
}
|
|
1157
2074
|
return false;
|
|
1158
2075
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
2076
|
+
static on(event, callback) {
|
|
2077
|
+
const prop = (event + 'Callbacks');
|
|
2078
|
+
if (!(prop in this)) {
|
|
2079
|
+
throw new Error('Invalid event type.');
|
|
2080
|
+
}
|
|
2081
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
2082
|
+
this[prop].push(callback);
|
|
2083
|
+
return () => this.off(event, callback);
|
|
2084
|
+
}
|
|
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;
|
|
1162
2097
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
User.DEFAULT_CLIENT_ENABLED_METHODS = [
|
|
1176
|
-
'$checkUsername',
|
|
1177
|
-
'$checkEmail',
|
|
1178
|
-
'$checkPhone',
|
|
1179
|
-
'$getAvatar',
|
|
1180
|
-
'$register',
|
|
1181
|
-
];
|
|
1182
|
-
User.DEFAULT_PRIVATE_DATA = [
|
|
1183
|
-
'email',
|
|
1184
|
-
'originalEmail',
|
|
1185
|
-
'phone',
|
|
1186
|
-
'group',
|
|
1187
|
-
'groups',
|
|
1188
|
-
'abilities',
|
|
1189
|
-
'inheritAbilities',
|
|
1190
|
-
'recoverSecret',
|
|
1191
|
-
'recoverSecretDate',
|
|
1192
|
-
'password',
|
|
1193
|
-
'salt',
|
|
1194
|
-
'secret',
|
|
1195
|
-
'newEmailAddress',
|
|
1196
|
-
'newEmailSecret',
|
|
1197
|
-
'cancelEmailAddress',
|
|
1198
|
-
'cancelEmailSecret',
|
|
1199
|
-
'emailChangeDate',
|
|
1200
|
-
];
|
|
1201
|
-
User.DEFAULT_ALLOWLIST_DATA = [];
|
|
1202
|
-
User.clientEnabledStaticMethods = [
|
|
1203
|
-
'current',
|
|
1204
|
-
'loginUser',
|
|
1205
|
-
'sendRecovery',
|
|
1206
|
-
'recover',
|
|
1207
|
-
'getClientConfig',
|
|
1208
|
-
];
|
|
1209
|
-
User.searchRestrictedData = [...User.DEFAULT_PRIVATE_DATA];
|
|
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;
|
|
1210
2110
|
//# sourceMappingURL=User.js.map
|