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