@stamhoofd/backend 2.26.0 → 2.28.0
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/package.json +10 -10
- package/src/crons.ts +5 -1
- package/src/endpoints/global/members/GetMembersEndpoint.ts +53 -4
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +46 -0
- package/src/helpers/MemberUserSyncer.ts +11 -2
- package/src/sql-filters/members.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.28.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"@simonbackx/simple-encoding": "2.15.1",
|
|
37
37
|
"@simonbackx/simple-endpoints": "1.14.0",
|
|
38
38
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
39
|
-
"@stamhoofd/backend-i18n": "2.
|
|
40
|
-
"@stamhoofd/backend-middleware": "2.
|
|
41
|
-
"@stamhoofd/email": "2.
|
|
42
|
-
"@stamhoofd/models": "2.
|
|
43
|
-
"@stamhoofd/queues": "2.
|
|
44
|
-
"@stamhoofd/sql": "2.
|
|
45
|
-
"@stamhoofd/structures": "2.
|
|
46
|
-
"@stamhoofd/utility": "2.
|
|
39
|
+
"@stamhoofd/backend-i18n": "2.28.0",
|
|
40
|
+
"@stamhoofd/backend-middleware": "2.28.0",
|
|
41
|
+
"@stamhoofd/email": "2.28.0",
|
|
42
|
+
"@stamhoofd/models": "2.28.0",
|
|
43
|
+
"@stamhoofd/queues": "2.28.0",
|
|
44
|
+
"@stamhoofd/sql": "2.28.0",
|
|
45
|
+
"@stamhoofd/structures": "2.28.0",
|
|
46
|
+
"@stamhoofd/utility": "2.28.0",
|
|
47
47
|
"archiver": "^7.0.1",
|
|
48
48
|
"aws-sdk": "^2.885.0",
|
|
49
49
|
"axios": "1.6.8",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"postmark": "4.0.2",
|
|
61
61
|
"stripe": "^16.6.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "3598a1baed3fc513175f081cd0aa1e6741226fbd"
|
|
64
64
|
}
|
package/src/crons.ts
CHANGED
|
@@ -586,7 +586,11 @@ let lastDripCheck: Date | null = null
|
|
|
586
586
|
let lastDripId = ""
|
|
587
587
|
async function checkDrips() {
|
|
588
588
|
if (STAMHOOFD.environment === "development") {
|
|
589
|
-
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (STAMHOOFD.userMode === 'platform') {
|
|
593
|
+
return;
|
|
590
594
|
}
|
|
591
595
|
|
|
592
596
|
if (lastDripCheck && lastDripCheck > new Date(new Date().getTime() - 6 * 60 * 60 * 1000)) {
|
|
@@ -4,9 +4,10 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Member, Platform } from '@stamhoofd/models';
|
|
6
6
|
import { SQL, compileToSQLFilter, compileToSQLSorter } from "@stamhoofd/sql";
|
|
7
|
-
import { CountFilteredRequest, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
7
|
+
import { CountFilteredRequest, Country, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
8
8
|
import { DataValidator } from '@stamhoofd/utility';
|
|
9
9
|
|
|
10
|
+
import parsePhoneNumber from "libphonenumber-js/max";
|
|
10
11
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
11
12
|
import { Context } from '../../../helpers/Context';
|
|
12
13
|
import { memberFilterCompilers } from '../../../sql-filters/members';
|
|
@@ -131,11 +132,55 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
if (q.search) {
|
|
134
|
-
let searchFilter: StamhoofdFilter|null = null
|
|
135
|
+
let searchFilter: StamhoofdFilter|null = null
|
|
136
|
+
|
|
137
|
+
// is phone?
|
|
138
|
+
if (!searchFilter && q.search.match(/^\+?[0-9\s-]+$/)) {
|
|
139
|
+
// Try to format as phone so we have 1:1 space matches
|
|
140
|
+
try {
|
|
141
|
+
const phoneNumber = parsePhoneNumber(q.search, (Context.i18n.country as Country) || Country.Belgium)
|
|
142
|
+
if (phoneNumber && phoneNumber.isValid()) {
|
|
143
|
+
const formatted = phoneNumber.formatInternational();
|
|
144
|
+
searchFilter = {
|
|
145
|
+
$or: [
|
|
146
|
+
{
|
|
147
|
+
phone: {
|
|
148
|
+
$eq: formatted
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
parentPhone: {
|
|
153
|
+
$eq: formatted
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
unverifiedPhone: {
|
|
158
|
+
$eq: formatted
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.error('Failed to parse phone number', q.search, e)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Is lidnummer?
|
|
171
|
+
if (!searchFilter && q.search.match(/^[0-9]{4}-[0-9]{6}-[0-9]{1,2}$/) || q.search.match(/^[0-9]{10}$/)) {
|
|
172
|
+
searchFilter = {
|
|
173
|
+
memberNumber: {
|
|
174
|
+
$eq: q.search
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
135
178
|
|
|
136
179
|
// Two search modes:
|
|
137
180
|
// e-mail or name based searching
|
|
138
|
-
if (
|
|
181
|
+
if (searchFilter) {
|
|
182
|
+
// already done
|
|
183
|
+
} else if (q.search.includes('@')) {
|
|
139
184
|
const isCompleteAddress = DataValidator.isEmailValid(q.search);
|
|
140
185
|
|
|
141
186
|
// Member email address contains, or member parent contains
|
|
@@ -150,6 +195,11 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
150
195
|
parentEmail: {
|
|
151
196
|
[(isCompleteAddress ? '$eq' : '$contains')]: q.search
|
|
152
197
|
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
unverifiedEmail: {
|
|
201
|
+
[(isCompleteAddress ? '$eq' : '$contains')]: q.search
|
|
202
|
+
}
|
|
153
203
|
}
|
|
154
204
|
]
|
|
155
205
|
} as any as StamhoofdFilter
|
|
@@ -162,7 +212,6 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
162
212
|
}
|
|
163
213
|
|
|
164
214
|
// todo: Address search detection
|
|
165
|
-
// todo: Phone number search detection
|
|
166
215
|
|
|
167
216
|
if (searchFilter) {
|
|
168
217
|
query.where(compileToSQLFilter(searchFilter, filterCompilers))
|
|
@@ -81,6 +81,9 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
81
81
|
let deleteUnreachable = false
|
|
82
82
|
const allowedIds: string[] = []
|
|
83
83
|
|
|
84
|
+
//#region prevent patch category lock if no full platform access
|
|
85
|
+
const originalCategories = organizationPeriod.settings.categories;
|
|
86
|
+
|
|
84
87
|
if (await Context.auth.hasFullAccess(organization.id)) {
|
|
85
88
|
if (patch.settings) {
|
|
86
89
|
if(patch.settings.categories) {
|
|
@@ -117,6 +120,49 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
117
120
|
}
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
//#region handle locked categories
|
|
124
|
+
if(!Context.auth.hasPlatformFullAccess()) {
|
|
125
|
+
const categoriesAfterPatch = organizationPeriod.settings.categories;
|
|
126
|
+
|
|
127
|
+
for(const categoryBefore of originalCategories) {
|
|
128
|
+
const locked = categoryBefore.settings.locked;
|
|
129
|
+
|
|
130
|
+
if(locked) {
|
|
131
|
+
// todo: use existing function, now a category could still be deleted if the category is moved to another category and that catetory is deleted
|
|
132
|
+
const categoryId = categoryBefore.id;
|
|
133
|
+
const refCountBefore = originalCategories.filter(c => c.categoryIds.includes(categoryId)).length;
|
|
134
|
+
const refCountAfter = categoriesAfterPatch.filter(c => c.categoryIds.includes(categoryId)).length;
|
|
135
|
+
const isDeleted = refCountAfter < refCountBefore;
|
|
136
|
+
|
|
137
|
+
if(isDeleted) {
|
|
138
|
+
throw Context.auth.error('Je hebt geen toegangsrechten om deze vergrendelde categorie te verwijderen.')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const categoryAfter = categoriesAfterPatch.find(c => c.id === categoryBefore.id);
|
|
143
|
+
|
|
144
|
+
if(!categoryAfter) {
|
|
145
|
+
if(locked) {
|
|
146
|
+
throw Context.auth.error('Je hebt geen toegangsrechten om deze vergrendelde categorie te verwijderen.')
|
|
147
|
+
}
|
|
148
|
+
} else if(locked !== categoryAfter.settings.locked) {
|
|
149
|
+
throw Context.auth.error('Je hebt geen toegangsrechten om deze categorie te vergrendelen of ontgrendelen.')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if(!locked || !categoryAfter) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const settingsBefore = categoryBefore.settings;
|
|
157
|
+
const settingsAfter = categoryAfter.settings;
|
|
158
|
+
|
|
159
|
+
if(settingsBefore.name !== settingsAfter.name) {
|
|
160
|
+
throw Context.auth.error('Je hebt geen toegangsrechten de naam van deze vergrendelde categorie te wijzigen.')
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
|
|
120
166
|
await organizationPeriod.save();
|
|
121
167
|
|
|
122
168
|
// Check changes to groups
|
|
@@ -9,8 +9,12 @@ export class MemberUserSyncerStatic {
|
|
|
9
9
|
* - email addresses have changed
|
|
10
10
|
*/
|
|
11
11
|
async onChangeMember(member: MemberWithRegistrations, unlinkUsers: boolean = false) {
|
|
12
|
+
console.log('onchange member', member.id, 'unlink', unlinkUsers)
|
|
13
|
+
|
|
12
14
|
const {userEmails, parentAndUnverifiedEmails} = this.getMemberAccessEmails(member.details)
|
|
13
15
|
|
|
16
|
+
console.log('emails', userEmails, parentAndUnverifiedEmails)
|
|
17
|
+
|
|
14
18
|
// Make sure all these users have access to the member
|
|
15
19
|
for (const email of userEmails) {
|
|
16
20
|
// Link users that are found with these email addresses.
|
|
@@ -18,8 +22,11 @@ export class MemberUserSyncerStatic {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
for (const email of parentAndUnverifiedEmails) {
|
|
25
|
+
if (userEmails.includes(email)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
// Link parents and unverified emails
|
|
22
|
-
|
|
23
30
|
// Now we add the responsibility permissions to the parent if there are no userEmails
|
|
24
31
|
await this.linkUser(email, member, userEmails.length > 0, userEmails.length > 0)
|
|
25
32
|
}
|
|
@@ -59,7 +66,7 @@ export class MemberUserSyncerStatic {
|
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
const unverifiedEmails: string[] = details.unverifiedEmails;
|
|
62
|
-
const parentAndUnverifiedEmails = details.parentsHaveAccess ? details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(unverifiedEmails) :
|
|
69
|
+
const parentAndUnverifiedEmails = details.parentsHaveAccess ? details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(unverifiedEmails) : details.unverifiedEmails
|
|
63
70
|
|
|
64
71
|
return {
|
|
65
72
|
userEmails,
|
|
@@ -163,6 +170,8 @@ export class MemberUserSyncerStatic {
|
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
async linkUser(email: string, member: MemberWithRegistrations, asParent: boolean, updateNameIfEqual = true) {
|
|
173
|
+
console.log('Linking user', email, 'to member', member.id, 'as parent', asParent, 'update name if equal', updateNameIfEqual)
|
|
174
|
+
|
|
166
175
|
let user = member.users.find(u => u.email.toLocaleLowerCase() === email.toLocaleLowerCase()) ?? await User.getForAuthentication(member.organizationId, email, {allowWithoutAccount: true})
|
|
167
176
|
|
|
168
177
|
if (user) {
|
|
@@ -9,6 +9,7 @@ import { registrationFilterCompilers } from "./registrations";
|
|
|
9
9
|
export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
10
10
|
...baseSQLFilterCompilers,
|
|
11
11
|
id: createSQLColumnFilterCompiler('id'),
|
|
12
|
+
memberNumber: createSQLColumnFilterCompiler('memberNumber'),
|
|
12
13
|
firstName: createSQLColumnFilterCompiler('firstName'),
|
|
13
14
|
lastName: createSQLColumnFilterCompiler('lastName'),
|
|
14
15
|
name: createSQLExpressionFilterCompiler(
|
|
@@ -49,6 +50,26 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
|
49
50
|
{isJSONValue: true, isJSONObject: true}
|
|
50
51
|
),
|
|
51
52
|
|
|
53
|
+
unverifiedEmail: createSQLExpressionFilterCompiler(
|
|
54
|
+
SQL.jsonValue(SQL.column('details'), '$.value.unverifiedEmails'),
|
|
55
|
+
{isJSONValue: true, isJSONObject: true}
|
|
56
|
+
),
|
|
57
|
+
|
|
58
|
+
phone: createSQLExpressionFilterCompiler(
|
|
59
|
+
SQL.jsonValue(SQL.column('details'), '$.value.phone'),
|
|
60
|
+
{isJSONValue: true}
|
|
61
|
+
),
|
|
62
|
+
|
|
63
|
+
parentPhone: createSQLExpressionFilterCompiler(
|
|
64
|
+
SQL.jsonValue(SQL.column('details'), '$.value.parents[*].phone'),
|
|
65
|
+
{isJSONValue: true, isJSONObject: true}
|
|
66
|
+
),
|
|
67
|
+
|
|
68
|
+
unverifiedPhone: createSQLExpressionFilterCompiler(
|
|
69
|
+
SQL.jsonValue(SQL.column('details'), '$.value.unverifiedPhones'),
|
|
70
|
+
{isJSONValue: true, isJSONObject: true}
|
|
71
|
+
),
|
|
72
|
+
|
|
52
73
|
registrations: createSQLRelationFilterCompiler(
|
|
53
74
|
SQL.select()
|
|
54
75
|
.from(
|