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