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