@stamhoofd/backend 2.71.0 → 2.73.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/index.ts +1 -0
- package/package.json +10 -10
- package/src/audit-logs/OrganizationLogger.ts +1 -1
- package/src/audit-logs/PlatformLogger.ts +1 -0
- package/src/email-recipient-loaders/orders.ts +1 -1
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +7 -7
- package/src/endpoints/auth/CreateTokenEndpoint.ts +11 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +26 -2
- package/src/endpoints/auth/PatchUserEndpoint.ts +24 -2
- package/src/endpoints/auth/SignupEndpoint.ts +1 -1
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +23 -3
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +3 -3
- package/src/endpoints/global/events/GetEventsEndpoint.ts +6 -6
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +36 -4
- package/src/endpoints/global/members/GetMembersEndpoint.ts +9 -7
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +24 -14
- package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +34 -0
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -1
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +20 -12
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +11 -3
- package/src/endpoints/global/sso/GetSSOEndpoint.ts +8 -1
- package/src/endpoints/global/sso/SetSSOEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +6 -6
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +51 -9
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +6 -6
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +6 -6
- package/src/excel-loaders/members.ts +8 -0
- package/src/excel-loaders/receivable-balances.ts +294 -0
- package/src/helpers/AdminPermissionChecker.ts +4 -3
- package/src/helpers/AuthenticatedStructures.ts +32 -6
- package/src/helpers/SetupStepUpdater.ts +10 -4
- package/src/helpers/xlsxAddressTransformerColumnFactory.ts +8 -0
- package/src/services/PaymentReallocationService.ts +3 -2
- package/src/services/PaymentService.ts +17 -1
- package/src/services/SSOService.ts +68 -4
- package/src/sql-filters/members.ts +20 -1
- package/src/sql-filters/organizations.ts +1 -0
- package/src/sql-filters/receivable-balances.ts +53 -1
- package/src/sql-sorters/organizations.ts +11 -0
package/index.ts
CHANGED
|
@@ -97,6 +97,7 @@ const start = async () => {
|
|
|
97
97
|
await import('./src/excel-loaders/members');
|
|
98
98
|
await import('./src/excel-loaders/payments');
|
|
99
99
|
await import('./src/excel-loaders/organizations');
|
|
100
|
+
await import('./src/excel-loaders/receivable-balances');
|
|
100
101
|
|
|
101
102
|
// Register Email Recipient loaders
|
|
102
103
|
await import('./src/email-recipient-loaders/members');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.73.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@simonbackx/simple-encoding": "2.19.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.15.0",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.73.0",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.73.0",
|
|
42
|
+
"@stamhoofd/email": "2.73.0",
|
|
43
|
+
"@stamhoofd/models": "2.73.0",
|
|
44
|
+
"@stamhoofd/queues": "2.73.0",
|
|
45
|
+
"@stamhoofd/sql": "2.73.0",
|
|
46
|
+
"@stamhoofd/structures": "2.73.0",
|
|
47
|
+
"@stamhoofd/utility": "2.73.0",
|
|
48
48
|
"archiver": "^7.0.1",
|
|
49
49
|
"aws-sdk": "^2.885.0",
|
|
50
50
|
"axios": "1.6.8",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "fee6c6f01c19d15010807f2bac6a3ab5ee14019e"
|
|
68
68
|
}
|
|
@@ -3,7 +3,7 @@ import { AuditLogType } from '@stamhoofd/structures';
|
|
|
3
3
|
import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
4
4
|
|
|
5
5
|
export const OrganizationLogger = new ModelLogger(Organization, {
|
|
6
|
-
skipKeys: ['searchIndex'],
|
|
6
|
+
skipKeys: ['searchIndex', 'serverMeta'],
|
|
7
7
|
optionsGenerator: getDefaultGenerator({
|
|
8
8
|
created: AuditLogType.OrganizationAdded,
|
|
9
9
|
updated: AuditLogType.OrganizationEdited,
|
|
@@ -3,6 +3,7 @@ import { getDefaultGenerator, ModelLogger } from './ModelLogger';
|
|
|
3
3
|
import { AuditLogType } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
export const PlatformLogger = new ModelLogger(Platform, {
|
|
6
|
+
skipKeys: ['serverConfig'],
|
|
6
7
|
optionsGenerator: getDefaultGenerator({
|
|
7
8
|
updated: AuditLogType.PlatformSettingsChanged,
|
|
8
9
|
}),
|
|
@@ -28,7 +28,7 @@ export class GetOrganizationsCountEndpoint extends Endpoint<Params, Query, Body,
|
|
|
28
28
|
|
|
29
29
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
30
|
await Context.authenticate();
|
|
31
|
-
const query = GetOrganizationsEndpoint.buildQuery(request.query);
|
|
31
|
+
const query = await GetOrganizationsEndpoint.buildQuery(request.query);
|
|
32
32
|
|
|
33
33
|
const count = await query
|
|
34
34
|
.count();
|
|
@@ -5,11 +5,11 @@ import { Organization } from '@stamhoofd/models';
|
|
|
5
5
|
import { SQL, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
|
|
6
6
|
import { CountFilteredRequest, LimitedFilteredRequest, Organization as OrganizationStruct, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
7
7
|
|
|
8
|
+
import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
8
9
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
9
10
|
import { Context } from '../../../helpers/Context';
|
|
10
11
|
import { organizationFilterCompilers } from '../../../sql-filters/organizations';
|
|
11
12
|
import { organizationSorters } from '../../../sql-sorters/organizations';
|
|
12
|
-
import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
13
13
|
|
|
14
14
|
type Params = Record<string, never>;
|
|
15
15
|
type Query = LimitedFilteredRequest;
|
|
@@ -35,7 +35,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
35
35
|
return [false];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
static buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
38
|
+
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
39
39
|
const tags = Context.auth.getPlatformAccessibleOrganizationTags(PermissionLevel.Read);
|
|
40
40
|
if (tags !== 'all' && tags.length === 0) {
|
|
41
41
|
throw Context.auth.error();
|
|
@@ -62,11 +62,11 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
if (scopeFilter) {
|
|
65
|
-
query.where(compileToSQLFilter(scopeFilter, filterCompilers));
|
|
65
|
+
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (q.filter) {
|
|
69
|
-
query.where(compileToSQLFilter(q.filter, filterCompilers));
|
|
69
|
+
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (q.search) {
|
|
@@ -80,13 +80,13 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
if (searchFilter) {
|
|
83
|
-
query.where(compileToSQLFilter(searchFilter, filterCompilers));
|
|
83
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
if (q instanceof LimitedFilteredRequest) {
|
|
88
88
|
if (q.pageFilter) {
|
|
89
|
-
query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
89
|
+
query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
@@ -116,7 +116,7 @@ export class GetOrganizationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
const query = GetOrganizationsEndpoint.buildQuery(requestQuery);
|
|
119
|
+
const query = await GetOrganizationsEndpoint.buildQuery(requestQuery);
|
|
120
120
|
let data: SQLResultNamespacedRow[];
|
|
121
121
|
|
|
122
122
|
try {
|
|
@@ -86,7 +86,8 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
86
86
|
|
|
87
87
|
if (STAMHOOFD.userMode === 'platform') {
|
|
88
88
|
const platform = await Platform.getShared();
|
|
89
|
-
|
|
89
|
+
const config = platform.config.loginMethods.get(LoginMethod.Password);
|
|
90
|
+
if (!config) {
|
|
90
91
|
throw new SimpleError({
|
|
91
92
|
code: 'not_supported',
|
|
92
93
|
message: 'This platform does not support password login',
|
|
@@ -94,6 +95,15 @@ export class CreateTokenEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
94
95
|
statusCode: 400,
|
|
95
96
|
});
|
|
96
97
|
}
|
|
98
|
+
|
|
99
|
+
if (!config.isEnabledForEmail(request.body.username)) {
|
|
100
|
+
throw new SimpleError({
|
|
101
|
+
code: 'not_supported',
|
|
102
|
+
message: 'Login method not supported',
|
|
103
|
+
human: 'Je kan op dit account niet inloggen met een wachtwoord. Gebruik een andere methode om in te loggen.',
|
|
104
|
+
statusCode: 400,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
const user = await User.login(organization?.id ?? null, request.body.username, request.body.password);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
-
import { PasswordToken, sendEmailTemplate, User } from '@stamhoofd/models';
|
|
4
|
-
import { EmailTemplateType, ForgotPasswordRequest, Recipient, Replacement } from '@stamhoofd/structures';
|
|
3
|
+
import { PasswordToken, Platform, sendEmailTemplate, User } from '@stamhoofd/models';
|
|
4
|
+
import { EmailTemplateType, ForgotPasswordRequest, LoginMethod, Recipient, Replacement } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
7
|
import { Context } from '../../helpers/Context';
|
|
7
8
|
|
|
8
9
|
type Params = Record<string, never>;
|
|
@@ -28,6 +29,29 @@ export class ForgotPasswordEndpoint extends Endpoint<Params, Query, Body, Respon
|
|
|
28
29
|
|
|
29
30
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
30
31
|
const organization = await Context.setOptionalOrganizationScope();
|
|
32
|
+
|
|
33
|
+
if (STAMHOOFD.userMode === 'platform') {
|
|
34
|
+
const platform = await Platform.getShared();
|
|
35
|
+
const config = platform.config.loginMethods.get(LoginMethod.Password);
|
|
36
|
+
if (!config) {
|
|
37
|
+
throw new SimpleError({
|
|
38
|
+
code: 'not_supported',
|
|
39
|
+
message: 'This platform does not support password login',
|
|
40
|
+
human: 'Dit platform ondersteunt geen wachtwoord login',
|
|
41
|
+
statusCode: 400,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!config.isEnabledForEmail(request.body.email)) {
|
|
46
|
+
throw new SimpleError({
|
|
47
|
+
code: 'not_supported',
|
|
48
|
+
message: 'Login method not supported',
|
|
49
|
+
human: 'Je kan op dit account geen wachtwoord gebruiken om in te loggen.',
|
|
50
|
+
statusCode: 400,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
31
55
|
const user = await User.getForAuthentication(organization?.id ?? null, request.body.email, { allowWithoutAccount: true });
|
|
32
56
|
|
|
33
57
|
if (!user) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, isPatch } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
|
-
import { EmailVerificationCode, Member, PasswordToken, Token, User } from '@stamhoofd/models';
|
|
5
|
-
import { NewUser, PermissionLevel, SignupResponse, UserPermissions, UserWithMembers } from '@stamhoofd/structures';
|
|
4
|
+
import { EmailVerificationCode, Member, PasswordToken, Platform, Token, User } from '@stamhoofd/models';
|
|
5
|
+
import { LoginMethod, NewUser, PermissionLevel, SignupResponse, UserPermissions, UserWithMembers } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
7
|
import { Context } from '../../helpers/Context';
|
|
8
8
|
import { MemberUserSyncer } from '../../helpers/MemberUserSyncer';
|
|
@@ -135,6 +135,28 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
if (editUser.id === user.id && request.body.password) {
|
|
138
|
+
if (STAMHOOFD.userMode === 'platform') {
|
|
139
|
+
const platform = await Platform.getShared();
|
|
140
|
+
const config = platform.config.loginMethods.get(LoginMethod.Password);
|
|
141
|
+
if (!config) {
|
|
142
|
+
throw new SimpleError({
|
|
143
|
+
code: 'not_supported',
|
|
144
|
+
message: 'This platform does not support password login',
|
|
145
|
+
human: 'Dit platform ondersteunt geen wachtwoord login',
|
|
146
|
+
statusCode: 400,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!config.isEnabledForEmail(editUser.email)) {
|
|
151
|
+
throw new SimpleError({
|
|
152
|
+
code: 'not_supported',
|
|
153
|
+
message: 'Login method not supported',
|
|
154
|
+
human: 'Je kan op dit account geen wachtwoord gebruiken om in te loggen.',
|
|
155
|
+
statusCode: 400,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
138
160
|
// password changes
|
|
139
161
|
await editUser.changePassword(request.body.password);
|
|
140
162
|
await PasswordToken.clearFor(editUser.id);
|
|
@@ -32,7 +32,7 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
|
|
|
32
32
|
|
|
33
33
|
if (STAMHOOFD.userMode === 'platform') {
|
|
34
34
|
const platform = await Platform.getShared();
|
|
35
|
-
if (!platform.config.loginMethods.
|
|
35
|
+
if (!platform.config.loginMethods.has(LoginMethod.Password)) {
|
|
36
36
|
throw new SimpleError({
|
|
37
37
|
code: 'not_supported',
|
|
38
38
|
message: 'This platform does not support password login',
|
|
@@ -31,8 +31,28 @@ export class SearchRegionsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
31
31
|
|
|
32
32
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
33
33
|
// Escape query
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const rawQuery = request.query.query.replace(/([-+><()~*"@\s]+)/g, ' ');
|
|
35
|
+
const words = rawQuery.split(' ').filter(w => w.length > 0);
|
|
36
|
+
|
|
37
|
+
// Escape words
|
|
38
|
+
const cleanedWords: string[] = [];
|
|
39
|
+
for (const [index, word] of words.entries()) {
|
|
40
|
+
// If contains special char (non a-zA-Z) - escape with " character
|
|
41
|
+
if (/^[a-zA-Z0-9]*$/.test(word)) {
|
|
42
|
+
if (index === words.length - 1) {
|
|
43
|
+
cleanedWords.push('+' + word + '*');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
cleanedWords.push('+' + word);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
cleanedWords.push('+"' + word + '"');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const query = cleanedWords.join(' ');
|
|
54
|
+
|
|
55
|
+
if (query.length === 0) {
|
|
36
56
|
// Do not try searching...
|
|
37
57
|
return new Response(SearchRegions.create({
|
|
38
58
|
cities: [],
|
|
@@ -43,7 +63,7 @@ export class SearchRegionsEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
43
63
|
|
|
44
64
|
const match = {
|
|
45
65
|
sign: 'MATCH',
|
|
46
|
-
value: query
|
|
66
|
+
value: query, // We replace special operators in boolean mode with spaces since special characters aren't indexed anyway
|
|
47
67
|
mode: 'BOOLEAN',
|
|
48
68
|
};
|
|
49
69
|
|
|
@@ -61,11 +61,11 @@ export class GetAuditLogsEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
if (scopeFilter) {
|
|
64
|
-
query.where(compileToSQLFilter(scopeFilter, filterCompilers));
|
|
64
|
+
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
if (q.filter) {
|
|
68
|
-
query.where(compileToSQLFilter(q.filter, filterCompilers));
|
|
68
|
+
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
if (q.search) {
|
|
@@ -78,7 +78,7 @@ export class GetAuditLogsEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
78
78
|
|
|
79
79
|
if (q instanceof LimitedFilteredRequest) {
|
|
80
80
|
if (q.pageFilter) {
|
|
81
|
-
query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
81
|
+
query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
@@ -34,7 +34,7 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
34
34
|
return [false];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
static buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
37
|
+
static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
|
|
38
38
|
const organization = Context.organization;
|
|
39
39
|
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
40
40
|
|
|
@@ -60,11 +60,11 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
if (scopeFilter) {
|
|
63
|
-
query.where(compileToSQLFilter(scopeFilter, filterCompilers));
|
|
63
|
+
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
if (q.filter) {
|
|
67
|
-
query.where(compileToSQLFilter(q.filter, filterCompilers));
|
|
67
|
+
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
if (q.search) {
|
|
@@ -78,13 +78,13 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
if (searchFilter) {
|
|
81
|
-
query.where(compileToSQLFilter(searchFilter, filterCompilers));
|
|
81
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
if (q instanceof LimitedFilteredRequest) {
|
|
86
86
|
if (q.pageFilter) {
|
|
87
|
-
query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
87
|
+
query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
@@ -96,7 +96,7 @@ export class GetEventsEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
static async buildData(requestQuery: LimitedFilteredRequest) {
|
|
99
|
-
const query = GetEventsEndpoint.buildQuery(requestQuery);
|
|
99
|
+
const query = await GetEventsEndpoint.buildQuery(requestQuery);
|
|
100
100
|
const data = await query.fetch();
|
|
101
101
|
|
|
102
102
|
const events = Event.fromRows(data, Event.table);
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, patchObject, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { Event, Group, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
4
|
-
import { Event as EventStruct,
|
|
4
|
+
import { AuditLogSource, Event as EventStruct, Group as GroupStruct, GroupType, NamedObject } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
6
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
7
|
import { SQL, SQLWhereSign } from '@stamhoofd/sql';
|
|
8
8
|
import { Formatter } from '@stamhoofd/utility';
|
|
9
9
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
10
10
|
import { Context } from '../../../helpers/Context';
|
|
11
|
-
import { PatchOrganizationRegistrationPeriodsEndpoint } from '../../organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
|
|
12
11
|
import { AuditLogService } from '../../../services/AuditLogService';
|
|
12
|
+
import { PatchOrganizationRegistrationPeriodsEndpoint } from '../../organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
|
|
13
13
|
|
|
14
14
|
type Params = { id: string };
|
|
15
15
|
type Query = undefined;
|
|
@@ -67,6 +67,22 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
if (event.meta.defaultAgeGroupIds && event.meta.defaultAgeGroupIds.length === 0) {
|
|
71
|
+
throw new SimpleError({
|
|
72
|
+
code: 'invalid_field',
|
|
73
|
+
message: 'Empty default age groups',
|
|
74
|
+
human: 'Kies minstens één standaard leeftijdsgroep',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (event.meta.organizationTagIds && event.meta.organizationTagIds.length === 0) {
|
|
79
|
+
throw new SimpleError({
|
|
80
|
+
code: 'invalid_field',
|
|
81
|
+
message: 'Empty organization tag ids',
|
|
82
|
+
human: 'Kies minstens één tag',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
const eventOrganization = await this.checkEventAccess(event);
|
|
71
87
|
event.id = put.id;
|
|
72
88
|
event.name = put.name;
|
|
@@ -85,7 +101,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
85
101
|
throw new SimpleError({
|
|
86
102
|
code: 'invalid_period',
|
|
87
103
|
message: 'No period found for this start date',
|
|
88
|
-
human:
|
|
104
|
+
human: Context.i18n.$t('5959a6a9-064a-413c-871f-c74a145ed569'),
|
|
89
105
|
field: 'startDate',
|
|
90
106
|
});
|
|
91
107
|
}
|
|
@@ -154,6 +170,22 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
154
170
|
});
|
|
155
171
|
}
|
|
156
172
|
|
|
173
|
+
if (event.meta.defaultAgeGroupIds && event.meta.defaultAgeGroupIds.length === 0) {
|
|
174
|
+
throw new SimpleError({
|
|
175
|
+
code: 'invalid_field',
|
|
176
|
+
message: 'Empty default age groups',
|
|
177
|
+
human: 'Kies minstens één standaard leeftijdsgroep',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (event.meta.organizationTagIds && event.meta.organizationTagIds.length === 0) {
|
|
182
|
+
throw new SimpleError({
|
|
183
|
+
code: 'invalid_field',
|
|
184
|
+
message: 'Empty organization tag ids',
|
|
185
|
+
human: 'Kies minstens één tag',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
157
189
|
const eventOrganization = await this.checkEventAccess(event);
|
|
158
190
|
if (eventOrganization) {
|
|
159
191
|
event.meta.organizationCache = NamedObject.create({ id: eventOrganization.id, name: eventOrganization.name });
|
|
@@ -210,7 +242,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
210
242
|
throw new SimpleError({
|
|
211
243
|
code: 'invalid_period',
|
|
212
244
|
message: 'No period found for this start date',
|
|
213
|
-
human:
|
|
245
|
+
human: Context.i18n.$t('5959a6a9-064a-413c-871f-c74a145ed569'),
|
|
214
246
|
field: 'startDate',
|
|
215
247
|
});
|
|
216
248
|
}
|
|
@@ -3,15 +3,15 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
|
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
4
|
import { Member, Platform } from '@stamhoofd/models';
|
|
5
5
|
import { SQL, compileToSQLFilter, compileToSQLSorter } from '@stamhoofd/sql';
|
|
6
|
-
import { CountFilteredRequest, Country, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
6
|
+
import { CountFilteredRequest, Country, CountryCode, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort, getSortFilter } from '@stamhoofd/structures';
|
|
7
7
|
import { DataValidator } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
|
+
import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
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';
|
|
13
14
|
import { memberSorters } from '../../../sql-sorters/members';
|
|
14
|
-
import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
|
|
15
15
|
|
|
16
16
|
type Params = Record<string, never>;
|
|
17
17
|
type Query = LimitedFilteredRequest;
|
|
@@ -127,11 +127,11 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
127
127
|
);
|
|
128
128
|
|
|
129
129
|
if (scopeFilter) {
|
|
130
|
-
query.where(compileToSQLFilter(scopeFilter, filterCompilers));
|
|
130
|
+
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
if (q.filter) {
|
|
134
|
-
query.where(compileToSQLFilter(q.filter, filterCompilers));
|
|
134
|
+
query.where(await compileToSQLFilter(q.filter, filterCompilers));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
if (q.search) {
|
|
@@ -141,7 +141,9 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
141
141
|
if (!searchFilter && q.search.match(/^\+?[0-9\s-]+$/)) {
|
|
142
142
|
// Try to format as phone so we have 1:1 space matches
|
|
143
143
|
try {
|
|
144
|
-
const
|
|
144
|
+
const country = (Context.i18n.country as CountryCode) || Country.Belgium;
|
|
145
|
+
|
|
146
|
+
const phoneNumber = parsePhoneNumber(q.search, country);
|
|
145
147
|
if (phoneNumber && phoneNumber.isValid()) {
|
|
146
148
|
const formatted = phoneNumber.formatInternational();
|
|
147
149
|
searchFilter = {
|
|
@@ -219,13 +221,13 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
219
221
|
// todo: Address search detection
|
|
220
222
|
|
|
221
223
|
if (searchFilter) {
|
|
222
|
-
query.where(compileToSQLFilter(searchFilter, filterCompilers));
|
|
224
|
+
query.where(await compileToSQLFilter(searchFilter, filterCompilers));
|
|
223
225
|
}
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
if (q instanceof LimitedFilteredRequest) {
|
|
227
229
|
if (q.pageFilter) {
|
|
228
|
-
query.where(compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
230
|
+
query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
q.sort = assertSort(q.sort, [{ key: 'id' }]);
|
|
@@ -16,6 +16,7 @@ import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
|
|
|
16
16
|
import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
|
|
17
17
|
import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
|
|
18
18
|
import { RegistrationService } from '../../../services/RegistrationService';
|
|
19
|
+
import { shouldCheckIfMemberIsDuplicateForPatch, shouldCheckIfMemberIsDuplicateForPut } from './shouldCheckIfMemberIsDuplicate';
|
|
19
20
|
|
|
20
21
|
type Params = Record<string, never>;
|
|
21
22
|
type Query = undefined;
|
|
@@ -107,10 +108,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
107
108
|
struct.details.cleanData();
|
|
108
109
|
member.details = struct.details;
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
if (shouldCheckIfMemberIsDuplicateForPut(struct)) {
|
|
112
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
|
|
113
|
+
if (duplicate) {
|
|
112
114
|
// Merge data
|
|
113
|
-
|
|
115
|
+
member = duplicate;
|
|
116
|
+
}
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
// We risk creating a new member without being able to access it manually afterwards
|
|
@@ -166,6 +169,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
166
169
|
patch = await Context.auth.filterMemberPatch(member, patch);
|
|
167
170
|
const originalDetails = member.details.clone();
|
|
168
171
|
|
|
172
|
+
let shouldCheckDuplicate = false;
|
|
173
|
+
|
|
169
174
|
if (patch.details) {
|
|
170
175
|
if (patch.details.isPut()) {
|
|
171
176
|
throw new SimpleError({
|
|
@@ -176,6 +181,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
176
181
|
});
|
|
177
182
|
}
|
|
178
183
|
|
|
184
|
+
shouldCheckDuplicate = shouldCheckIfMemberIsDuplicateForPatch(patch, originalDetails);
|
|
185
|
+
|
|
179
186
|
const wasReduced = member.details.shouldApplyReducedPrice;
|
|
180
187
|
member.details.patchOrPut(patch.details);
|
|
181
188
|
member.details.cleanData();
|
|
@@ -185,17 +192,20 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Remove the member from the list
|
|
191
|
-
const iii = members.findIndex(m => m.id === member.id);
|
|
192
|
-
if (iii !== -1) {
|
|
193
|
-
members.splice(iii, 1);
|
|
194
|
-
}
|
|
195
|
+
if (shouldCheckDuplicate) {
|
|
196
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
if (duplicate) {
|
|
199
|
+
// Remove the member from the list
|
|
200
|
+
const iii = members.findIndex(m => m.id === member.id);
|
|
201
|
+
if (iii !== -1) {
|
|
202
|
+
members.splice(iii, 1);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add new
|
|
206
|
+
members.push(duplicate);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
199
209
|
}
|
|
200
210
|
|
|
201
211
|
await member.save();
|
|
@@ -315,7 +325,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
315
325
|
throw new SimpleError({
|
|
316
326
|
code: 'invalid_field',
|
|
317
327
|
message: 'Invalid organization',
|
|
318
|
-
human: '
|
|
328
|
+
human: Context.i18n.$t('d41cdbe3-57e3-4a2e-83bc-cb9e65c9c840'),
|
|
319
329
|
});
|
|
320
330
|
}
|
|
321
331
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { MemberDetails, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
|
|
3
|
+
|
|
4
|
+
export function shouldCheckIfMemberIsDuplicateForPatch(patch: { details: MemberDetails | AutoEncoderPatchType<MemberDetails> | undefined }, originalDetails: MemberDetails): boolean {
|
|
5
|
+
if (patch.details === undefined) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
// has long first name
|
|
11
|
+
((patch.details.firstName !== undefined && patch.details.firstName.length > 3) || (patch.details.firstName === undefined && originalDetails.firstName.length > 3))
|
|
12
|
+
// or has long last name
|
|
13
|
+
|| ((patch.details.lastName !== undefined && patch.details.lastName.length > 3) || (patch.details.lastName === undefined && originalDetails.lastName.length > 3))
|
|
14
|
+
)
|
|
15
|
+
// has name change or birthday change
|
|
16
|
+
&& (
|
|
17
|
+
// has first name change
|
|
18
|
+
(patch.details.firstName !== undefined && patch.details.firstName !== originalDetails.firstName)
|
|
19
|
+
// has last name change
|
|
20
|
+
|| (patch.details.lastName !== undefined && patch.details.lastName !== originalDetails.lastName)
|
|
21
|
+
// has birth day change
|
|
22
|
+
|| (patch.details.birthDay !== undefined && patch.details.birthDay?.getTime() !== originalDetails.birthDay?.getTime())
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function shouldCheckIfMemberIsDuplicateForPut(put: MemberWithRegistrationsBlob): boolean {
|
|
27
|
+
if (put.details.firstName.length <= 3 && put.details.lastName.length <= 3) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const age = put.details.age;
|
|
32
|
+
// do not check if member is duplicate for historical members
|
|
33
|
+
return age !== null && age < 81;
|
|
34
|
+
}
|