@nymphjs/tilmeld 1.0.0-beta.9 → 1.0.0-beta.90
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 +408 -0
- package/README.md +15 -2
- 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 +140 -15
- package/dist/Group.js +466 -191
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +83 -13
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +239 -12
- package/dist/Tilmeld.js +634 -254
- package/dist/Tilmeld.js.map +1 -1
- package/dist/Tilmeld.types.d.ts +2 -2
- package/dist/Tilmeld.types.js +1 -2
- package/dist/User.d.ts +365 -22
- package/dist/User.js +1265 -364
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +132 -13
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +287 -7
- package/dist/conf/d.js +1 -2
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +188 -141
- package/dist/conf/defaults.js.map +1 -1
- package/dist/conf/index.d.ts +2 -2
- package/dist/conf/index.js +2 -8
- package/dist/conf/index.js.map +1 -1
- 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 +11 -10
- package/dist/index.js +16 -35
- package/dist/index.js.map +1 -1
- package/dist/tilmeldJoiProps.js +13 -18
- package/dist/tilmeldJoiProps.js.map +1 -1
- package/jest.config.js +11 -2
- package/package.json +34 -31
- package/src/AbleObject.ts +2 -2
- package/src/Group.test.ts +104 -6
- package/src/Group.ts +478 -226
- package/src/Tilmeld.ts +418 -254
- package/src/Tilmeld.types.ts +2 -2
- package/src/User.test.ts +163 -6
- package/src/User.ts +1428 -444
- package/src/conf/d.ts +63 -17
- package/src/conf/defaults.ts +162 -109
- package/src/conf/index.ts +2 -2
- 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 +12 -10
- package/src/tilmeldJoiProps.ts +11 -11
- package/src/type/locutus.d.ts +3 -0
- package/tsconfig.json +5 -3
- package/typedoc.json +4 -0
package/dist/Group.js
CHANGED
|
@@ -1,16 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { difference, xor } from 'lodash-es';
|
|
3
|
+
import { splitn } from '@sciactive/splitn';
|
|
4
|
+
import { enforceTilmeld } from './enforceTilmeld.js';
|
|
5
|
+
import AbleObject from './AbleObject.js';
|
|
6
|
+
import { AccessControlError, BadDataError, BadEmailError, BadUsernameError, CouldNotChangeDefaultPrimaryGroupError, } from './errors/index.js';
|
|
7
|
+
import UserClass from './User.js';
|
|
8
|
+
/**
|
|
9
|
+
* A user group data model.
|
|
10
|
+
*
|
|
11
|
+
* Written by Hunter Perrin for SciActive.
|
|
12
|
+
*
|
|
13
|
+
* @author Hunter Perrin <hperrin@gmail.com>
|
|
14
|
+
* @copyright SciActive Inc
|
|
15
|
+
* @see http://nymph.io/
|
|
16
|
+
*/
|
|
17
|
+
export default class Group extends AbleObject {
|
|
18
|
+
/**
|
|
19
|
+
* The instance of Tilmeld to use for queries.
|
|
20
|
+
*/
|
|
21
|
+
static ETYPE = 'tilmeld_group';
|
|
22
|
+
static class = 'Group';
|
|
23
|
+
static DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
|
|
24
|
+
static DEFAULT_ALLOWLIST_DATA = [];
|
|
25
|
+
$tags = [];
|
|
26
|
+
$clientEnabledMethods = [
|
|
27
|
+
'$checkGroupname',
|
|
28
|
+
'$checkEmail',
|
|
29
|
+
'$getAvatar',
|
|
30
|
+
'$getChildren',
|
|
31
|
+
'$getDescendants',
|
|
32
|
+
'$getLevel',
|
|
33
|
+
'$isDescendant',
|
|
34
|
+
];
|
|
35
|
+
static clientEnabledStaticMethods = [
|
|
36
|
+
'getPrimaryGroups',
|
|
37
|
+
'getSecondaryGroups',
|
|
38
|
+
];
|
|
39
|
+
$privateData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
40
|
+
static searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
41
|
+
$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
|
|
42
|
+
$allowlistTags = [];
|
|
43
|
+
/**
|
|
44
|
+
* This should only be used by the backend.
|
|
45
|
+
*/
|
|
46
|
+
$skipAcWhenSaving = false;
|
|
47
|
+
/**
|
|
48
|
+
* This should only be used by the backend.
|
|
49
|
+
*/
|
|
50
|
+
$skipAcWhenDeleting = false;
|
|
51
|
+
/**
|
|
52
|
+
* Used to save the current groupname for domain admin permissions.
|
|
53
|
+
*/
|
|
54
|
+
$originalGroupname;
|
|
14
55
|
static async factoryGroupname(groupname) {
|
|
15
56
|
const entity = new this();
|
|
16
57
|
if (groupname != null) {
|
|
@@ -26,63 +67,89 @@ class Group extends AbleObject_1.default {
|
|
|
26
67
|
}
|
|
27
68
|
return entity;
|
|
28
69
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
'$checkGroupname',
|
|
37
|
-
'$checkEmail',
|
|
38
|
-
'$getAvatar',
|
|
39
|
-
'$getChildren',
|
|
40
|
-
'$getDescendants',
|
|
41
|
-
'$getLevel',
|
|
42
|
-
'$isDescendant',
|
|
43
|
-
];
|
|
44
|
-
this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
45
|
-
this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
|
|
46
|
-
this.$allowlistTags = [];
|
|
47
|
-
this.$skipAcWhenSaving = false;
|
|
48
|
-
if (this.guid == null) {
|
|
49
|
-
this.$data.enabled = true;
|
|
50
|
-
this.$data.abilities = [];
|
|
51
|
-
this.$updateDataProtection();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
70
|
+
/**
|
|
71
|
+
* Get all the groups that can be assigned as primary groups.
|
|
72
|
+
*
|
|
73
|
+
* @param options The options for an optional search query.
|
|
74
|
+
* @param selectors The selectors for an optional search query.
|
|
75
|
+
* @returns An array of the assignable primary groups.
|
|
76
|
+
*/
|
|
54
77
|
static async getPrimaryGroups(options, selectors) {
|
|
55
|
-
|
|
78
|
+
const tilmeld = enforceTilmeld(this);
|
|
79
|
+
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
56
80
|
throw new Error("You don't have permission to do that.");
|
|
57
81
|
}
|
|
58
|
-
return await this.getAssignableGroups(
|
|
82
|
+
return await this.getAssignableGroups(tilmeld.config.highestPrimary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
|
|
59
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Get all the groups that can be assigned as secondary groups.
|
|
86
|
+
*
|
|
87
|
+
* @param options The options for an optional search query.
|
|
88
|
+
* @param selectors The selectors for an optional search query.
|
|
89
|
+
* @returns An array of the assignable secondary groups.
|
|
90
|
+
*/
|
|
60
91
|
static async getSecondaryGroups(options, selectors) {
|
|
61
|
-
|
|
92
|
+
const tilmeld = enforceTilmeld(this);
|
|
93
|
+
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
62
94
|
throw new Error("You don't have permission to do that.");
|
|
63
95
|
}
|
|
64
|
-
return await this.getAssignableGroups(
|
|
96
|
+
return await this.getAssignableGroups(tilmeld.config.highestSecondary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
|
|
65
97
|
}
|
|
66
98
|
static async getAssignableGroups(highestParent, options, selectors) {
|
|
99
|
+
const tilmeld = enforceTilmeld(this);
|
|
67
100
|
let assignableGroups = [];
|
|
68
101
|
if (highestParent === false) {
|
|
69
102
|
return assignableGroups;
|
|
70
103
|
}
|
|
71
|
-
assignableGroups = await this.nymph.getEntities({ ...options, class:
|
|
104
|
+
assignableGroups = await this.nymph.getEntities({ ...options, class: tilmeld.Group }, ...selectors);
|
|
72
105
|
if (highestParent !== true) {
|
|
73
|
-
assignableGroups = assignableGroups.
|
|
106
|
+
assignableGroups = (await Promise.all(assignableGroups.map(async (group) => {
|
|
74
107
|
let curGroup = group;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
let parent = curGroup.parent;
|
|
109
|
+
await parent?.$wake();
|
|
110
|
+
while (parent != null && parent?.cdate != null) {
|
|
111
|
+
if (parent?.guid === highestParent) {
|
|
112
|
+
return group;
|
|
78
113
|
}
|
|
79
|
-
curGroup =
|
|
114
|
+
curGroup = parent;
|
|
115
|
+
parent = curGroup.parent;
|
|
116
|
+
await parent?.$wake();
|
|
80
117
|
}
|
|
81
|
-
return
|
|
82
|
-
});
|
|
118
|
+
return null;
|
|
119
|
+
}))).filter((group) => group != null);
|
|
83
120
|
}
|
|
84
121
|
return assignableGroups;
|
|
85
122
|
}
|
|
123
|
+
constructor() {
|
|
124
|
+
super();
|
|
125
|
+
// Defaults.
|
|
126
|
+
this.$data.enabled = true;
|
|
127
|
+
this.$data.abilities = [];
|
|
128
|
+
this.$updateDataProtection();
|
|
129
|
+
}
|
|
130
|
+
$setNymph(nymph) {
|
|
131
|
+
this.$nymph = nymph;
|
|
132
|
+
if (!this.$asleep()) {
|
|
133
|
+
if (this.$data.user && this.$data.user.$nymph !== nymph) {
|
|
134
|
+
this.$data.user.$setNymph(nymph);
|
|
135
|
+
}
|
|
136
|
+
if (this.$data.parent && this.$data.parent.$nymph !== nymph) {
|
|
137
|
+
this.$data.parent.$setNymph(nymph);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async $getUniques() {
|
|
142
|
+
const tilmeld = enforceTilmeld(this);
|
|
143
|
+
const uniques = [`u:${this.$data.groupname}`];
|
|
144
|
+
if (!tilmeld.config.emailUsernames &&
|
|
145
|
+
tilmeld.config.userFields.includes('email')) {
|
|
146
|
+
uniques.push(`e:${this.$data.email}`);
|
|
147
|
+
}
|
|
148
|
+
if (this.$data.defaultPrimary) {
|
|
149
|
+
uniques.push(`o:default-primary`);
|
|
150
|
+
}
|
|
151
|
+
return uniques;
|
|
152
|
+
}
|
|
86
153
|
$getAvatar() {
|
|
87
154
|
if (this.$data.avatar != null && this.$data.avatar !== '') {
|
|
88
155
|
return this.$data.avatar;
|
|
@@ -91,60 +158,117 @@ class Group extends AbleObject_1.default {
|
|
|
91
158
|
return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
|
|
92
159
|
}
|
|
93
160
|
return ('https://secure.gravatar.com/avatar/' +
|
|
94
|
-
|
|
161
|
+
crypto
|
|
162
|
+
.createHash('sha256')
|
|
163
|
+
.update(this.$data.email.trim().toLowerCase())
|
|
164
|
+
.digest('hex')
|
|
165
|
+
.toLowerCase() +
|
|
95
166
|
'?d=identicon&s=40');
|
|
96
167
|
}
|
|
97
168
|
$jsonAcceptData(input, allowConflict = false) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.$data.abilities?.indexOf('system/admin') === -1 &&
|
|
102
|
-
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
103
|
-
!tilmeld.gatekeeper('system/admin')) {
|
|
104
|
-
throw new errors_1.BadDataError("You don't have the authority to make this group a system admin.");
|
|
169
|
+
this.$check();
|
|
170
|
+
if ('abilities' in input.data) {
|
|
171
|
+
this.$checkIncomingAbilities(input.data.abilities);
|
|
105
172
|
}
|
|
106
173
|
super.$jsonAcceptData(input, allowConflict);
|
|
107
174
|
}
|
|
108
175
|
$jsonAcceptPatch(patch, allowConflict = false) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.$data.abilities?.indexOf('system/admin') === -1 &&
|
|
113
|
-
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
114
|
-
!tilmeld.gatekeeper('system/admin')) {
|
|
115
|
-
throw new errors_1.BadDataError("You don't have the authority to make this group a system admin.");
|
|
176
|
+
this.$check();
|
|
177
|
+
if ('abilities' in patch.set) {
|
|
178
|
+
this.$checkIncomingAbilities(patch.set.abilities);
|
|
116
179
|
}
|
|
117
180
|
super.$jsonAcceptPatch(patch, allowConflict);
|
|
118
181
|
}
|
|
119
|
-
$
|
|
120
|
-
|
|
182
|
+
$checkIncomingAbilities(incoming) {
|
|
183
|
+
const tilmeld = enforceTilmeld(this);
|
|
184
|
+
if (!Array.isArray(incoming) ||
|
|
185
|
+
incoming.find((ability) => typeof ability !== 'string')) {
|
|
186
|
+
throw new BadDataError('Abilities must be an array of strings.');
|
|
187
|
+
}
|
|
188
|
+
if (incoming.includes('system/admin') !==
|
|
189
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
190
|
+
(!tilmeld.currentUser ||
|
|
191
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
192
|
+
throw new BadDataError("You don't have the authority to grant or revoke system/admin.");
|
|
193
|
+
}
|
|
194
|
+
if (incoming.includes('tilmeld/admin') !==
|
|
195
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
196
|
+
(!tilmeld.currentUser ||
|
|
197
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
198
|
+
throw new BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
|
|
199
|
+
}
|
|
200
|
+
if (incoming.includes('tilmeld/switch') !==
|
|
201
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
202
|
+
(!tilmeld.currentUser ||
|
|
203
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
204
|
+
throw new BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
|
|
205
|
+
}
|
|
206
|
+
if (xor(incoming.filter((ability) => ability.startsWith('tilmeld/')), this.$data.abilities?.filter((ability) => ability.startsWith('tilmeld/')) ?? []).length &&
|
|
207
|
+
(!tilmeld.currentUser ||
|
|
208
|
+
(!tilmeld.currentUser.abilities?.includes('system/admin') &&
|
|
209
|
+
!tilmeld.currentUser.abilities?.includes('tilmeld/admin')))) {
|
|
210
|
+
throw new BadDataError("You don't have the authority to grant or revoke Tilmeld abilities.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
$putData(data, sdata, source) {
|
|
214
|
+
super.$putData(data, sdata, source);
|
|
215
|
+
if (source === 'server') {
|
|
216
|
+
this.$originalGroupname = this.$data.groupname;
|
|
217
|
+
}
|
|
121
218
|
this.$updateDataProtection();
|
|
122
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Update the data protection arrays for a user.
|
|
222
|
+
*
|
|
223
|
+
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
224
|
+
*/
|
|
123
225
|
$updateDataProtection(givenUser) {
|
|
124
|
-
const tilmeld = this
|
|
226
|
+
const tilmeld = enforceTilmeld(this);
|
|
125
227
|
let user = givenUser ?? tilmeld.User.current();
|
|
228
|
+
let [_groupname, domain] = tilmeld.config.domainSupport
|
|
229
|
+
? splitn(this.$data.groupname ?? '', '@', 2)
|
|
230
|
+
: [this.$data.groupname, null];
|
|
231
|
+
// Lowercase the domain part.
|
|
232
|
+
if (domain) {
|
|
233
|
+
domain = domain.toLowerCase();
|
|
234
|
+
}
|
|
126
235
|
this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
127
236
|
this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
|
|
128
237
|
if (tilmeld.config.emailUsernames) {
|
|
129
238
|
this.$privateData.push('groupname');
|
|
130
239
|
}
|
|
131
240
|
if (user != null &&
|
|
132
|
-
(
|
|
133
|
-
|
|
241
|
+
(user.abilities?.includes('tilmeld/admin') ||
|
|
242
|
+
user.abilities?.includes('system/admin'))) {
|
|
243
|
+
// Users who can edit groups can see their data.
|
|
134
244
|
this.$privateData = [];
|
|
135
245
|
this.$allowlistData = undefined;
|
|
136
246
|
}
|
|
137
247
|
else if (this.$data.user != null &&
|
|
138
248
|
user != null &&
|
|
139
|
-
this.$data.user
|
|
249
|
+
this.$data.user.guid === user.guid) {
|
|
250
|
+
// Users can see their group's data.
|
|
251
|
+
this.$privateData = [];
|
|
252
|
+
}
|
|
253
|
+
else if (user != null &&
|
|
254
|
+
tilmeld.config.domainSupport &&
|
|
255
|
+
domain &&
|
|
256
|
+
user.abilities?.includes(`tilmeld/domain/${domain}/admin`)) {
|
|
257
|
+
// Domain admins can see their groups' data.
|
|
140
258
|
this.$privateData = [];
|
|
141
259
|
}
|
|
142
260
|
}
|
|
143
|
-
|
|
144
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Check whether the group is a descendant of a group.
|
|
263
|
+
*
|
|
264
|
+
* @param group The group, or the group's GUID.
|
|
265
|
+
* @returns True or false.
|
|
266
|
+
*/
|
|
267
|
+
async $isDescendant(givenGroup) {
|
|
268
|
+
const tilmeld = enforceTilmeld(this);
|
|
145
269
|
let group;
|
|
146
270
|
if (typeof givenGroup === 'string') {
|
|
147
|
-
group = tilmeld.Group.
|
|
271
|
+
group = await tilmeld.Group.factory(givenGroup);
|
|
148
272
|
}
|
|
149
273
|
else {
|
|
150
274
|
group = givenGroup;
|
|
@@ -152,27 +276,41 @@ class Group extends AbleObject_1.default {
|
|
|
152
276
|
if (group.guid == null) {
|
|
153
277
|
return false;
|
|
154
278
|
}
|
|
155
|
-
if
|
|
279
|
+
// Check to see if the group is a descendant of the given group.
|
|
280
|
+
const parent = this.$data.parent;
|
|
281
|
+
await parent?.$wake();
|
|
282
|
+
if (parent == null) {
|
|
156
283
|
return false;
|
|
157
284
|
}
|
|
158
|
-
if (
|
|
285
|
+
if (parent.$is(group)) {
|
|
159
286
|
return true;
|
|
160
287
|
}
|
|
161
|
-
if (
|
|
288
|
+
if (await parent.$isDescendant(group)) {
|
|
162
289
|
return true;
|
|
163
290
|
}
|
|
164
291
|
return false;
|
|
165
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Gets an array of the group's child groups.
|
|
295
|
+
*
|
|
296
|
+
* @returns An array of groups.
|
|
297
|
+
*/
|
|
166
298
|
async $getChildren() {
|
|
167
|
-
const tilmeld = this
|
|
299
|
+
const tilmeld = enforceTilmeld(this);
|
|
168
300
|
return await this.$nymph.getEntities({ class: tilmeld.Group }, {
|
|
169
301
|
type: '&',
|
|
170
302
|
equal: ['enabled', true],
|
|
171
303
|
ref: ['parent', this],
|
|
172
304
|
});
|
|
173
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Gets an array of the group's descendant groups.
|
|
308
|
+
*
|
|
309
|
+
* @param andSelf Include this group in the returned array.
|
|
310
|
+
* @returns An array of groups.
|
|
311
|
+
*/
|
|
174
312
|
async $getDescendants(andSelf = false) {
|
|
175
|
-
const tilmeld = this
|
|
313
|
+
const tilmeld = enforceTilmeld(this);
|
|
176
314
|
let groups = [];
|
|
177
315
|
const entities = await this.$nymph.getEntities({ class: tilmeld.Group }, {
|
|
178
316
|
type: '&',
|
|
@@ -187,39 +325,41 @@ class Group extends AbleObject_1.default {
|
|
|
187
325
|
}
|
|
188
326
|
return groups;
|
|
189
327
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (andSelf) {
|
|
207
|
-
groups.push(this);
|
|
208
|
-
}
|
|
209
|
-
return groups;
|
|
210
|
-
}
|
|
211
|
-
$getLevel() {
|
|
212
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
213
|
-
let group = tilmeld.Group.factorySync(this.guid ?? undefined);
|
|
328
|
+
/**
|
|
329
|
+
* Get the number of parents the group has.
|
|
330
|
+
*
|
|
331
|
+
* If the group is a top level group, this will return 0. If it is a child of
|
|
332
|
+
* a top level group, this will return 1. If it is a grandchild of a top level
|
|
333
|
+
* group, this will return 2, and so on.
|
|
334
|
+
*
|
|
335
|
+
* Levels will max out at 1024 to avoid recursive loops.
|
|
336
|
+
*
|
|
337
|
+
* @returns The level of the group.
|
|
338
|
+
*/
|
|
339
|
+
async $getLevel() {
|
|
340
|
+
const tilmeld = enforceTilmeld(this);
|
|
341
|
+
let group = await tilmeld.Group.factory(this.guid ?? undefined);
|
|
342
|
+
let parent = group.parent;
|
|
343
|
+
await parent?.$wake();
|
|
214
344
|
let level = 0;
|
|
215
|
-
while (
|
|
345
|
+
while (parent != null && parent.cdate != null && level < 1024) {
|
|
216
346
|
level++;
|
|
217
|
-
group =
|
|
347
|
+
group = parent;
|
|
348
|
+
parent = group.parent;
|
|
349
|
+
await parent?.$wake();
|
|
218
350
|
}
|
|
219
351
|
return level;
|
|
220
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Gets an array of users in the group.
|
|
355
|
+
*
|
|
356
|
+
* @param descendants Include users in all descendant groups too.
|
|
357
|
+
* @param limit The limit for the query.
|
|
358
|
+
* @param offset The offset for the query.
|
|
359
|
+
* @returns An array of users.
|
|
360
|
+
*/
|
|
221
361
|
async $getUsers(descendants = false, limit, offset) {
|
|
222
|
-
const tilmeld = this
|
|
362
|
+
const tilmeld = enforceTilmeld(this);
|
|
223
363
|
let groups = [];
|
|
224
364
|
if (descendants) {
|
|
225
365
|
groups = await this.$getDescendants();
|
|
@@ -247,14 +387,37 @@ class Group extends AbleObject_1.default {
|
|
|
247
387
|
],
|
|
248
388
|
});
|
|
249
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Check that a groupname is valid.
|
|
392
|
+
*
|
|
393
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
394
|
+
*/
|
|
250
395
|
async $checkGroupname() {
|
|
251
|
-
const tilmeld = this
|
|
396
|
+
const tilmeld = enforceTilmeld(this);
|
|
252
397
|
if (!tilmeld.config.emailUsernames) {
|
|
253
|
-
|
|
398
|
+
let [groupname, domain] = tilmeld.config.domainSupport
|
|
399
|
+
? splitn(this.$data.groupname ?? '', '@', 2)
|
|
400
|
+
: [this.$data.groupname, null];
|
|
401
|
+
// Lowercase the domain part.
|
|
402
|
+
if (domain) {
|
|
403
|
+
domain = domain.toLowerCase();
|
|
404
|
+
this.$data.groupname = `${groupname}@${domain}`;
|
|
405
|
+
}
|
|
406
|
+
if (this.$data.groupname == null ||
|
|
407
|
+
!this.$data.groupname.length ||
|
|
408
|
+
groupname == null ||
|
|
409
|
+
!groupname.length) {
|
|
254
410
|
return { result: false, message: 'Please specify a groupname.' };
|
|
255
411
|
}
|
|
256
|
-
if (tilmeld.config.
|
|
257
|
-
|
|
412
|
+
if (groupname.length < tilmeld.config.minUsernameLength) {
|
|
413
|
+
return {
|
|
414
|
+
result: false,
|
|
415
|
+
message: 'Groupnames must be at least ' +
|
|
416
|
+
tilmeld.config.minUsernameLength +
|
|
417
|
+
' characters.',
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (groupname.length > tilmeld.config.maxUsernameLength) {
|
|
258
421
|
return {
|
|
259
422
|
result: false,
|
|
260
423
|
message: 'Groupnames must not exceed ' +
|
|
@@ -262,18 +425,34 @@ class Group extends AbleObject_1.default {
|
|
|
262
425
|
' characters.',
|
|
263
426
|
};
|
|
264
427
|
}
|
|
265
|
-
if (
|
|
428
|
+
if (difference(groupname.split(''), tilmeld.config.validChars.split(''))
|
|
429
|
+
.length) {
|
|
266
430
|
return {
|
|
267
431
|
result: false,
|
|
268
432
|
message: tilmeld.config.validCharsNotice,
|
|
269
433
|
};
|
|
270
434
|
}
|
|
271
|
-
if (!tilmeld.config.validRegex.test(
|
|
435
|
+
if (!tilmeld.config.validRegex.test(groupname)) {
|
|
272
436
|
return {
|
|
273
437
|
result: false,
|
|
274
438
|
message: tilmeld.config.validRegexNotice,
|
|
275
439
|
};
|
|
276
440
|
}
|
|
441
|
+
if (domain != null) {
|
|
442
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
443
|
+
return {
|
|
444
|
+
result: false,
|
|
445
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (!this.$is(tilmeld.currentUser?.group) &&
|
|
449
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
450
|
+
return {
|
|
451
|
+
result: false,
|
|
452
|
+
message: "You don't have the authority to manage groups on this domain.",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
277
456
|
const selector = {
|
|
278
457
|
type: '&',
|
|
279
458
|
ilike: [
|
|
@@ -298,8 +477,15 @@ class Group extends AbleObject_1.default {
|
|
|
298
477
|
if (this.$data.groupname == null || !this.$data.groupname.length) {
|
|
299
478
|
return { result: false, message: 'Please specify an email.' };
|
|
300
479
|
}
|
|
301
|
-
if (tilmeld.config.
|
|
302
|
-
|
|
480
|
+
if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
|
|
481
|
+
return {
|
|
482
|
+
result: false,
|
|
483
|
+
message: 'Emails must be at least ' +
|
|
484
|
+
tilmeld.config.minUsernameLength +
|
|
485
|
+
' characters.',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
|
|
303
489
|
return {
|
|
304
490
|
result: false,
|
|
305
491
|
message: 'Emails must not exceed ' +
|
|
@@ -310,8 +496,13 @@ class Group extends AbleObject_1.default {
|
|
|
310
496
|
return await this.$checkEmail();
|
|
311
497
|
}
|
|
312
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Check that an email is unique.
|
|
501
|
+
*
|
|
502
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
503
|
+
*/
|
|
313
504
|
async $checkEmail() {
|
|
314
|
-
const tilmeld = this
|
|
505
|
+
const tilmeld = enforceTilmeld(this);
|
|
315
506
|
if (this.$data.email === '') {
|
|
316
507
|
return { result: true, message: '' };
|
|
317
508
|
}
|
|
@@ -344,10 +535,42 @@ class Group extends AbleObject_1.default {
|
|
|
344
535
|
};
|
|
345
536
|
}
|
|
346
537
|
async $save() {
|
|
347
|
-
const tilmeld = this
|
|
538
|
+
const tilmeld = enforceTilmeld(this);
|
|
348
539
|
if (this.$data.groupname == null || !this.$data.groupname.trim().length) {
|
|
349
540
|
return false;
|
|
350
541
|
}
|
|
542
|
+
// Lowercase the domain part.
|
|
543
|
+
if (tilmeld.config.domainSupport) {
|
|
544
|
+
const [groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
|
|
545
|
+
if (domain) {
|
|
546
|
+
this.$data.groupname = `${groupname}@${domain.toLowerCase()}`;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (tilmeld.config.domainSupport) {
|
|
550
|
+
const [_groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
|
|
551
|
+
const [_originalGroupname, originalDomain] = splitn(this.$originalGroupname ?? '', '@', 2);
|
|
552
|
+
if (this.guid != null &&
|
|
553
|
+
((originalDomain == null && domain != null) ||
|
|
554
|
+
(domain == null && originalDomain != null))) {
|
|
555
|
+
throw new AccessControlError("Groups can't switch to or from having a domain once created.");
|
|
556
|
+
}
|
|
557
|
+
if (!this.$skipAcWhenSaving &&
|
|
558
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
559
|
+
!tilmeld.gatekeeper('system/admin')) {
|
|
560
|
+
if (domain != null &&
|
|
561
|
+
!this.$is(tilmeld.currentUser) &&
|
|
562
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
563
|
+
throw new AccessControlError("You don't have the authority to modify groups on this domain.");
|
|
564
|
+
}
|
|
565
|
+
if (originalDomain != null &&
|
|
566
|
+
originalDomain !== domain &&
|
|
567
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
568
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))) {
|
|
569
|
+
throw new AccessControlError("You don't have the authority to modify groups on this domain.");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Formatting.
|
|
351
574
|
this.$data.groupname = this.$data.groupname.trim();
|
|
352
575
|
if (tilmeld.config.emailUsernames) {
|
|
353
576
|
this.$data.email = this.$data.groupname;
|
|
@@ -356,16 +579,19 @@ class Group extends AbleObject_1.default {
|
|
|
356
579
|
this.$data.name = (this.$data.name ?? '').trim();
|
|
357
580
|
this.$data.phone = (this.$data.phone ?? '').trim();
|
|
358
581
|
this.$data.enabled = !!this.$data.enabled;
|
|
359
|
-
|
|
582
|
+
// Clear empty values.
|
|
583
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
360
584
|
delete this.$data.name;
|
|
361
585
|
}
|
|
362
|
-
if (this.$data.email === ''
|
|
586
|
+
if (this.$data.email === '' ||
|
|
587
|
+
!tilmeld.config.userFields.includes('email')) {
|
|
363
588
|
delete this.$data.email;
|
|
364
589
|
}
|
|
365
590
|
if (this.$data.avatar === '') {
|
|
366
591
|
delete this.$data.avatar;
|
|
367
592
|
}
|
|
368
|
-
if (this.$data.phone === ''
|
|
593
|
+
if (this.$data.phone === '' ||
|
|
594
|
+
!tilmeld.config.userFields.includes('phone')) {
|
|
369
595
|
delete this.$data.phone;
|
|
370
596
|
}
|
|
371
597
|
if (this.$data.parent == null) {
|
|
@@ -383,46 +609,70 @@ class Group extends AbleObject_1.default {
|
|
|
383
609
|
if (!this.$data.unverifiedSecondary) {
|
|
384
610
|
delete this.$data.unverifiedSecondary;
|
|
385
611
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
612
|
+
// Groups should never have 'system/admin', 'tilmeld/admin',
|
|
613
|
+
// 'tilmeld/switch', or domain admin abilities.
|
|
614
|
+
this.$data.abilities = (this.$data.abilities ?? []).filter((ability) => {
|
|
615
|
+
if (['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
|
|
616
|
+
-1) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
if (ability == null) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
if (ability.startsWith('tilmeld/domain/') && ability.endsWith('/admin')) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
return true;
|
|
626
|
+
});
|
|
627
|
+
// Verification.
|
|
392
628
|
const unCheck = await this.$checkGroupname();
|
|
393
629
|
if (!unCheck.result) {
|
|
394
|
-
throw new
|
|
630
|
+
throw new BadUsernameError(unCheck.message);
|
|
395
631
|
}
|
|
396
|
-
if (!tilmeld.config.emailUsernames
|
|
632
|
+
if (!tilmeld.config.emailUsernames &&
|
|
633
|
+
tilmeld.config.userFields.includes('email')) {
|
|
397
634
|
const emCheck = await this.$checkEmail();
|
|
398
635
|
if (!emCheck.result) {
|
|
399
|
-
throw new
|
|
636
|
+
throw new BadEmailError(emCheck.message);
|
|
400
637
|
}
|
|
401
638
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
639
|
+
// Validate group parent. Make sure it's not a descendant of this group.
|
|
640
|
+
const parent = this.$data.parent;
|
|
641
|
+
await parent?.$wake();
|
|
642
|
+
if (parent &&
|
|
643
|
+
(parent.cdate == null ||
|
|
644
|
+
this.$is(parent) ||
|
|
645
|
+
(await parent.$isDescendant(this)))) {
|
|
646
|
+
throw new BadDataError("Group parent can't be itself or descendant of itself.");
|
|
407
647
|
}
|
|
408
648
|
try {
|
|
409
|
-
tilmeld.config.validatorGroup(this);
|
|
649
|
+
tilmeld.config.validatorGroup(tilmeld, this);
|
|
410
650
|
}
|
|
411
651
|
catch (e) {
|
|
412
|
-
throw new
|
|
652
|
+
throw new BadDataError(e?.message);
|
|
413
653
|
}
|
|
654
|
+
// Only one default primary group is allowed.
|
|
414
655
|
if (this.$data.defaultPrimary) {
|
|
415
656
|
const currentPrimary = await this.$nymph.getEntity({ class: tilmeld.Group }, { type: '&', truthy: 'defaultPrimary' });
|
|
416
657
|
if (currentPrimary != null && !this.$is(currentPrimary)) {
|
|
417
658
|
currentPrimary.defaultPrimary = false;
|
|
418
|
-
if (
|
|
419
|
-
|
|
659
|
+
if (this.$skipAcWhenSaving
|
|
660
|
+
? !(await currentPrimary.$saveSkipAC())
|
|
661
|
+
: !(await currentPrimary.$save())) {
|
|
662
|
+
throw new CouldNotChangeDefaultPrimaryGroupError('Could not change new user primary group from ' +
|
|
420
663
|
`${currentPrimary.groupname}.`);
|
|
421
664
|
}
|
|
422
665
|
}
|
|
423
666
|
}
|
|
424
|
-
|
|
667
|
+
const ret = await super.$save();
|
|
668
|
+
if (ret) {
|
|
669
|
+
this.$originalGroupname = this.$data.groupname;
|
|
670
|
+
}
|
|
671
|
+
return ret;
|
|
425
672
|
}
|
|
673
|
+
/*
|
|
674
|
+
* This should *never* be accessible on the client.
|
|
675
|
+
*/
|
|
426
676
|
async $saveSkipAC() {
|
|
427
677
|
this.$skipAcWhenSaving = true;
|
|
428
678
|
return await this.$save();
|
|
@@ -435,73 +685,98 @@ class Group extends AbleObject_1.default {
|
|
|
435
685
|
return false;
|
|
436
686
|
}
|
|
437
687
|
async $delete() {
|
|
438
|
-
let tilmeld = this
|
|
439
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
440
|
-
throw new
|
|
688
|
+
let tilmeld = enforceTilmeld(this);
|
|
689
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
690
|
+
throw new AccessControlError("You don't have the authority to delete groups.");
|
|
691
|
+
}
|
|
692
|
+
if (this.$data.groupname !== this.$originalGroupname) {
|
|
693
|
+
throw new AccessControlError("You can't delete a group while switching its groupname.");
|
|
441
694
|
}
|
|
442
695
|
const transaction = 'tilmeld-delete-' + this.guid;
|
|
443
696
|
const nymph = this.$nymph;
|
|
444
697
|
const tnymph = await nymph.startTransaction(transaction);
|
|
445
|
-
this.$
|
|
446
|
-
tilmeld = this
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
698
|
+
this.$setNymph(tnymph);
|
|
699
|
+
tilmeld = enforceTilmeld(this);
|
|
700
|
+
const User = tnymph.getEntityClass(UserClass);
|
|
701
|
+
try {
|
|
702
|
+
// Delete descendants.
|
|
703
|
+
const descendants = await this.$getDescendants();
|
|
704
|
+
if (descendants.length) {
|
|
705
|
+
for (let curGroup of descendants) {
|
|
706
|
+
if (this.$skipAcWhenDeleting
|
|
707
|
+
? !(await curGroup.$deleteSkipAC())
|
|
708
|
+
: !(await curGroup.$delete())) {
|
|
709
|
+
await tnymph.rollback(transaction);
|
|
710
|
+
this.$setNymph(nymph);
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// Remove users from this primary group.
|
|
716
|
+
const primaryUsers = await tnymph.getEntities({
|
|
717
|
+
class: User,
|
|
718
|
+
skipAc: true,
|
|
719
|
+
}, {
|
|
720
|
+
type: '&',
|
|
721
|
+
ref: ['group', this],
|
|
722
|
+
});
|
|
723
|
+
for (let user of primaryUsers) {
|
|
724
|
+
delete user.group;
|
|
725
|
+
if (this.$skipAcWhenDeleting
|
|
726
|
+
? !(await user.$saveSkipAC())
|
|
727
|
+
: !(await user.$save())) {
|
|
451
728
|
await tnymph.rollback(transaction);
|
|
452
|
-
this.$nymph
|
|
729
|
+
this.$setNymph(nymph);
|
|
453
730
|
return false;
|
|
454
731
|
}
|
|
455
732
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
733
|
+
// Remove users from this secondary group.
|
|
734
|
+
const secondaryUsers = await tnymph.getEntities({
|
|
735
|
+
class: User,
|
|
736
|
+
skipAc: true,
|
|
737
|
+
}, {
|
|
738
|
+
type: '&',
|
|
739
|
+
ref: ['groups', this],
|
|
740
|
+
});
|
|
741
|
+
for (let user of secondaryUsers) {
|
|
742
|
+
user.$delGroup(this);
|
|
743
|
+
if (this.$skipAcWhenDeleting
|
|
744
|
+
? !(await user.$saveSkipAC())
|
|
745
|
+
: !(await user.$save())) {
|
|
746
|
+
await tnymph.rollback(transaction);
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
470
749
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
ref: ['groups', this],
|
|
478
|
-
});
|
|
479
|
-
for (let user of secondaryUsers) {
|
|
480
|
-
user.$delGroup(this);
|
|
481
|
-
if (!(await user.$save())) {
|
|
750
|
+
// Delete the group.
|
|
751
|
+
let success = await super.$delete();
|
|
752
|
+
if (success) {
|
|
753
|
+
success = await tnymph.commit(transaction);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
482
756
|
await tnymph.rollback(transaction);
|
|
483
|
-
return false;
|
|
484
757
|
}
|
|
758
|
+
this.$setNymph(nymph);
|
|
759
|
+
return success;
|
|
485
760
|
}
|
|
486
|
-
|
|
487
|
-
if (success) {
|
|
488
|
-
await tnymph.commit(transaction);
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
761
|
+
catch (e) {
|
|
491
762
|
await tnymph.rollback(transaction);
|
|
763
|
+
this.$setNymph(nymph);
|
|
764
|
+
throw e;
|
|
492
765
|
}
|
|
493
|
-
|
|
494
|
-
|
|
766
|
+
}
|
|
767
|
+
/*
|
|
768
|
+
* This should *never* be accessible on the client.
|
|
769
|
+
*/
|
|
770
|
+
async $deleteSkipAC() {
|
|
771
|
+
this.$skipAcWhenDeleting = true;
|
|
772
|
+
return await this.$delete();
|
|
773
|
+
}
|
|
774
|
+
$tilmeldDeleteSkipAC() {
|
|
775
|
+
if (this.$skipAcWhenDeleting) {
|
|
776
|
+
this.$skipAcWhenDeleting = false;
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
495
780
|
}
|
|
496
781
|
}
|
|
497
|
-
exports.default = Group;
|
|
498
|
-
Group.ETYPE = 'tilmeld_group';
|
|
499
|
-
Group.class = 'Group';
|
|
500
|
-
Group.DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
|
|
501
|
-
Group.DEFAULT_ALLOWLIST_DATA = [];
|
|
502
|
-
Group.clientEnabledStaticMethods = [
|
|
503
|
-
'getPrimaryGroups',
|
|
504
|
-
'getSecondaryGroups',
|
|
505
|
-
];
|
|
506
|
-
Group.searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
507
782
|
//# sourceMappingURL=Group.js.map
|