@nymphjs/tilmeld 1.0.0-beta.121 → 1.0.0-beta.123

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -3
  3. package/dist/Group.d.ts +9 -9
  4. package/dist/Group.js +790 -793
  5. package/dist/Group.js.map +1 -1
  6. package/dist/Group.testArtifacts.js +3 -3
  7. package/dist/Group.testArtifacts.js.map +1 -1
  8. package/dist/Tilmeld.js +6 -15
  9. package/dist/Tilmeld.js.map +1 -1
  10. package/dist/User.d.ts +8 -9
  11. package/dist/User.js +1838 -1957
  12. package/dist/User.js.map +1 -1
  13. package/dist/User.testArtifacts.js +7 -7
  14. package/dist/User.testArtifacts.js.map +1 -1
  15. package/dist/decorators/index.d.ts +2 -0
  16. package/dist/decorators/index.js +3 -0
  17. package/dist/decorators/index.js.map +1 -0
  18. package/dist/decorators/requireAbility.d.ts +10 -0
  19. package/dist/decorators/requireAbility.js +21 -0
  20. package/dist/decorators/requireAbility.js.map +1 -0
  21. package/dist/decorators/requireAuthentication.d.ts +8 -0
  22. package/dist/decorators/requireAuthentication.js +16 -0
  23. package/dist/decorators/requireAuthentication.js.map +1 -0
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/testArtifacts.d.ts +5 -1
  28. package/dist/testArtifacts.js +211 -67
  29. package/dist/testArtifacts.js.map +1 -1
  30. package/package.json +13 -7
  31. package/src/Group.testArtifacts.ts +3 -3
  32. package/src/Group.ts +108 -158
  33. package/src/Tilmeld.ts +6 -35
  34. package/src/User.testArtifacts.ts +7 -7
  35. package/src/User.ts +364 -515
  36. package/src/decorators/index.ts +2 -0
  37. package/src/decorators/requireAbility.ts +26 -0
  38. package/src/decorators/requireAuthentication.ts +19 -0
  39. package/src/index.ts +1 -0
  40. package/src/testArtifacts.ts +165 -46
package/dist/Group.js CHANGED
@@ -1,4 +1,39 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
1
35
  import crypto from 'node:crypto';
36
+ import { transaction, transactional, } from '@nymphjs/nymph';
2
37
  import { nanoid } from '@nymphjs/guid';
3
38
  import { difference, xor } from 'lodash-es';
4
39
  import { splitn } from '@sciactive/splitn';
@@ -6,882 +41,844 @@ import { enforceTilmeld } from './enforceTilmeld.js';
6
41
  import AbleObject from './AbleObject.js';
7
42
  import { AccessControlError, BadDataError, BadEmailError, BadUsernameError, CouldNotChangeDefaultPrimaryGroupError, } from './errors/index.js';
8
43
  import UserClass from './User.js';
9
- /**
10
- * A user group data model.
11
- *
12
- * Written by Hunter Perrin for SciActive.
13
- *
14
- * @author Hunter Perrin <hperrin@port87.com>
15
- * @copyright SciActive Inc
16
- * @see http://nymph.io/
17
- */
18
- export default class Group extends AbleObject {
19
- /**
20
- * The instance of Tilmeld to use for queries.
21
- */
22
- static ETYPE = 'tilmeld_group';
23
- static class = 'Group';
24
- static checkGroupnameCallbacks = [];
25
- static beforeSaveCallbacks = [];
26
- static afterSaveCallbacks = [];
27
- static beforeDeleteCallbacks = [];
28
- static afterDeleteCallbacks = [];
29
- static DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
30
- static DEFAULT_ALLOWLIST_DATA = [];
31
- $tags = [];
32
- $clientEnabledMethods = [
33
- '$checkGroupname',
34
- '$checkEmail',
35
- '$getAvatar',
36
- '$getChildren',
37
- '$getDescendants',
38
- '$getLevel',
39
- '$isDescendant',
40
- ];
41
- static clientEnabledStaticMethods = [
42
- 'getPrimaryGroups',
43
- 'getSecondaryGroups',
44
- ];
45
- $privateData = [...Group.DEFAULT_PRIVATE_DATA];
46
- static searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
47
- $allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
48
- $allowlistTags = [];
49
- /**
50
- * This should only be used by the backend.
51
- */
52
- $skipAcWhenSaving = false;
53
- /**
54
- * This should only be used by the backend.
55
- */
56
- $skipAcWhenDeleting = false;
57
- /**
58
- * Used to save the current groupname for domain admin permissions.
59
- */
60
- $originalGroupname;
61
- static getFTSText(name, value) {
62
- if ([
63
- 'abilities',
64
- 'parent',
65
- 'user',
66
- 'enabled',
67
- 'defaultPrimary',
68
- 'defaultSecondary',
69
- 'unverifiedSecondary',
70
- ].indexOf(name) !== -1) {
71
- return null;
72
- }
73
- return super.getFTSText(name, value);
74
- }
75
- static async factoryGroupname(groupname) {
76
- const entity = new this();
77
- if (groupname != null) {
78
- const entity = await this.nymph.getEntity({
79
- class: this,
80
- }, {
81
- type: '&',
82
- ilike: ['groupname', groupname.replace(/([\\%_])/g, (s) => `\\${s}`)],
83
- });
84
- if (entity != null) {
85
- return entity;
44
+ let Group = (() => {
45
+ let _classSuper = AbleObject;
46
+ let _instanceExtraInitializers = [];
47
+ let _$delete_decorators;
48
+ return class Group extends _classSuper {
49
+ static {
50
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
51
+ _$delete_decorators = [transactional];
52
+ __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);
53
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
54
+ }
55
+ /**
56
+ * The instance of Tilmeld to use for queries.
57
+ */
58
+ static ETYPE = 'tilmeld_group';
59
+ static class = 'Group';
60
+ static checkGroupnameCallbacks = [];
61
+ static beforeSaveCallbacks = [];
62
+ static afterSaveCallbacks = [];
63
+ static beforeDeleteCallbacks = [];
64
+ static afterDeleteCallbacks = [];
65
+ static DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
66
+ static DEFAULT_ALLOWLIST_DATA = [];
67
+ $tags = (__runInitializers(this, _instanceExtraInitializers), []);
68
+ $clientEnabledMethods = [
69
+ '$checkGroupname',
70
+ '$checkEmail',
71
+ '$getAvatar',
72
+ '$getChildren',
73
+ '$getDescendants',
74
+ '$getLevel',
75
+ '$isDescendant',
76
+ ];
77
+ static clientEnabledStaticMethods = [
78
+ 'getPrimaryGroups',
79
+ 'getSecondaryGroups',
80
+ ];
81
+ $privateData = [...Group.DEFAULT_PRIVATE_DATA];
82
+ static searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
83
+ $allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
84
+ $allowlistTags = [];
85
+ /**
86
+ * This should only be used by the backend.
87
+ */
88
+ $skipAcWhenSaving = false;
89
+ /**
90
+ * This should only be used by the backend.
91
+ */
92
+ $skipAcWhenDeleting = false;
93
+ /**
94
+ * Used to save the current groupname for domain admin permissions.
95
+ */
96
+ $originalGroupname;
97
+ static getFTSText(name, value) {
98
+ if ([
99
+ 'abilities',
100
+ 'parent',
101
+ 'user',
102
+ 'enabled',
103
+ 'defaultPrimary',
104
+ 'defaultSecondary',
105
+ 'unverifiedSecondary',
106
+ ].indexOf(name) !== -1) {
107
+ return null;
86
108
  }
87
- }
88
- return entity;
89
- }
90
- /**
91
- * Get all the groups that can be assigned as primary groups.
92
- *
93
- * @param options The options for an optional search query.
94
- * @param selectors The selectors for an optional search query.
95
- * @returns An array of the assignable primary groups.
96
- */
97
- static async getPrimaryGroups(options, selectors) {
98
- const tilmeld = enforceTilmeld(this);
99
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
100
- throw new Error("You don't have permission to do that.");
101
- }
102
- return await this.getAssignableGroups(tilmeld.config.highestPrimary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
103
- }
104
- /**
105
- * Get all the groups that can be assigned as secondary groups.
106
- *
107
- * @param options The options for an optional search query.
108
- * @param selectors The selectors for an optional search query.
109
- * @returns An array of the assignable secondary groups.
110
- */
111
- static async getSecondaryGroups(options, selectors) {
112
- const tilmeld = enforceTilmeld(this);
113
- if (!tilmeld.gatekeeper('tilmeld/admin')) {
114
- throw new Error("You don't have permission to do that.");
115
- }
116
- return await this.getAssignableGroups(tilmeld.config.highestSecondary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
117
- }
118
- static async getAssignableGroups(highestParent, options, selectors) {
119
- const tilmeld = enforceTilmeld(this);
120
- let assignableGroups = [];
121
- if (highestParent === false) {
122
- return assignableGroups;
123
- }
124
- assignableGroups = await this.nymph.getEntities({ ...options, class: tilmeld.Group }, ...selectors);
125
- if (highestParent !== true) {
126
- assignableGroups = (await Promise.all(assignableGroups.map(async (group) => {
127
- let curGroup = group;
128
- let parent = curGroup.parent;
129
- await parent?.$wake();
130
- while (parent != null && parent?.cdate != null) {
131
- if (parent?.guid === highestParent) {
132
- return group;
133
- }
134
- curGroup = parent;
135
- parent = curGroup.parent;
136
- await parent?.$wake();
109
+ return super.getFTSText(name, value);
110
+ }
111
+ static async factoryGroupname(groupname) {
112
+ const entity = new this();
113
+ if (groupname != null) {
114
+ const entity = await this.nymph.getEntity({
115
+ class: this,
116
+ }, {
117
+ type: '&',
118
+ ilike: ['groupname', groupname.replace(/([\\%_])/g, (s) => `\\${s}`)],
119
+ });
120
+ if (entity != null) {
121
+ return entity;
137
122
  }
138
- return null;
139
- }))).filter((group) => group != null);
140
- }
141
- return assignableGroups;
142
- }
143
- constructor() {
144
- super();
145
- // Defaults.
146
- this.$data.enabled = true;
147
- this.$data.abilities = [];
148
- this.$updateDataProtection();
149
- }
150
- $setNymph(nymph) {
151
- this.$nymph = nymph;
152
- if (!this.$asleep()) {
153
- if (this.$data.user && this.$data.user.$nymph !== nymph) {
154
- this.$data.user.$setNymph(nymph);
155
123
  }
156
- if (this.$data.parent && this.$data.parent.$nymph !== nymph) {
157
- this.$data.parent.$setNymph(nymph);
124
+ return entity;
125
+ }
126
+ /**
127
+ * Get all the groups that can be assigned as primary groups.
128
+ *
129
+ * @param options The options for an optional search query.
130
+ * @param selectors The selectors for an optional search query.
131
+ * @returns An array of the assignable primary groups.
132
+ */
133
+ static async getPrimaryGroups(options, selectors) {
134
+ const tilmeld = enforceTilmeld(this);
135
+ if (!tilmeld.gatekeeper('tilmeld/admin')) {
136
+ throw new Error("You don't have permission to do that.");
137
+ }
138
+ return await this.getAssignableGroups(tilmeld.config.highestPrimary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
139
+ }
140
+ /**
141
+ * Get all the groups that can be assigned as secondary groups.
142
+ *
143
+ * @param options The options for an optional search query.
144
+ * @param selectors The selectors for an optional search query.
145
+ * @returns An array of the assignable secondary groups.
146
+ */
147
+ static async getSecondaryGroups(options, selectors) {
148
+ const tilmeld = enforceTilmeld(this);
149
+ if (!tilmeld.gatekeeper('tilmeld/admin')) {
150
+ throw new Error("You don't have permission to do that.");
151
+ }
152
+ return await this.getAssignableGroups(tilmeld.config.highestSecondary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
153
+ }
154
+ static async getAssignableGroups(highestParent, options, selectors) {
155
+ const tilmeld = enforceTilmeld(this);
156
+ let assignableGroups = [];
157
+ if (highestParent === false) {
158
+ return assignableGroups;
159
+ }
160
+ assignableGroups = await this.nymph.getEntities({ ...options, class: tilmeld.Group }, ...selectors);
161
+ if (highestParent !== true) {
162
+ assignableGroups = (await Promise.all(assignableGroups.map(async (group) => {
163
+ let curGroup = group;
164
+ let parent = curGroup.parent;
165
+ await parent?.$wake();
166
+ while (parent != null && parent?.cdate != null) {
167
+ if (parent?.guid === highestParent) {
168
+ return group;
169
+ }
170
+ curGroup = parent;
171
+ parent = curGroup.parent;
172
+ await parent?.$wake();
173
+ }
174
+ return null;
175
+ }))).filter((group) => group != null);
158
176
  }
177
+ return assignableGroups;
159
178
  }
160
- }
161
- async $getUniques() {
162
- const tilmeld = enforceTilmeld(this);
163
- const uniques = [`u:${this.$data.groupname}`];
164
- if (!tilmeld.config.emailUsernames &&
165
- tilmeld.config.userFields.includes('email')) {
166
- uniques.push(`e:${this.$data.email}`);
167
- }
168
- if (this.$data.defaultPrimary) {
169
- uniques.push(`o:default-primary`);
170
- }
171
- return uniques;
172
- }
173
- $getAvatar() {
174
- if (this.$data.avatar != null && this.$data.avatar !== '') {
175
- return this.$data.avatar;
176
- }
177
- if (this.$data.email == null || !this.$data.email.length) {
178
- return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
179
- }
180
- return ('https://secure.gravatar.com/avatar/' +
181
- crypto
182
- .createHash('sha256')
183
- .update(this.$data.email.trim().toLowerCase())
184
- .digest('hex')
185
- .toLowerCase() +
186
- '?d=identicon&s=40');
187
- }
188
- $jsonAcceptData(input, allowConflict = false) {
189
- this.$check();
190
- if ('abilities' in input.data) {
191
- this.$checkIncomingAbilities(input.data.abilities);
192
- }
193
- super.$jsonAcceptData(input, allowConflict);
194
- }
195
- $jsonAcceptPatch(patch, allowConflict = false) {
196
- this.$check();
197
- if ('abilities' in patch.set) {
198
- this.$checkIncomingAbilities(patch.set.abilities);
199
- }
200
- super.$jsonAcceptPatch(patch, allowConflict);
201
- }
202
- $checkIncomingAbilities(incoming) {
203
- const tilmeld = enforceTilmeld(this);
204
- if (!Array.isArray(incoming) ||
205
- incoming.find((ability) => typeof ability !== 'string')) {
206
- throw new BadDataError('Abilities must be an array of strings.');
207
- }
208
- if (incoming.includes('system/admin') !==
209
- this.$data.abilities?.includes('system/admin') &&
210
- (!tilmeld.currentUser ||
211
- !tilmeld.currentUser.abilities?.includes('system/admin'))) {
212
- throw new BadDataError("You don't have the authority to grant or revoke system/admin.");
213
- }
214
- if (incoming.includes('tilmeld/admin') !==
215
- this.$data.abilities?.includes('tilmeld/admin') &&
216
- (!tilmeld.currentUser ||
217
- !tilmeld.currentUser.abilities?.includes('system/admin'))) {
218
- throw new BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
219
- }
220
- if (incoming.includes('tilmeld/switch') !==
221
- this.$data.abilities?.includes('tilmeld/switch') &&
222
- (!tilmeld.currentUser ||
223
- !tilmeld.currentUser.abilities?.includes('system/admin'))) {
224
- throw new BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
225
- }
226
- if (xor(incoming.filter((ability) => ability.startsWith('tilmeld/')), this.$data.abilities?.filter((ability) => ability.startsWith('tilmeld/')) ?? []).length &&
227
- (!tilmeld.currentUser ||
228
- (!tilmeld.currentUser.abilities?.includes('system/admin') &&
229
- !tilmeld.currentUser.abilities?.includes('tilmeld/admin')))) {
230
- throw new BadDataError("You don't have the authority to grant or revoke Tilmeld abilities.");
231
- }
232
- }
233
- $putData(data, sdata, source) {
234
- super.$putData(data, sdata, source);
235
- // TODO: Fix these being added by the drivers.
236
- // @ts-ignore
237
- delete this.$data.acRead;
238
- // @ts-ignore
239
- delete this.$data.acWrite;
240
- // @ts-ignore
241
- delete this.$data.acFull;
242
- if (source === 'server') {
243
- this.$originalGroupname = this.$data.groupname;
244
- }
245
- this.$updateDataProtection();
246
- }
247
- /**
248
- * Update the data protection arrays for a user.
249
- *
250
- * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
251
- */
252
- $updateDataProtection(givenUser) {
253
- const tilmeld = enforceTilmeld(this);
254
- let user = givenUser ?? tilmeld.User.current();
255
- let [_groupname, domain] = tilmeld.config.domainSupport
256
- ? splitn(this.$data.groupname ?? '', '@', 2)
257
- : [this.$data.groupname, null];
258
- // Lowercase the domain part.
259
- if (domain) {
260
- domain = domain.toLowerCase();
261
- }
262
- this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
263
- this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
264
- if (tilmeld.config.emailUsernames) {
265
- this.$privateData.push('groupname');
266
- }
267
- if (user != null &&
268
- (user.abilities?.includes('tilmeld/admin') ||
269
- user.abilities?.includes('system/admin'))) {
270
- // Users who can edit groups can see their data.
271
- this.$privateData = [];
272
- this.$allowlistData = undefined;
273
- }
274
- else if (this.$data.user != null &&
275
- user != null &&
276
- this.$data.user.guid === user.guid) {
277
- // Users can see their group's data.
278
- this.$privateData = [];
279
- }
280
- else if (user != null &&
281
- tilmeld.config.domainSupport &&
282
- domain &&
283
- user.abilities?.includes(`tilmeld/domain/${domain}/admin`)) {
284
- // Domain admins can see their groups' data.
285
- this.$privateData = [];
286
- }
287
- }
288
- /**
289
- * Check whether the group is a descendant of a group.
290
- *
291
- * @param group The group, or the group's GUID.
292
- * @returns True or false.
293
- */
294
- async $isDescendant(givenGroup) {
295
- const tilmeld = enforceTilmeld(this);
296
- let group;
297
- if (typeof givenGroup === 'string') {
298
- group = await tilmeld.Group.factory(givenGroup);
299
- }
300
- else {
301
- group = givenGroup;
302
- }
303
- if (group.guid == null) {
304
- return false;
305
- }
306
- // Check to see if the group is a descendant of the given group.
307
- const parent = this.$data.parent;
308
- await parent?.$wake();
309
- if (parent == null) {
310
- return false;
311
- }
312
- if (parent.$is(group)) {
313
- return true;
314
- }
315
- if (await parent.$isDescendant(group)) {
316
- return true;
317
- }
318
- return false;
319
- }
320
- /**
321
- * Gets an array of the group's child groups.
322
- *
323
- * @returns An array of groups.
324
- */
325
- async $getChildren() {
326
- const tilmeld = enforceTilmeld(this);
327
- return await this.$nymph.getEntities({ class: tilmeld.Group }, {
328
- type: '&',
329
- equal: ['enabled', true],
330
- ref: ['parent', this],
331
- });
332
- }
333
- /**
334
- * Gets an array of the group's descendant groups.
335
- *
336
- * @param andSelf Include this group in the returned array.
337
- * @returns An array of groups.
338
- */
339
- async $getDescendants(andSelf = false) {
340
- const tilmeld = enforceTilmeld(this);
341
- let groups = [];
342
- const entities = await this.$nymph.getEntities({ class: tilmeld.Group }, {
343
- type: '&',
344
- equal: ['enabled', true],
345
- ref: ['parent', this],
346
- });
347
- for (let entity of entities) {
348
- groups = (await entity.$getDescendants(true)).concat(groups);
349
- }
350
- if (andSelf) {
351
- groups.push(this);
352
- }
353
- return groups;
354
- }
355
- /**
356
- * Get the number of parents the group has.
357
- *
358
- * If the group is a top level group, this will return 0. If it is a child of
359
- * a top level group, this will return 1. If it is a grandchild of a top level
360
- * group, this will return 2, and so on.
361
- *
362
- * Levels will max out at 1024 to avoid recursive loops.
363
- *
364
- * @returns The level of the group.
365
- */
366
- async $getLevel() {
367
- const tilmeld = enforceTilmeld(this);
368
- let group = await tilmeld.Group.factory(this.guid ?? undefined);
369
- let parent = group.parent;
370
- await parent?.$wake();
371
- let level = 0;
372
- while (parent != null && parent.cdate != null && level < 1024) {
373
- level++;
374
- group = parent;
375
- parent = group.parent;
376
- await parent?.$wake();
377
- }
378
- return level;
379
- }
380
- /**
381
- * Gets an array of users in the group.
382
- *
383
- * @param descendants Include users in all descendant groups too.
384
- * @param limit The limit for the query.
385
- * @param offset The offset for the query.
386
- * @returns An array of users.
387
- */
388
- async $getUsers(descendants = false, limit, offset) {
389
- const tilmeld = enforceTilmeld(this);
390
- let groups = [];
391
- if (descendants) {
392
- groups = await this.$getDescendants();
393
- }
394
- return await this.$nymph.getEntities({
395
- class: tilmeld.User,
396
- limit,
397
- offset,
398
- }, {
399
- type: '&',
400
- equal: ['enabled', true],
401
- }, {
402
- type: '|',
403
- ref: [
404
- ['group', this],
405
- ['groups', this],
406
- ...groups.map((group) => [
407
- 'group',
408
- group,
409
- ]),
410
- ...groups.map((group) => [
411
- 'groups',
412
- group,
413
- ]),
414
- ],
415
- });
416
- }
417
- /**
418
- * Check that a groupname is valid.
419
- *
420
- * @returns An object with a boolean 'result' entry and a 'message' entry.
421
- */
422
- async $checkGroupname() {
423
- const tilmeld = enforceTilmeld(this);
424
- if (!tilmeld.config.emailUsernames) {
425
- let [groupname, domain] = tilmeld.config.domainSupport
179
+ constructor() {
180
+ super();
181
+ // Defaults.
182
+ this.$data.enabled = true;
183
+ this.$data.abilities = [];
184
+ this.$updateDataProtection();
185
+ }
186
+ async $getUniques() {
187
+ const tilmeld = enforceTilmeld(this);
188
+ const uniques = [`u:${this.$data.groupname}`];
189
+ if (!tilmeld.config.emailUsernames &&
190
+ tilmeld.config.userFields.includes('email')) {
191
+ uniques.push(`e:${this.$data.email}`);
192
+ }
193
+ if (this.$data.defaultPrimary) {
194
+ uniques.push(`o:default-primary`);
195
+ }
196
+ return uniques;
197
+ }
198
+ $getAvatar() {
199
+ if (this.$data.avatar != null && this.$data.avatar !== '') {
200
+ return this.$data.avatar;
201
+ }
202
+ if (this.$data.email == null || !this.$data.email.length) {
203
+ return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
204
+ }
205
+ return ('https://secure.gravatar.com/avatar/' +
206
+ crypto
207
+ .createHash('sha256')
208
+ .update(this.$data.email.trim().toLowerCase())
209
+ .digest('hex')
210
+ .toLowerCase() +
211
+ '?d=identicon&s=40');
212
+ }
213
+ $jsonAcceptData(input, allowConflict = false) {
214
+ this.$check();
215
+ if ('abilities' in input.data) {
216
+ this.$checkIncomingAbilities(input.data.abilities);
217
+ }
218
+ super.$jsonAcceptData(input, allowConflict);
219
+ }
220
+ $jsonAcceptPatch(patch, allowConflict = false) {
221
+ this.$check();
222
+ if ('abilities' in patch.set) {
223
+ this.$checkIncomingAbilities(patch.set.abilities);
224
+ }
225
+ super.$jsonAcceptPatch(patch, allowConflict);
226
+ }
227
+ $checkIncomingAbilities(incoming) {
228
+ const tilmeld = enforceTilmeld(this);
229
+ if (!Array.isArray(incoming) ||
230
+ incoming.find((ability) => typeof ability !== 'string')) {
231
+ throw new BadDataError('Abilities must be an array of strings.');
232
+ }
233
+ if (incoming.includes('system/admin') !==
234
+ this.$data.abilities?.includes('system/admin') &&
235
+ (!tilmeld.currentUser ||
236
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
237
+ throw new BadDataError("You don't have the authority to grant or revoke system/admin.");
238
+ }
239
+ if (incoming.includes('tilmeld/admin') !==
240
+ this.$data.abilities?.includes('tilmeld/admin') &&
241
+ (!tilmeld.currentUser ||
242
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
243
+ throw new BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
244
+ }
245
+ if (incoming.includes('tilmeld/switch') !==
246
+ this.$data.abilities?.includes('tilmeld/switch') &&
247
+ (!tilmeld.currentUser ||
248
+ !tilmeld.currentUser.abilities?.includes('system/admin'))) {
249
+ throw new BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
250
+ }
251
+ if (xor(incoming.filter((ability) => ability.startsWith('tilmeld/')), this.$data.abilities?.filter((ability) => ability.startsWith('tilmeld/')) ?? []).length &&
252
+ (!tilmeld.currentUser ||
253
+ (!tilmeld.currentUser.abilities?.includes('system/admin') &&
254
+ !tilmeld.currentUser.abilities?.includes('tilmeld/admin')))) {
255
+ throw new BadDataError("You don't have the authority to grant or revoke Tilmeld abilities.");
256
+ }
257
+ }
258
+ $putData(data, sdata, source) {
259
+ super.$putData(data, sdata, source);
260
+ // TODO: Fix these being added by the drivers.
261
+ // @ts-ignore
262
+ delete this.$data.acRead;
263
+ // @ts-ignore
264
+ delete this.$data.acWrite;
265
+ // @ts-ignore
266
+ delete this.$data.acFull;
267
+ if (source === 'server') {
268
+ this.$originalGroupname = this.$data.groupname;
269
+ }
270
+ this.$updateDataProtection();
271
+ }
272
+ /**
273
+ * Update the data protection arrays for a user.
274
+ *
275
+ * @param givenUser User to update protection for. If undefined, will use the currently logged in user.
276
+ */
277
+ $updateDataProtection(givenUser) {
278
+ const tilmeld = enforceTilmeld(this);
279
+ let user = givenUser ?? tilmeld.User.current();
280
+ let [_groupname, domain] = tilmeld.config.domainSupport
426
281
  ? splitn(this.$data.groupname ?? '', '@', 2)
427
282
  : [this.$data.groupname, null];
428
283
  // Lowercase the domain part.
429
284
  if (domain) {
430
285
  domain = domain.toLowerCase();
431
- this.$data.groupname = `${groupname}@${domain}`;
432
286
  }
433
- if (this.$data.groupname == null ||
434
- !this.$data.groupname.length ||
435
- groupname == null ||
436
- !groupname.length) {
437
- return { result: false, message: 'Please specify a groupname.' };
287
+ this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
288
+ this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
289
+ if (tilmeld.config.emailUsernames) {
290
+ this.$privateData.push('groupname');
291
+ }
292
+ if (user != null &&
293
+ (user.abilities?.includes('tilmeld/admin') ||
294
+ user.abilities?.includes('system/admin'))) {
295
+ // Users who can edit groups can see their data.
296
+ this.$privateData = [];
297
+ this.$allowlistData = undefined;
298
+ }
299
+ else if (this.$data.user != null &&
300
+ user != null &&
301
+ this.$data.user.guid === user.guid) {
302
+ // Users can see their group's data.
303
+ this.$privateData = [];
304
+ }
305
+ else if (user != null &&
306
+ tilmeld.config.domainSupport &&
307
+ domain &&
308
+ user.abilities?.includes(`tilmeld/domain/${domain}/admin`)) {
309
+ // Domain admins can see their groups' data.
310
+ this.$privateData = [];
311
+ }
312
+ }
313
+ /**
314
+ * Check whether the group is a descendant of the given group.
315
+ *
316
+ * @param group The group, or the group's GUID.
317
+ * @returns True or false.
318
+ */
319
+ async $isDescendant(givenGroup) {
320
+ const tilmeld = enforceTilmeld(this);
321
+ let group;
322
+ if (typeof givenGroup === 'string') {
323
+ group = await tilmeld.Group.factory(givenGroup);
438
324
  }
439
- if (groupname.length < tilmeld.config.minUsernameLength) {
440
- return {
441
- result: false,
442
- message: 'Groupnames must be at least ' +
443
- tilmeld.config.minUsernameLength +
444
- ' characters.',
445
- };
325
+ else {
326
+ group = givenGroup;
446
327
  }
447
- if (groupname.length > tilmeld.config.maxUsernameLength) {
448
- return {
449
- result: false,
450
- message: 'Groupnames must not exceed ' +
451
- tilmeld.config.maxUsernameLength +
452
- ' characters.',
453
- };
328
+ if (group.guid == null) {
329
+ return false;
454
330
  }
455
- if (difference(groupname.split(''), tilmeld.config.validChars.split(''))
456
- .length) {
457
- return {
458
- result: false,
459
- message: tilmeld.config.validCharsNotice,
460
- };
331
+ // Check to see if the group is a descendant of the given group.
332
+ const parent = this.$data.parent;
333
+ await parent?.$wake();
334
+ if (parent == null) {
335
+ return false;
336
+ }
337
+ if (parent.$is(group)) {
338
+ return true;
339
+ }
340
+ if (await parent.$isDescendant(group)) {
341
+ return true;
342
+ }
343
+ return false;
344
+ }
345
+ /**
346
+ * Gets an array of the group's child groups.
347
+ *
348
+ * @returns An array of groups.
349
+ */
350
+ async $getChildren() {
351
+ const tilmeld = enforceTilmeld(this);
352
+ return await this.$nymph.getEntities({ class: tilmeld.Group }, {
353
+ type: '&',
354
+ equal: ['enabled', true],
355
+ ref: ['parent', this],
356
+ });
357
+ }
358
+ /**
359
+ * Gets an array of the group's descendant groups.
360
+ *
361
+ * @param andSelf Include this group in the returned array.
362
+ * @param andDisabled Include disabled groups.
363
+ * @returns An array of groups.
364
+ */
365
+ async $getDescendants(andSelf = false, andDisabled = false) {
366
+ const tilmeld = enforceTilmeld(this);
367
+ let groups = [];
368
+ const entities = await this.$nymph.getEntities({ class: tilmeld.Group }, {
369
+ type: '&',
370
+ ...(andDisabled ? {} : { equal: ['enabled', true] }),
371
+ ref: ['parent', this],
372
+ });
373
+ for (let entity of entities) {
374
+ groups = (await entity.$getDescendants(true)).concat(groups);
375
+ }
376
+ if (andSelf) {
377
+ groups.push(this);
378
+ }
379
+ return groups;
380
+ }
381
+ /**
382
+ * Get the number of parents the group has.
383
+ *
384
+ * If the group is a top level group, this will return 0. If it is a child of
385
+ * a top level group, this will return 1. If it is a grandchild of a top level
386
+ * group, this will return 2, and so on.
387
+ *
388
+ * Levels will max out at 1024 to avoid recursive loops.
389
+ *
390
+ * @returns The level of the group.
391
+ */
392
+ async $getLevel() {
393
+ const tilmeld = enforceTilmeld(this);
394
+ let group = await tilmeld.Group.factory(this.guid ?? undefined);
395
+ let parent = group.parent;
396
+ await parent?.$wake();
397
+ let level = 0;
398
+ while (parent != null && parent.cdate != null && level < 1024) {
399
+ level++;
400
+ group = parent;
401
+ parent = group.parent;
402
+ await parent?.$wake();
461
403
  }
462
- if (!tilmeld.config.validRegex.test(groupname)) {
404
+ return level;
405
+ }
406
+ /**
407
+ * Gets an array of enabled users in the group.
408
+ *
409
+ * @param descendants Include users in all descendant groups too.
410
+ * @param limit The limit for the query.
411
+ * @param offset The offset for the query.
412
+ * @returns An array of users.
413
+ */
414
+ async $getUsers(descendants = false, limit, offset) {
415
+ const tilmeld = enforceTilmeld(this);
416
+ let groups = [];
417
+ if (descendants) {
418
+ groups = await this.$getDescendants();
419
+ }
420
+ return await this.$nymph.getEntities({
421
+ class: tilmeld.User,
422
+ limit,
423
+ offset,
424
+ }, { type: '&', equal: ['enabled', true] }, {
425
+ type: '|',
426
+ ref: [
427
+ ['group', this],
428
+ ['groups', this],
429
+ ...groups.map((group) => [
430
+ 'group',
431
+ group,
432
+ ]),
433
+ ...groups.map((group) => [
434
+ 'groups',
435
+ group,
436
+ ]),
437
+ ],
438
+ });
439
+ }
440
+ /**
441
+ * Check that a groupname is valid.
442
+ *
443
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
444
+ */
445
+ async $checkGroupname() {
446
+ const tilmeld = enforceTilmeld(this);
447
+ if (!tilmeld.config.emailUsernames) {
448
+ let [groupname, domain] = tilmeld.config.domainSupport
449
+ ? splitn(this.$data.groupname ?? '', '@', 2)
450
+ : [this.$data.groupname, null];
451
+ // Lowercase the domain part.
452
+ if (domain) {
453
+ domain = domain.toLowerCase();
454
+ this.$data.groupname = `${groupname}@${domain}`;
455
+ }
456
+ if (this.$data.groupname == null ||
457
+ !this.$data.groupname.length ||
458
+ groupname == null ||
459
+ !groupname.length) {
460
+ return { result: false, message: 'Please specify a groupname.' };
461
+ }
462
+ if (groupname.length < tilmeld.config.minUsernameLength) {
463
+ return {
464
+ result: false,
465
+ message: 'Groupnames must be at least ' +
466
+ tilmeld.config.minUsernameLength +
467
+ ' characters.',
468
+ };
469
+ }
470
+ if (groupname.length > tilmeld.config.maxUsernameLength) {
471
+ return {
472
+ result: false,
473
+ message: 'Groupnames must not exceed ' +
474
+ tilmeld.config.maxUsernameLength +
475
+ ' characters.',
476
+ };
477
+ }
478
+ if (difference(groupname.split(''), tilmeld.config.validChars.split(''))
479
+ .length) {
480
+ return {
481
+ result: false,
482
+ message: tilmeld.config.validCharsNotice,
483
+ };
484
+ }
485
+ if (!tilmeld.config.validRegex.test(groupname)) {
486
+ return {
487
+ result: false,
488
+ message: tilmeld.config.validRegexNotice,
489
+ };
490
+ }
491
+ if (domain != null) {
492
+ if (!tilmeld.config.validDomainRegex.test(domain)) {
493
+ return {
494
+ result: false,
495
+ message: tilmeld.config.validDomainRegexNotice,
496
+ };
497
+ }
498
+ if (!this.$is(tilmeld.currentUser?.group) &&
499
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
500
+ return {
501
+ result: false,
502
+ message: "You don't have the authority to manage groups on this domain.",
503
+ };
504
+ }
505
+ }
506
+ const selector = {
507
+ type: '&',
508
+ ilike: [
509
+ 'groupname',
510
+ this.$data.groupname.replace(/([\\%_])/g, (s) => `\\${s}`),
511
+ ],
512
+ };
513
+ if (this.guid != null) {
514
+ selector['!guid'] = this.guid;
515
+ }
516
+ const test = await this.$nymph.getEntity({ class: tilmeld.Group, skipAc: true }, selector);
517
+ if (test != null) {
518
+ return { result: false, message: 'That groupname is taken.' };
519
+ }
520
+ for (let callback of this.constructor
521
+ .checkGroupnameCallbacks) {
522
+ if (callback) {
523
+ const result = await callback(this, {
524
+ groupname: this.$data.groupname,
525
+ });
526
+ if (!result.result) {
527
+ return { message: 'Groupname is not available.', ...result };
528
+ }
529
+ }
530
+ }
463
531
  return {
464
- result: false,
465
- message: tilmeld.config.validRegexNotice,
532
+ result: true,
533
+ message: this.guid != null ? 'Groupname is valid.' : 'Groupname is available!',
466
534
  };
467
535
  }
468
- if (domain != null) {
469
- if (!tilmeld.config.validDomainRegex.test(domain)) {
536
+ else {
537
+ this.$data.email = this.$data.groupname;
538
+ if (this.$data.groupname == null || !this.$data.groupname.length) {
539
+ return { result: false, message: 'Please specify an email.' };
540
+ }
541
+ if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
470
542
  return {
471
543
  result: false,
472
- message: tilmeld.config.validDomainRegexNotice,
544
+ message: 'Emails must be at least ' +
545
+ tilmeld.config.minUsernameLength +
546
+ ' characters.',
473
547
  };
474
548
  }
475
- if (!this.$is(tilmeld.currentUser?.group) &&
476
- !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
549
+ if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
477
550
  return {
478
551
  result: false,
479
- message: "You don't have the authority to manage groups on this domain.",
552
+ message: 'Emails must not exceed ' +
553
+ tilmeld.config.maxUsernameLength +
554
+ ' characters.',
480
555
  };
481
556
  }
557
+ return await this.$checkEmail();
558
+ }
559
+ }
560
+ /**
561
+ * Check that an email is unique.
562
+ *
563
+ * @returns An object with a boolean 'result' entry and a 'message' entry.
564
+ */
565
+ async $checkEmail() {
566
+ const tilmeld = enforceTilmeld(this);
567
+ if (this.$data.email === '') {
568
+ return { result: true, message: '' };
569
+ }
570
+ if (this.$data.email == null) {
571
+ return { result: false, message: 'Please specify a valid email.' };
572
+ }
573
+ if (!tilmeld.config.validEmailRegex.test(this.$data.email)) {
574
+ return {
575
+ result: false,
576
+ message: tilmeld.config.validEmailRegexNotice,
577
+ };
482
578
  }
483
579
  const selector = {
484
580
  type: '&',
485
- ilike: [
486
- 'groupname',
487
- this.$data.groupname.replace(/([\\%_])/g, (s) => `\\${s}`),
488
- ],
581
+ ilike: ['email', this.$data.email.replace(/([\\%_])/g, (s) => `\\${s}`)],
489
582
  };
490
583
  if (this.guid != null) {
491
584
  selector['!guid'] = this.guid;
492
585
  }
493
586
  const test = await this.$nymph.getEntity({ class: tilmeld.Group, skipAc: true }, selector);
494
587
  if (test != null) {
495
- return { result: false, message: 'That groupname is taken.' };
496
- }
497
- for (let callback of this.constructor
498
- .checkGroupnameCallbacks) {
499
- if (callback) {
500
- const result = await callback(this, {
501
- groupname: this.$data.groupname,
502
- });
503
- if (!result.result) {
504
- return { message: 'Groupname is not available.', ...result };
505
- }
506
- }
507
- }
508
- return {
509
- result: true,
510
- message: this.guid != null ? 'Groupname is valid.' : 'Groupname is available!',
511
- };
512
- }
513
- else {
514
- this.$data.email = this.$data.groupname;
515
- if (this.$data.groupname == null || !this.$data.groupname.length) {
516
- return { result: false, message: 'Please specify an email.' };
517
- }
518
- if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
519
588
  return {
520
589
  result: false,
521
- message: 'Emails must be at least ' +
522
- tilmeld.config.minUsernameLength +
523
- ' characters.',
590
+ message: 'That email address is already registered.',
524
591
  };
525
592
  }
526
- if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
527
- return {
528
- result: false,
529
- message: 'Emails must not exceed ' +
530
- tilmeld.config.maxUsernameLength +
531
- ' characters.',
532
- };
533
- }
534
- return await this.$checkEmail();
535
- }
536
- }
537
- /**
538
- * Check that an email is unique.
539
- *
540
- * @returns An object with a boolean 'result' entry and a 'message' entry.
541
- */
542
- async $checkEmail() {
543
- const tilmeld = enforceTilmeld(this);
544
- if (this.$data.email === '') {
545
- return { result: true, message: '' };
546
- }
547
- if (this.$data.email == null) {
548
- return { result: false, message: 'Please specify a valid email.' };
549
- }
550
- if (!tilmeld.config.validEmailRegex.test(this.$data.email)) {
551
- return {
552
- result: false,
553
- message: tilmeld.config.validEmailRegexNotice,
554
- };
555
- }
556
- const selector = {
557
- type: '&',
558
- ilike: ['email', this.$data.email.replace(/([\\%_])/g, (s) => `\\${s}`)],
559
- };
560
- if (this.guid != null) {
561
- selector['!guid'] = this.guid;
562
- }
563
- const test = await this.$nymph.getEntity({ class: tilmeld.Group, skipAc: true }, selector);
564
- if (test != null) {
565
593
  return {
566
- result: false,
567
- message: 'That email address is already registered.',
594
+ result: true,
595
+ message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
568
596
  };
569
597
  }
570
- return {
571
- result: true,
572
- message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
573
- };
574
- }
575
- async $save() {
576
- let tilmeld = enforceTilmeld(this);
577
- if (this.$data.groupname == null || !this.$data.groupname.trim().length) {
578
- return false;
579
- }
580
- // Lowercase the domain part.
581
- if (tilmeld.config.domainSupport) {
582
- const [groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
583
- if (domain) {
584
- this.$data.groupname = `${groupname}@${domain.toLowerCase()}`;
598
+ async $save() {
599
+ let tilmeld = enforceTilmeld(this);
600
+ if (this.$data.groupname == null || !this.$data.groupname.trim().length) {
601
+ throw new BadUsernameError('Groupname must not be empty.');
585
602
  }
586
- }
587
- if (tilmeld.config.domainSupport) {
588
- const [_groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
589
- const [_originalGroupname, originalDomain] = splitn(this.$originalGroupname ?? '', '@', 2);
590
- if (this.guid != null &&
591
- ((originalDomain == null && domain != null) ||
592
- (domain == null && originalDomain != null))) {
593
- throw new AccessControlError("Groups can't switch to or from having a domain once created.");
594
- }
595
- if (!this.$skipAcWhenSaving &&
596
- !tilmeld.gatekeeper('tilmeld/admin') &&
597
- !tilmeld.gatekeeper('system/admin')) {
598
- if (domain != null &&
599
- !this.$is(tilmeld.currentUser) &&
600
- !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
601
- throw new AccessControlError("You don't have the authority to modify groups on this domain.");
602
- }
603
- if (originalDomain != null &&
604
- originalDomain !== domain &&
605
- (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
606
- !tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))) {
607
- throw new AccessControlError("You don't have the authority to modify groups on this domain.");
603
+ // Lowercase the domain part.
604
+ if (tilmeld.config.domainSupport) {
605
+ const [groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
606
+ if (domain) {
607
+ this.$data.groupname = `${groupname}@${domain.toLowerCase()}`;
608
608
  }
609
609
  }
610
- }
611
- // Formatting.
612
- this.$data.groupname = this.$data.groupname.trim();
613
- if (tilmeld.config.emailUsernames) {
614
- this.$data.email = this.$data.groupname;
615
- }
616
- this.$data.email = (this.$data.email ?? '').trim();
617
- this.$data.name = (this.$data.name ?? '').trim();
618
- this.$data.phone = (this.$data.phone ?? '').trim();
619
- this.$data.enabled = !!this.$data.enabled;
620
- // Clear empty values.
621
- if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
622
- delete this.$data.name;
623
- }
624
- if (this.$data.email === '' ||
625
- !tilmeld.config.userFields.includes('email')) {
626
- delete this.$data.email;
627
- }
628
- if (this.$data.avatar === '') {
629
- delete this.$data.avatar;
630
- }
631
- if (this.$data.phone === '' ||
632
- !tilmeld.config.userFields.includes('phone')) {
633
- delete this.$data.phone;
634
- }
635
- if (this.$data.parent == null) {
636
- delete this.$data.parent;
637
- }
638
- if (this.$data.user == null) {
639
- delete this.$data.user;
640
- }
641
- if (!this.$data.defaultPrimary) {
642
- delete this.$data.defaultPrimary;
643
- }
644
- if (!this.$data.defaultSecondary) {
645
- delete this.$data.defaultSecondary;
646
- }
647
- if (!this.$data.unverifiedSecondary) {
648
- delete this.$data.unverifiedSecondary;
649
- }
650
- // Groups should never have 'system/admin', 'tilmeld/admin',
651
- // 'tilmeld/switch', or domain admin abilities.
652
- this.$data.abilities = (this.$data.abilities ?? []).filter((ability) => {
653
- if (['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
654
- -1) {
655
- return false;
610
+ if (tilmeld.config.domainSupport) {
611
+ const [_groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
612
+ const [_originalGroupname, originalDomain] = splitn(this.$originalGroupname ?? '', '@', 2);
613
+ if (this.guid != null &&
614
+ ((originalDomain == null && domain != null) ||
615
+ (domain == null && originalDomain != null))) {
616
+ throw new AccessControlError("Groups can't switch to or from having a domain once created.");
617
+ }
618
+ if (!this.$skipAcWhenSaving &&
619
+ !tilmeld.gatekeeper('tilmeld/admin') &&
620
+ !tilmeld.gatekeeper('system/admin')) {
621
+ if (domain != null &&
622
+ !this.$is(tilmeld.currentUser) &&
623
+ !tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
624
+ throw new AccessControlError("You don't have the authority to modify groups on this domain.");
625
+ }
626
+ if (originalDomain != null &&
627
+ originalDomain !== domain &&
628
+ (!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
629
+ !tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))) {
630
+ throw new AccessControlError("You don't have the authority to modify groups on this domain.");
631
+ }
632
+ }
656
633
  }
657
- if (ability == null) {
658
- return false;
634
+ // Formatting.
635
+ this.$data.groupname = this.$data.groupname.trim();
636
+ if (tilmeld.config.emailUsernames) {
637
+ this.$data.email = this.$data.groupname;
659
638
  }
660
- if (ability.startsWith('tilmeld/domain/') && ability.endsWith('/admin')) {
661
- return false;
639
+ this.$data.email = (this.$data.email ?? '').trim();
640
+ this.$data.name = (this.$data.name ?? '').trim();
641
+ this.$data.phone = (this.$data.phone ?? '').trim();
642
+ this.$data.enabled = !!this.$data.enabled;
643
+ // Clear empty values.
644
+ if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
645
+ delete this.$data.name;
662
646
  }
663
- return true;
664
- });
665
- // Verification.
666
- const unCheck = await this.$checkGroupname();
667
- if (!unCheck.result) {
668
- throw new BadUsernameError(unCheck.message);
669
- }
670
- if (!tilmeld.config.emailUsernames &&
671
- tilmeld.config.userFields.includes('email')) {
672
- const emCheck = await this.$checkEmail();
673
- if (!emCheck.result) {
674
- throw new BadEmailError(emCheck.message);
647
+ if (this.$data.email === '' ||
648
+ !tilmeld.config.userFields.includes('email')) {
649
+ delete this.$data.email;
675
650
  }
676
- }
677
- // Validate group parent. Make sure it's not a descendant of this group.
678
- const parent = this.$data.parent;
679
- await parent?.$wake();
680
- if (parent &&
681
- (parent.cdate == null ||
682
- this.$is(parent) ||
683
- (await parent.$isDescendant(this)))) {
684
- throw new BadDataError("Group parent can't be itself or descendant of itself.");
685
- }
686
- try {
687
- tilmeld.config.validatorGroup(tilmeld, this);
688
- }
689
- catch (e) {
690
- throw new BadDataError(e?.message);
691
- }
692
- // Only one default primary group is allowed.
693
- if (this.$data.defaultPrimary) {
694
- const currentPrimary = await this.$nymph.getEntity({ class: tilmeld.Group }, { type: '&', truthy: 'defaultPrimary' });
695
- if (currentPrimary != null && !this.$is(currentPrimary)) {
696
- currentPrimary.defaultPrimary = false;
697
- if (this.$skipAcWhenSaving
698
- ? !(await currentPrimary.$saveSkipAC())
699
- : !(await currentPrimary.$save())) {
700
- throw new CouldNotChangeDefaultPrimaryGroupError('Could not change new user primary group from ' +
701
- `${currentPrimary.groupname}.`);
702
- }
651
+ if (this.$data.avatar === '') {
652
+ delete this.$data.avatar;
703
653
  }
704
- }
705
- const transaction = 'tilmeld-save-group-' + (this.guid || nanoid());
706
- const nymph = this.$nymph;
707
- const tnymph = await nymph.startTransaction(transaction);
708
- this.$setNymph(tnymph);
709
- tilmeld = enforceTilmeld(this);
710
- let preGuid = this.guid;
711
- let preCdate = this.cdate;
712
- let preMdate = this.mdate;
713
- try {
714
- for (let callback of this.constructor
715
- .beforeSaveCallbacks) {
716
- if (callback) {
717
- await callback(this);
718
- }
654
+ if (this.$data.phone === '' ||
655
+ !tilmeld.config.userFields.includes('phone')) {
656
+ delete this.$data.phone;
719
657
  }
720
- let ret = await super.$save();
721
- if (ret) {
722
- this.$originalGroupname = this.$data.groupname;
658
+ if (this.$data.parent == null) {
659
+ delete this.$data.parent;
723
660
  }
724
- for (let callback of this.constructor
725
- .afterSaveCallbacks) {
726
- if (callback) {
727
- await callback(this);
661
+ if (this.$data.user == null) {
662
+ delete this.$data.user;
663
+ }
664
+ if (!this.$data.defaultPrimary) {
665
+ delete this.$data.defaultPrimary;
666
+ }
667
+ if (!this.$data.defaultSecondary) {
668
+ delete this.$data.defaultSecondary;
669
+ }
670
+ if (!this.$data.unverifiedSecondary) {
671
+ delete this.$data.unverifiedSecondary;
672
+ }
673
+ // Groups should never have 'system/admin', 'tilmeld/admin',
674
+ // 'tilmeld/switch', or domain admin abilities.
675
+ this.$data.abilities = (this.$data.abilities ?? []).filter((ability) => {
676
+ if (['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
677
+ -1) {
678
+ return false;
679
+ }
680
+ if (ability == null) {
681
+ return false;
682
+ }
683
+ if (ability.startsWith('tilmeld/domain/') && ability.endsWith('/admin')) {
684
+ return false;
685
+ }
686
+ return true;
687
+ });
688
+ // Verification.
689
+ const unCheck = await this.$checkGroupname();
690
+ if (!unCheck.result) {
691
+ throw new BadUsernameError(unCheck.message);
692
+ }
693
+ if (!tilmeld.config.emailUsernames &&
694
+ tilmeld.config.userFields.includes('email')) {
695
+ const emCheck = await this.$checkEmail();
696
+ if (!emCheck.result) {
697
+ throw new BadEmailError(emCheck.message);
728
698
  }
729
699
  }
730
- ret = await tnymph.commit(transaction);
731
- this.$setNymph(nymph);
732
- return ret;
733
- }
734
- catch (e) {
735
- await tnymph.rollback(transaction);
736
- this.guid = preGuid;
737
- this.cdate = preCdate;
738
- this.mdate = preMdate;
739
- this.$setNymph(nymph);
740
- throw e;
741
- }
742
- }
743
- /*
744
- * This should *never* be accessible on the client.
745
- */
746
- async $saveSkipAC() {
747
- this.$skipAcWhenSaving = true;
748
- return await this.$save();
749
- }
750
- $tilmeldSaveSkipAC() {
751
- if (this.$skipAcWhenSaving) {
752
- this.$skipAcWhenSaving = false;
753
- return true;
700
+ // Validate group parent. Make sure it's not a descendant of this group.
701
+ const parent = this.$data.parent;
702
+ await parent?.$wake();
703
+ if (parent &&
704
+ (parent.cdate == null ||
705
+ this.$is(parent) ||
706
+ (await parent.$isDescendant(this)))) {
707
+ throw new BadDataError("Group parent can't be itself or descendant of itself.");
708
+ }
709
+ try {
710
+ tilmeld.config.validatorGroup(tilmeld, this);
711
+ }
712
+ catch (e) {
713
+ throw new BadDataError(e?.message);
714
+ }
715
+ // Only one default primary group is allowed.
716
+ if (this.$data.defaultPrimary) {
717
+ const currentPrimary = await this.$nymph.getEntity({ class: tilmeld.Group }, { type: '&', truthy: 'defaultPrimary' });
718
+ if (currentPrimary != null && !this.$is(currentPrimary)) {
719
+ currentPrimary.defaultPrimary = false;
720
+ try {
721
+ if (this.$skipAcWhenSaving) {
722
+ await currentPrimary.$saveSkipAC();
723
+ }
724
+ else {
725
+ await currentPrimary.$save();
726
+ }
727
+ }
728
+ catch (e) {
729
+ throw new CouldNotChangeDefaultPrimaryGroupError('Could not change new user primary group from ' +
730
+ `${currentPrimary.groupname}.`);
731
+ }
732
+ }
733
+ }
734
+ let preGuid = this.guid;
735
+ let preCdate = this.cdate;
736
+ let preMdate = this.mdate;
737
+ return await transaction(this.$nymph, 'tilmeld-save-group-' + (this.guid || nanoid()), async (nymph) => {
738
+ this.$setNymph(nymph);
739
+ for (let callback of this.constructor
740
+ .beforeSaveCallbacks) {
741
+ if (callback) {
742
+ await callback(this);
743
+ }
744
+ }
745
+ await super.$save();
746
+ for (let callback of this.constructor
747
+ .afterSaveCallbacks) {
748
+ if (callback) {
749
+ await callback(this);
750
+ }
751
+ }
752
+ }, async (nymph, committed) => {
753
+ this.$setNymph(nymph);
754
+ if (committed) {
755
+ this.$originalGroupname = this.$data.groupname;
756
+ }
757
+ else {
758
+ this.guid = preGuid;
759
+ this.cdate = preCdate;
760
+ this.mdate = preMdate;
761
+ }
762
+ });
754
763
  }
755
- return false;
756
- }
757
- async $delete() {
758
- let tilmeld = enforceTilmeld(this);
759
- if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
760
- throw new AccessControlError("You don't have the authority to delete groups.");
764
+ /*
765
+ * This should *never* be accessible on the client.
766
+ */
767
+ async $saveSkipAC() {
768
+ this.$skipAcWhenSaving = true;
769
+ await this.$save();
761
770
  }
762
- if (this.$data.groupname !== this.$originalGroupname) {
763
- throw new AccessControlError("You can't delete a group while switching its groupname.");
771
+ $tilmeldSaveSkipAC() {
772
+ if (this.$skipAcWhenSaving) {
773
+ this.$skipAcWhenSaving = false;
774
+ return true;
775
+ }
776
+ return false;
764
777
  }
765
- const transaction = 'tilmeld-delete-group-' + this.guid;
766
- const nymph = this.$nymph;
767
- const tnymph = await nymph.startTransaction(transaction);
768
- this.$setNymph(tnymph);
769
- tilmeld = enforceTilmeld(this);
770
- const User = tnymph.getEntityClass(UserClass);
771
- try {
778
+ async $delete() {
779
+ let tilmeld = enforceTilmeld(this);
780
+ const User = this.$nymph.getEntityClass(UserClass);
781
+ if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
782
+ throw new AccessControlError("You don't have the authority to delete groups.");
783
+ }
784
+ if (this.$data.groupname !== this.$originalGroupname) {
785
+ throw new AccessControlError("You can't delete a group while switching its groupname.");
786
+ }
772
787
  for (let callback of this.constructor
773
788
  .beforeDeleteCallbacks) {
774
789
  if (callback) {
775
790
  await callback(this);
776
791
  }
777
792
  }
778
- // Delete descendants.
779
- const descendants = await this.$getDescendants();
780
- if (descendants.length) {
781
- for (let curGroup of descendants) {
782
- if (this.$skipAcWhenDeleting
783
- ? !(await curGroup.$deleteSkipAC())
784
- : !(await curGroup.$delete())) {
785
- await tnymph.rollback(transaction);
786
- this.$setNymph(nymph);
787
- return false;
793
+ // Delete children.
794
+ const children = await this.$nymph.getEntities({ class: tilmeld.Group, skipAc: true }, { type: '&', ref: ['parent', this] });
795
+ if (children.length) {
796
+ for (let curGroup of children) {
797
+ if (this.$skipAcWhenDeleting) {
798
+ await curGroup.$deleteSkipAC();
799
+ }
800
+ else {
801
+ await curGroup.$delete();
788
802
  }
789
803
  }
790
804
  }
791
805
  // Remove users from this primary group.
792
- const primaryUsers = await tnymph.getEntities({
793
- class: User,
794
- skipAc: true,
795
- }, {
796
- type: '&',
797
- ref: ['group', this],
798
- });
806
+ const primaryUsers = await this.$nymph.getEntities({ class: User, skipAc: true }, { type: '&', ref: ['group', this] });
799
807
  for (let user of primaryUsers) {
800
808
  delete user.group;
801
- if (this.$skipAcWhenDeleting
802
- ? !(await user.$saveSkipAC())
803
- : !(await user.$save())) {
804
- await tnymph.rollback(transaction);
805
- this.$setNymph(nymph);
806
- return false;
809
+ if (this.$skipAcWhenDeleting) {
810
+ await user.$saveSkipAC();
811
+ }
812
+ else {
813
+ await user.$save();
807
814
  }
808
815
  }
809
816
  // Remove users from this secondary group.
810
- const secondaryUsers = await tnymph.getEntities({
811
- class: User,
812
- skipAc: true,
813
- }, {
814
- type: '&',
815
- ref: ['groups', this],
816
- });
817
+ const secondaryUsers = await this.$nymph.getEntities({ class: User, skipAc: true }, { type: '&', ref: ['groups', this] });
817
818
  for (let user of secondaryUsers) {
818
819
  user.$delGroup(this);
819
- if (this.$skipAcWhenDeleting
820
- ? !(await user.$saveSkipAC())
821
- : !(await user.$save())) {
822
- await tnymph.rollback(transaction);
823
- this.$setNymph(nymph);
824
- return false;
820
+ if (this.$skipAcWhenDeleting) {
821
+ await user.$saveSkipAC();
822
+ }
823
+ else {
824
+ await user.$save();
825
825
  }
826
826
  }
827
827
  // Delete the group.
828
- let success = await super.$delete();
829
- if (success) {
830
- for (let callback of this.constructor
831
- .afterDeleteCallbacks) {
832
- if (callback) {
833
- await callback(this);
834
- }
828
+ await super.$delete();
829
+ for (let callback of this.constructor
830
+ .afterDeleteCallbacks) {
831
+ if (callback) {
832
+ await callback(this);
835
833
  }
836
- success = await tnymph.commit(transaction);
837
834
  }
838
- else {
839
- await tnymph.rollback(transaction);
840
- }
841
- this.$setNymph(nymph);
842
- return success;
843
835
  }
844
- catch (e) {
845
- await tnymph.rollback(transaction);
846
- this.$setNymph(nymph);
847
- throw e;
836
+ /*
837
+ * This should *never* be accessible on the client.
838
+ */
839
+ async $deleteSkipAC() {
840
+ this.$skipAcWhenDeleting = true;
841
+ await this.$delete();
848
842
  }
849
- }
850
- /*
851
- * This should *never* be accessible on the client.
852
- */
853
- async $deleteSkipAC() {
854
- this.$skipAcWhenDeleting = true;
855
- return await this.$delete();
856
- }
857
- $tilmeldDeleteSkipAC() {
858
- if (this.$skipAcWhenDeleting) {
859
- this.$skipAcWhenDeleting = false;
860
- return true;
861
- }
862
- return false;
863
- }
864
- static on(event, callback) {
865
- const prop = (event + 'Callbacks');
866
- if (!(prop in this)) {
867
- throw new Error('Invalid event type.');
868
- }
869
- // @ts-ignore: The callback should always be the right type here.
870
- this[prop].push(callback);
871
- return () => this.off(event, callback);
872
- }
873
- static off(event, callback) {
874
- const prop = (event + 'Callbacks');
875
- if (!(prop in this)) {
843
+ $tilmeldDeleteSkipAC() {
844
+ if (this.$skipAcWhenDeleting) {
845
+ this.$skipAcWhenDeleting = false;
846
+ return true;
847
+ }
876
848
  return false;
877
849
  }
878
- // @ts-ignore: The callback should always be the right type here.
879
- const i = this[prop].indexOf(callback);
880
- if (i > -1) {
850
+ static on(event, callback) {
851
+ const prop = (event + 'Callbacks');
852
+ if (!(prop in this)) {
853
+ throw new Error('Invalid event type.');
854
+ }
881
855
  // @ts-ignore: The callback should always be the right type here.
882
- this[prop].splice(i, 1);
856
+ this[prop].push(callback);
857
+ return () => this.off(event, callback);
883
858
  }
884
- return true;
885
- }
886
- }
859
+ static off(event, callback) {
860
+ const prop = (event + 'Callbacks');
861
+ if (!(prop in this)) {
862
+ return false;
863
+ }
864
+ // @ts-ignore: The callback should always be the right type here.
865
+ const i = this[prop].indexOf(callback);
866
+ if (i > -1) {
867
+ // @ts-ignore: The callback should always be the right type here.
868
+ this[prop].splice(i, 1);
869
+ }
870
+ return true;
871
+ }
872
+ };
873
+ })();
874
+ /**
875
+ * A user group data model.
876
+ *
877
+ * Written by Hunter Perrin for SciActive.
878
+ *
879
+ * @author Hunter Perrin <hperrin@port87.com>
880
+ * @copyright SciActive Inc
881
+ * @see http://nymph.io/
882
+ */
883
+ export default Group;
887
884
  //# sourceMappingURL=Group.js.map