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