@nymphjs/tilmeld 1.0.0-beta.11 → 1.0.0-beta.110
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 +498 -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 +182 -15
- package/dist/Group.js +564 -191
- package/dist/Group.js.map +1 -1
- package/dist/Group.test.js +86 -13
- package/dist/Group.test.js.map +1 -1
- package/dist/Tilmeld.d.ts +241 -12
- package/dist/Tilmeld.js +657 -257
- package/dist/Tilmeld.js.map +1 -1
- package/dist/Tilmeld.types.d.ts +5 -5
- package/dist/Tilmeld.types.js +1 -2
- package/dist/User.d.ts +396 -22
- package/dist/User.js +1379 -367
- package/dist/User.js.map +1 -1
- package/dist/User.test.js +133 -13
- package/dist/User.test.js.map +1 -1
- package/dist/conf/d.d.ts +306 -7
- package/dist/conf/d.js +1 -2
- package/dist/conf/defaults.d.ts +1 -1
- package/dist/conf/defaults.js +196 -141
- package/dist/conf/defaults.js.map +1 -1
- package/dist/conf/index.d.ts +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 +12 -10
- package/dist/index.js +17 -35
- package/dist/index.js.map +1 -1
- package/dist/testArtifacts.d.ts +19 -0
- package/dist/testArtifacts.js +920 -0
- package/dist/testArtifacts.js.map +1 -0
- package/dist/tilmeldJoiProps.d.ts +3 -3
- package/dist/tilmeldJoiProps.js +13 -18
- package/dist/tilmeldJoiProps.js.map +1 -1
- package/jest.config.js +11 -2
- package/package.json +32 -31
- package/src/AbleObject.ts +2 -2
- package/src/Group.test.ts +107 -6
- package/src/Group.ts +676 -224
- package/src/Tilmeld.ts +450 -262
- package/src/Tilmeld.types.ts +5 -5
- package/src/User.test.ts +163 -7
- package/src/User.ts +1621 -454
- package/src/conf/d.ts +83 -17
- package/src/conf/defaults.ts +172 -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 +14 -10
- package/src/testArtifacts.ts +1485 -0
- package/src/tilmeldJoiProps.ts +8 -23
- package/src/type/locutus.d.ts +3 -0
- package/tsconfig.json +5 -3
- package/typedoc.json +4 -0
package/src/Group.ts
CHANGED
|
@@ -1,24 +1,74 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
Nymph,
|
|
2
4
|
EntityData,
|
|
3
|
-
EntityInterface,
|
|
4
5
|
EntityJson,
|
|
5
6
|
EntityPatch,
|
|
6
7
|
Options,
|
|
7
8
|
Selector,
|
|
8
9
|
SerializedEntityData,
|
|
9
10
|
} from '@nymphjs/nymph';
|
|
10
|
-
import
|
|
11
|
-
import { difference } from 'lodash';
|
|
11
|
+
import { nanoid } from '@nymphjs/guid';
|
|
12
|
+
import { difference, xor } from 'lodash-es';
|
|
13
|
+
import { splitn } from '@sciactive/splitn';
|
|
12
14
|
|
|
13
|
-
import
|
|
14
|
-
import AbleObject from './AbleObject';
|
|
15
|
+
import { enforceTilmeld } from './enforceTilmeld.js';
|
|
16
|
+
import AbleObject from './AbleObject.js';
|
|
15
17
|
import {
|
|
18
|
+
AccessControlError,
|
|
16
19
|
BadDataError,
|
|
17
20
|
BadEmailError,
|
|
18
21
|
BadUsernameError,
|
|
19
22
|
CouldNotChangeDefaultPrimaryGroupError,
|
|
20
|
-
} from './errors';
|
|
21
|
-
import
|
|
23
|
+
} from './errors/index.js';
|
|
24
|
+
import UserClass from './User.js';
|
|
25
|
+
import type { UserData } from './User.js';
|
|
26
|
+
|
|
27
|
+
export type GroupEventType =
|
|
28
|
+
| 'checkGroupname'
|
|
29
|
+
| 'beforeSave'
|
|
30
|
+
| 'afterSave'
|
|
31
|
+
| 'beforeDelete'
|
|
32
|
+
| 'afterDelete';
|
|
33
|
+
/**
|
|
34
|
+
* This is run when the user has entered an otherwise valid groupname into the
|
|
35
|
+
* signup form. It should return a result, which when false will stop the
|
|
36
|
+
* process and return the included message, disallowing the groupname.
|
|
37
|
+
*/
|
|
38
|
+
export type TilmeldCheckGroupnameCallback = (
|
|
39
|
+
group: Group & GroupData,
|
|
40
|
+
data: { groupname: string },
|
|
41
|
+
) => Promise<{ result: boolean; message?: string }>;
|
|
42
|
+
/**
|
|
43
|
+
* This is run just before the group is saved, so all checks have passed.
|
|
44
|
+
*
|
|
45
|
+
* The transaction is available in `group.$nymph`.
|
|
46
|
+
*/
|
|
47
|
+
export type TilmeldBeforeGroupSaveCallback = (
|
|
48
|
+
group: Group & GroupData,
|
|
49
|
+
) => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* This is run before the transaction is committed, and you can perform
|
|
52
|
+
* additional functions on the transaction, which is available in
|
|
53
|
+
* `group.$nymph`.
|
|
54
|
+
*/
|
|
55
|
+
export type TilmeldAfterGroupSaveCallback = (
|
|
56
|
+
group: Group & GroupData,
|
|
57
|
+
) => Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* The transaction is available in `group.$nymph`.
|
|
60
|
+
*/
|
|
61
|
+
export type TilmeldBeforeGroupDeleteCallback = (
|
|
62
|
+
group: Group & GroupData,
|
|
63
|
+
) => Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* This is run before the transaction is committed, and you can perform
|
|
66
|
+
* additional functions on the transaction, which is available in
|
|
67
|
+
* `group.$nymph`.
|
|
68
|
+
*/
|
|
69
|
+
export type TilmeldAfterGroupDeleteCallback = (
|
|
70
|
+
group: Group & GroupData,
|
|
71
|
+
) => Promise<void>;
|
|
22
72
|
|
|
23
73
|
export type GroupData = {
|
|
24
74
|
/**
|
|
@@ -52,7 +102,7 @@ export type GroupData = {
|
|
|
52
102
|
/**
|
|
53
103
|
* If generatePrimary is on, this will be the user who generated this group.
|
|
54
104
|
*/
|
|
55
|
-
user?: (
|
|
105
|
+
user?: (UserClass & UserData) | null;
|
|
56
106
|
|
|
57
107
|
/**
|
|
58
108
|
* Whether the group can be used.
|
|
@@ -85,9 +135,13 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
85
135
|
/**
|
|
86
136
|
* The instance of Tilmeld to use for queries.
|
|
87
137
|
*/
|
|
88
|
-
static tilmeld: Tilmeld;
|
|
89
138
|
static ETYPE = 'tilmeld_group';
|
|
90
139
|
static class = 'Group';
|
|
140
|
+
private static checkGroupnameCallbacks: TilmeldCheckGroupnameCallback[] = [];
|
|
141
|
+
private static beforeSaveCallbacks: TilmeldBeforeGroupSaveCallback[] = [];
|
|
142
|
+
private static afterSaveCallbacks: TilmeldAfterGroupSaveCallback[] = [];
|
|
143
|
+
private static beforeDeleteCallbacks: TilmeldBeforeGroupDeleteCallback[] = [];
|
|
144
|
+
private static afterDeleteCallbacks: TilmeldAfterGroupDeleteCallback[] = [];
|
|
91
145
|
|
|
92
146
|
private static DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
|
|
93
147
|
private static DEFAULT_ALLOWLIST_DATA: string[] = [];
|
|
@@ -112,16 +166,37 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
112
166
|
protected $allowlistTags?: string[] = [];
|
|
113
167
|
|
|
114
168
|
/**
|
|
115
|
-
* This
|
|
169
|
+
* This should only be used by the backend.
|
|
116
170
|
*/
|
|
117
171
|
private $skipAcWhenSaving = false;
|
|
172
|
+
/**
|
|
173
|
+
* This should only be used by the backend.
|
|
174
|
+
*/
|
|
175
|
+
private $skipAcWhenDeleting = false;
|
|
176
|
+
/**
|
|
177
|
+
* Used to save the current groupname for domain admin permissions.
|
|
178
|
+
*/
|
|
179
|
+
public $originalGroupname?: string;
|
|
118
180
|
|
|
119
|
-
static
|
|
120
|
-
|
|
181
|
+
public static getFTSText(name: string, value: any) {
|
|
182
|
+
if (
|
|
183
|
+
[
|
|
184
|
+
'abilities',
|
|
185
|
+
'parent',
|
|
186
|
+
'user',
|
|
187
|
+
'enabled',
|
|
188
|
+
'defaultPrimary',
|
|
189
|
+
'defaultSecondary',
|
|
190
|
+
'unverifiedSecondary',
|
|
191
|
+
].indexOf(name) !== -1
|
|
192
|
+
) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return super.getFTSText(name, value);
|
|
121
196
|
}
|
|
122
197
|
|
|
123
198
|
static async factoryGroupname(
|
|
124
|
-
groupname?: string
|
|
199
|
+
groupname?: string,
|
|
125
200
|
): Promise<Group & GroupData> {
|
|
126
201
|
const entity = new this();
|
|
127
202
|
if (groupname != null) {
|
|
@@ -132,7 +207,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
132
207
|
{
|
|
133
208
|
type: '&',
|
|
134
209
|
ilike: ['groupname', groupname.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
135
|
-
}
|
|
210
|
+
},
|
|
136
211
|
);
|
|
137
212
|
if (entity != null) {
|
|
138
213
|
return entity;
|
|
@@ -141,21 +216,6 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
141
216
|
return entity;
|
|
142
217
|
}
|
|
143
218
|
|
|
144
|
-
static factorySync(guid?: string): Group & GroupData {
|
|
145
|
-
return super.factorySync(guid) as Group & GroupData;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
constructor(guid?: string) {
|
|
149
|
-
super(guid);
|
|
150
|
-
|
|
151
|
-
if (this.guid == null) {
|
|
152
|
-
// Defaults.
|
|
153
|
-
this.$data.enabled = true;
|
|
154
|
-
this.$data.abilities = [];
|
|
155
|
-
this.$updateDataProtection();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
219
|
/**
|
|
160
220
|
* Get all the groups that can be assigned as primary groups.
|
|
161
221
|
*
|
|
@@ -165,16 +225,17 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
165
225
|
*/
|
|
166
226
|
public static async getPrimaryGroups(
|
|
167
227
|
options?: Options,
|
|
168
|
-
selectors?: Selector[]
|
|
228
|
+
selectors?: Selector[],
|
|
169
229
|
) {
|
|
170
|
-
|
|
230
|
+
const tilmeld = enforceTilmeld(this);
|
|
231
|
+
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
171
232
|
throw new Error("You don't have permission to do that.");
|
|
172
233
|
}
|
|
173
234
|
|
|
174
235
|
return await this.getAssignableGroups(
|
|
175
|
-
|
|
176
|
-
{ ...options, class:
|
|
177
|
-
[...(selectors ?? [])]
|
|
236
|
+
tilmeld.config.highestPrimary,
|
|
237
|
+
{ ...options, class: tilmeld.Group, return: 'entity' },
|
|
238
|
+
[...(selectors ?? [])],
|
|
178
239
|
);
|
|
179
240
|
}
|
|
180
241
|
|
|
@@ -187,24 +248,26 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
187
248
|
*/
|
|
188
249
|
public static async getSecondaryGroups(
|
|
189
250
|
options?: Options,
|
|
190
|
-
selectors?: Selector[]
|
|
251
|
+
selectors?: Selector[],
|
|
191
252
|
) {
|
|
192
|
-
|
|
253
|
+
const tilmeld = enforceTilmeld(this);
|
|
254
|
+
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
193
255
|
throw new Error("You don't have permission to do that.");
|
|
194
256
|
}
|
|
195
257
|
|
|
196
258
|
return await this.getAssignableGroups(
|
|
197
|
-
|
|
198
|
-
{ ...options, class:
|
|
199
|
-
[...(selectors ?? [])]
|
|
259
|
+
tilmeld.config.highestSecondary,
|
|
260
|
+
{ ...options, class: tilmeld.Group, return: 'entity' },
|
|
261
|
+
[...(selectors ?? [])],
|
|
200
262
|
);
|
|
201
263
|
}
|
|
202
264
|
|
|
203
265
|
private static async getAssignableGroups(
|
|
204
266
|
highestParent: string | boolean,
|
|
205
267
|
options: Options<typeof Group>,
|
|
206
|
-
selectors: Selector[]
|
|
268
|
+
selectors: Selector[],
|
|
207
269
|
) {
|
|
270
|
+
const tilmeld = enforceTilmeld(this);
|
|
208
271
|
let assignableGroups: (Group & GroupData)[] = [];
|
|
209
272
|
|
|
210
273
|
if (highestParent === false) {
|
|
@@ -212,24 +275,68 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
212
275
|
}
|
|
213
276
|
|
|
214
277
|
assignableGroups = await this.nymph.getEntities(
|
|
215
|
-
{ ...options, class:
|
|
216
|
-
...selectors
|
|
278
|
+
{ ...options, class: tilmeld.Group },
|
|
279
|
+
...selectors,
|
|
217
280
|
);
|
|
218
281
|
if (highestParent !== true) {
|
|
219
|
-
assignableGroups =
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
282
|
+
assignableGroups = (
|
|
283
|
+
await Promise.all(
|
|
284
|
+
assignableGroups.map(async (group) => {
|
|
285
|
+
let curGroup = group;
|
|
286
|
+
let parent = curGroup.parent;
|
|
287
|
+
await parent?.$wake();
|
|
288
|
+
while (parent != null && parent?.cdate != null) {
|
|
289
|
+
if (parent?.guid === highestParent) {
|
|
290
|
+
return group;
|
|
291
|
+
}
|
|
292
|
+
curGroup = parent;
|
|
293
|
+
parent = curGroup.parent;
|
|
294
|
+
await parent?.$wake();
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}),
|
|
298
|
+
)
|
|
299
|
+
).filter((group) => group != null) as (Group & GroupData)[];
|
|
229
300
|
}
|
|
230
301
|
return assignableGroups;
|
|
231
302
|
}
|
|
232
303
|
|
|
304
|
+
constructor() {
|
|
305
|
+
super();
|
|
306
|
+
|
|
307
|
+
// Defaults.
|
|
308
|
+
this.$data.enabled = true;
|
|
309
|
+
this.$data.abilities = [];
|
|
310
|
+
this.$updateDataProtection();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
$setNymph(nymph: Nymph) {
|
|
314
|
+
this.$nymph = nymph;
|
|
315
|
+
if (!this.$asleep()) {
|
|
316
|
+
if (this.$data.user && this.$data.user.$nymph !== nymph) {
|
|
317
|
+
this.$data.user.$setNymph(nymph);
|
|
318
|
+
}
|
|
319
|
+
if (this.$data.parent && this.$data.parent.$nymph !== nymph) {
|
|
320
|
+
this.$data.parent.$setNymph(nymph);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async $getUniques(): Promise<string[]> {
|
|
326
|
+
const tilmeld = enforceTilmeld(this);
|
|
327
|
+
const uniques = [`u:${this.$data.groupname}`];
|
|
328
|
+
if (
|
|
329
|
+
!tilmeld.config.emailUsernames &&
|
|
330
|
+
tilmeld.config.userFields.includes('email')
|
|
331
|
+
) {
|
|
332
|
+
uniques.push(`e:${this.$data.email}`);
|
|
333
|
+
}
|
|
334
|
+
if (this.$data.defaultPrimary) {
|
|
335
|
+
uniques.push(`o:default-primary`);
|
|
336
|
+
}
|
|
337
|
+
return uniques;
|
|
338
|
+
}
|
|
339
|
+
|
|
233
340
|
public $getAvatar() {
|
|
234
341
|
if (this.$data.avatar != null && this.$data.avatar !== '') {
|
|
235
342
|
return this.$data.avatar;
|
|
@@ -239,49 +346,104 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
239
346
|
}
|
|
240
347
|
return (
|
|
241
348
|
'https://secure.gravatar.com/avatar/' +
|
|
242
|
-
|
|
349
|
+
crypto
|
|
350
|
+
.createHash('sha256')
|
|
351
|
+
.update(this.$data.email.trim().toLowerCase())
|
|
352
|
+
.digest('hex')
|
|
353
|
+
.toLowerCase() +
|
|
243
354
|
'?d=identicon&s=40'
|
|
244
355
|
);
|
|
245
356
|
}
|
|
246
357
|
|
|
247
358
|
public $jsonAcceptData(input: EntityJson, allowConflict = false) {
|
|
248
|
-
|
|
249
|
-
this.$referenceWake();
|
|
359
|
+
this.$check();
|
|
250
360
|
|
|
251
|
-
if (
|
|
252
|
-
input.data.abilities
|
|
253
|
-
this.$data.abilities?.indexOf('system/admin') === -1 &&
|
|
254
|
-
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
255
|
-
!tilmeld.gatekeeper('system/admin')
|
|
256
|
-
) {
|
|
257
|
-
throw new BadDataError(
|
|
258
|
-
"You don't have the authority to make this group a system admin."
|
|
259
|
-
);
|
|
361
|
+
if ('abilities' in input.data) {
|
|
362
|
+
this.$checkIncomingAbilities(input.data.abilities);
|
|
260
363
|
}
|
|
261
364
|
|
|
262
365
|
super.$jsonAcceptData(input, allowConflict);
|
|
263
366
|
}
|
|
264
367
|
|
|
265
368
|
public $jsonAcceptPatch(patch: EntityPatch, allowConflict = false) {
|
|
266
|
-
|
|
267
|
-
|
|
369
|
+
this.$check();
|
|
370
|
+
|
|
371
|
+
if ('abilities' in patch.set) {
|
|
372
|
+
this.$checkIncomingAbilities(patch.set.abilities);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
super.$jsonAcceptPatch(patch, allowConflict);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private $checkIncomingAbilities(incoming: any) {
|
|
379
|
+
const tilmeld = enforceTilmeld(this);
|
|
380
|
+
|
|
381
|
+
if (
|
|
382
|
+
!Array.isArray(incoming) ||
|
|
383
|
+
incoming.find((ability) => typeof ability !== 'string')
|
|
384
|
+
) {
|
|
385
|
+
throw new BadDataError('Abilities must be an array of strings.');
|
|
386
|
+
}
|
|
268
387
|
|
|
269
388
|
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
tilmeld.
|
|
273
|
-
|
|
389
|
+
incoming.includes('system/admin') !==
|
|
390
|
+
this.$data.abilities?.includes('system/admin') &&
|
|
391
|
+
(!tilmeld.currentUser ||
|
|
392
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
274
393
|
) {
|
|
275
394
|
throw new BadDataError(
|
|
276
|
-
"You don't have the authority to
|
|
395
|
+
"You don't have the authority to grant or revoke system/admin.",
|
|
277
396
|
);
|
|
278
397
|
}
|
|
279
398
|
|
|
280
|
-
|
|
399
|
+
if (
|
|
400
|
+
incoming.includes('tilmeld/admin') !==
|
|
401
|
+
this.$data.abilities?.includes('tilmeld/admin') &&
|
|
402
|
+
(!tilmeld.currentUser ||
|
|
403
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
404
|
+
) {
|
|
405
|
+
throw new BadDataError(
|
|
406
|
+
"You don't have the authority to grant or revoke tilmeld/admin.",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (
|
|
411
|
+
incoming.includes('tilmeld/switch') !==
|
|
412
|
+
this.$data.abilities?.includes('tilmeld/switch') &&
|
|
413
|
+
(!tilmeld.currentUser ||
|
|
414
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))
|
|
415
|
+
) {
|
|
416
|
+
throw new BadDataError(
|
|
417
|
+
"You don't have the authority to grant or revoke tilmeld/switch.",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (
|
|
422
|
+
xor(
|
|
423
|
+
incoming.filter((ability: string) => ability.startsWith('tilmeld/')),
|
|
424
|
+
this.$data.abilities?.filter((ability: string) =>
|
|
425
|
+
ability.startsWith('tilmeld/'),
|
|
426
|
+
) ?? [],
|
|
427
|
+
).length &&
|
|
428
|
+
(!tilmeld.currentUser ||
|
|
429
|
+
(!tilmeld.currentUser.abilities?.includes('system/admin') &&
|
|
430
|
+
!tilmeld.currentUser.abilities?.includes('tilmeld/admin')))
|
|
431
|
+
) {
|
|
432
|
+
throw new BadDataError(
|
|
433
|
+
"You don't have the authority to grant or revoke Tilmeld abilities.",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
281
436
|
}
|
|
282
437
|
|
|
283
|
-
public $putData(
|
|
284
|
-
|
|
438
|
+
public $putData(
|
|
439
|
+
data: EntityData,
|
|
440
|
+
sdata?: SerializedEntityData,
|
|
441
|
+
source?: 'server',
|
|
442
|
+
) {
|
|
443
|
+
super.$putData(data, sdata, source);
|
|
444
|
+
if (source === 'server') {
|
|
445
|
+
this.$originalGroupname = this.$data.groupname;
|
|
446
|
+
}
|
|
285
447
|
this.$updateDataProtection();
|
|
286
448
|
}
|
|
287
449
|
|
|
@@ -290,9 +452,16 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
290
452
|
*
|
|
291
453
|
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
292
454
|
*/
|
|
293
|
-
public $updateDataProtection(givenUser?:
|
|
294
|
-
const tilmeld = this
|
|
455
|
+
public $updateDataProtection(givenUser?: UserClass & UserData) {
|
|
456
|
+
const tilmeld = enforceTilmeld(this);
|
|
295
457
|
let user = givenUser ?? tilmeld.User.current();
|
|
458
|
+
let [_groupname, domain] = tilmeld.config.domainSupport
|
|
459
|
+
? splitn(this.$data.groupname ?? '', '@', 2)
|
|
460
|
+
: [this.$data.groupname, null];
|
|
461
|
+
// Lowercase the domain part.
|
|
462
|
+
if (domain) {
|
|
463
|
+
domain = domain.toLowerCase();
|
|
464
|
+
}
|
|
296
465
|
|
|
297
466
|
this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
298
467
|
this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
|
|
@@ -303,8 +472,8 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
303
472
|
|
|
304
473
|
if (
|
|
305
474
|
user != null &&
|
|
306
|
-
(
|
|
307
|
-
|
|
475
|
+
(user.abilities?.includes('tilmeld/admin') ||
|
|
476
|
+
user.abilities?.includes('system/admin'))
|
|
308
477
|
) {
|
|
309
478
|
// Users who can edit groups can see their data.
|
|
310
479
|
this.$privateData = [];
|
|
@@ -312,10 +481,18 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
312
481
|
} else if (
|
|
313
482
|
this.$data.user != null &&
|
|
314
483
|
user != null &&
|
|
315
|
-
this.$data.user
|
|
484
|
+
this.$data.user.guid === user.guid
|
|
316
485
|
) {
|
|
317
486
|
// Users can see their group's data.
|
|
318
487
|
this.$privateData = [];
|
|
488
|
+
} else if (
|
|
489
|
+
user != null &&
|
|
490
|
+
tilmeld.config.domainSupport &&
|
|
491
|
+
domain &&
|
|
492
|
+
user.abilities?.includes(`tilmeld/domain/${domain}/admin`)
|
|
493
|
+
) {
|
|
494
|
+
// Domain admins can see their groups' data.
|
|
495
|
+
this.$privateData = [];
|
|
319
496
|
}
|
|
320
497
|
}
|
|
321
498
|
|
|
@@ -325,11 +502,13 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
325
502
|
* @param group The group, or the group's GUID.
|
|
326
503
|
* @returns True or false.
|
|
327
504
|
*/
|
|
328
|
-
public $isDescendant(
|
|
329
|
-
|
|
505
|
+
public async $isDescendant(
|
|
506
|
+
givenGroup: (Group & GroupData) | string,
|
|
507
|
+
): Promise<boolean> {
|
|
508
|
+
const tilmeld = enforceTilmeld(this);
|
|
330
509
|
let group: Group & GroupData;
|
|
331
510
|
if (typeof givenGroup === 'string') {
|
|
332
|
-
group = tilmeld.Group.
|
|
511
|
+
group = await tilmeld.Group.factory(givenGroup);
|
|
333
512
|
} else {
|
|
334
513
|
group = givenGroup;
|
|
335
514
|
}
|
|
@@ -337,13 +516,15 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
337
516
|
return false;
|
|
338
517
|
}
|
|
339
518
|
// Check to see if the group is a descendant of the given group.
|
|
340
|
-
|
|
519
|
+
const parent = this.$data.parent;
|
|
520
|
+
await parent?.$wake();
|
|
521
|
+
if (parent == null) {
|
|
341
522
|
return false;
|
|
342
523
|
}
|
|
343
|
-
if (
|
|
524
|
+
if (parent.$is(group)) {
|
|
344
525
|
return true;
|
|
345
526
|
}
|
|
346
|
-
if (
|
|
527
|
+
if (await parent.$isDescendant(group)) {
|
|
347
528
|
return true;
|
|
348
529
|
}
|
|
349
530
|
return false;
|
|
@@ -355,14 +536,14 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
355
536
|
* @returns An array of groups.
|
|
356
537
|
*/
|
|
357
538
|
public async $getChildren() {
|
|
358
|
-
const tilmeld = this
|
|
539
|
+
const tilmeld = enforceTilmeld(this);
|
|
359
540
|
return await this.$nymph.getEntities(
|
|
360
541
|
{ class: tilmeld.Group },
|
|
361
542
|
{
|
|
362
543
|
type: '&',
|
|
363
544
|
equal: ['enabled', true],
|
|
364
545
|
ref: ['parent', this],
|
|
365
|
-
}
|
|
546
|
+
},
|
|
366
547
|
);
|
|
367
548
|
}
|
|
368
549
|
|
|
@@ -373,9 +554,9 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
373
554
|
* @returns An array of groups.
|
|
374
555
|
*/
|
|
375
556
|
public async $getDescendants(
|
|
376
|
-
andSelf = false
|
|
557
|
+
andSelf = false,
|
|
377
558
|
): Promise<(Group & GroupData)[]> {
|
|
378
|
-
const tilmeld = this
|
|
559
|
+
const tilmeld = enforceTilmeld(this);
|
|
379
560
|
let groups: (Group & GroupData)[] = [];
|
|
380
561
|
const entities = await this.$nymph.getEntities(
|
|
381
562
|
{ class: tilmeld.Group },
|
|
@@ -383,7 +564,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
383
564
|
type: '&',
|
|
384
565
|
equal: ['enabled', true],
|
|
385
566
|
ref: ['parent', this],
|
|
386
|
-
}
|
|
567
|
+
},
|
|
387
568
|
);
|
|
388
569
|
for (let entity of entities) {
|
|
389
570
|
groups = (await entity.$getDescendants(true)).concat(groups);
|
|
@@ -394,37 +575,6 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
394
575
|
return groups;
|
|
395
576
|
}
|
|
396
577
|
|
|
397
|
-
/**
|
|
398
|
-
* Gets an array of the group's descendant groups.
|
|
399
|
-
*
|
|
400
|
-
* @param andSelf Include this group in the returned array.
|
|
401
|
-
* @returns An array of groups.
|
|
402
|
-
*/
|
|
403
|
-
public $getDescendantsSync(andSelf = false): (Group & GroupData)[] {
|
|
404
|
-
const tilmeld = this.$nymph.tilmeld as Tilmeld;
|
|
405
|
-
let groups: (Group & GroupData)[] = [];
|
|
406
|
-
let entity: EntityInterface | null;
|
|
407
|
-
let offset = 0;
|
|
408
|
-
do {
|
|
409
|
-
entity = this.$nymph.driver.getEntitySync(
|
|
410
|
-
{ class: tilmeld.Group, offset },
|
|
411
|
-
{
|
|
412
|
-
type: '&',
|
|
413
|
-
equal: ['enabled', true],
|
|
414
|
-
ref: ['parent', this],
|
|
415
|
-
}
|
|
416
|
-
);
|
|
417
|
-
if (entity != null) {
|
|
418
|
-
groups = entity.$getDescendantsSync(true).concat(groups);
|
|
419
|
-
}
|
|
420
|
-
offset++;
|
|
421
|
-
} while (entity != null);
|
|
422
|
-
if (andSelf) {
|
|
423
|
-
groups.push(this);
|
|
424
|
-
}
|
|
425
|
-
return groups;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
578
|
/**
|
|
429
579
|
* Get the number of parents the group has.
|
|
430
580
|
*
|
|
@@ -436,13 +586,17 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
436
586
|
*
|
|
437
587
|
* @returns The level of the group.
|
|
438
588
|
*/
|
|
439
|
-
public $getLevel() {
|
|
440
|
-
const tilmeld = this
|
|
441
|
-
let group = tilmeld.Group.
|
|
589
|
+
public async $getLevel() {
|
|
590
|
+
const tilmeld = enforceTilmeld(this);
|
|
591
|
+
let group = await tilmeld.Group.factory(this.guid ?? undefined);
|
|
592
|
+
let parent = group.parent;
|
|
593
|
+
await parent?.$wake();
|
|
442
594
|
let level = 0;
|
|
443
|
-
while (
|
|
595
|
+
while (parent != null && parent.cdate != null && level < 1024) {
|
|
444
596
|
level++;
|
|
445
|
-
group =
|
|
597
|
+
group = parent;
|
|
598
|
+
parent = group.parent;
|
|
599
|
+
await parent?.$wake();
|
|
446
600
|
}
|
|
447
601
|
return level;
|
|
448
602
|
}
|
|
@@ -458,9 +612,9 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
458
612
|
public async $getUsers(
|
|
459
613
|
descendants = false,
|
|
460
614
|
limit?: number,
|
|
461
|
-
offset?: number
|
|
462
|
-
): Promise<(
|
|
463
|
-
const tilmeld = this
|
|
615
|
+
offset?: number,
|
|
616
|
+
): Promise<(UserClass & UserData)[]> {
|
|
617
|
+
const tilmeld = enforceTilmeld(this);
|
|
464
618
|
let groups: (Group & GroupData)[] = [];
|
|
465
619
|
if (descendants) {
|
|
466
620
|
groups = await this.$getDescendants();
|
|
@@ -489,7 +643,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
489
643
|
group,
|
|
490
644
|
]),
|
|
491
645
|
],
|
|
492
|
-
}
|
|
646
|
+
},
|
|
493
647
|
);
|
|
494
648
|
}
|
|
495
649
|
|
|
@@ -502,15 +656,36 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
502
656
|
result: boolean;
|
|
503
657
|
message: string;
|
|
504
658
|
}> {
|
|
505
|
-
const tilmeld = this
|
|
659
|
+
const tilmeld = enforceTilmeld(this);
|
|
506
660
|
if (!tilmeld.config.emailUsernames) {
|
|
507
|
-
|
|
508
|
-
|
|
661
|
+
let [groupname, domain] = tilmeld.config.domainSupport
|
|
662
|
+
? splitn(this.$data.groupname ?? '', '@', 2)
|
|
663
|
+
: [this.$data.groupname, null];
|
|
664
|
+
|
|
665
|
+
// Lowercase the domain part.
|
|
666
|
+
if (domain) {
|
|
667
|
+
domain = domain.toLowerCase();
|
|
668
|
+
this.$data.groupname = `${groupname}@${domain}`;
|
|
509
669
|
}
|
|
670
|
+
|
|
510
671
|
if (
|
|
511
|
-
|
|
512
|
-
this.$data.groupname.length
|
|
672
|
+
this.$data.groupname == null ||
|
|
673
|
+
!this.$data.groupname.length ||
|
|
674
|
+
groupname == null ||
|
|
675
|
+
!groupname.length
|
|
513
676
|
) {
|
|
677
|
+
return { result: false, message: 'Please specify a groupname.' };
|
|
678
|
+
}
|
|
679
|
+
if (groupname.length < tilmeld.config.minUsernameLength) {
|
|
680
|
+
return {
|
|
681
|
+
result: false,
|
|
682
|
+
message:
|
|
683
|
+
'Groupnames must be at least ' +
|
|
684
|
+
tilmeld.config.minUsernameLength +
|
|
685
|
+
' characters.',
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
if (groupname.length > tilmeld.config.maxUsernameLength) {
|
|
514
689
|
return {
|
|
515
690
|
result: false,
|
|
516
691
|
message:
|
|
@@ -520,22 +695,38 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
520
695
|
};
|
|
521
696
|
}
|
|
522
697
|
if (
|
|
523
|
-
difference(
|
|
524
|
-
|
|
525
|
-
tilmeld.config.validChars.split('')
|
|
526
|
-
).length
|
|
698
|
+
difference(groupname.split(''), tilmeld.config.validChars.split(''))
|
|
699
|
+
.length
|
|
527
700
|
) {
|
|
528
701
|
return {
|
|
529
702
|
result: false,
|
|
530
703
|
message: tilmeld.config.validCharsNotice,
|
|
531
704
|
};
|
|
532
705
|
}
|
|
533
|
-
if (!tilmeld.config.validRegex.test(
|
|
706
|
+
if (!tilmeld.config.validRegex.test(groupname)) {
|
|
534
707
|
return {
|
|
535
708
|
result: false,
|
|
536
709
|
message: tilmeld.config.validRegexNotice,
|
|
537
710
|
};
|
|
538
711
|
}
|
|
712
|
+
if (domain != null) {
|
|
713
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
714
|
+
return {
|
|
715
|
+
result: false,
|
|
716
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (
|
|
720
|
+
!this.$is(tilmeld.currentUser?.group) &&
|
|
721
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
722
|
+
) {
|
|
723
|
+
return {
|
|
724
|
+
result: false,
|
|
725
|
+
message:
|
|
726
|
+
"You don't have the authority to manage groups on this domain.",
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
539
730
|
|
|
540
731
|
const selector: Selector = {
|
|
541
732
|
type: '&',
|
|
@@ -549,12 +740,24 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
549
740
|
}
|
|
550
741
|
const test = await this.$nymph.getEntity(
|
|
551
742
|
{ class: tilmeld.Group, skipAc: true },
|
|
552
|
-
selector
|
|
743
|
+
selector,
|
|
553
744
|
);
|
|
554
745
|
if (test != null) {
|
|
555
746
|
return { result: false, message: 'That groupname is taken.' };
|
|
556
747
|
}
|
|
557
748
|
|
|
749
|
+
for (let callback of (this.constructor as typeof Group)
|
|
750
|
+
.checkGroupnameCallbacks) {
|
|
751
|
+
if (callback) {
|
|
752
|
+
const result = await callback(this, {
|
|
753
|
+
groupname: this.$data.groupname,
|
|
754
|
+
});
|
|
755
|
+
if (!result.result) {
|
|
756
|
+
return { message: 'Groupname is not available.', ...result };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
558
761
|
return {
|
|
559
762
|
result: true,
|
|
560
763
|
message:
|
|
@@ -565,10 +768,16 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
565
768
|
if (this.$data.groupname == null || !this.$data.groupname.length) {
|
|
566
769
|
return { result: false, message: 'Please specify an email.' };
|
|
567
770
|
}
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
771
|
+
if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
|
|
772
|
+
return {
|
|
773
|
+
result: false,
|
|
774
|
+
message:
|
|
775
|
+
'Emails must be at least ' +
|
|
776
|
+
tilmeld.config.minUsernameLength +
|
|
777
|
+
' characters.',
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
|
|
572
781
|
return {
|
|
573
782
|
result: false,
|
|
574
783
|
message:
|
|
@@ -588,7 +797,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
588
797
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
589
798
|
*/
|
|
590
799
|
public async $checkEmail(): Promise<{ result: boolean; message: string }> {
|
|
591
|
-
const tilmeld = this
|
|
800
|
+
const tilmeld = enforceTilmeld(this);
|
|
592
801
|
if (this.$data.email === '') {
|
|
593
802
|
return { result: true, message: '' };
|
|
594
803
|
}
|
|
@@ -610,7 +819,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
610
819
|
}
|
|
611
820
|
const test = await this.$nymph.getEntity(
|
|
612
821
|
{ class: tilmeld.Group, skipAc: true },
|
|
613
|
-
selector
|
|
822
|
+
selector,
|
|
614
823
|
);
|
|
615
824
|
if (test != null) {
|
|
616
825
|
return {
|
|
@@ -627,11 +836,64 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
627
836
|
}
|
|
628
837
|
|
|
629
838
|
public async $save() {
|
|
630
|
-
|
|
839
|
+
let tilmeld = enforceTilmeld(this);
|
|
631
840
|
if (this.$data.groupname == null || !this.$data.groupname.trim().length) {
|
|
632
841
|
return false;
|
|
633
842
|
}
|
|
634
843
|
|
|
844
|
+
// Lowercase the domain part.
|
|
845
|
+
if (tilmeld.config.domainSupport) {
|
|
846
|
+
const [groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
|
|
847
|
+
if (domain) {
|
|
848
|
+
this.$data.groupname = `${groupname}@${domain.toLowerCase()}`;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (tilmeld.config.domainSupport) {
|
|
853
|
+
const [_groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
|
|
854
|
+
const [_originalGroupname, originalDomain] = splitn(
|
|
855
|
+
this.$originalGroupname ?? '',
|
|
856
|
+
'@',
|
|
857
|
+
2,
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
if (
|
|
861
|
+
this.guid != null &&
|
|
862
|
+
((originalDomain == null && domain != null) ||
|
|
863
|
+
(domain == null && originalDomain != null))
|
|
864
|
+
) {
|
|
865
|
+
throw new AccessControlError(
|
|
866
|
+
"Groups can't switch to or from having a domain once created.",
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (
|
|
871
|
+
!this.$skipAcWhenSaving &&
|
|
872
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
873
|
+
!tilmeld.gatekeeper('system/admin')
|
|
874
|
+
) {
|
|
875
|
+
if (
|
|
876
|
+
domain != null &&
|
|
877
|
+
!this.$is(tilmeld.currentUser) &&
|
|
878
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
879
|
+
) {
|
|
880
|
+
throw new AccessControlError(
|
|
881
|
+
"You don't have the authority to modify groups on this domain.",
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (
|
|
885
|
+
originalDomain != null &&
|
|
886
|
+
originalDomain !== domain &&
|
|
887
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
888
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))
|
|
889
|
+
) {
|
|
890
|
+
throw new AccessControlError(
|
|
891
|
+
"You don't have the authority to modify groups on this domain.",
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
635
897
|
// Formatting.
|
|
636
898
|
this.$data.groupname = this.$data.groupname.trim();
|
|
637
899
|
if (tilmeld.config.emailUsernames) {
|
|
@@ -643,16 +905,22 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
643
905
|
this.$data.enabled = !!this.$data.enabled;
|
|
644
906
|
|
|
645
907
|
// Clear empty values.
|
|
646
|
-
if (this.$data.name === '') {
|
|
908
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
647
909
|
delete this.$data.name;
|
|
648
910
|
}
|
|
649
|
-
if (
|
|
911
|
+
if (
|
|
912
|
+
this.$data.email === '' ||
|
|
913
|
+
!tilmeld.config.userFields.includes('email')
|
|
914
|
+
) {
|
|
650
915
|
delete this.$data.email;
|
|
651
916
|
}
|
|
652
917
|
if (this.$data.avatar === '') {
|
|
653
918
|
delete this.$data.avatar;
|
|
654
919
|
}
|
|
655
|
-
if (
|
|
920
|
+
if (
|
|
921
|
+
this.$data.phone === '' ||
|
|
922
|
+
!tilmeld.config.userFields.includes('phone')
|
|
923
|
+
) {
|
|
656
924
|
delete this.$data.phone;
|
|
657
925
|
}
|
|
658
926
|
if (this.$data.parent == null) {
|
|
@@ -671,20 +939,33 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
671
939
|
delete this.$data.unverifiedSecondary;
|
|
672
940
|
}
|
|
673
941
|
|
|
674
|
-
// Groups should never have 'system/admin'
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
942
|
+
// Groups should never have 'system/admin', 'tilmeld/admin',
|
|
943
|
+
// 'tilmeld/switch', or domain admin abilities.
|
|
944
|
+
this.$data.abilities = (this.$data.abilities ?? []).filter((ability) => {
|
|
945
|
+
if (
|
|
946
|
+
['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
|
|
947
|
+
-1
|
|
948
|
+
) {
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (ability == null) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
if (ability.startsWith('tilmeld/domain/') && ability.endsWith('/admin')) {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
return true;
|
|
958
|
+
}) as string[];
|
|
681
959
|
|
|
682
960
|
// Verification.
|
|
683
961
|
const unCheck = await this.$checkGroupname();
|
|
684
962
|
if (!unCheck.result) {
|
|
685
963
|
throw new BadUsernameError(unCheck.message);
|
|
686
964
|
}
|
|
687
|
-
if (
|
|
965
|
+
if (
|
|
966
|
+
!tilmeld.config.emailUsernames &&
|
|
967
|
+
tilmeld.config.userFields.includes('email')
|
|
968
|
+
) {
|
|
688
969
|
const emCheck = await this.$checkEmail();
|
|
689
970
|
if (!emCheck.result) {
|
|
690
971
|
throw new BadEmailError(emCheck.message);
|
|
@@ -692,19 +973,21 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
692
973
|
}
|
|
693
974
|
|
|
694
975
|
// Validate group parent. Make sure it's not a descendant of this group.
|
|
976
|
+
const parent = this.$data.parent;
|
|
977
|
+
await parent?.$wake();
|
|
695
978
|
if (
|
|
696
|
-
|
|
697
|
-
(
|
|
698
|
-
this.$is(
|
|
699
|
-
|
|
979
|
+
parent &&
|
|
980
|
+
(parent.cdate == null ||
|
|
981
|
+
this.$is(parent) ||
|
|
982
|
+
(await parent.$isDescendant(this)))
|
|
700
983
|
) {
|
|
701
984
|
throw new BadDataError(
|
|
702
|
-
"Group parent can't be itself or descendant of itself."
|
|
985
|
+
"Group parent can't be itself or descendant of itself.",
|
|
703
986
|
);
|
|
704
987
|
}
|
|
705
988
|
|
|
706
989
|
try {
|
|
707
|
-
tilmeld.config.validatorGroup(this);
|
|
990
|
+
tilmeld.config.validatorGroup(tilmeld, this);
|
|
708
991
|
} catch (e: any) {
|
|
709
992
|
throw new BadDataError(e?.message);
|
|
710
993
|
}
|
|
@@ -713,20 +996,62 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
713
996
|
if (this.$data.defaultPrimary) {
|
|
714
997
|
const currentPrimary = await this.$nymph.getEntity(
|
|
715
998
|
{ class: tilmeld.Group },
|
|
716
|
-
{ type: '&', truthy: 'defaultPrimary' }
|
|
999
|
+
{ type: '&', truthy: 'defaultPrimary' },
|
|
717
1000
|
);
|
|
718
1001
|
if (currentPrimary != null && !this.$is(currentPrimary)) {
|
|
719
1002
|
currentPrimary.defaultPrimary = false;
|
|
720
|
-
if (
|
|
1003
|
+
if (
|
|
1004
|
+
this.$skipAcWhenSaving
|
|
1005
|
+
? !(await currentPrimary.$saveSkipAC())
|
|
1006
|
+
: !(await currentPrimary.$save())
|
|
1007
|
+
) {
|
|
721
1008
|
throw new CouldNotChangeDefaultPrimaryGroupError(
|
|
722
1009
|
'Could not change new user primary group from ' +
|
|
723
|
-
`${currentPrimary.groupname}
|
|
1010
|
+
`${currentPrimary.groupname}.`,
|
|
724
1011
|
);
|
|
725
1012
|
}
|
|
726
1013
|
}
|
|
727
1014
|
}
|
|
728
1015
|
|
|
729
|
-
|
|
1016
|
+
const transaction = 'tilmeld-save-group-' + (this.guid || nanoid());
|
|
1017
|
+
const nymph = this.$nymph;
|
|
1018
|
+
const tnymph = await nymph.startTransaction(transaction);
|
|
1019
|
+
this.$setNymph(tnymph);
|
|
1020
|
+
tilmeld = enforceTilmeld(this);
|
|
1021
|
+
|
|
1022
|
+
let preGuid = this.guid;
|
|
1023
|
+
let preCdate = this.cdate;
|
|
1024
|
+
let preMdate = this.mdate;
|
|
1025
|
+
|
|
1026
|
+
try {
|
|
1027
|
+
for (let callback of (this.constructor as typeof Group)
|
|
1028
|
+
.beforeSaveCallbacks) {
|
|
1029
|
+
if (callback) {
|
|
1030
|
+
await callback(this);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
let ret = await super.$save();
|
|
1035
|
+
if (ret) {
|
|
1036
|
+
this.$originalGroupname = this.$data.groupname;
|
|
1037
|
+
}
|
|
1038
|
+
for (let callback of (this.constructor as typeof Group)
|
|
1039
|
+
.afterSaveCallbacks) {
|
|
1040
|
+
if (callback) {
|
|
1041
|
+
await callback(this);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
ret = await tnymph.commit(transaction);
|
|
1045
|
+
this.$setNymph(nymph);
|
|
1046
|
+
return ret;
|
|
1047
|
+
} catch (e: any) {
|
|
1048
|
+
await tnymph.rollback(transaction);
|
|
1049
|
+
this.guid = preGuid;
|
|
1050
|
+
this.cdate = preCdate;
|
|
1051
|
+
this.mdate = preMdate;
|
|
1052
|
+
this.$setNymph(nymph);
|
|
1053
|
+
throw e;
|
|
1054
|
+
}
|
|
730
1055
|
}
|
|
731
1056
|
|
|
732
1057
|
/*
|
|
@@ -746,76 +1071,203 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
746
1071
|
}
|
|
747
1072
|
|
|
748
1073
|
public async $delete() {
|
|
749
|
-
let tilmeld = this
|
|
750
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
751
|
-
throw new
|
|
1074
|
+
let tilmeld = enforceTilmeld(this);
|
|
1075
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1076
|
+
throw new AccessControlError(
|
|
1077
|
+
"You don't have the authority to delete groups.",
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
if (this.$data.groupname !== this.$originalGroupname) {
|
|
1081
|
+
throw new AccessControlError(
|
|
1082
|
+
"You can't delete a group while switching its groupname.",
|
|
1083
|
+
);
|
|
752
1084
|
}
|
|
753
1085
|
|
|
754
|
-
const transaction = 'tilmeld-delete-' + this.guid;
|
|
1086
|
+
const transaction = 'tilmeld-delete-group-' + this.guid;
|
|
755
1087
|
const nymph = this.$nymph;
|
|
756
1088
|
const tnymph = await nymph.startTransaction(transaction);
|
|
757
|
-
this.$
|
|
758
|
-
tilmeld = this
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (
|
|
1089
|
+
this.$setNymph(tnymph);
|
|
1090
|
+
tilmeld = enforceTilmeld(this);
|
|
1091
|
+
const User = tnymph.getEntityClass(UserClass);
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
for (let callback of (this.constructor as typeof Group)
|
|
1095
|
+
.beforeDeleteCallbacks) {
|
|
1096
|
+
if (callback) {
|
|
1097
|
+
await callback(this);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Delete descendants.
|
|
1102
|
+
const descendants = await this.$getDescendants();
|
|
1103
|
+
if (descendants.length) {
|
|
1104
|
+
for (let curGroup of descendants) {
|
|
1105
|
+
if (
|
|
1106
|
+
this.$skipAcWhenDeleting
|
|
1107
|
+
? !(await curGroup.$deleteSkipAC())
|
|
1108
|
+
: !(await curGroup.$delete())
|
|
1109
|
+
) {
|
|
1110
|
+
await tnymph.rollback(transaction);
|
|
1111
|
+
this.$setNymph(nymph);
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Remove users from this primary group.
|
|
1118
|
+
const primaryUsers = await tnymph.getEntities(
|
|
1119
|
+
{
|
|
1120
|
+
class: User,
|
|
1121
|
+
skipAc: true,
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
type: '&',
|
|
1125
|
+
ref: ['group', this],
|
|
1126
|
+
},
|
|
1127
|
+
);
|
|
1128
|
+
for (let user of primaryUsers) {
|
|
1129
|
+
delete user.group;
|
|
1130
|
+
if (
|
|
1131
|
+
this.$skipAcWhenDeleting
|
|
1132
|
+
? !(await user.$saveSkipAC())
|
|
1133
|
+
: !(await user.$save())
|
|
1134
|
+
) {
|
|
765
1135
|
await tnymph.rollback(transaction);
|
|
766
|
-
this.$nymph
|
|
1136
|
+
this.$setNymph(nymph);
|
|
767
1137
|
return false;
|
|
768
1138
|
}
|
|
769
1139
|
}
|
|
770
|
-
}
|
|
771
1140
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1141
|
+
// Remove users from this secondary group.
|
|
1142
|
+
const secondaryUsers = await tnymph.getEntities(
|
|
1143
|
+
{
|
|
1144
|
+
class: User,
|
|
1145
|
+
skipAc: true,
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
type: '&',
|
|
1149
|
+
ref: ['groups', this],
|
|
1150
|
+
},
|
|
1151
|
+
);
|
|
1152
|
+
for (let user of secondaryUsers) {
|
|
1153
|
+
user.$delGroup(this);
|
|
1154
|
+
if (
|
|
1155
|
+
this.$skipAcWhenDeleting
|
|
1156
|
+
? !(await user.$saveSkipAC())
|
|
1157
|
+
: !(await user.$save())
|
|
1158
|
+
) {
|
|
1159
|
+
await tnymph.rollback(transaction);
|
|
1160
|
+
this.$setNymph(nymph);
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
781
1163
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
delete
|
|
785
|
-
if (
|
|
1164
|
+
|
|
1165
|
+
// Delete the group.
|
|
1166
|
+
let success = await super.$delete();
|
|
1167
|
+
if (success) {
|
|
1168
|
+
for (let callback of (this.constructor as typeof Group)
|
|
1169
|
+
.afterDeleteCallbacks) {
|
|
1170
|
+
if (callback) {
|
|
1171
|
+
await callback(this);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
success = await tnymph.commit(transaction);
|
|
1176
|
+
} else {
|
|
786
1177
|
await tnymph.rollback(transaction);
|
|
787
|
-
this.$nymph = nymph;
|
|
788
|
-
return false;
|
|
789
1178
|
}
|
|
1179
|
+
this.$setNymph(nymph);
|
|
1180
|
+
return success;
|
|
1181
|
+
} catch (e: any) {
|
|
1182
|
+
await tnymph.rollback(transaction);
|
|
1183
|
+
this.$setNymph(nymph);
|
|
1184
|
+
throw e;
|
|
790
1185
|
}
|
|
1186
|
+
}
|
|
791
1187
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
user.$delGroup(this);
|
|
805
|
-
if (!(await user.$save())) {
|
|
806
|
-
await tnymph.rollback(transaction);
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
1188
|
+
/*
|
|
1189
|
+
* This should *never* be accessible on the client.
|
|
1190
|
+
*/
|
|
1191
|
+
async $deleteSkipAC() {
|
|
1192
|
+
this.$skipAcWhenDeleting = true;
|
|
1193
|
+
return await this.$delete();
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
$tilmeldDeleteSkipAC() {
|
|
1197
|
+
if (this.$skipAcWhenDeleting) {
|
|
1198
|
+
this.$skipAcWhenDeleting = false;
|
|
1199
|
+
return true;
|
|
809
1200
|
}
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
810
1203
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1204
|
+
public static on<T extends GroupEventType>(
|
|
1205
|
+
event: T,
|
|
1206
|
+
callback: T extends 'checkGroupname'
|
|
1207
|
+
? TilmeldCheckGroupnameCallback
|
|
1208
|
+
: T extends 'beforeSave'
|
|
1209
|
+
? TilmeldBeforeGroupSaveCallback
|
|
1210
|
+
: T extends 'afterSave'
|
|
1211
|
+
? TilmeldAfterGroupSaveCallback
|
|
1212
|
+
: T extends 'beforeDelete'
|
|
1213
|
+
? TilmeldBeforeGroupDeleteCallback
|
|
1214
|
+
: T extends 'afterDelete'
|
|
1215
|
+
? TilmeldAfterGroupDeleteCallback
|
|
1216
|
+
: never,
|
|
1217
|
+
) {
|
|
1218
|
+
const prop = (event + 'Callbacks') as T extends 'checkGroupname'
|
|
1219
|
+
? 'checkGroupnameCallbacks'
|
|
1220
|
+
: T extends 'beforeSave'
|
|
1221
|
+
? 'beforeSaveCallbacks'
|
|
1222
|
+
: T extends 'afterSave'
|
|
1223
|
+
? 'afterSaveCallbacks'
|
|
1224
|
+
: T extends 'beforeDelete'
|
|
1225
|
+
? 'beforeDeleteCallbacks'
|
|
1226
|
+
: T extends 'afterDelete'
|
|
1227
|
+
? 'afterDeleteCallbacks'
|
|
1228
|
+
: never;
|
|
1229
|
+
if (!(prop in this)) {
|
|
1230
|
+
throw new Error('Invalid event type.');
|
|
817
1231
|
}
|
|
818
|
-
|
|
819
|
-
|
|
1232
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1233
|
+
this[prop].push(callback);
|
|
1234
|
+
return () => this.off(event, callback);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
public static off<T extends GroupEventType>(
|
|
1238
|
+
event: T,
|
|
1239
|
+
callback: T extends 'checkGroupname'
|
|
1240
|
+
? TilmeldCheckGroupnameCallback
|
|
1241
|
+
: T extends 'beforeSave'
|
|
1242
|
+
? TilmeldBeforeGroupSaveCallback
|
|
1243
|
+
: T extends 'afterSave'
|
|
1244
|
+
? TilmeldAfterGroupSaveCallback
|
|
1245
|
+
: T extends 'beforeDelete'
|
|
1246
|
+
? TilmeldBeforeGroupDeleteCallback
|
|
1247
|
+
: T extends 'afterDelete'
|
|
1248
|
+
? TilmeldAfterGroupDeleteCallback
|
|
1249
|
+
: never,
|
|
1250
|
+
) {
|
|
1251
|
+
const prop = (event + 'Callbacks') as T extends 'checkGroupname'
|
|
1252
|
+
? 'checkGroupnameCallbacks'
|
|
1253
|
+
: T extends 'beforeSave'
|
|
1254
|
+
? 'beforeSaveCallbacks'
|
|
1255
|
+
: T extends 'afterSave'
|
|
1256
|
+
? 'afterSaveCallbacks'
|
|
1257
|
+
: T extends 'beforeDelete'
|
|
1258
|
+
? 'beforeDeleteCallbacks'
|
|
1259
|
+
: T extends 'afterDelete'
|
|
1260
|
+
? 'afterDeleteCallbacks'
|
|
1261
|
+
: never;
|
|
1262
|
+
if (!(prop in this)) {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1266
|
+
const i = this[prop].indexOf(callback);
|
|
1267
|
+
if (i > -1) {
|
|
1268
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1269
|
+
this[prop].splice(i, 1);
|
|
1270
|
+
}
|
|
1271
|
+
return true;
|
|
820
1272
|
}
|
|
821
1273
|
}
|