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