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