@nymphjs/tilmeld 1.0.0-beta.11 → 1.0.0-beta.111
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 +504 -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 +571 -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 +1386 -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 +683 -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 +1628 -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,111 @@ 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
|
+
// TODO: Fix these being added by the drivers.
|
|
445
|
+
// @ts-ignore
|
|
446
|
+
delete this.$data.acRead;
|
|
447
|
+
// @ts-ignore
|
|
448
|
+
delete this.$data.acWrite;
|
|
449
|
+
// @ts-ignore
|
|
450
|
+
delete this.$data.acFull;
|
|
451
|
+
if (source === 'server') {
|
|
452
|
+
this.$originalGroupname = this.$data.groupname;
|
|
453
|
+
}
|
|
285
454
|
this.$updateDataProtection();
|
|
286
455
|
}
|
|
287
456
|
|
|
@@ -290,9 +459,16 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
290
459
|
*
|
|
291
460
|
* @param givenUser User to update protection for. If undefined, will use the currently logged in user.
|
|
292
461
|
*/
|
|
293
|
-
public $updateDataProtection(givenUser?:
|
|
294
|
-
const tilmeld = this
|
|
462
|
+
public $updateDataProtection(givenUser?: UserClass & UserData) {
|
|
463
|
+
const tilmeld = enforceTilmeld(this);
|
|
295
464
|
let user = givenUser ?? tilmeld.User.current();
|
|
465
|
+
let [_groupname, domain] = tilmeld.config.domainSupport
|
|
466
|
+
? splitn(this.$data.groupname ?? '', '@', 2)
|
|
467
|
+
: [this.$data.groupname, null];
|
|
468
|
+
// Lowercase the domain part.
|
|
469
|
+
if (domain) {
|
|
470
|
+
domain = domain.toLowerCase();
|
|
471
|
+
}
|
|
296
472
|
|
|
297
473
|
this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
298
474
|
this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
|
|
@@ -303,8 +479,8 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
303
479
|
|
|
304
480
|
if (
|
|
305
481
|
user != null &&
|
|
306
|
-
(
|
|
307
|
-
|
|
482
|
+
(user.abilities?.includes('tilmeld/admin') ||
|
|
483
|
+
user.abilities?.includes('system/admin'))
|
|
308
484
|
) {
|
|
309
485
|
// Users who can edit groups can see their data.
|
|
310
486
|
this.$privateData = [];
|
|
@@ -312,10 +488,18 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
312
488
|
} else if (
|
|
313
489
|
this.$data.user != null &&
|
|
314
490
|
user != null &&
|
|
315
|
-
this.$data.user
|
|
491
|
+
this.$data.user.guid === user.guid
|
|
316
492
|
) {
|
|
317
493
|
// Users can see their group's data.
|
|
318
494
|
this.$privateData = [];
|
|
495
|
+
} else if (
|
|
496
|
+
user != null &&
|
|
497
|
+
tilmeld.config.domainSupport &&
|
|
498
|
+
domain &&
|
|
499
|
+
user.abilities?.includes(`tilmeld/domain/${domain}/admin`)
|
|
500
|
+
) {
|
|
501
|
+
// Domain admins can see their groups' data.
|
|
502
|
+
this.$privateData = [];
|
|
319
503
|
}
|
|
320
504
|
}
|
|
321
505
|
|
|
@@ -325,11 +509,13 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
325
509
|
* @param group The group, or the group's GUID.
|
|
326
510
|
* @returns True or false.
|
|
327
511
|
*/
|
|
328
|
-
public $isDescendant(
|
|
329
|
-
|
|
512
|
+
public async $isDescendant(
|
|
513
|
+
givenGroup: (Group & GroupData) | string,
|
|
514
|
+
): Promise<boolean> {
|
|
515
|
+
const tilmeld = enforceTilmeld(this);
|
|
330
516
|
let group: Group & GroupData;
|
|
331
517
|
if (typeof givenGroup === 'string') {
|
|
332
|
-
group = tilmeld.Group.
|
|
518
|
+
group = await tilmeld.Group.factory(givenGroup);
|
|
333
519
|
} else {
|
|
334
520
|
group = givenGroup;
|
|
335
521
|
}
|
|
@@ -337,13 +523,15 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
337
523
|
return false;
|
|
338
524
|
}
|
|
339
525
|
// Check to see if the group is a descendant of the given group.
|
|
340
|
-
|
|
526
|
+
const parent = this.$data.parent;
|
|
527
|
+
await parent?.$wake();
|
|
528
|
+
if (parent == null) {
|
|
341
529
|
return false;
|
|
342
530
|
}
|
|
343
|
-
if (
|
|
531
|
+
if (parent.$is(group)) {
|
|
344
532
|
return true;
|
|
345
533
|
}
|
|
346
|
-
if (
|
|
534
|
+
if (await parent.$isDescendant(group)) {
|
|
347
535
|
return true;
|
|
348
536
|
}
|
|
349
537
|
return false;
|
|
@@ -355,14 +543,14 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
355
543
|
* @returns An array of groups.
|
|
356
544
|
*/
|
|
357
545
|
public async $getChildren() {
|
|
358
|
-
const tilmeld = this
|
|
546
|
+
const tilmeld = enforceTilmeld(this);
|
|
359
547
|
return await this.$nymph.getEntities(
|
|
360
548
|
{ class: tilmeld.Group },
|
|
361
549
|
{
|
|
362
550
|
type: '&',
|
|
363
551
|
equal: ['enabled', true],
|
|
364
552
|
ref: ['parent', this],
|
|
365
|
-
}
|
|
553
|
+
},
|
|
366
554
|
);
|
|
367
555
|
}
|
|
368
556
|
|
|
@@ -373,9 +561,9 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
373
561
|
* @returns An array of groups.
|
|
374
562
|
*/
|
|
375
563
|
public async $getDescendants(
|
|
376
|
-
andSelf = false
|
|
564
|
+
andSelf = false,
|
|
377
565
|
): Promise<(Group & GroupData)[]> {
|
|
378
|
-
const tilmeld = this
|
|
566
|
+
const tilmeld = enforceTilmeld(this);
|
|
379
567
|
let groups: (Group & GroupData)[] = [];
|
|
380
568
|
const entities = await this.$nymph.getEntities(
|
|
381
569
|
{ class: tilmeld.Group },
|
|
@@ -383,7 +571,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
383
571
|
type: '&',
|
|
384
572
|
equal: ['enabled', true],
|
|
385
573
|
ref: ['parent', this],
|
|
386
|
-
}
|
|
574
|
+
},
|
|
387
575
|
);
|
|
388
576
|
for (let entity of entities) {
|
|
389
577
|
groups = (await entity.$getDescendants(true)).concat(groups);
|
|
@@ -394,37 +582,6 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
394
582
|
return groups;
|
|
395
583
|
}
|
|
396
584
|
|
|
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
585
|
/**
|
|
429
586
|
* Get the number of parents the group has.
|
|
430
587
|
*
|
|
@@ -436,13 +593,17 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
436
593
|
*
|
|
437
594
|
* @returns The level of the group.
|
|
438
595
|
*/
|
|
439
|
-
public $getLevel() {
|
|
440
|
-
const tilmeld = this
|
|
441
|
-
let group = tilmeld.Group.
|
|
596
|
+
public async $getLevel() {
|
|
597
|
+
const tilmeld = enforceTilmeld(this);
|
|
598
|
+
let group = await tilmeld.Group.factory(this.guid ?? undefined);
|
|
599
|
+
let parent = group.parent;
|
|
600
|
+
await parent?.$wake();
|
|
442
601
|
let level = 0;
|
|
443
|
-
while (
|
|
602
|
+
while (parent != null && parent.cdate != null && level < 1024) {
|
|
444
603
|
level++;
|
|
445
|
-
group =
|
|
604
|
+
group = parent;
|
|
605
|
+
parent = group.parent;
|
|
606
|
+
await parent?.$wake();
|
|
446
607
|
}
|
|
447
608
|
return level;
|
|
448
609
|
}
|
|
@@ -458,9 +619,9 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
458
619
|
public async $getUsers(
|
|
459
620
|
descendants = false,
|
|
460
621
|
limit?: number,
|
|
461
|
-
offset?: number
|
|
462
|
-
): Promise<(
|
|
463
|
-
const tilmeld = this
|
|
622
|
+
offset?: number,
|
|
623
|
+
): Promise<(UserClass & UserData)[]> {
|
|
624
|
+
const tilmeld = enforceTilmeld(this);
|
|
464
625
|
let groups: (Group & GroupData)[] = [];
|
|
465
626
|
if (descendants) {
|
|
466
627
|
groups = await this.$getDescendants();
|
|
@@ -489,7 +650,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
489
650
|
group,
|
|
490
651
|
]),
|
|
491
652
|
],
|
|
492
|
-
}
|
|
653
|
+
},
|
|
493
654
|
);
|
|
494
655
|
}
|
|
495
656
|
|
|
@@ -502,15 +663,36 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
502
663
|
result: boolean;
|
|
503
664
|
message: string;
|
|
504
665
|
}> {
|
|
505
|
-
const tilmeld = this
|
|
666
|
+
const tilmeld = enforceTilmeld(this);
|
|
506
667
|
if (!tilmeld.config.emailUsernames) {
|
|
507
|
-
|
|
508
|
-
|
|
668
|
+
let [groupname, domain] = tilmeld.config.domainSupport
|
|
669
|
+
? splitn(this.$data.groupname ?? '', '@', 2)
|
|
670
|
+
: [this.$data.groupname, null];
|
|
671
|
+
|
|
672
|
+
// Lowercase the domain part.
|
|
673
|
+
if (domain) {
|
|
674
|
+
domain = domain.toLowerCase();
|
|
675
|
+
this.$data.groupname = `${groupname}@${domain}`;
|
|
509
676
|
}
|
|
677
|
+
|
|
510
678
|
if (
|
|
511
|
-
|
|
512
|
-
this.$data.groupname.length
|
|
679
|
+
this.$data.groupname == null ||
|
|
680
|
+
!this.$data.groupname.length ||
|
|
681
|
+
groupname == null ||
|
|
682
|
+
!groupname.length
|
|
513
683
|
) {
|
|
684
|
+
return { result: false, message: 'Please specify a groupname.' };
|
|
685
|
+
}
|
|
686
|
+
if (groupname.length < tilmeld.config.minUsernameLength) {
|
|
687
|
+
return {
|
|
688
|
+
result: false,
|
|
689
|
+
message:
|
|
690
|
+
'Groupnames must be at least ' +
|
|
691
|
+
tilmeld.config.minUsernameLength +
|
|
692
|
+
' characters.',
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
if (groupname.length > tilmeld.config.maxUsernameLength) {
|
|
514
696
|
return {
|
|
515
697
|
result: false,
|
|
516
698
|
message:
|
|
@@ -520,22 +702,38 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
520
702
|
};
|
|
521
703
|
}
|
|
522
704
|
if (
|
|
523
|
-
difference(
|
|
524
|
-
|
|
525
|
-
tilmeld.config.validChars.split('')
|
|
526
|
-
).length
|
|
705
|
+
difference(groupname.split(''), tilmeld.config.validChars.split(''))
|
|
706
|
+
.length
|
|
527
707
|
) {
|
|
528
708
|
return {
|
|
529
709
|
result: false,
|
|
530
710
|
message: tilmeld.config.validCharsNotice,
|
|
531
711
|
};
|
|
532
712
|
}
|
|
533
|
-
if (!tilmeld.config.validRegex.test(
|
|
713
|
+
if (!tilmeld.config.validRegex.test(groupname)) {
|
|
534
714
|
return {
|
|
535
715
|
result: false,
|
|
536
716
|
message: tilmeld.config.validRegexNotice,
|
|
537
717
|
};
|
|
538
718
|
}
|
|
719
|
+
if (domain != null) {
|
|
720
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
721
|
+
return {
|
|
722
|
+
result: false,
|
|
723
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (
|
|
727
|
+
!this.$is(tilmeld.currentUser?.group) &&
|
|
728
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
729
|
+
) {
|
|
730
|
+
return {
|
|
731
|
+
result: false,
|
|
732
|
+
message:
|
|
733
|
+
"You don't have the authority to manage groups on this domain.",
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
}
|
|
539
737
|
|
|
540
738
|
const selector: Selector = {
|
|
541
739
|
type: '&',
|
|
@@ -549,12 +747,24 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
549
747
|
}
|
|
550
748
|
const test = await this.$nymph.getEntity(
|
|
551
749
|
{ class: tilmeld.Group, skipAc: true },
|
|
552
|
-
selector
|
|
750
|
+
selector,
|
|
553
751
|
);
|
|
554
752
|
if (test != null) {
|
|
555
753
|
return { result: false, message: 'That groupname is taken.' };
|
|
556
754
|
}
|
|
557
755
|
|
|
756
|
+
for (let callback of (this.constructor as typeof Group)
|
|
757
|
+
.checkGroupnameCallbacks) {
|
|
758
|
+
if (callback) {
|
|
759
|
+
const result = await callback(this, {
|
|
760
|
+
groupname: this.$data.groupname,
|
|
761
|
+
});
|
|
762
|
+
if (!result.result) {
|
|
763
|
+
return { message: 'Groupname is not available.', ...result };
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
558
768
|
return {
|
|
559
769
|
result: true,
|
|
560
770
|
message:
|
|
@@ -565,10 +775,16 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
565
775
|
if (this.$data.groupname == null || !this.$data.groupname.length) {
|
|
566
776
|
return { result: false, message: 'Please specify an email.' };
|
|
567
777
|
}
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
778
|
+
if (this.$data.groupname.length < tilmeld.config.minUsernameLength) {
|
|
779
|
+
return {
|
|
780
|
+
result: false,
|
|
781
|
+
message:
|
|
782
|
+
'Emails must be at least ' +
|
|
783
|
+
tilmeld.config.minUsernameLength +
|
|
784
|
+
' characters.',
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
|
|
572
788
|
return {
|
|
573
789
|
result: false,
|
|
574
790
|
message:
|
|
@@ -588,7 +804,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
588
804
|
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
589
805
|
*/
|
|
590
806
|
public async $checkEmail(): Promise<{ result: boolean; message: string }> {
|
|
591
|
-
const tilmeld = this
|
|
807
|
+
const tilmeld = enforceTilmeld(this);
|
|
592
808
|
if (this.$data.email === '') {
|
|
593
809
|
return { result: true, message: '' };
|
|
594
810
|
}
|
|
@@ -610,7 +826,7 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
610
826
|
}
|
|
611
827
|
const test = await this.$nymph.getEntity(
|
|
612
828
|
{ class: tilmeld.Group, skipAc: true },
|
|
613
|
-
selector
|
|
829
|
+
selector,
|
|
614
830
|
);
|
|
615
831
|
if (test != null) {
|
|
616
832
|
return {
|
|
@@ -627,11 +843,64 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
627
843
|
}
|
|
628
844
|
|
|
629
845
|
public async $save() {
|
|
630
|
-
|
|
846
|
+
let tilmeld = enforceTilmeld(this);
|
|
631
847
|
if (this.$data.groupname == null || !this.$data.groupname.trim().length) {
|
|
632
848
|
return false;
|
|
633
849
|
}
|
|
634
850
|
|
|
851
|
+
// Lowercase the domain part.
|
|
852
|
+
if (tilmeld.config.domainSupport) {
|
|
853
|
+
const [groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
|
|
854
|
+
if (domain) {
|
|
855
|
+
this.$data.groupname = `${groupname}@${domain.toLowerCase()}`;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (tilmeld.config.domainSupport) {
|
|
860
|
+
const [_groupname, domain] = splitn(this.$data.groupname ?? '', '@', 2);
|
|
861
|
+
const [_originalGroupname, originalDomain] = splitn(
|
|
862
|
+
this.$originalGroupname ?? '',
|
|
863
|
+
'@',
|
|
864
|
+
2,
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
if (
|
|
868
|
+
this.guid != null &&
|
|
869
|
+
((originalDomain == null && domain != null) ||
|
|
870
|
+
(domain == null && originalDomain != null))
|
|
871
|
+
) {
|
|
872
|
+
throw new AccessControlError(
|
|
873
|
+
"Groups can't switch to or from having a domain once created.",
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (
|
|
878
|
+
!this.$skipAcWhenSaving &&
|
|
879
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
880
|
+
!tilmeld.gatekeeper('system/admin')
|
|
881
|
+
) {
|
|
882
|
+
if (
|
|
883
|
+
domain != null &&
|
|
884
|
+
!this.$is(tilmeld.currentUser) &&
|
|
885
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)
|
|
886
|
+
) {
|
|
887
|
+
throw new AccessControlError(
|
|
888
|
+
"You don't have the authority to modify groups on this domain.",
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
if (
|
|
892
|
+
originalDomain != null &&
|
|
893
|
+
originalDomain !== domain &&
|
|
894
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
895
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))
|
|
896
|
+
) {
|
|
897
|
+
throw new AccessControlError(
|
|
898
|
+
"You don't have the authority to modify groups on this domain.",
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
635
904
|
// Formatting.
|
|
636
905
|
this.$data.groupname = this.$data.groupname.trim();
|
|
637
906
|
if (tilmeld.config.emailUsernames) {
|
|
@@ -643,16 +912,22 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
643
912
|
this.$data.enabled = !!this.$data.enabled;
|
|
644
913
|
|
|
645
914
|
// Clear empty values.
|
|
646
|
-
if (this.$data.name === '') {
|
|
915
|
+
if (this.$data.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
647
916
|
delete this.$data.name;
|
|
648
917
|
}
|
|
649
|
-
if (
|
|
918
|
+
if (
|
|
919
|
+
this.$data.email === '' ||
|
|
920
|
+
!tilmeld.config.userFields.includes('email')
|
|
921
|
+
) {
|
|
650
922
|
delete this.$data.email;
|
|
651
923
|
}
|
|
652
924
|
if (this.$data.avatar === '') {
|
|
653
925
|
delete this.$data.avatar;
|
|
654
926
|
}
|
|
655
|
-
if (
|
|
927
|
+
if (
|
|
928
|
+
this.$data.phone === '' ||
|
|
929
|
+
!tilmeld.config.userFields.includes('phone')
|
|
930
|
+
) {
|
|
656
931
|
delete this.$data.phone;
|
|
657
932
|
}
|
|
658
933
|
if (this.$data.parent == null) {
|
|
@@ -671,20 +946,33 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
671
946
|
delete this.$data.unverifiedSecondary;
|
|
672
947
|
}
|
|
673
948
|
|
|
674
|
-
// Groups should never have 'system/admin'
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
949
|
+
// Groups should never have 'system/admin', 'tilmeld/admin',
|
|
950
|
+
// 'tilmeld/switch', or domain admin abilities.
|
|
951
|
+
this.$data.abilities = (this.$data.abilities ?? []).filter((ability) => {
|
|
952
|
+
if (
|
|
953
|
+
['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
|
|
954
|
+
-1
|
|
955
|
+
) {
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
if (ability == null) {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
if (ability.startsWith('tilmeld/domain/') && ability.endsWith('/admin')) {
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
return true;
|
|
965
|
+
}) as string[];
|
|
681
966
|
|
|
682
967
|
// Verification.
|
|
683
968
|
const unCheck = await this.$checkGroupname();
|
|
684
969
|
if (!unCheck.result) {
|
|
685
970
|
throw new BadUsernameError(unCheck.message);
|
|
686
971
|
}
|
|
687
|
-
if (
|
|
972
|
+
if (
|
|
973
|
+
!tilmeld.config.emailUsernames &&
|
|
974
|
+
tilmeld.config.userFields.includes('email')
|
|
975
|
+
) {
|
|
688
976
|
const emCheck = await this.$checkEmail();
|
|
689
977
|
if (!emCheck.result) {
|
|
690
978
|
throw new BadEmailError(emCheck.message);
|
|
@@ -692,19 +980,21 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
692
980
|
}
|
|
693
981
|
|
|
694
982
|
// Validate group parent. Make sure it's not a descendant of this group.
|
|
983
|
+
const parent = this.$data.parent;
|
|
984
|
+
await parent?.$wake();
|
|
695
985
|
if (
|
|
696
|
-
|
|
697
|
-
(
|
|
698
|
-
this.$is(
|
|
699
|
-
|
|
986
|
+
parent &&
|
|
987
|
+
(parent.cdate == null ||
|
|
988
|
+
this.$is(parent) ||
|
|
989
|
+
(await parent.$isDescendant(this)))
|
|
700
990
|
) {
|
|
701
991
|
throw new BadDataError(
|
|
702
|
-
"Group parent can't be itself or descendant of itself."
|
|
992
|
+
"Group parent can't be itself or descendant of itself.",
|
|
703
993
|
);
|
|
704
994
|
}
|
|
705
995
|
|
|
706
996
|
try {
|
|
707
|
-
tilmeld.config.validatorGroup(this);
|
|
997
|
+
tilmeld.config.validatorGroup(tilmeld, this);
|
|
708
998
|
} catch (e: any) {
|
|
709
999
|
throw new BadDataError(e?.message);
|
|
710
1000
|
}
|
|
@@ -713,20 +1003,62 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
713
1003
|
if (this.$data.defaultPrimary) {
|
|
714
1004
|
const currentPrimary = await this.$nymph.getEntity(
|
|
715
1005
|
{ class: tilmeld.Group },
|
|
716
|
-
{ type: '&', truthy: 'defaultPrimary' }
|
|
1006
|
+
{ type: '&', truthy: 'defaultPrimary' },
|
|
717
1007
|
);
|
|
718
1008
|
if (currentPrimary != null && !this.$is(currentPrimary)) {
|
|
719
1009
|
currentPrimary.defaultPrimary = false;
|
|
720
|
-
if (
|
|
1010
|
+
if (
|
|
1011
|
+
this.$skipAcWhenSaving
|
|
1012
|
+
? !(await currentPrimary.$saveSkipAC())
|
|
1013
|
+
: !(await currentPrimary.$save())
|
|
1014
|
+
) {
|
|
721
1015
|
throw new CouldNotChangeDefaultPrimaryGroupError(
|
|
722
1016
|
'Could not change new user primary group from ' +
|
|
723
|
-
`${currentPrimary.groupname}
|
|
1017
|
+
`${currentPrimary.groupname}.`,
|
|
724
1018
|
);
|
|
725
1019
|
}
|
|
726
1020
|
}
|
|
727
1021
|
}
|
|
728
1022
|
|
|
729
|
-
|
|
1023
|
+
const transaction = 'tilmeld-save-group-' + (this.guid || nanoid());
|
|
1024
|
+
const nymph = this.$nymph;
|
|
1025
|
+
const tnymph = await nymph.startTransaction(transaction);
|
|
1026
|
+
this.$setNymph(tnymph);
|
|
1027
|
+
tilmeld = enforceTilmeld(this);
|
|
1028
|
+
|
|
1029
|
+
let preGuid = this.guid;
|
|
1030
|
+
let preCdate = this.cdate;
|
|
1031
|
+
let preMdate = this.mdate;
|
|
1032
|
+
|
|
1033
|
+
try {
|
|
1034
|
+
for (let callback of (this.constructor as typeof Group)
|
|
1035
|
+
.beforeSaveCallbacks) {
|
|
1036
|
+
if (callback) {
|
|
1037
|
+
await callback(this);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
let ret = await super.$save();
|
|
1042
|
+
if (ret) {
|
|
1043
|
+
this.$originalGroupname = this.$data.groupname;
|
|
1044
|
+
}
|
|
1045
|
+
for (let callback of (this.constructor as typeof Group)
|
|
1046
|
+
.afterSaveCallbacks) {
|
|
1047
|
+
if (callback) {
|
|
1048
|
+
await callback(this);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
ret = await tnymph.commit(transaction);
|
|
1052
|
+
this.$setNymph(nymph);
|
|
1053
|
+
return ret;
|
|
1054
|
+
} catch (e: any) {
|
|
1055
|
+
await tnymph.rollback(transaction);
|
|
1056
|
+
this.guid = preGuid;
|
|
1057
|
+
this.cdate = preCdate;
|
|
1058
|
+
this.mdate = preMdate;
|
|
1059
|
+
this.$setNymph(nymph);
|
|
1060
|
+
throw e;
|
|
1061
|
+
}
|
|
730
1062
|
}
|
|
731
1063
|
|
|
732
1064
|
/*
|
|
@@ -746,76 +1078,203 @@ export default class Group extends AbleObject<GroupData> {
|
|
|
746
1078
|
}
|
|
747
1079
|
|
|
748
1080
|
public async $delete() {
|
|
749
|
-
let tilmeld = this
|
|
750
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
751
|
-
throw new
|
|
1081
|
+
let tilmeld = enforceTilmeld(this);
|
|
1082
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
1083
|
+
throw new AccessControlError(
|
|
1084
|
+
"You don't have the authority to delete groups.",
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (this.$data.groupname !== this.$originalGroupname) {
|
|
1088
|
+
throw new AccessControlError(
|
|
1089
|
+
"You can't delete a group while switching its groupname.",
|
|
1090
|
+
);
|
|
752
1091
|
}
|
|
753
1092
|
|
|
754
|
-
const transaction = 'tilmeld-delete-' + this.guid;
|
|
1093
|
+
const transaction = 'tilmeld-delete-group-' + this.guid;
|
|
755
1094
|
const nymph = this.$nymph;
|
|
756
1095
|
const tnymph = await nymph.startTransaction(transaction);
|
|
757
|
-
this.$
|
|
758
|
-
tilmeld = this
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (
|
|
1096
|
+
this.$setNymph(tnymph);
|
|
1097
|
+
tilmeld = enforceTilmeld(this);
|
|
1098
|
+
const User = tnymph.getEntityClass(UserClass);
|
|
1099
|
+
|
|
1100
|
+
try {
|
|
1101
|
+
for (let callback of (this.constructor as typeof Group)
|
|
1102
|
+
.beforeDeleteCallbacks) {
|
|
1103
|
+
if (callback) {
|
|
1104
|
+
await callback(this);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Delete descendants.
|
|
1109
|
+
const descendants = await this.$getDescendants();
|
|
1110
|
+
if (descendants.length) {
|
|
1111
|
+
for (let curGroup of descendants) {
|
|
1112
|
+
if (
|
|
1113
|
+
this.$skipAcWhenDeleting
|
|
1114
|
+
? !(await curGroup.$deleteSkipAC())
|
|
1115
|
+
: !(await curGroup.$delete())
|
|
1116
|
+
) {
|
|
1117
|
+
await tnymph.rollback(transaction);
|
|
1118
|
+
this.$setNymph(nymph);
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Remove users from this primary group.
|
|
1125
|
+
const primaryUsers = await tnymph.getEntities(
|
|
1126
|
+
{
|
|
1127
|
+
class: User,
|
|
1128
|
+
skipAc: true,
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
type: '&',
|
|
1132
|
+
ref: ['group', this],
|
|
1133
|
+
},
|
|
1134
|
+
);
|
|
1135
|
+
for (let user of primaryUsers) {
|
|
1136
|
+
delete user.group;
|
|
1137
|
+
if (
|
|
1138
|
+
this.$skipAcWhenDeleting
|
|
1139
|
+
? !(await user.$saveSkipAC())
|
|
1140
|
+
: !(await user.$save())
|
|
1141
|
+
) {
|
|
765
1142
|
await tnymph.rollback(transaction);
|
|
766
|
-
this.$nymph
|
|
1143
|
+
this.$setNymph(nymph);
|
|
767
1144
|
return false;
|
|
768
1145
|
}
|
|
769
1146
|
}
|
|
770
|
-
}
|
|
771
1147
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1148
|
+
// Remove users from this secondary group.
|
|
1149
|
+
const secondaryUsers = await tnymph.getEntities(
|
|
1150
|
+
{
|
|
1151
|
+
class: User,
|
|
1152
|
+
skipAc: true,
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
type: '&',
|
|
1156
|
+
ref: ['groups', this],
|
|
1157
|
+
},
|
|
1158
|
+
);
|
|
1159
|
+
for (let user of secondaryUsers) {
|
|
1160
|
+
user.$delGroup(this);
|
|
1161
|
+
if (
|
|
1162
|
+
this.$skipAcWhenDeleting
|
|
1163
|
+
? !(await user.$saveSkipAC())
|
|
1164
|
+
: !(await user.$save())
|
|
1165
|
+
) {
|
|
1166
|
+
await tnymph.rollback(transaction);
|
|
1167
|
+
this.$setNymph(nymph);
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
781
1170
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
delete
|
|
785
|
-
if (
|
|
1171
|
+
|
|
1172
|
+
// Delete the group.
|
|
1173
|
+
let success = await super.$delete();
|
|
1174
|
+
if (success) {
|
|
1175
|
+
for (let callback of (this.constructor as typeof Group)
|
|
1176
|
+
.afterDeleteCallbacks) {
|
|
1177
|
+
if (callback) {
|
|
1178
|
+
await callback(this);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
success = await tnymph.commit(transaction);
|
|
1183
|
+
} else {
|
|
786
1184
|
await tnymph.rollback(transaction);
|
|
787
|
-
this.$nymph = nymph;
|
|
788
|
-
return false;
|
|
789
1185
|
}
|
|
1186
|
+
this.$setNymph(nymph);
|
|
1187
|
+
return success;
|
|
1188
|
+
} catch (e: any) {
|
|
1189
|
+
await tnymph.rollback(transaction);
|
|
1190
|
+
this.$setNymph(nymph);
|
|
1191
|
+
throw e;
|
|
790
1192
|
}
|
|
1193
|
+
}
|
|
791
1194
|
|
|
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
|
-
}
|
|
1195
|
+
/*
|
|
1196
|
+
* This should *never* be accessible on the client.
|
|
1197
|
+
*/
|
|
1198
|
+
async $deleteSkipAC() {
|
|
1199
|
+
this.$skipAcWhenDeleting = true;
|
|
1200
|
+
return await this.$delete();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
$tilmeldDeleteSkipAC() {
|
|
1204
|
+
if (this.$skipAcWhenDeleting) {
|
|
1205
|
+
this.$skipAcWhenDeleting = false;
|
|
1206
|
+
return true;
|
|
809
1207
|
}
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
810
1210
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1211
|
+
public static on<T extends GroupEventType>(
|
|
1212
|
+
event: T,
|
|
1213
|
+
callback: T extends 'checkGroupname'
|
|
1214
|
+
? TilmeldCheckGroupnameCallback
|
|
1215
|
+
: T extends 'beforeSave'
|
|
1216
|
+
? TilmeldBeforeGroupSaveCallback
|
|
1217
|
+
: T extends 'afterSave'
|
|
1218
|
+
? TilmeldAfterGroupSaveCallback
|
|
1219
|
+
: T extends 'beforeDelete'
|
|
1220
|
+
? TilmeldBeforeGroupDeleteCallback
|
|
1221
|
+
: T extends 'afterDelete'
|
|
1222
|
+
? TilmeldAfterGroupDeleteCallback
|
|
1223
|
+
: never,
|
|
1224
|
+
) {
|
|
1225
|
+
const prop = (event + 'Callbacks') as T extends 'checkGroupname'
|
|
1226
|
+
? 'checkGroupnameCallbacks'
|
|
1227
|
+
: T extends 'beforeSave'
|
|
1228
|
+
? 'beforeSaveCallbacks'
|
|
1229
|
+
: T extends 'afterSave'
|
|
1230
|
+
? 'afterSaveCallbacks'
|
|
1231
|
+
: T extends 'beforeDelete'
|
|
1232
|
+
? 'beforeDeleteCallbacks'
|
|
1233
|
+
: T extends 'afterDelete'
|
|
1234
|
+
? 'afterDeleteCallbacks'
|
|
1235
|
+
: never;
|
|
1236
|
+
if (!(prop in this)) {
|
|
1237
|
+
throw new Error('Invalid event type.');
|
|
817
1238
|
}
|
|
818
|
-
|
|
819
|
-
|
|
1239
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1240
|
+
this[prop].push(callback);
|
|
1241
|
+
return () => this.off(event, callback);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
public static off<T extends GroupEventType>(
|
|
1245
|
+
event: T,
|
|
1246
|
+
callback: T extends 'checkGroupname'
|
|
1247
|
+
? TilmeldCheckGroupnameCallback
|
|
1248
|
+
: T extends 'beforeSave'
|
|
1249
|
+
? TilmeldBeforeGroupSaveCallback
|
|
1250
|
+
: T extends 'afterSave'
|
|
1251
|
+
? TilmeldAfterGroupSaveCallback
|
|
1252
|
+
: T extends 'beforeDelete'
|
|
1253
|
+
? TilmeldBeforeGroupDeleteCallback
|
|
1254
|
+
: T extends 'afterDelete'
|
|
1255
|
+
? TilmeldAfterGroupDeleteCallback
|
|
1256
|
+
: never,
|
|
1257
|
+
) {
|
|
1258
|
+
const prop = (event + 'Callbacks') as T extends 'checkGroupname'
|
|
1259
|
+
? 'checkGroupnameCallbacks'
|
|
1260
|
+
: T extends 'beforeSave'
|
|
1261
|
+
? 'beforeSaveCallbacks'
|
|
1262
|
+
: T extends 'afterSave'
|
|
1263
|
+
? 'afterSaveCallbacks'
|
|
1264
|
+
: T extends 'beforeDelete'
|
|
1265
|
+
? 'beforeDeleteCallbacks'
|
|
1266
|
+
: T extends 'afterDelete'
|
|
1267
|
+
? 'afterDeleteCallbacks'
|
|
1268
|
+
: never;
|
|
1269
|
+
if (!(prop in this)) {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1273
|
+
const i = this[prop].indexOf(callback);
|
|
1274
|
+
if (i > -1) {
|
|
1275
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
1276
|
+
this[prop].splice(i, 1);
|
|
1277
|
+
}
|
|
1278
|
+
return true;
|
|
820
1279
|
}
|
|
821
1280
|
}
|