@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/dist/Group.js
CHANGED
|
@@ -1,38 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
const md5_1 = __importDefault(require("crypto-js/md5"));
|
|
7
|
-
const lodash_1 = require("lodash");
|
|
8
|
-
const AbleObject_1 = __importDefault(require("./AbleObject"));
|
|
9
|
-
const errors_1 = require("./errors");
|
|
10
|
-
class Group extends AbleObject_1.default {
|
|
11
|
-
static async factory(guid) {
|
|
12
|
-
return (await super.factory(guid));
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
13
5
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
26
30
|
}
|
|
27
|
-
return entity;
|
|
28
|
-
}
|
|
29
|
-
static factorySync(guid) {
|
|
30
|
-
return super.factorySync(guid);
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import crypto from 'node:crypto';
|
|
36
|
+
import { clearSearchRestrictedData, transaction, transactional, } from '@nymphjs/nymph';
|
|
37
|
+
import { nanoid } from '@nymphjs/guid';
|
|
38
|
+
import { difference, xor } from 'lodash-es';
|
|
39
|
+
import { splitn } from '@sciactive/splitn';
|
|
40
|
+
import { enforceTilmeld } from './enforceTilmeld.js';
|
|
41
|
+
import { AbleObject } from './AbleObject.js';
|
|
42
|
+
import { AccessControlError, BadDataError, BadEmailError, BadUsernameError, CouldNotChangeDefaultPrimaryGroupError, } from './errors/index.js';
|
|
43
|
+
import { User as UserClass } from './User.js';
|
|
44
|
+
/**
|
|
45
|
+
* A user group data model.
|
|
46
|
+
*
|
|
47
|
+
* Written by Hunter Perrin for SciActive.
|
|
48
|
+
*
|
|
49
|
+
* @author Hunter Perrin <hperrin@port87.com>
|
|
50
|
+
* @copyright SciActive Inc
|
|
51
|
+
* @see http://nymph.io/
|
|
52
|
+
*/
|
|
53
|
+
let Group = (() => {
|
|
54
|
+
let _classSuper = AbleObject;
|
|
55
|
+
let _instanceExtraInitializers = [];
|
|
56
|
+
let _$delete_decorators;
|
|
57
|
+
return class Group extends _classSuper {
|
|
58
|
+
static {
|
|
59
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
60
|
+
_$delete_decorators = [transactional];
|
|
61
|
+
__esDecorate(this, null, _$delete_decorators, { kind: "method", name: "$delete", static: false, private: false, access: { has: obj => "$delete" in obj, get: obj => obj.$delete }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
62
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
63
|
+
}
|
|
64
|
+
static ETYPE = 'tilmeld_group';
|
|
65
|
+
static class = 'Group';
|
|
66
|
+
static checkGroupnameCallbacks = [];
|
|
67
|
+
static beforeSaveCallbacks = [];
|
|
68
|
+
static afterSaveCallbacks = [];
|
|
69
|
+
static beforeDeleteCallbacks = [];
|
|
70
|
+
static afterDeleteCallbacks = [];
|
|
71
|
+
static DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
|
|
72
|
+
static DEFAULT_ALLOWLIST_DATA = [];
|
|
73
|
+
$tags = (__runInitializers(this, _instanceExtraInitializers), []);
|
|
74
|
+
$clientEnabledMethods = [
|
|
36
75
|
'$checkGroupname',
|
|
37
76
|
'$checkEmail',
|
|
38
77
|
'$getAvatar',
|
|
@@ -41,467 +80,815 @@ class Group extends AbleObject_1.default {
|
|
|
41
80
|
'$getLevel',
|
|
42
81
|
'$isDescendant',
|
|
43
82
|
];
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
static clientEnabledStaticMethods = [
|
|
84
|
+
'getPrimaryGroups',
|
|
85
|
+
'getSecondaryGroups',
|
|
86
|
+
];
|
|
87
|
+
$privateData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
88
|
+
static searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
89
|
+
$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
|
|
90
|
+
$allowlistTags = [];
|
|
91
|
+
/**
|
|
92
|
+
* This should only be used by the backend.
|
|
93
|
+
*/
|
|
94
|
+
$skipAcWhenSaving = false;
|
|
95
|
+
/**
|
|
96
|
+
* This should only be used by the backend.
|
|
97
|
+
*/
|
|
98
|
+
$skipAcWhenDeleting = false;
|
|
99
|
+
/**
|
|
100
|
+
* Used to save the current groupname for domain admin permissions.
|
|
101
|
+
*/
|
|
102
|
+
$originalGroupname;
|
|
103
|
+
static getFTSText(name, value) {
|
|
104
|
+
if ([
|
|
105
|
+
'abilities',
|
|
106
|
+
'parent',
|
|
107
|
+
'user',
|
|
108
|
+
'enabled',
|
|
109
|
+
'defaultPrimary',
|
|
110
|
+
'defaultSecondary',
|
|
111
|
+
'unverifiedSecondary',
|
|
112
|
+
].indexOf(name) !== -1) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return super.getFTSText(name, value);
|
|
116
|
+
}
|
|
117
|
+
static async factoryGroupname(groupname) {
|
|
118
|
+
const entity = new this();
|
|
119
|
+
if (groupname != null) {
|
|
120
|
+
const entity = await this.nymph.getEntity({
|
|
121
|
+
class: this,
|
|
122
|
+
}, {
|
|
123
|
+
type: '&',
|
|
124
|
+
ilike: ['groupname', groupname.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
125
|
+
});
|
|
126
|
+
if (entity != null) {
|
|
127
|
+
return entity;
|
|
80
128
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
129
|
+
}
|
|
130
|
+
return entity;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get all the groups that can be assigned as primary groups.
|
|
134
|
+
*
|
|
135
|
+
* @param options The options for an optional search query.
|
|
136
|
+
* @param selectors The selectors for an optional search query.
|
|
137
|
+
* @returns An array of the assignable primary groups.
|
|
138
|
+
*/
|
|
139
|
+
static async getPrimaryGroups(options, selectors) {
|
|
140
|
+
const tilmeld = enforceTilmeld(this);
|
|
141
|
+
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
142
|
+
throw new Error("You don't have permission to do that.");
|
|
143
|
+
}
|
|
144
|
+
return await this.getAssignableGroups(tilmeld.config.highestPrimary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get all the groups that can be assigned as secondary groups.
|
|
148
|
+
*
|
|
149
|
+
* @param options The options for an optional search query.
|
|
150
|
+
* @param selectors The selectors for an optional search query.
|
|
151
|
+
* @returns An array of the assignable secondary groups.
|
|
152
|
+
*/
|
|
153
|
+
static async getSecondaryGroups(options, selectors) {
|
|
154
|
+
const tilmeld = enforceTilmeld(this);
|
|
155
|
+
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
156
|
+
throw new Error("You don't have permission to do that.");
|
|
157
|
+
}
|
|
158
|
+
return await this.getAssignableGroups(tilmeld.config.highestSecondary, { ...options, class: tilmeld.Group, return: 'entity' }, [...(selectors ?? [])]);
|
|
92
159
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
160
|
+
static async getAssignableGroups(highestParent, options, selectors) {
|
|
161
|
+
const tilmeld = enforceTilmeld(this);
|
|
162
|
+
let assignableGroups = [];
|
|
163
|
+
if (highestParent === false) {
|
|
164
|
+
return assignableGroups;
|
|
165
|
+
}
|
|
166
|
+
({ options, selectors } = clearSearchRestrictedData({
|
|
167
|
+
options,
|
|
168
|
+
selectors,
|
|
169
|
+
}));
|
|
170
|
+
assignableGroups = await this.nymph.getEntities({ ...options, class: tilmeld.Group }, ...selectors);
|
|
171
|
+
if (highestParent !== true) {
|
|
172
|
+
assignableGroups = (await Promise.all(assignableGroups.map(async (group) => {
|
|
173
|
+
let curGroup = group;
|
|
174
|
+
let parent = curGroup.parent;
|
|
175
|
+
await parent?.$wake();
|
|
176
|
+
while (parent != null && parent?.cdate != null) {
|
|
177
|
+
if (parent?.guid === highestParent) {
|
|
178
|
+
return group;
|
|
179
|
+
}
|
|
180
|
+
curGroup = parent;
|
|
181
|
+
parent = curGroup.parent;
|
|
182
|
+
await parent?.$wake();
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}))).filter((group) => group != null);
|
|
186
|
+
}
|
|
187
|
+
return assignableGroups;
|
|
105
188
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.$data.abilities?.indexOf('system/admin') === -1 &&
|
|
113
|
-
tilmeld.gatekeeper('tilmeld/admin') &&
|
|
114
|
-
!tilmeld.gatekeeper('system/admin')) {
|
|
115
|
-
throw new errors_1.BadDataError("You don't have the authority to make this group a system admin.");
|
|
189
|
+
constructor() {
|
|
190
|
+
super();
|
|
191
|
+
// Defaults.
|
|
192
|
+
this.enabled = true;
|
|
193
|
+
this.abilities = [];
|
|
194
|
+
this.$updateDataProtection();
|
|
116
195
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (tilmeld.config.emailUsernames) {
|
|
129
|
-
this.$privateData.push('groupname');
|
|
196
|
+
async $getUniques() {
|
|
197
|
+
const tilmeld = enforceTilmeld(this);
|
|
198
|
+
const uniques = [`u:${this.groupname}`];
|
|
199
|
+
if (!tilmeld.config.emailUsernames &&
|
|
200
|
+
tilmeld.config.userFields.includes('email')) {
|
|
201
|
+
uniques.push(`e:${this.email}`);
|
|
202
|
+
}
|
|
203
|
+
if (this.defaultPrimary) {
|
|
204
|
+
uniques.push(`o:default-primary`);
|
|
205
|
+
}
|
|
206
|
+
return uniques;
|
|
130
207
|
}
|
|
131
|
-
|
|
132
|
-
(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this
|
|
208
|
+
$getAvatar() {
|
|
209
|
+
if (this.avatar != null && this.avatar !== '') {
|
|
210
|
+
return this.avatar;
|
|
211
|
+
}
|
|
212
|
+
if (this.email == null || !this.email.length) {
|
|
213
|
+
return 'https://secure.gravatar.com/avatar/?d=mm&s=40';
|
|
214
|
+
}
|
|
215
|
+
return ('https://secure.gravatar.com/avatar/' +
|
|
216
|
+
crypto
|
|
217
|
+
.createHash('sha256')
|
|
218
|
+
.update(this.email.trim().toLowerCase())
|
|
219
|
+
.digest('hex')
|
|
220
|
+
.toLowerCase() +
|
|
221
|
+
'?d=identicon&s=40');
|
|
222
|
+
}
|
|
223
|
+
$jsonAcceptData(input, allowConflict = false) {
|
|
224
|
+
this.$check();
|
|
225
|
+
if ('abilities' in input.data) {
|
|
226
|
+
this.$checkIncomingAbilities(input.data.abilities);
|
|
227
|
+
}
|
|
228
|
+
else if ((this.$allowlistData == null ||
|
|
229
|
+
this.$allowlistData.includes('abilities')) &&
|
|
230
|
+
(this.$privateData == null || !this.$privateData.includes('abilities'))) {
|
|
231
|
+
this.$checkIncomingAbilities(undefined);
|
|
232
|
+
}
|
|
233
|
+
super.$jsonAcceptData(input, allowConflict);
|
|
136
234
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
235
|
+
$jsonAcceptPatch(patch, allowConflict = false) {
|
|
236
|
+
this.$check();
|
|
237
|
+
if ('abilities' in patch.set) {
|
|
238
|
+
this.$checkIncomingAbilities(patch.set.abilities);
|
|
239
|
+
}
|
|
240
|
+
if (patch.unset.includes('abilities')) {
|
|
241
|
+
this.$checkIncomingAbilities(undefined);
|
|
242
|
+
}
|
|
243
|
+
super.$jsonAcceptPatch(patch, allowConflict);
|
|
141
244
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
245
|
+
$checkIncomingAbilities(incoming) {
|
|
246
|
+
const tilmeld = enforceTilmeld(this);
|
|
247
|
+
if (!Array.isArray(incoming) ||
|
|
248
|
+
incoming.find((ability) => typeof ability !== 'string')) {
|
|
249
|
+
throw new BadDataError('Abilities must be an array of strings.');
|
|
250
|
+
}
|
|
251
|
+
if (incoming.includes('system/admin') !==
|
|
252
|
+
this.abilities?.includes('system/admin') &&
|
|
253
|
+
(!tilmeld.currentUser ||
|
|
254
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
255
|
+
throw new BadDataError("You don't have the authority to grant or revoke system/admin.");
|
|
256
|
+
}
|
|
257
|
+
if (xor(incoming.filter((ability) => ability.startsWith('system/')), this.abilities?.filter((ability) => ability.startsWith('system/')) ??
|
|
258
|
+
[]).length &&
|
|
259
|
+
(!tilmeld.currentUser ||
|
|
260
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
261
|
+
throw new BadDataError("You don't have the authority to grant or revoke system abilities.");
|
|
262
|
+
}
|
|
263
|
+
if (incoming.includes('tilmeld/admin') !==
|
|
264
|
+
this.abilities?.includes('tilmeld/admin') &&
|
|
265
|
+
(!tilmeld.currentUser ||
|
|
266
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
267
|
+
throw new BadDataError("You don't have the authority to grant or revoke tilmeld/admin.");
|
|
268
|
+
}
|
|
269
|
+
if (incoming.includes('tilmeld/switch') !==
|
|
270
|
+
this.abilities?.includes('tilmeld/switch') &&
|
|
271
|
+
(!tilmeld.currentUser ||
|
|
272
|
+
!tilmeld.currentUser.abilities?.includes('system/admin'))) {
|
|
273
|
+
throw new BadDataError("You don't have the authority to grant or revoke tilmeld/switch.");
|
|
274
|
+
}
|
|
275
|
+
if (xor(incoming.filter((ability) => ability.startsWith('tilmeld/')), this.abilities?.filter((ability) => ability.startsWith('tilmeld/')) ??
|
|
276
|
+
[]).length &&
|
|
277
|
+
(!tilmeld.currentUser ||
|
|
278
|
+
(!tilmeld.currentUser.abilities?.includes('system/admin') &&
|
|
279
|
+
!tilmeld.currentUser.abilities?.includes('tilmeld/admin')))) {
|
|
280
|
+
throw new BadDataError("You don't have the authority to grant or revoke Tilmeld abilities.");
|
|
281
|
+
}
|
|
148
282
|
}
|
|
149
|
-
|
|
150
|
-
|
|
283
|
+
$putData(data, sdata, source) {
|
|
284
|
+
let newData = { ...data };
|
|
285
|
+
let newSdata = sdata && { ...sdata };
|
|
286
|
+
// TODO: Fix these being added by the drivers.
|
|
287
|
+
// @ts-ignore
|
|
288
|
+
delete newData.acRead;
|
|
289
|
+
// @ts-ignore
|
|
290
|
+
delete newData.acWrite;
|
|
291
|
+
// @ts-ignore
|
|
292
|
+
delete newData.acFull;
|
|
293
|
+
if (newSdata) {
|
|
294
|
+
delete newSdata.acRead;
|
|
295
|
+
delete newSdata.acWrite;
|
|
296
|
+
delete newSdata.acFull;
|
|
297
|
+
}
|
|
298
|
+
super.$putData(newData, newSdata, source);
|
|
299
|
+
if (source === 'server') {
|
|
300
|
+
this.$originalGroupname = this.groupname;
|
|
301
|
+
}
|
|
302
|
+
this.$updateDataProtection();
|
|
151
303
|
}
|
|
152
|
-
|
|
153
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Update the data protection arrays for the current user.
|
|
306
|
+
*/
|
|
307
|
+
$updateDataProtection() {
|
|
308
|
+
const tilmeld = enforceTilmeld(this);
|
|
309
|
+
let [_groupname, domain] = tilmeld.config.domainSupport
|
|
310
|
+
? splitn(this.groupname ?? '', '@', 2)
|
|
311
|
+
: [this.groupname, null];
|
|
312
|
+
// Lowercase the domain part.
|
|
313
|
+
if (domain) {
|
|
314
|
+
domain = domain.toLowerCase();
|
|
315
|
+
}
|
|
316
|
+
this.$privateData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
317
|
+
this.$allowlistData = [...Group.DEFAULT_ALLOWLIST_DATA];
|
|
318
|
+
if (tilmeld.config.emailUsernames) {
|
|
319
|
+
this.$privateData.push('groupname');
|
|
320
|
+
}
|
|
321
|
+
if (tilmeld.gatekeeper('tilmeld/admin') ||
|
|
322
|
+
tilmeld.gatekeeper('system/admin')) {
|
|
323
|
+
// Users who can edit groups can see their data.
|
|
324
|
+
this.$privateData = [];
|
|
325
|
+
this.$allowlistData = undefined;
|
|
326
|
+
}
|
|
327
|
+
else if (this.user != null &&
|
|
328
|
+
tilmeld.currentUser != null &&
|
|
329
|
+
this.user.guid === tilmeld.currentUser.guid) {
|
|
330
|
+
// Users can see their group's data.
|
|
331
|
+
this.$privateData = [];
|
|
332
|
+
}
|
|
333
|
+
else if (tilmeld.config.domainSupport &&
|
|
334
|
+
domain &&
|
|
335
|
+
tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
336
|
+
// Domain admins can see their groups' data.
|
|
337
|
+
this.$privateData = [];
|
|
338
|
+
}
|
|
154
339
|
}
|
|
155
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Check whether the group is a descendant of the given group.
|
|
342
|
+
*
|
|
343
|
+
* @param group The group, or the group's GUID.
|
|
344
|
+
* @returns True or false.
|
|
345
|
+
*/
|
|
346
|
+
async $isDescendant(givenGroup) {
|
|
347
|
+
const tilmeld = enforceTilmeld(this);
|
|
348
|
+
let group;
|
|
349
|
+
if (typeof givenGroup === 'string') {
|
|
350
|
+
group = await tilmeld.Group.factory(givenGroup);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
group = givenGroup;
|
|
354
|
+
}
|
|
355
|
+
if (group.guid == null) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
// Check to see if the group is a descendant of the given group.
|
|
359
|
+
const parent = this.parent;
|
|
360
|
+
await parent?.$wake();
|
|
361
|
+
if (parent == null) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (parent.$is(group)) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (await parent.$isDescendant(group)) {
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
156
370
|
return false;
|
|
157
371
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
async $getChildren() {
|
|
167
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
168
|
-
return await this.$nymph.getEntities({ class: tilmeld.Group }, {
|
|
169
|
-
type: '&',
|
|
170
|
-
equal: ['enabled', true],
|
|
171
|
-
ref: ['parent', this],
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
async $getDescendants(andSelf = false) {
|
|
175
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
176
|
-
let groups = [];
|
|
177
|
-
const entities = await this.$nymph.getEntities({ class: tilmeld.Group }, {
|
|
178
|
-
type: '&',
|
|
179
|
-
equal: ['enabled', true],
|
|
180
|
-
ref: ['parent', this],
|
|
181
|
-
});
|
|
182
|
-
for (let entity of entities) {
|
|
183
|
-
groups = (await entity.$getDescendants(true)).concat(groups);
|
|
184
|
-
}
|
|
185
|
-
if (andSelf) {
|
|
186
|
-
groups.push(this);
|
|
187
|
-
}
|
|
188
|
-
return groups;
|
|
189
|
-
}
|
|
190
|
-
$getDescendantsSync(andSelf = false) {
|
|
191
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
192
|
-
let groups = [];
|
|
193
|
-
let entity;
|
|
194
|
-
let offset = 0;
|
|
195
|
-
do {
|
|
196
|
-
entity = this.$nymph.driver.getEntitySync({ class: tilmeld.Group, offset }, {
|
|
372
|
+
/**
|
|
373
|
+
* Gets an array of the group's child groups.
|
|
374
|
+
*
|
|
375
|
+
* @returns An array of groups.
|
|
376
|
+
*/
|
|
377
|
+
async $getChildren() {
|
|
378
|
+
const tilmeld = enforceTilmeld(this);
|
|
379
|
+
return await this.$nymph.getEntities({ class: tilmeld.Group }, {
|
|
197
380
|
type: '&',
|
|
198
381
|
equal: ['enabled', true],
|
|
199
382
|
ref: ['parent', this],
|
|
200
383
|
});
|
|
201
|
-
if (entity != null) {
|
|
202
|
-
groups = entity.$getDescendantsSync(true).concat(groups);
|
|
203
|
-
}
|
|
204
|
-
offset++;
|
|
205
|
-
} while (entity != null);
|
|
206
|
-
if (andSelf) {
|
|
207
|
-
groups.push(this);
|
|
208
|
-
}
|
|
209
|
-
return groups;
|
|
210
|
-
}
|
|
211
|
-
$getLevel() {
|
|
212
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
213
|
-
let group = tilmeld.Group.factorySync(this.guid ?? undefined);
|
|
214
|
-
let level = 0;
|
|
215
|
-
while (group.parent != null && group.parent.cdate != null && level < 1024) {
|
|
216
|
-
level++;
|
|
217
|
-
group = group.parent;
|
|
218
384
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Gets an array of the group's descendant groups.
|
|
387
|
+
*
|
|
388
|
+
* @param andSelf Include this group in the returned array.
|
|
389
|
+
* @param andDisabled Include disabled groups.
|
|
390
|
+
* @returns An array of groups.
|
|
391
|
+
*/
|
|
392
|
+
async $getDescendants(andSelf = false, andDisabled = false) {
|
|
393
|
+
const tilmeld = enforceTilmeld(this);
|
|
394
|
+
let groups = [];
|
|
395
|
+
const entities = await this.$nymph.getEntities({ class: tilmeld.Group }, {
|
|
396
|
+
type: '&',
|
|
397
|
+
...(andDisabled ? {} : { equal: ['enabled', true] }),
|
|
398
|
+
ref: ['parent', this],
|
|
399
|
+
});
|
|
400
|
+
for (let entity of entities) {
|
|
401
|
+
groups = (await entity.$getDescendants(true)).concat(groups);
|
|
402
|
+
}
|
|
403
|
+
if (andSelf) {
|
|
404
|
+
groups.push(this);
|
|
405
|
+
}
|
|
406
|
+
return groups;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get the number of parents the group has.
|
|
410
|
+
*
|
|
411
|
+
* If the group is a top level group, this will return 0. If it is a child of
|
|
412
|
+
* a top level group, this will return 1. If it is a grandchild of a top level
|
|
413
|
+
* group, this will return 2, and so on.
|
|
414
|
+
*
|
|
415
|
+
* Levels will max out at 1024 to avoid recursive loops.
|
|
416
|
+
*
|
|
417
|
+
* @returns The level of the group.
|
|
418
|
+
*/
|
|
419
|
+
async $getLevel() {
|
|
420
|
+
const tilmeld = enforceTilmeld(this);
|
|
421
|
+
let group = await tilmeld.Group.factory(this.guid ?? undefined);
|
|
422
|
+
let parent = group.parent;
|
|
423
|
+
await parent?.$wake();
|
|
424
|
+
let level = 0;
|
|
425
|
+
while (parent != null && parent.cdate != null && level < 1024) {
|
|
426
|
+
level++;
|
|
427
|
+
group = parent;
|
|
428
|
+
parent = group.parent;
|
|
429
|
+
await parent?.$wake();
|
|
430
|
+
}
|
|
431
|
+
return level;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Gets an array of enabled users in the group.
|
|
435
|
+
*
|
|
436
|
+
* @param descendants Include users in all descendant groups too.
|
|
437
|
+
* @param limit The limit for the query.
|
|
438
|
+
* @param offset The offset for the query.
|
|
439
|
+
* @returns An array of users.
|
|
440
|
+
*/
|
|
441
|
+
async $getUsers(descendants = false, limit, offset) {
|
|
442
|
+
const tilmeld = enforceTilmeld(this);
|
|
443
|
+
let groups = [];
|
|
444
|
+
if (descendants) {
|
|
445
|
+
groups = await this.$getDescendants();
|
|
446
|
+
}
|
|
447
|
+
return await this.$nymph.getEntities({
|
|
448
|
+
class: tilmeld.User,
|
|
449
|
+
limit,
|
|
450
|
+
offset,
|
|
451
|
+
}, { type: '&', equal: ['enabled', true] }, {
|
|
452
|
+
type: '|',
|
|
453
|
+
ref: [
|
|
454
|
+
['group', this],
|
|
455
|
+
['groups', this],
|
|
456
|
+
...groups.map((group) => ['group', group]),
|
|
457
|
+
...groups.map((group) => ['groups', group]),
|
|
458
|
+
],
|
|
459
|
+
});
|
|
226
460
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
461
|
+
/**
|
|
462
|
+
* Check that a groupname is valid.
|
|
463
|
+
*
|
|
464
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
465
|
+
*/
|
|
466
|
+
async $checkGroupname() {
|
|
467
|
+
const tilmeld = enforceTilmeld(this);
|
|
468
|
+
if (!tilmeld.config.emailUsernames) {
|
|
469
|
+
let [groupname, domain] = tilmeld.config.domainSupport
|
|
470
|
+
? splitn(this.groupname ?? '', '@', 2)
|
|
471
|
+
: [this.groupname, null];
|
|
472
|
+
// Lowercase the domain part.
|
|
473
|
+
if (domain) {
|
|
474
|
+
domain = domain.toLowerCase();
|
|
475
|
+
this.groupname = `${groupname}@${domain}`;
|
|
476
|
+
}
|
|
477
|
+
if (this.groupname == null ||
|
|
478
|
+
!this.groupname.length ||
|
|
479
|
+
groupname == null ||
|
|
480
|
+
!groupname.length) {
|
|
481
|
+
return { result: false, message: 'Please specify a groupname.' };
|
|
482
|
+
}
|
|
483
|
+
if (groupname.length < tilmeld.config.minUsernameLength) {
|
|
484
|
+
return {
|
|
485
|
+
result: false,
|
|
486
|
+
message: 'Groupnames must be at least ' +
|
|
487
|
+
tilmeld.config.minUsernameLength +
|
|
488
|
+
' characters.',
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (groupname.length > tilmeld.config.maxUsernameLength) {
|
|
492
|
+
return {
|
|
493
|
+
result: false,
|
|
494
|
+
message: 'Groupnames must not exceed ' +
|
|
495
|
+
tilmeld.config.maxUsernameLength +
|
|
496
|
+
' characters.',
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (difference(groupname.split(''), tilmeld.config.validChars.split(''))
|
|
500
|
+
.length) {
|
|
501
|
+
return {
|
|
502
|
+
result: false,
|
|
503
|
+
message: tilmeld.config.validCharsNotice,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (!tilmeld.config.validRegex.test(groupname)) {
|
|
507
|
+
return {
|
|
508
|
+
result: false,
|
|
509
|
+
message: tilmeld.config.validRegexNotice,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (domain != null) {
|
|
513
|
+
if (!tilmeld.config.validDomainRegex.test(domain)) {
|
|
514
|
+
return {
|
|
515
|
+
result: false,
|
|
516
|
+
message: tilmeld.config.validDomainRegexNotice,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (!this.$is(tilmeld.currentUser?.group) &&
|
|
520
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
521
|
+
return {
|
|
522
|
+
result: false,
|
|
523
|
+
message: "You don't have the authority to manage groups on this domain.",
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const selector = {
|
|
528
|
+
type: '&',
|
|
529
|
+
ilike: [
|
|
530
|
+
'groupname',
|
|
531
|
+
this.groupname.replace(/([\\%_])/g, (s) => `\\${s}`),
|
|
532
|
+
],
|
|
263
533
|
};
|
|
264
|
-
|
|
265
|
-
|
|
534
|
+
if (this.guid != null) {
|
|
535
|
+
selector['!guid'] = this.guid;
|
|
536
|
+
}
|
|
537
|
+
const test = await this.$nymph.getEntity({ class: tilmeld.Group, skipAc: true }, selector);
|
|
538
|
+
if (test != null) {
|
|
539
|
+
return { result: false, message: 'That groupname is taken.' };
|
|
540
|
+
}
|
|
541
|
+
for (let callback of this.constructor
|
|
542
|
+
.checkGroupnameCallbacks) {
|
|
543
|
+
if (callback) {
|
|
544
|
+
const result = await callback(this, {
|
|
545
|
+
groupname: this.groupname,
|
|
546
|
+
});
|
|
547
|
+
if (!result.result) {
|
|
548
|
+
return { message: 'Groupname is not available.', ...result };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
266
552
|
return {
|
|
267
|
-
result:
|
|
268
|
-
message:
|
|
553
|
+
result: true,
|
|
554
|
+
message: this.guid != null ? 'Groupname is valid.' : 'Groupname is available!',
|
|
269
555
|
};
|
|
270
556
|
}
|
|
271
|
-
|
|
557
|
+
else {
|
|
558
|
+
this.email = this.groupname;
|
|
559
|
+
if (this.groupname == null || !this.groupname.length) {
|
|
560
|
+
return { result: false, message: 'Please specify an email.' };
|
|
561
|
+
}
|
|
562
|
+
if (this.groupname.length < tilmeld.config.minUsernameLength) {
|
|
563
|
+
return {
|
|
564
|
+
result: false,
|
|
565
|
+
message: 'Emails must be at least ' +
|
|
566
|
+
tilmeld.config.minUsernameLength +
|
|
567
|
+
' characters.',
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (this.groupname.length > tilmeld.config.maxUsernameLength) {
|
|
571
|
+
return {
|
|
572
|
+
result: false,
|
|
573
|
+
message: 'Emails must not exceed ' +
|
|
574
|
+
tilmeld.config.maxUsernameLength +
|
|
575
|
+
' characters.',
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return await this.$checkEmail();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Check that an email is unique.
|
|
583
|
+
*
|
|
584
|
+
* @returns An object with a boolean 'result' entry and a 'message' entry.
|
|
585
|
+
*/
|
|
586
|
+
async $checkEmail() {
|
|
587
|
+
const tilmeld = enforceTilmeld(this);
|
|
588
|
+
if (this.email === '') {
|
|
589
|
+
return { result: true, message: '' };
|
|
590
|
+
}
|
|
591
|
+
if (this.email == null) {
|
|
592
|
+
return { result: false, message: 'Please specify a valid email.' };
|
|
593
|
+
}
|
|
594
|
+
if (!tilmeld.config.validEmailRegex.test(this.email)) {
|
|
272
595
|
return {
|
|
273
596
|
result: false,
|
|
274
|
-
message: tilmeld.config.
|
|
597
|
+
message: tilmeld.config.validEmailRegexNotice,
|
|
275
598
|
};
|
|
276
599
|
}
|
|
277
600
|
const selector = {
|
|
278
601
|
type: '&',
|
|
279
|
-
ilike: [
|
|
280
|
-
'groupname',
|
|
281
|
-
this.$data.groupname.replace(/([\\%_])/g, (s) => `\\${s}`),
|
|
282
|
-
],
|
|
602
|
+
ilike: ['email', this.email.replace(/([\\%_])/g, (s) => `\\${s}`)],
|
|
283
603
|
};
|
|
284
604
|
if (this.guid != null) {
|
|
285
605
|
selector['!guid'] = this.guid;
|
|
286
606
|
}
|
|
287
607
|
const test = await this.$nymph.getEntity({ class: tilmeld.Group, skipAc: true }, selector);
|
|
288
608
|
if (test != null) {
|
|
289
|
-
return { result: false, message: 'That groupname is taken.' };
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
result: true,
|
|
293
|
-
message: this.guid != null ? 'Groupname is valid.' : 'Groupname is available!',
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
this.$data.email = this.$data.groupname;
|
|
298
|
-
if (this.$data.groupname == null || !this.$data.groupname.length) {
|
|
299
|
-
return { result: false, message: 'Please specify an email.' };
|
|
300
|
-
}
|
|
301
|
-
if (tilmeld.config.maxUsernameLength > 0 &&
|
|
302
|
-
this.$data.groupname.length > tilmeld.config.maxUsernameLength) {
|
|
303
609
|
return {
|
|
304
610
|
result: false,
|
|
305
|
-
message: '
|
|
306
|
-
tilmeld.config.maxUsernameLength +
|
|
307
|
-
' characters.',
|
|
611
|
+
message: 'That email address is already registered.',
|
|
308
612
|
};
|
|
309
613
|
}
|
|
310
|
-
return await this.$checkEmail();
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
async $checkEmail() {
|
|
314
|
-
const tilmeld = this.$nymph.tilmeld;
|
|
315
|
-
if (this.$data.email === '') {
|
|
316
|
-
return { result: true, message: '' };
|
|
317
|
-
}
|
|
318
|
-
if (this.$data.email == null) {
|
|
319
|
-
return { result: false, message: 'Please specify a valid email.' };
|
|
320
|
-
}
|
|
321
|
-
if (!tilmeld.config.validEmailRegex.test(this.$data.email)) {
|
|
322
614
|
return {
|
|
323
|
-
result:
|
|
324
|
-
message:
|
|
615
|
+
result: true,
|
|
616
|
+
message: this.guid != null ? 'Email is valid.' : 'Email address is valid!',
|
|
325
617
|
};
|
|
326
618
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
619
|
+
async $save() {
|
|
620
|
+
let tilmeld = enforceTilmeld(this);
|
|
621
|
+
if (this.groupname == null || !this.groupname.trim().length) {
|
|
622
|
+
throw new BadUsernameError('Groupname must not be empty.');
|
|
623
|
+
}
|
|
624
|
+
// Lowercase the domain part.
|
|
625
|
+
if (tilmeld.config.domainSupport) {
|
|
626
|
+
const [groupname, domain] = splitn(this.groupname ?? '', '@', 2);
|
|
627
|
+
if (domain) {
|
|
628
|
+
this.groupname = `${groupname}@${domain.toLowerCase()}`;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (tilmeld.config.domainSupport) {
|
|
632
|
+
const [_groupname, domain] = splitn(this.groupname ?? '', '@', 2);
|
|
633
|
+
const [_originalGroupname, originalDomain] = splitn(this.$originalGroupname ?? '', '@', 2);
|
|
634
|
+
if (this.guid != null &&
|
|
635
|
+
((originalDomain == null && domain != null) ||
|
|
636
|
+
(domain == null && originalDomain != null))) {
|
|
637
|
+
throw new AccessControlError("Groups can't switch to or from having a domain once created.");
|
|
638
|
+
}
|
|
639
|
+
if (!this.$skipAcWhenSaving &&
|
|
640
|
+
!tilmeld.gatekeeper('tilmeld/admin') &&
|
|
641
|
+
!tilmeld.gatekeeper('system/admin')) {
|
|
642
|
+
if (domain != null &&
|
|
643
|
+
!this.$is(tilmeld.currentUser) &&
|
|
644
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`)) {
|
|
645
|
+
throw new AccessControlError("You don't have the authority to modify groups on this domain.");
|
|
646
|
+
}
|
|
647
|
+
if (originalDomain != null &&
|
|
648
|
+
originalDomain !== domain &&
|
|
649
|
+
(!tilmeld.gatekeeper(`tilmeld/domain/${domain}/admin`) ||
|
|
650
|
+
!tilmeld.gatekeeper(`tilmeld/domain/${originalDomain}/admin`))) {
|
|
651
|
+
throw new AccessControlError("You don't have the authority to modify groups on this domain.");
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Formatting.
|
|
656
|
+
this.groupname = this.groupname.trim();
|
|
657
|
+
if (tilmeld.config.emailUsernames) {
|
|
658
|
+
this.email = this.groupname;
|
|
659
|
+
}
|
|
660
|
+
this.email = (this.email ?? '').trim();
|
|
661
|
+
this.name = (this.name ?? '').trim();
|
|
662
|
+
this.phone = (this.phone ?? '').trim();
|
|
663
|
+
this.enabled = !!this.enabled;
|
|
664
|
+
// Clear empty values.
|
|
665
|
+
if (this.name === '' || !tilmeld.config.userFields.includes('name')) {
|
|
666
|
+
delete this.name;
|
|
667
|
+
}
|
|
668
|
+
if (this.email === '' || !tilmeld.config.userFields.includes('email')) {
|
|
669
|
+
delete this.email;
|
|
670
|
+
}
|
|
671
|
+
if (this.avatar === '') {
|
|
672
|
+
delete this.avatar;
|
|
673
|
+
}
|
|
674
|
+
if (this.phone === '' || !tilmeld.config.userFields.includes('phone')) {
|
|
675
|
+
delete this.phone;
|
|
676
|
+
}
|
|
677
|
+
if (this.parent == null) {
|
|
678
|
+
delete this.parent;
|
|
679
|
+
}
|
|
680
|
+
if (this.user == null) {
|
|
681
|
+
delete this.user;
|
|
682
|
+
}
|
|
683
|
+
if (!this.defaultPrimary) {
|
|
684
|
+
delete this.defaultPrimary;
|
|
685
|
+
}
|
|
686
|
+
if (!this.defaultSecondary) {
|
|
687
|
+
delete this.defaultSecondary;
|
|
688
|
+
}
|
|
689
|
+
if (!this.unverifiedSecondary) {
|
|
690
|
+
delete this.unverifiedSecondary;
|
|
691
|
+
}
|
|
692
|
+
// Groups should never have 'system/admin', 'tilmeld/admin',
|
|
693
|
+
// 'tilmeld/switch', or system abilities.
|
|
694
|
+
this.abilities = (this.abilities ?? []).filter((ability) => {
|
|
695
|
+
if (['system/admin', 'tilmeld/admin', 'tilmeld/switch'].indexOf(ability) !==
|
|
696
|
+
-1) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
if (ability == null) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
if (ability.startsWith('system/')) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
return true;
|
|
706
|
+
});
|
|
707
|
+
// Verification.
|
|
708
|
+
const unCheck = await this.$checkGroupname();
|
|
709
|
+
if (!unCheck.result) {
|
|
710
|
+
throw new BadUsernameError(unCheck.message);
|
|
711
|
+
}
|
|
712
|
+
if (!tilmeld.config.emailUsernames &&
|
|
713
|
+
tilmeld.config.userFields.includes('email')) {
|
|
714
|
+
const emCheck = await this.$checkEmail();
|
|
715
|
+
if (!emCheck.result) {
|
|
716
|
+
throw new BadEmailError(emCheck.message);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Validate group parent. Make sure it's not a descendant of this group.
|
|
720
|
+
const parent = this.parent;
|
|
721
|
+
await parent?.$wake();
|
|
722
|
+
if (parent &&
|
|
723
|
+
(parent.cdate == null ||
|
|
724
|
+
this.$is(parent) ||
|
|
725
|
+
(await parent.$isDescendant(this)))) {
|
|
726
|
+
throw new BadDataError("Group parent can't be itself or descendant of itself.");
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
tilmeld.config.validatorGroup(tilmeld, this);
|
|
730
|
+
}
|
|
731
|
+
catch (e) {
|
|
732
|
+
throw new BadDataError(e?.message);
|
|
733
|
+
}
|
|
734
|
+
// Only one default primary group is allowed.
|
|
735
|
+
if (this.defaultPrimary) {
|
|
736
|
+
const currentPrimary = await this.$nymph.getEntity({ class: tilmeld.Group }, { type: '&', truthy: 'defaultPrimary' });
|
|
737
|
+
if (currentPrimary != null && !this.$is(currentPrimary)) {
|
|
738
|
+
currentPrimary.defaultPrimary = false;
|
|
739
|
+
try {
|
|
740
|
+
if (this.$skipAcWhenSaving) {
|
|
741
|
+
await currentPrimary.$saveSkipAC();
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
await currentPrimary.$save();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
catch (e) {
|
|
748
|
+
throw new CouldNotChangeDefaultPrimaryGroupError('Could not change new user primary group from ' +
|
|
749
|
+
`${currentPrimary.groupname}.`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
let preGuid = this.guid;
|
|
754
|
+
let preCdate = this.cdate;
|
|
755
|
+
let preMdate = this.mdate;
|
|
756
|
+
return await transaction(this.$nymph, 'tilmeld-save-group-' + (this.guid || nanoid()), async (nymph) => {
|
|
757
|
+
this.$setNymph(nymph);
|
|
758
|
+
for (let callback of this.constructor
|
|
759
|
+
.beforeSaveCallbacks) {
|
|
760
|
+
if (callback) {
|
|
761
|
+
await callback(this);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
await super.$save();
|
|
765
|
+
for (let callback of this.constructor
|
|
766
|
+
.afterSaveCallbacks) {
|
|
767
|
+
if (callback) {
|
|
768
|
+
await callback(this);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}, async (nymph, committed) => {
|
|
772
|
+
this.$setNymph(nymph);
|
|
773
|
+
if (committed) {
|
|
774
|
+
this.$originalGroupname = this.groupname;
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
this.guid = preGuid;
|
|
778
|
+
this.cdate = preCdate;
|
|
779
|
+
this.mdate = preMdate;
|
|
780
|
+
}
|
|
781
|
+
});
|
|
340
782
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
783
|
+
/*
|
|
784
|
+
* This should *never* be accessible on the client.
|
|
785
|
+
*/
|
|
786
|
+
async $saveSkipAC() {
|
|
787
|
+
this.$skipAcWhenSaving = true;
|
|
788
|
+
await this.$save();
|
|
789
|
+
}
|
|
790
|
+
$tilmeldSaveSkipAC() {
|
|
791
|
+
if (this.$skipAcWhenSaving) {
|
|
792
|
+
this.$skipAcWhenSaving = false;
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
349
795
|
return false;
|
|
350
796
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
this.$data.name = (this.$data.name ?? '').trim();
|
|
357
|
-
this.$data.phone = (this.$data.phone ?? '').trim();
|
|
358
|
-
this.$data.enabled = !!this.$data.enabled;
|
|
359
|
-
if (this.$data.name === '') {
|
|
360
|
-
delete this.$data.name;
|
|
361
|
-
}
|
|
362
|
-
if (this.$data.email === '') {
|
|
363
|
-
delete this.$data.email;
|
|
364
|
-
}
|
|
365
|
-
if (this.$data.avatar === '') {
|
|
366
|
-
delete this.$data.avatar;
|
|
367
|
-
}
|
|
368
|
-
if (this.$data.phone === '') {
|
|
369
|
-
delete this.$data.phone;
|
|
370
|
-
}
|
|
371
|
-
if (this.$data.parent == null) {
|
|
372
|
-
delete this.$data.parent;
|
|
373
|
-
}
|
|
374
|
-
if (this.$data.user == null) {
|
|
375
|
-
delete this.$data.user;
|
|
376
|
-
}
|
|
377
|
-
if (!this.$data.defaultPrimary) {
|
|
378
|
-
delete this.$data.defaultPrimary;
|
|
379
|
-
}
|
|
380
|
-
if (!this.$data.defaultSecondary) {
|
|
381
|
-
delete this.$data.defaultSecondary;
|
|
382
|
-
}
|
|
383
|
-
if (!this.$data.unverifiedSecondary) {
|
|
384
|
-
delete this.$data.unverifiedSecondary;
|
|
385
|
-
}
|
|
386
|
-
this.$data.abilities = (0, lodash_1.difference)(this.$data.abilities ?? [], [
|
|
387
|
-
'system/admin',
|
|
388
|
-
'tilmeld/admin',
|
|
389
|
-
null,
|
|
390
|
-
undefined,
|
|
391
|
-
]);
|
|
392
|
-
const unCheck = await this.$checkGroupname();
|
|
393
|
-
if (!unCheck.result) {
|
|
394
|
-
throw new errors_1.BadUsernameError(unCheck.message);
|
|
395
|
-
}
|
|
396
|
-
if (!tilmeld.config.emailUsernames) {
|
|
397
|
-
const emCheck = await this.$checkEmail();
|
|
398
|
-
if (!emCheck.result) {
|
|
399
|
-
throw new errors_1.BadEmailError(emCheck.message);
|
|
797
|
+
async $delete() {
|
|
798
|
+
let tilmeld = enforceTilmeld(this);
|
|
799
|
+
const User = this.$nymph.getEntityClass(UserClass);
|
|
800
|
+
if (!this.$skipAcWhenDeleting && !tilmeld.gatekeeper('tilmeld/admin')) {
|
|
801
|
+
throw new AccessControlError("You don't have the authority to delete groups.");
|
|
400
802
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
tilmeld.config.validatorGroup(this);
|
|
410
|
-
}
|
|
411
|
-
catch (e) {
|
|
412
|
-
throw new errors_1.BadDataError(e?.message);
|
|
413
|
-
}
|
|
414
|
-
if (this.$data.defaultPrimary) {
|
|
415
|
-
const currentPrimary = await this.$nymph.getEntity({ class: tilmeld.Group }, { type: '&', truthy: 'defaultPrimary' });
|
|
416
|
-
if (currentPrimary != null && !this.$is(currentPrimary)) {
|
|
417
|
-
currentPrimary.defaultPrimary = false;
|
|
418
|
-
if (!(await currentPrimary.$save())) {
|
|
419
|
-
throw new errors_1.CouldNotChangeDefaultPrimaryGroupError('Could not change new user primary group from ' +
|
|
420
|
-
`${currentPrimary.groupname}.`);
|
|
803
|
+
if (this.groupname !== this.$originalGroupname) {
|
|
804
|
+
throw new AccessControlError("You can't delete a group while switching its groupname.");
|
|
805
|
+
}
|
|
806
|
+
for (let callback of this.constructor
|
|
807
|
+
.beforeDeleteCallbacks) {
|
|
808
|
+
if (callback) {
|
|
809
|
+
await callback(this);
|
|
421
810
|
}
|
|
422
811
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
async $delete() {
|
|
438
|
-
let tilmeld = this.$nymph.tilmeld;
|
|
439
|
-
if (!tilmeld.gatekeeper('tilmeld/admin')) {
|
|
440
|
-
throw new errors_1.BadDataError("You don't have the authority to delete groups.");
|
|
441
|
-
}
|
|
442
|
-
const transaction = 'tilmeld-delete-' + this.guid;
|
|
443
|
-
const nymph = this.$nymph;
|
|
444
|
-
const tnymph = await nymph.startTransaction(transaction);
|
|
445
|
-
this.$nymph = tnymph;
|
|
446
|
-
tilmeld = this.$nymph.tilmeld;
|
|
447
|
-
const descendants = await this.$getDescendants();
|
|
448
|
-
if (descendants.length) {
|
|
449
|
-
for (let curGroup of descendants) {
|
|
450
|
-
if (!(await curGroup.$delete())) {
|
|
451
|
-
await tnymph.rollback(transaction);
|
|
452
|
-
this.$nymph = nymph;
|
|
453
|
-
return false;
|
|
812
|
+
// Delete children.
|
|
813
|
+
const children = await this.$nymph.getEntities({ class: tilmeld.Group, skipAc: true }, { type: '&', ref: ['parent', this] });
|
|
814
|
+
if (children.length) {
|
|
815
|
+
for (let curGroup of children) {
|
|
816
|
+
if (this.$skipAcWhenDeleting) {
|
|
817
|
+
await curGroup.$deleteSkipAC();
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
await curGroup.$delete();
|
|
821
|
+
}
|
|
454
822
|
}
|
|
455
823
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
824
|
+
// Remove users from this primary group.
|
|
825
|
+
const primaryUsers = await this.$nymph.getEntities({ class: User, skipAc: true }, { type: '&', ref: ['group', this] });
|
|
826
|
+
for (let user of primaryUsers) {
|
|
827
|
+
delete user.group;
|
|
828
|
+
if (this.$skipAcWhenDeleting) {
|
|
829
|
+
await user.$saveSkipAC();
|
|
830
|
+
}
|
|
831
|
+
else {
|
|
832
|
+
await user.$save();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// Remove users from this secondary group.
|
|
836
|
+
const secondaryUsers = await this.$nymph.getEntities({ class: User, skipAc: true }, { type: '&', ref: ['groups', this] });
|
|
837
|
+
for (let user of secondaryUsers) {
|
|
838
|
+
user.$delGroup(this);
|
|
839
|
+
if (this.$skipAcWhenDeleting) {
|
|
840
|
+
await user.$saveSkipAC();
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
await user.$save();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// Delete the group.
|
|
847
|
+
await super.$delete();
|
|
848
|
+
for (let callback of this.constructor
|
|
849
|
+
.afterDeleteCallbacks) {
|
|
850
|
+
if (callback) {
|
|
851
|
+
await callback(this);
|
|
852
|
+
}
|
|
470
853
|
}
|
|
471
854
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return false;
|
|
855
|
+
/*
|
|
856
|
+
* This should *never* be accessible on the client.
|
|
857
|
+
*/
|
|
858
|
+
async $deleteSkipAC() {
|
|
859
|
+
this.$skipAcWhenDeleting = true;
|
|
860
|
+
await this.$delete();
|
|
861
|
+
}
|
|
862
|
+
$tilmeldDeleteSkipAC() {
|
|
863
|
+
if (this.$skipAcWhenDeleting) {
|
|
864
|
+
this.$skipAcWhenDeleting = false;
|
|
865
|
+
return true;
|
|
484
866
|
}
|
|
867
|
+
return false;
|
|
485
868
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
869
|
+
static on(event, callback) {
|
|
870
|
+
const prop = (event + 'Callbacks');
|
|
871
|
+
if (!(prop in this)) {
|
|
872
|
+
throw new Error('Invalid event type.');
|
|
873
|
+
}
|
|
874
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
875
|
+
this[prop].push(callback);
|
|
876
|
+
return () => this.off(event, callback);
|
|
489
877
|
}
|
|
490
|
-
|
|
491
|
-
|
|
878
|
+
static off(event, callback) {
|
|
879
|
+
const prop = (event + 'Callbacks');
|
|
880
|
+
if (!(prop in this)) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
884
|
+
const i = this[prop].indexOf(callback);
|
|
885
|
+
if (i > -1) {
|
|
886
|
+
// @ts-ignore: The callback should always be the right type here.
|
|
887
|
+
this[prop].splice(i, 1);
|
|
888
|
+
}
|
|
889
|
+
return true;
|
|
492
890
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
exports.default = Group;
|
|
498
|
-
Group.ETYPE = 'tilmeld_group';
|
|
499
|
-
Group.class = 'Group';
|
|
500
|
-
Group.DEFAULT_PRIVATE_DATA = ['email', 'phone', 'abilities', 'user'];
|
|
501
|
-
Group.DEFAULT_ALLOWLIST_DATA = [];
|
|
502
|
-
Group.clientEnabledStaticMethods = [
|
|
503
|
-
'getPrimaryGroups',
|
|
504
|
-
'getSecondaryGroups',
|
|
505
|
-
];
|
|
506
|
-
Group.searchRestrictedData = [...Group.DEFAULT_PRIVATE_DATA];
|
|
891
|
+
};
|
|
892
|
+
})();
|
|
893
|
+
export { Group };
|
|
507
894
|
//# sourceMappingURL=Group.js.map
|