@stamhoofd/backend 2.137.4 → 2.138.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 +26 -17
- package/src/boot.ts +5 -0
- package/src/crons/balance-emails.ts +2 -1
- package/src/crons/delete-expired-mfa-tokens.ts +35 -0
- package/src/crons/drip-emails.ts +2 -1
- package/src/crons/index.ts +1 -0
- package/src/crons/invoices.ts +5 -3
- package/src/crons.ts +4 -2
- package/src/email-recipient-loaders/orders.ts +2 -1
- package/src/email-recipient-loaders/organizations.ts +3 -2
- package/src/email-recipient-loaders/payments.ts +3 -2
- package/src/endpoints/auth/ConfirmTOTPEndpoint.ts +84 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -2
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +61 -1
- package/src/endpoints/auth/CreateTokenEndpoint.ts +135 -7
- package/src/endpoints/auth/DeletePasskeyEndpoint.ts +61 -0
- package/src/endpoints/auth/DeleteTOTPEndpoint.ts +62 -0
- package/src/endpoints/auth/DeleteUserEndpoint.ts +1 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +4 -2
- package/src/endpoints/auth/GetMFAChallengeEndpoint.ts +56 -0
- package/src/endpoints/auth/GetMFAStatusEndpoint.ts +31 -0
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -1
- package/src/endpoints/auth/MFA.security.test.ts +688 -0
- package/src/endpoints/auth/MFA.test.ts +1398 -0
- package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +2 -2
- package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
- package/src/endpoints/auth/RegenerateRecoveryCodesEndpoint.ts +51 -0
- package/src/endpoints/auth/RegisterPasskeyEndpoint.ts +99 -0
- package/src/endpoints/auth/RegisterPasskeyOptionsEndpoint.ts +49 -0
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
- package/src/endpoints/auth/SetupTOTPEndpoint.ts +54 -0
- package/src/endpoints/auth/SignupEndpoint.ts +17 -4
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +16 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +5 -3
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +5 -3
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +2 -1
- package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -1
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +11 -6
- package/src/endpoints/global/files/UploadFile.test.ts +206 -0
- package/src/endpoints/global/files/UploadFile.ts +31 -6
- package/src/endpoints/global/files/UploadImage.test.ts +177 -0
- package/src/endpoints/global/files/UploadImage.ts +23 -4
- package/src/endpoints/global/files/upload-security.test.ts +837 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.ts +2 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +4 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +35 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +8 -0
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +92 -0
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +32 -3
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +100 -0
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +5 -5
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +160 -0
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.ts +44 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
- package/src/excel-loaders/balance-items.ts +1 -1
- package/src/excel-loaders/event-notifications.ts +6 -7
- package/src/excel-loaders/members.test.ts +2 -2
- package/src/excel-loaders/members.ts +9 -10
- package/src/excel-loaders/organizations.ts +14 -20
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/platform-memberships.ts +7 -7
- package/src/excel-loaders/platform-sheets.test.ts +113 -0
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +9 -9
- package/src/helpers/AdminPermissionChecker.ts +1 -1
- package/src/helpers/AuthenticatedStructures.ts +21 -10
- package/src/helpers/Context.ts +114 -4
- package/src/helpers/EmailBuilder.test.ts +287 -0
- package/src/helpers/EmailBuilder.ts +811 -0
- package/src/helpers/EmailResumer.ts +2 -1
- package/src/helpers/ForwardHandler.ts +2 -1
- package/src/helpers/MFAEncryption.ts +34 -0
- package/src/helpers/MembershipCharger.ts +3 -2
- package/src/helpers/RecoveryCodeHelper.ts +81 -0
- package/src/helpers/TOTPHelper.ts +79 -0
- package/src/helpers/TenantContext.test.ts +96 -0
- package/src/helpers/TenantContext.ts +67 -0
- package/src/helpers/TwoFactorHelper.ts +356 -0
- package/src/helpers/WebauthnHelper.ts +211 -0
- package/src/helpers/data/aaguids.json +1006 -0
- package/src/middleware/TenantScopeMiddleware.test.ts +41 -0
- package/src/middleware/TenantScopeMiddleware.ts +24 -0
- package/src/seeds/1785417302-fill-user-last-active-at.sql +6 -0
- package/src/services/AdminSessionService.ts +41 -0
- package/src/services/BalanceItemService.ts +2 -1
- package/src/services/DocumentRenderService.test.ts +229 -0
- package/src/services/DocumentRenderService.ts +180 -0
- package/src/services/EmailPreviewService.test.ts +300 -0
- package/src/services/EmailPreviewService.ts +239 -0
- package/src/services/EmailSendService.test.ts +1218 -0
- package/src/services/EmailSendService.ts +705 -0
- package/src/services/EventNotificationService.ts +2 -1
- package/src/services/FileSignService.test.ts +85 -0
- package/src/services/FileSignService.ts +23 -1
- package/src/services/InvoicePdfService.ts +3 -3
- package/src/services/InvoiceService.ts +4 -2
- package/src/services/InvoiceXMLService.ts +2 -1
- package/src/services/OrderService.test.ts +308 -0
- package/src/services/OrderService.ts +215 -0
- package/src/services/OrganizationAdminService.test.ts +47 -0
- package/src/services/OrganizationAdminService.ts +139 -0
- package/src/services/OrganizationDNSService.test.ts +177 -0
- package/src/services/OrganizationDNSService.ts +282 -0
- package/src/services/OrganizationEmailService.test.ts +57 -0
- package/src/services/OrganizationEmailService.ts +212 -0
- package/src/services/PasswordForgotService.test.ts +99 -0
- package/src/services/PasswordForgotService.ts +38 -0
- package/src/services/PaymentService.ts +5 -3
- package/src/services/PlatformMembershipService.test.ts +180 -0
- package/src/services/PlatformMembershipService.ts +281 -4
- package/src/services/ReferralService.ts +3 -2
- package/src/services/RegistrationService.ts +4 -2
- package/src/services/SSOService.ts +106 -14
- package/src/services/STPackageService.test.ts +191 -0
- package/src/services/STPackageService.ts +115 -1
- package/src/services/TwoFactorAuditLogService.ts +64 -0
- package/src/services/VerificationCodeService.test.ts +71 -0
- package/src/services/VerificationCodeService.ts +101 -0
- package/tests/e2e/api-rate-limits.test.ts +1 -1
- package/tests/e2e/documents.test.ts +2 -1
- package/tests/e2e/private-files.test.ts +77 -14
- package/tests/helpers/MFATestHelper.ts +42 -0
- package/tests/helpers/TestServer.ts +4 -0
- package/tests/helpers/index.ts +1 -0
- package/tsconfig.build.json +2 -1
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import type { Organization, User } from '@stamhoofd/models';
|
|
3
|
+
import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
import { PermissionLevel, Permissions, UserPermissions } from '@stamhoofd/structures';
|
|
5
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
6
|
+
|
|
7
|
+
import { testServer } from '../../../../../tests/helpers/TestServer.js';
|
|
8
|
+
import { AdminSessionService } from '../../../../services/AdminSessionService.js';
|
|
9
|
+
import { SignOutOrganizationAdminsEndpoint } from './SignOutOrganizationAdminsEndpoint.js';
|
|
10
|
+
|
|
11
|
+
describe('Endpoint.SignOutOrganizationAdmins', () => {
|
|
12
|
+
const endpoint = new SignOutOrganizationAdminsEndpoint();
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
TestUtils.setEnvironment('userMode', 'platform');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const signOut = async (organization: Organization, token: Token) => {
|
|
19
|
+
const request = Request.buildJson('POST', '/organization/admins/sign-out', organization.getApiHost());
|
|
20
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
21
|
+
return await testServer.test(endpoint, request);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* In platform mode users are not owned by an organization: they have permissions for it.
|
|
26
|
+
*/
|
|
27
|
+
const createAdmin = async (organization: Organization, level = PermissionLevel.Full) => {
|
|
28
|
+
const user = await new UserFactory({
|
|
29
|
+
organization,
|
|
30
|
+
permissions: Permissions.create({ level }),
|
|
31
|
+
}).create();
|
|
32
|
+
user.organizationId = null;
|
|
33
|
+
await user.save();
|
|
34
|
+
return user;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const tokenExists = async (token: Token) => {
|
|
38
|
+
return !!(await Token.getByAccessToken(token.accessToken, true));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
test('all sessions of all admins are signed out, except the one making the request', async () => {
|
|
42
|
+
const organization = await new OrganizationFactory({}).create();
|
|
43
|
+
const otherOrganization = await new OrganizationFactory({}).create();
|
|
44
|
+
|
|
45
|
+
const admin = await createAdmin(organization);
|
|
46
|
+
const otherAdmin = await createAdmin(organization);
|
|
47
|
+
const otherOrganizationAdmin = await createAdmin(otherOrganization);
|
|
48
|
+
const member = await new UserFactory({ organization }).create();
|
|
49
|
+
|
|
50
|
+
const currentToken = await Token.createToken(admin, new Date());
|
|
51
|
+
const secondDeviceOfAdmin = await Token.createToken(admin, new Date());
|
|
52
|
+
const otherAdminToken = await Token.createToken(otherAdmin, new Date());
|
|
53
|
+
const otherOrganizationAdminToken = await Token.createToken(otherOrganizationAdmin, new Date());
|
|
54
|
+
const memberToken = await Token.createToken(member, new Date());
|
|
55
|
+
|
|
56
|
+
const response = await signOut(organization, currentToken);
|
|
57
|
+
|
|
58
|
+
// The other device of the admin is signed out too: only this session is spared
|
|
59
|
+
expect(response.body.count).toBe(2);
|
|
60
|
+
expect(await tokenExists(currentToken)).toBe(true);
|
|
61
|
+
expect(await tokenExists(secondDeviceOfAdmin)).toBe(false);
|
|
62
|
+
expect(await tokenExists(otherAdminToken)).toBe(false);
|
|
63
|
+
|
|
64
|
+
// Users without permissions in this organization are not affected
|
|
65
|
+
expect(await tokenExists(otherOrganizationAdminToken)).toBe(true);
|
|
66
|
+
expect(await tokenExists(memberToken)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('an admin without full access cannot sign out the other admins', async () => {
|
|
70
|
+
const organization = await new OrganizationFactory({}).create();
|
|
71
|
+
|
|
72
|
+
const admin = await createAdmin(organization, PermissionLevel.Write);
|
|
73
|
+
const otherAdmin = await createAdmin(organization);
|
|
74
|
+
const otherAdminToken = await Token.createToken(otherAdmin, new Date());
|
|
75
|
+
|
|
76
|
+
await expect(signOut(organization, await Token.createToken(admin, new Date()))).rejects.toThrow(STExpect.simpleError({
|
|
77
|
+
code: 'permission_denied',
|
|
78
|
+
}));
|
|
79
|
+
expect(await tokenExists(otherAdminToken)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('an api user cannot sign out the admins', async () => {
|
|
83
|
+
const organization = await new OrganizationFactory({}).create();
|
|
84
|
+
|
|
85
|
+
const apiUser = await new UserFactory({ organization, apiUser: true }).create();
|
|
86
|
+
apiUser.permissions = UserPermissions.create({
|
|
87
|
+
organizationPermissions: new Map([[organization.id, Permissions.create({ level: PermissionLevel.Full })]]),
|
|
88
|
+
});
|
|
89
|
+
await apiUser.save();
|
|
90
|
+
|
|
91
|
+
const otherAdmin = await createAdmin(organization);
|
|
92
|
+
const otherAdminToken = await Token.createToken(otherAdmin, new Date());
|
|
93
|
+
|
|
94
|
+
await expect(signOut(organization, await Token.createApiToken(apiUser))).rejects.toThrow(STExpect.simpleError({
|
|
95
|
+
code: 'permission_denied',
|
|
96
|
+
}));
|
|
97
|
+
expect(await tokenExists(otherAdminToken)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('an organization without admins is a no-op', async () => {
|
|
101
|
+
const organization = await new OrganizationFactory({}).create();
|
|
102
|
+
const member = await new UserFactory({ organization }).create();
|
|
103
|
+
const memberToken = await Token.createToken(member, new Date());
|
|
104
|
+
|
|
105
|
+
// Not reachable through the endpoint (the caller is an admin themselves), but the
|
|
106
|
+
// empty list of users may not turn into a query that deletes everything.
|
|
107
|
+
expect(await AdminSessionService.signOutOrganizationAdmins(organization.id, null)).toBe(0);
|
|
108
|
+
expect(await tokenExists(memberToken)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('userMode organization', () => {
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
TestUtils.setEnvironment('userMode', 'organization');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('all sessions of all admins are signed out, except the one making the request', async () => {
|
|
117
|
+
const organization = await new OrganizationFactory({}).create();
|
|
118
|
+
|
|
119
|
+
const admin: User = await new UserFactory({
|
|
120
|
+
organization,
|
|
121
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
122
|
+
}).create();
|
|
123
|
+
const otherAdmin: User = await new UserFactory({
|
|
124
|
+
organization,
|
|
125
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
126
|
+
}).create();
|
|
127
|
+
const member = await new UserFactory({ organization }).create();
|
|
128
|
+
|
|
129
|
+
const otherAdminToken = await Token.createToken(otherAdmin, new Date());
|
|
130
|
+
const memberToken = await Token.createToken(member, new Date());
|
|
131
|
+
|
|
132
|
+
const response = await signOut(organization, await Token.createToken(admin, new Date()));
|
|
133
|
+
|
|
134
|
+
expect(response.body.count).toBe(1);
|
|
135
|
+
expect(await tokenExists(otherAdminToken)).toBe(false);
|
|
136
|
+
expect(await tokenExists(memberToken)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('api users keep their token, because they cannot complete a login', async () => {
|
|
140
|
+
const organization = await new OrganizationFactory({}).create();
|
|
141
|
+
|
|
142
|
+
const admin: User = await new UserFactory({
|
|
143
|
+
organization,
|
|
144
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
145
|
+
}).create();
|
|
146
|
+
|
|
147
|
+
const apiUser = await new UserFactory({ organization, apiUser: true }).create();
|
|
148
|
+
apiUser.permissions = UserPermissions.create({
|
|
149
|
+
organizationPermissions: new Map([[organization.id, Permissions.create({ level: PermissionLevel.Full })]]),
|
|
150
|
+
});
|
|
151
|
+
await apiUser.save();
|
|
152
|
+
const apiToken = await Token.createApiToken(apiUser);
|
|
153
|
+
|
|
154
|
+
const response = await signOut(organization, await Token.createToken(admin, new Date()));
|
|
155
|
+
|
|
156
|
+
expect(response.body.count).toBe(0);
|
|
157
|
+
expect(await tokenExists(apiToken)).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { CountResponse } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
import { Context } from '../../../../helpers/Context.js';
|
|
6
|
+
import { AdminSessionService } from '../../../../services/AdminSessionService.js';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, never>;
|
|
9
|
+
type Query = undefined;
|
|
10
|
+
type Body = undefined;
|
|
11
|
+
type ResponseBody = CountResponse;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* End the sessions of all administrators of this organization, so they have to sign in
|
|
15
|
+
* again. Used after making two-factor authentication required, which is only enforced on
|
|
16
|
+
* new logins.
|
|
17
|
+
*/
|
|
18
|
+
export class SignOutOrganizationAdminsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
19
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
20
|
+
if (request.method !== 'POST') {
|
|
21
|
+
return [false];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const params = Endpoint.parseParameters(request.url, '/organization/admins/sign-out', {});
|
|
25
|
+
|
|
26
|
+
if (params) {
|
|
27
|
+
return [true, params as Params];
|
|
28
|
+
}
|
|
29
|
+
return [false];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async handle(_: DecodedRequest<Params, Query, Body>) {
|
|
33
|
+
const organization = await Context.setOrganizationScope();
|
|
34
|
+
const { token } = await Context.authenticate();
|
|
35
|
+
|
|
36
|
+
if (!await Context.auth.canManageAdmins(organization.id)) {
|
|
37
|
+
throw Context.auth.error();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const count = await AdminSessionService.signOutOrganizationAdmins(organization.id, token.accessToken);
|
|
41
|
+
|
|
42
|
+
return new Response(CountResponse.create({ count }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -10,6 +10,7 @@ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceIt
|
|
|
10
10
|
import { Context } from '../../../../helpers/Context.js';
|
|
11
11
|
import { ServiceFeeHelper } from '../../../../helpers/ServiceFeeHelper.js';
|
|
12
12
|
import { AuditLogService } from '../../../../services/AuditLogService.js';
|
|
13
|
+
import { OrderService } from '../../../../services/OrderService.js';
|
|
13
14
|
import { PaymentService } from '../../../../services/PaymentService.js';
|
|
14
15
|
import { shouldReserveUitpasNumbers, UitpasService } from '../../../../services/uitpas/UitpasService.js';
|
|
15
16
|
|
|
@@ -151,7 +152,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
151
152
|
order.data.paymentMethod = PaymentMethod.Unknown;
|
|
152
153
|
|
|
153
154
|
// Mark this order as paid
|
|
154
|
-
await
|
|
155
|
+
await OrderService.markPaid(order, null, organization, webshop);
|
|
155
156
|
await order.save();
|
|
156
157
|
} else {
|
|
157
158
|
const payment = new Payment();
|
|
@@ -217,12 +218,12 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
217
218
|
await balanceItemPayment.save();
|
|
218
219
|
|
|
219
220
|
if (payment.method === PaymentMethod.Transfer) {
|
|
220
|
-
await
|
|
221
|
+
await OrderService.markValid(order, payment, []);
|
|
221
222
|
await payment.save();
|
|
222
223
|
await order.save();
|
|
223
224
|
} else if (payment.method === PaymentMethod.PointOfSale) {
|
|
224
225
|
// Not really paid, but needed to create the tickets if needed
|
|
225
|
-
await
|
|
226
|
+
await OrderService.markPaid(order, payment, organization, webshop);
|
|
226
227
|
await payment.save();
|
|
227
228
|
await order.save();
|
|
228
229
|
} else {
|
|
@@ -5,6 +5,7 @@ import { signInternal } from '@stamhoofd/backend-env';
|
|
|
5
5
|
import { Document, Organization } from '@stamhoofd/models';
|
|
6
6
|
|
|
7
7
|
import { Context } from '../../../helpers/Context.js';
|
|
8
|
+
import { DocumentRenderService } from '../../../services/DocumentRenderService.js';
|
|
8
9
|
type Params = { id: string };
|
|
9
10
|
type Query = undefined;
|
|
10
11
|
type Body = undefined;
|
|
@@ -45,7 +46,7 @@ export class GetDocumentHtml extends Endpoint<Params, Query, Body, ResponseBody>
|
|
|
45
46
|
});
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
const html = await
|
|
49
|
+
const html = await DocumentRenderService.getRenderedHtml(document, organization);
|
|
49
50
|
if (!html) {
|
|
50
51
|
throw new SimpleError({
|
|
51
52
|
code: 'failed_generating',
|
|
@@ -16,6 +16,7 @@ import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
|
|
|
16
16
|
import { StripeHelper } from '../../../helpers/StripeHelper.js';
|
|
17
17
|
import { AuditLogService } from '../../../services/AuditLogService.js';
|
|
18
18
|
import { MollieService } from '../../../services/MollieService.js';
|
|
19
|
+
import { OrderService } from '../../../services/OrderService.js';
|
|
19
20
|
import { PaymentService } from '../../../services/PaymentService.js';
|
|
20
21
|
import { UitpasService } from '../../../services/uitpas/UitpasService.js';
|
|
21
22
|
import { WebshopAuthHelper } from './WebshopAuthHelper.js';
|
|
@@ -171,7 +172,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
171
172
|
order.data.paymentMethod = PaymentMethod.Unknown;
|
|
172
173
|
|
|
173
174
|
// Mark this order as paid
|
|
174
|
-
await
|
|
175
|
+
await OrderService.markPaid(order, null, organization, webshop);
|
|
175
176
|
await order.save();
|
|
176
177
|
} else {
|
|
177
178
|
const payment = new Payment();
|
|
@@ -242,7 +243,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
242
243
|
const description = webshop.meta.name + ' - ' + organization.name;
|
|
243
244
|
|
|
244
245
|
if (payment.method === PaymentMethod.Transfer) {
|
|
245
|
-
await
|
|
246
|
+
await OrderService.markValid(order, payment, []);
|
|
246
247
|
|
|
247
248
|
if (order.number) {
|
|
248
249
|
balanceItem.description = order.generateBalanceDescription(webshop);
|
|
@@ -253,7 +254,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
253
254
|
await payment.save();
|
|
254
255
|
} else if (payment.method === PaymentMethod.PointOfSale) {
|
|
255
256
|
// Not really paid, but needed to create the tickets if needed
|
|
256
|
-
await
|
|
257
|
+
await OrderService.markPaid(order, payment, organization, webshop);
|
|
257
258
|
|
|
258
259
|
if (order.number) {
|
|
259
260
|
balanceItem.description = order.generateBalanceDescription(webshop);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
|
|
2
2
|
import { XlsxBuiltInNumberFormat } from '@stamhoofd/excel-writer';
|
|
3
|
-
import type { EventNotification, LimitedFilteredRequest } from '@stamhoofd/structures';
|
|
4
|
-
import { EventNotificationStatus, EventNotificationStatusHelper, ExcelExportType
|
|
3
|
+
import type { EventNotification, LimitedFilteredRequest, Platform as PlatformStruct } from '@stamhoofd/structures';
|
|
4
|
+
import { EventNotificationStatus, EventNotificationStatusHelper, ExcelExportType } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { GetEventNotificationsEndpoint } from '../endpoints/global/events/GetEventNotificationsEndpoint.js';
|
|
7
7
|
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
|
|
8
8
|
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
|
|
9
9
|
|
|
10
10
|
// Assign to a typed variable to assure we have correct type checking in place
|
|
11
|
-
const
|
|
11
|
+
const getSheet = (platform: PlatformStruct): XlsxTransformerSheet<EventNotification, EventNotification> => ({
|
|
12
12
|
id: 'event-notifications',
|
|
13
13
|
name: $t(`%1FR`),
|
|
14
14
|
columns: [
|
|
@@ -118,18 +118,17 @@ const sheet: XlsxTransformerSheet<EventNotification, EventNotification> = {
|
|
|
118
118
|
matchId: 'recordAnswers',
|
|
119
119
|
getRecordAnswers: (notification: EventNotification) => notification.recordAnswers,
|
|
120
120
|
getRecordCategories: () => {
|
|
121
|
-
const platform = PlatformStruct.shared;
|
|
122
121
|
return platform.config.eventNotificationTypes.flatMap(r => r.recordCategories);
|
|
123
122
|
},
|
|
124
123
|
}),
|
|
125
124
|
],
|
|
126
|
-
};
|
|
125
|
+
});
|
|
127
126
|
|
|
128
127
|
ExportToExcelEndpoint.loaders.set(ExcelExportType.EventNotifications, {
|
|
129
128
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
130
129
|
return await GetEventNotificationsEndpoint.buildData(query);
|
|
131
130
|
},
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
getSheets: platform => [
|
|
132
|
+
getSheet(platform),
|
|
134
133
|
],
|
|
135
134
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
|
|
2
2
|
import type { PlatformMember } from '@stamhoofd/structures';
|
|
3
3
|
import { EmergencyContact, MemberDetails, MembersBlob, MemberWithRegistrationsBlob, Organization, Platform, PlatformFamily } from '@stamhoofd/structures';
|
|
4
|
-
import {
|
|
4
|
+
import { getBaseMemberColumns } from './members.js';
|
|
5
5
|
|
|
6
6
|
describe('Member excel export', () => {
|
|
7
7
|
describe('emergencyContacts column', () => {
|
|
@@ -21,7 +21,7 @@ describe('Member excel export', () => {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function getColumn() {
|
|
24
|
-
const column =
|
|
24
|
+
const column = getBaseMemberColumns(Platform.create({})).find(c => 'id' in c && c.id === 'emergencyContacts') as XlsxTransformerConcreteColumn<PlatformMember> | undefined;
|
|
25
25
|
|
|
26
26
|
if (!column) {
|
|
27
27
|
throw new Error('Column emergencyContacts not found');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { XlsxTransformerColumn, XlsxTransformerSheet } from '@stamhoofd/excel-writer';
|
|
2
2
|
import { XlsxBuiltInNumberFormat } from '@stamhoofd/excel-writer';
|
|
3
3
|
import { Platform } from '@stamhoofd/models';
|
|
4
|
-
import type { LimitedFilteredRequest, PlatformMember } from '@stamhoofd/structures';
|
|
5
|
-
import { ExcelExportType, Gender, GroupType, MembershipStatus, PlatformFamily,
|
|
4
|
+
import type { LimitedFilteredRequest, PlatformMember, Platform as PlatformStruct } from '@stamhoofd/structures';
|
|
5
|
+
import { ExcelExportType, Gender, GroupType, MembershipStatus, PlatformFamily, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
|
|
6
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
7
|
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
|
|
8
8
|
import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint.js';
|
|
@@ -10,7 +10,7 @@ import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
|
|
|
10
10
|
import { Context } from '../helpers/Context.js';
|
|
11
11
|
import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
|
|
12
12
|
|
|
13
|
-
export const
|
|
13
|
+
export const getBaseMemberColumns = (platform: PlatformStruct): XlsxTransformerColumn<PlatformMember>[] => [
|
|
14
14
|
{
|
|
15
15
|
id: 'id',
|
|
16
16
|
name: $t(`%d`),
|
|
@@ -244,7 +244,6 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
|
|
|
244
244
|
matchId: 'recordAnswers',
|
|
245
245
|
getRecordAnswers: ({ patchedMember: object }: PlatformMember) => object.details.recordAnswers,
|
|
246
246
|
getRecordCategories: () => {
|
|
247
|
-
const platform = PlatformStruct.shared;
|
|
248
247
|
const organization = Context.organization;
|
|
249
248
|
|
|
250
249
|
return [
|
|
@@ -256,11 +255,11 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
|
|
|
256
255
|
];
|
|
257
256
|
|
|
258
257
|
// Assign to a typed variable to assure we have correct type checking in place
|
|
259
|
-
const
|
|
258
|
+
const getSheet = (platform: PlatformStruct): XlsxTransformerSheet<PlatformMember, PlatformMember> => ({
|
|
260
259
|
id: 'members',
|
|
261
260
|
name: $t(`%1EH`),
|
|
262
261
|
columns: [
|
|
263
|
-
...
|
|
262
|
+
...getBaseMemberColumns(platform),
|
|
264
263
|
{
|
|
265
264
|
id: 'group',
|
|
266
265
|
name: $t(`%wH`),
|
|
@@ -281,7 +280,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
|
281
280
|
getValue: (member: PlatformMember) => {
|
|
282
281
|
const groups = member.filterRegistrations({ currentPeriod: true, types: [GroupType.Membership], organizationId: Context.organization?.id });
|
|
283
282
|
const defaultAgeGroupIds = Formatter.uniqueArray(groups.filter(o => o.group.defaultAgeGroupId));
|
|
284
|
-
const defaultAgeGroups = defaultAgeGroupIds.map(o =>
|
|
283
|
+
const defaultAgeGroups = defaultAgeGroupIds.map(o => platform.config.defaultAgeGroups.find(g => g.id === o.group.defaultAgeGroupId)?.name ?? $t(`%wJ`));
|
|
285
284
|
const str = Formatter.joinLast(Formatter.uniqueArray(defaultAgeGroups).sort(), ', ', ' ' + $t(`%M1`) + ' ') || Context.i18n.$t('%5D');
|
|
286
285
|
|
|
287
286
|
return {
|
|
@@ -472,7 +471,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
|
|
|
472
471
|
},
|
|
473
472
|
},
|
|
474
473
|
],
|
|
475
|
-
};
|
|
474
|
+
});
|
|
476
475
|
|
|
477
476
|
ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
478
477
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
@@ -486,8 +485,8 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
486
485
|
next: result.next,
|
|
487
486
|
});
|
|
488
487
|
},
|
|
489
|
-
|
|
490
|
-
|
|
488
|
+
getSheets: platform => [
|
|
489
|
+
getSheet(platform),
|
|
491
490
|
],
|
|
492
491
|
});
|
|
493
492
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ArrayDecoder, field } from '@simonbackx/simple-encoding';
|
|
2
2
|
import type { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
|
|
3
|
-
import { Group, Member, MemberResponsibilityRecord } from '@stamhoofd/models';
|
|
4
|
-
import type { LimitedFilteredRequest, Premise } from '@stamhoofd/structures';
|
|
5
|
-
import { ExcelExportType, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, Organization as OrganizationStruct, PaginatedResponse
|
|
3
|
+
import { Group, Member, MemberResponsibilityRecord, Platform } from '@stamhoofd/models';
|
|
4
|
+
import type { LimitedFilteredRequest, Platform as PlatformStruct, Premise } from '@stamhoofd/structures';
|
|
5
|
+
import { ExcelExportType, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, Organization as OrganizationStruct, PaginatedResponse } from '@stamhoofd/structures';
|
|
6
6
|
import { Formatter, Sorter } from '@stamhoofd/utility';
|
|
7
7
|
import { GetOrganizationsEndpoint } from '../endpoints/admin/organizations/GetOrganizationsEndpoint.js';
|
|
8
8
|
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
|
|
@@ -27,7 +27,7 @@ class MemberResponsibilityRecordWithMemberAndOrganization extends MemberResponsi
|
|
|
27
27
|
type Object = OrganizationWithResponsibilities;
|
|
28
28
|
|
|
29
29
|
// Assign to a typed variable to assure we have correct type checking in place
|
|
30
|
-
const
|
|
30
|
+
const getSheet = (platform: PlatformStruct): XlsxTransformerSheet<Object, Object> => ({
|
|
31
31
|
id: 'organizations',
|
|
32
32
|
name: $t(`%wP`),
|
|
33
33
|
columns: [
|
|
@@ -60,8 +60,6 @@ const sheet: XlsxTransformerSheet<Object, Object> = {
|
|
|
60
60
|
name: $t(`%13`),
|
|
61
61
|
width: 50,
|
|
62
62
|
getValue: (object: Object) => {
|
|
63
|
-
const platform = PlatformStruct.shared;
|
|
64
|
-
|
|
65
63
|
return {
|
|
66
64
|
value: object.meta.tags.map(tag => platform.config.tags.find(t => t.id === tag)?.name ?? $t(`%Gr`)).join(', '),
|
|
67
65
|
};
|
|
@@ -76,17 +74,15 @@ const sheet: XlsxTransformerSheet<Object, Object> = {
|
|
|
76
74
|
matchId: 'recordAnswers',
|
|
77
75
|
getRecordAnswers: (object: Object) => object.getRecordAnswers(),
|
|
78
76
|
getRecordCategories: () => {
|
|
79
|
-
const platform = PlatformStruct.shared;
|
|
80
|
-
|
|
81
77
|
return [
|
|
82
78
|
...platform.config.organizationLevelRecordsConfiguration.recordCategories,
|
|
83
79
|
];
|
|
84
80
|
},
|
|
85
81
|
}),
|
|
86
82
|
],
|
|
87
|
-
};
|
|
83
|
+
});
|
|
88
84
|
|
|
89
|
-
const
|
|
85
|
+
const getResponsibilitiesSheet = (platform: PlatformStruct): XlsxTransformerSheet<Object, MemberResponsibilityRecordWithMemberAndOrganization> => ({
|
|
90
86
|
id: 'responsibilities',
|
|
91
87
|
name: $t(`%7D`),
|
|
92
88
|
transform(organization) {
|
|
@@ -125,7 +121,6 @@ const responsibilities: XlsxTransformerSheet<Object, MemberResponsibilityRecordW
|
|
|
125
121
|
name: $t(`%H2`),
|
|
126
122
|
width: 50,
|
|
127
123
|
getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => {
|
|
128
|
-
const platform = PlatformStruct.shared;
|
|
129
124
|
const responsibility = platform.config.responsibilities.find(r => r.id === object.responsibilityId) ?? object.organization.privateMeta?.responsibilities.find(r => r.id === object.responsibilityId);
|
|
130
125
|
|
|
131
126
|
if (!responsibility) {
|
|
@@ -168,10 +163,10 @@ const responsibilities: XlsxTransformerSheet<Object, MemberResponsibilityRecordW
|
|
|
168
163
|
getAddress: object => object.member.details.address ?? object.member.details.parents[0]?.address ?? object.member.details.parents[1]?.address ?? null,
|
|
169
164
|
}),
|
|
170
165
|
],
|
|
171
|
-
};
|
|
166
|
+
});
|
|
172
167
|
|
|
173
168
|
type PremiseWithOrganization = { organization: Object; premise: Premise };
|
|
174
|
-
const
|
|
169
|
+
const getPremisesSheet = (platform: PlatformStruct): XlsxTransformerSheet<Object, PremiseWithOrganization> => ({
|
|
175
170
|
id: 'premises',
|
|
176
171
|
name: $t(`%6c`),
|
|
177
172
|
transform(organization) {
|
|
@@ -219,7 +214,6 @@ const premises: XlsxTransformerSheet<Object, PremiseWithOrganization> = {
|
|
|
219
214
|
width: 20,
|
|
220
215
|
getValue: (object: PremiseWithOrganization) => {
|
|
221
216
|
const ids = object.premise.premiseTypeIds;
|
|
222
|
-
const platform = PlatformStruct.shared;
|
|
223
217
|
return {
|
|
224
218
|
value: ids.map(id => platform.config.premiseTypes.find(t => t.id === id)?.name ?? $t(`%Gr`)).join(', '),
|
|
225
219
|
};
|
|
@@ -230,7 +224,7 @@ const premises: XlsxTransformerSheet<Object, PremiseWithOrganization> = {
|
|
|
230
224
|
getAddress: object => object.premise.address,
|
|
231
225
|
}),
|
|
232
226
|
],
|
|
233
|
-
};
|
|
227
|
+
});
|
|
234
228
|
|
|
235
229
|
ExportToExcelEndpoint.loaders.set(ExcelExportType.Organizations, {
|
|
236
230
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
@@ -253,7 +247,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Organizations, {
|
|
|
253
247
|
const groups = await Group.getByIDs(...groupIds);
|
|
254
248
|
const memberStructs = await AuthenticatedStructures.members(members);
|
|
255
249
|
const groupStructs = await AuthenticatedStructures.groups(groups);
|
|
256
|
-
const platform =
|
|
250
|
+
const platform = await Platform.getSharedStruct();
|
|
257
251
|
|
|
258
252
|
const mappedOrganizations = organizations.results.map((o) => {
|
|
259
253
|
const resp = responsibilities.filter(r => r.organizationId === o.id);
|
|
@@ -295,9 +289,9 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Organizations, {
|
|
|
295
289
|
next: organizations.next,
|
|
296
290
|
});
|
|
297
291
|
},
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
292
|
+
getSheets: platform => [
|
|
293
|
+
getSheet(platform),
|
|
294
|
+
getResponsibilitiesSheet(platform),
|
|
295
|
+
getPremisesSheet(platform),
|
|
302
296
|
],
|
|
303
297
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
|
|
2
2
|
import { XlsxBuiltInNumberFormat } from '@stamhoofd/excel-writer';
|
|
3
|
-
import type { LimitedFilteredRequest, PlatformMembership } from '@stamhoofd/structures';
|
|
4
|
-
import { ExcelExportType, PaginatedResponse
|
|
3
|
+
import type { LimitedFilteredRequest, PlatformMembership, Platform as PlatformStruct } from '@stamhoofd/structures';
|
|
4
|
+
import { ExcelExportType, PaginatedResponse } from '@stamhoofd/structures';
|
|
5
5
|
import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
|
|
6
6
|
import { GetPlatformMembershipsEndpoint } from '../endpoints/global/platform-memberships/GetPlatformMembershipsEndpoint.js';
|
|
7
7
|
|
|
8
8
|
// Assign to a typed variable to assure we have correct type checking in place
|
|
9
|
-
const
|
|
9
|
+
const getSheet = (platform: PlatformStruct): XlsxTransformerSheet<PlatformMembership> => ({
|
|
10
10
|
id: 'platform-memberships',
|
|
11
11
|
name: $t('%1EI'),
|
|
12
12
|
columns: [
|
|
@@ -23,7 +23,7 @@ const sheet: XlsxTransformerSheet<PlatformMembership> = {
|
|
|
23
23
|
name: $t('%1LP'),
|
|
24
24
|
width: 40,
|
|
25
25
|
getValue: (membership: PlatformMembership) => {
|
|
26
|
-
const membershipType =
|
|
26
|
+
const membershipType = platform.config.membershipTypes.find(m => m.id === membership.membershipTypeId);
|
|
27
27
|
const value = membershipType ? membershipType.name : '';
|
|
28
28
|
return { value };
|
|
29
29
|
},
|
|
@@ -244,7 +244,7 @@ const sheet: XlsxTransformerSheet<PlatformMembership> = {
|
|
|
244
244
|
}),
|
|
245
245
|
},
|
|
246
246
|
],
|
|
247
|
-
};
|
|
247
|
+
});
|
|
248
248
|
|
|
249
249
|
ExportToExcelEndpoint.loaders.set(ExcelExportType.PlatformMemberships, {
|
|
250
250
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
@@ -255,7 +255,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.PlatformMemberships, {
|
|
|
255
255
|
next: data.next,
|
|
256
256
|
});
|
|
257
257
|
},
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
getSheets: platform => [
|
|
259
|
+
getSheet(platform),
|
|
260
260
|
],
|
|
261
261
|
});
|