@stamhoofd/backend 1.0.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.
Files changed (150) hide show
  1. package/.env.template.json +63 -0
  2. package/.eslintrc.js +61 -0
  3. package/README.md +40 -0
  4. package/index.ts +172 -0
  5. package/jest.config.js +11 -0
  6. package/migrations.ts +33 -0
  7. package/package.json +48 -0
  8. package/src/crons.ts +845 -0
  9. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +42 -0
  10. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +320 -0
  11. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +171 -0
  12. package/src/endpoints/auth/CreateAdminEndpoint.ts +137 -0
  13. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +68 -0
  14. package/src/endpoints/auth/CreateTokenEndpoint.ts +200 -0
  15. package/src/endpoints/auth/DeleteTokenEndpoint.ts +31 -0
  16. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +70 -0
  17. package/src/endpoints/auth/GetUserEndpoint.test.ts +64 -0
  18. package/src/endpoints/auth/GetUserEndpoint.ts +57 -0
  19. package/src/endpoints/auth/PatchApiUserEndpoint.ts +90 -0
  20. package/src/endpoints/auth/PatchUserEndpoint.ts +122 -0
  21. package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +37 -0
  22. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +41 -0
  23. package/src/endpoints/auth/SignupEndpoint.ts +107 -0
  24. package/src/endpoints/auth/VerifyEmailEndpoint.ts +89 -0
  25. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +95 -0
  26. package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +31 -0
  27. package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +101 -0
  28. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +53 -0
  29. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +57 -0
  30. package/src/endpoints/global/files/UploadFile.ts +147 -0
  31. package/src/endpoints/global/files/UploadImage.ts +119 -0
  32. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +76 -0
  33. package/src/endpoints/global/members/GetMembersCountEndpoint.ts +43 -0
  34. package/src/endpoints/global/members/GetMembersEndpoint.ts +429 -0
  35. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +734 -0
  36. package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +45 -0
  37. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +105 -0
  38. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +146 -0
  39. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +52 -0
  40. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +80 -0
  41. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +49 -0
  42. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +58 -0
  43. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +62 -0
  44. package/src/endpoints/global/payments/ExchangeSTPaymentEndpoint.ts +153 -0
  45. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +134 -0
  46. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +44 -0
  47. package/src/endpoints/global/platform/GetPlatformEnpoint.ts +39 -0
  48. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +63 -0
  49. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +68 -0
  50. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +39 -0
  51. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +80 -0
  52. package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +41 -0
  53. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +134 -0
  54. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +521 -0
  55. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +37 -0
  56. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +115 -0
  57. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +187 -0
  58. package/src/endpoints/organization/dashboard/billing/ActivatePackagesEndpoint.ts +424 -0
  59. package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +67 -0
  60. package/src/endpoints/organization/dashboard/billing/GetBillingStatusEndpoint.ts +39 -0
  61. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +57 -0
  62. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +50 -0
  63. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +50 -0
  64. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +129 -0
  65. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplateEndpoint.ts +114 -0
  66. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +50 -0
  67. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +234 -0
  68. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +62 -0
  69. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +85 -0
  70. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +80 -0
  71. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +54 -0
  72. package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +49 -0
  73. package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +63 -0
  74. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +61 -0
  75. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.test.ts +64 -0
  76. package/src/endpoints/organization/dashboard/organization/ApplyRegisterCodeEndpoint.ts +84 -0
  77. package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +43 -0
  78. package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +42 -0
  79. package/src/endpoints/organization/dashboard/organization/GetOrganizationSSOEndpoint.ts +43 -0
  80. package/src/endpoints/organization/dashboard/organization/GetRegisterCodeEndpoint.ts +65 -0
  81. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +281 -0
  82. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +338 -0
  83. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +196 -0
  84. package/src/endpoints/organization/dashboard/organization/SetOrganizationSSOEndpoint.ts +50 -0
  85. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +48 -0
  86. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +207 -0
  87. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +202 -0
  88. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +233 -0
  89. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +66 -0
  90. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -0
  91. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +93 -0
  92. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +59 -0
  93. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +78 -0
  94. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +40 -0
  95. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +69 -0
  96. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +52 -0
  97. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +73 -0
  98. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +60 -0
  99. package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +47 -0
  100. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +41 -0
  101. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +217 -0
  102. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +51 -0
  103. package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +47 -0
  104. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +83 -0
  105. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +68 -0
  106. package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +69 -0
  107. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +125 -0
  108. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +204 -0
  109. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +278 -0
  110. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +80 -0
  111. package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +60 -0
  112. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +379 -0
  113. package/src/endpoints/organization/shared/GetDocumentHtml.ts +54 -0
  114. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +45 -0
  115. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +78 -0
  116. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +34 -0
  117. package/src/endpoints/organization/shared/auth/OpenIDConnectCallbackEndpoint.ts +44 -0
  118. package/src/endpoints/organization/shared/auth/OpenIDConnectStartEndpoint.ts +82 -0
  119. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +59 -0
  120. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +51 -0
  121. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +40 -0
  122. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +124 -0
  123. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +130 -0
  124. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +50 -0
  125. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +450 -0
  126. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +335 -0
  127. package/src/helpers/AddressValidator.test.ts +40 -0
  128. package/src/helpers/AddressValidator.ts +256 -0
  129. package/src/helpers/AdminPermissionChecker.ts +1031 -0
  130. package/src/helpers/AuthenticatedStructures.ts +158 -0
  131. package/src/helpers/BuckarooHelper.ts +279 -0
  132. package/src/helpers/CheckSettlements.ts +215 -0
  133. package/src/helpers/Context.ts +202 -0
  134. package/src/helpers/CookieHelper.ts +45 -0
  135. package/src/helpers/ForwardHandler.test.ts +216 -0
  136. package/src/helpers/ForwardHandler.ts +140 -0
  137. package/src/helpers/OpenIDConnectHelper.ts +284 -0
  138. package/src/helpers/StripeHelper.ts +293 -0
  139. package/src/helpers/StripePayoutChecker.ts +188 -0
  140. package/src/middleware/ContextMiddleware.ts +16 -0
  141. package/src/migrations/1646578856-validate-addresses.ts +60 -0
  142. package/src/seeds/0000000000-example.ts +13 -0
  143. package/src/seeds/1715028563-user-permissions.ts +52 -0
  144. package/tests/e2e/stock.test.ts +2120 -0
  145. package/tests/e2e/tickets.test.ts +926 -0
  146. package/tests/helpers/StripeMocker.ts +362 -0
  147. package/tests/helpers/TestServer.ts +21 -0
  148. package/tests/jest.global.setup.ts +29 -0
  149. package/tests/jest.setup.ts +59 -0
  150. package/tsconfig.json +42 -0
@@ -0,0 +1,196 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
4
+ import { DNSRecord, DNSRecordType, Organization as OrganizationStruct,OrganizationDomains } from "@stamhoofd/structures";
5
+ import NodeRSA from 'node-rsa';
6
+
7
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
8
+ import { Context } from '../../../../helpers/Context';
9
+
10
+ type Params = Record<string, never>;
11
+ type Query = undefined;
12
+ type Body = OrganizationDomains;
13
+ type ResponseBody = OrganizationStruct
14
+
15
+ /**
16
+ * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
17
+ */
18
+
19
+ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
20
+ bodyDecoder = OrganizationDomains as Decoder<OrganizationDomains>
21
+
22
+ protected doesMatch(request: Request): [true, Params] | [false] {
23
+ if (request.method != "POST") {
24
+ return [false];
25
+ }
26
+
27
+ const params = Endpoint.parseParameters(request.url, "/organization/domain", {});
28
+
29
+ if (params) {
30
+ return [true, params as Params];
31
+ }
32
+ return [false];
33
+ }
34
+
35
+ async handle(request: DecodedRequest<Params, Query, Body>) {
36
+ const organization = await Context.setOrganizationScope();
37
+ await Context.authenticate()
38
+
39
+ if (!await Context.auth.canManageOrganizationDomain(organization.id)) {
40
+ throw Context.auth.error()
41
+ }
42
+
43
+ const errors = new SimpleErrors()
44
+
45
+ // check if changed
46
+ if (
47
+ (organization.privateMeta.pendingRegisterDomain ?? organization.registerDomain) !== request.body.registerDomain // changed register domain
48
+ ||
49
+ (organization.privateMeta.pendingMailDomain ?? organization.privateMeta.mailDomain) !== request.body.mailDomain // changed pending domain
50
+ ) {
51
+ console.log("Domains changed")
52
+
53
+ // Validate domains
54
+ // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
55
+ if (request.body.registerDomain !== null && !request.body.registerDomain.match(/^([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-]+\.[a-zA-Z]+$/)) {
56
+ throw new SimpleError({
57
+ code: "invalid_domain",
58
+ message: "registerDomain is invalid",
59
+ human: "De subdomeinnaam voor jouw registratiepagina is ongeldig",
60
+ field: "registerDomain"
61
+ })
62
+ }
63
+
64
+ // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
65
+ if (request.body.mailDomain !== null && !request.body.mailDomain.match(/^([a-zA-Z0-9-]+\.)?[a-zA-Z0-9-]+\.[a-zA-Z]+$/)) {
66
+ throw new SimpleError({
67
+ code: "invalid_domain",
68
+ message: "mailDomain is invalid",
69
+ human: "De domeinnaam voor e-mails is ongeldig",
70
+ field: "mailDomain"
71
+ })
72
+ }
73
+
74
+ const oldMailDomain = organization.privateMeta.mailDomain
75
+
76
+ organization.privateMeta.pendingRegisterDomain = request.body.registerDomain?.toLowerCase() ?? null
77
+ organization.privateMeta.pendingMailDomain = request.body.mailDomain?.toLowerCase() ?? null
78
+
79
+ // We don't keep the current register domain because we have no way to validate the old DNS-records
80
+ if (organization.privateMeta.pendingRegisterDomain === null || organization.registerDomain !== organization.privateMeta.pendingRegisterDomain) {
81
+ organization.registerDomain = null
82
+ }
83
+
84
+ // We don't keep the current mail domain because we have no way to validate the old DNS-records
85
+ if (organization.privateMeta.pendingMailDomain === null || organization.privateMeta.mailDomain !== organization.privateMeta.pendingMailDomain) {
86
+ organization.privateMeta.mailDomain = null
87
+ }
88
+
89
+ // Always temporary disable mail domain until validated
90
+ organization.privateMeta.mailDomainActive = false
91
+
92
+ // Reset notification counters
93
+ organization.serverMeta.DNSRecordWarningCount = 0
94
+ organization.serverMeta.firstInvalidDNSRecords = undefined
95
+ organization.serverMeta.didSendDomainSetupMail = false
96
+
97
+ // Generate new DNS-records
98
+ organization.privateMeta.dnsRecords = []
99
+
100
+ if (organization.privateMeta.pendingMailDomain !== null) {
101
+ const defaultFromDomain = "stamhoofd." + organization.privateMeta.pendingMailDomain;
102
+ if (organization.privateMeta.pendingRegisterDomain === null || !organization.privateMeta.pendingRegisterDomain.endsWith('.' + organization.privateMeta.pendingMailDomain)) {
103
+ // We set a custom domainname for webshops already
104
+ // This is not used at this moment
105
+ organization.privateMeta.mailFromDomain = defaultFromDomain;
106
+ } else {
107
+ // CNAME domain: for SPF + MX + A record
108
+ organization.privateMeta.mailFromDomain = organization.privateMeta.pendingRegisterDomain;
109
+ }
110
+
111
+ if (organization.privateMeta.mailFromDomain !== organization.privateMeta.pendingRegisterDomain) {
112
+
113
+ organization.privateMeta.dnsRecords.push(DNSRecord.create({
114
+ type: DNSRecordType.CNAME,
115
+ name: organization.privateMeta.mailFromDomain + ".",
116
+ // Use shops for mail domain, to allow reuse
117
+ value: STAMHOOFD.domains.webshopCname + "."
118
+ }))
119
+
120
+ if (STAMHOOFD.domains.registration && organization.privateMeta.pendingRegisterDomain) {
121
+ organization.privateMeta.dnsRecords.push(DNSRecord.create({
122
+ type: DNSRecordType.CNAME,
123
+ name: organization.privateMeta.pendingRegisterDomain+".",
124
+ // Use registration domain
125
+ value: STAMHOOFD.domains.registrationCname + "."
126
+ }))
127
+ }
128
+ } else {
129
+ organization.privateMeta.dnsRecords.push(DNSRecord.create({
130
+ type: DNSRecordType.CNAME,
131
+ name: organization.privateMeta.mailFromDomain+".",
132
+ // Use registration domain
133
+ value: STAMHOOFD.domains.registrationCname + "."
134
+ }))
135
+ }
136
+ }
137
+
138
+ if (request.body.mailDomain !== null) {
139
+
140
+ let priv: string
141
+ let pub: string
142
+
143
+ if (!organization.serverMeta.privateDKIMKey || !organization.serverMeta.publicDKIMKey ) {
144
+ const key = new NodeRSA({ b: request.body.useDkim1024bit ? 1024 : 2048 });
145
+ const privArr = (key.exportKey('private') as string).split("\n")
146
+ priv = privArr.splice(1, privArr.length - 2).join("");
147
+
148
+ const pubArr = (key.exportKey('public') as string).split("\n")
149
+ pub = pubArr.splice(1, pubArr.length - 2).join("");
150
+
151
+ organization.serverMeta.privateDKIMKey = priv
152
+ organization.serverMeta.publicDKIMKey = pub
153
+ } else {
154
+ priv = organization.serverMeta.privateDKIMKey
155
+ pub = organization.serverMeta.publicDKIMKey
156
+ }
157
+
158
+ // DKIM records
159
+ organization.privateMeta.dnsRecords.push(DNSRecord.create({
160
+ type: DNSRecordType.TXT,
161
+ name: "stamhoofd._domainkey." + request.body.mailDomain + ".",
162
+ value: "v=DKIM1; k=rsa; p=" + pub + ""
163
+ }))
164
+ } else {
165
+ if (oldMailDomain) {
166
+ organization.deleteAWSMailIdenitity(oldMailDomain).catch(console.error)
167
+ }
168
+
169
+ if (organization.serverMeta.privateDKIMKey && organization.serverMeta.publicDKIMKey ) {
170
+ // Allow creation of new keys for new domains
171
+ console.log("Backup DKIM keys for "+organization.id)
172
+ console.log("Private: "+organization.serverMeta.privateDKIMKey)
173
+ console.log("Public: "+organization.serverMeta.publicDKIMKey)
174
+
175
+ // Delete keys if mail domain is deleted -> to allow new keys
176
+ organization.serverMeta.privateDKIMKey = undefined
177
+ organization.serverMeta.publicDKIMKey = undefined
178
+ }
179
+ }
180
+
181
+ await organization.save()
182
+
183
+ } else {
184
+ // Validate DNS-records if not empty
185
+ console.log("Validating domains")
186
+ await organization.updateDNSRecords()
187
+ }
188
+
189
+ console.log("Done.")
190
+
191
+ errors.throwIfNotEmpty()
192
+ return new Response(await AuthenticatedStructures.organization(organization));
193
+ }
194
+
195
+
196
+ }
@@ -0,0 +1,50 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { OpenIDClientConfiguration } from "@stamhoofd/structures";
4
+
5
+ import { Context } from '../../../../helpers/Context';
6
+ import { OpenIDConnectHelper } from '../../../../helpers/OpenIDConnectHelper';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = OpenIDClientConfiguration;
11
+ type ResponseBody = OpenIDClientConfiguration
12
+
13
+ /**
14
+ * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
15
+ */
16
+
17
+ export class SetOrganizationSSOEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
+ bodyDecoder = OpenIDClientConfiguration as Decoder<OpenIDClientConfiguration>
19
+
20
+ protected doesMatch(request: Request): [true, Params] | [false] {
21
+ if (request.method != "POST") {
22
+ return [false];
23
+ }
24
+
25
+ const params = Endpoint.parseParameters(request.url, "/organization/sso", {});
26
+
27
+ if (params) {
28
+ return [true, params as Params];
29
+ }
30
+ return [false];
31
+ }
32
+
33
+ async handle(request: DecodedRequest<Params, Query, Body>) {
34
+ const organization = await Context.setOrganizationScope();
35
+ await Context.authenticate()
36
+
37
+ if (!await Context.auth.canManageSSOSettings(organization.id)) {
38
+ throw Context.auth.error()
39
+ }
40
+
41
+ // Validate configuration
42
+ const helper = new OpenIDConnectHelper(organization, request.body)
43
+ await helper.getClient()
44
+
45
+ organization.serverMeta.ssoConfiguration = request.body
46
+ await organization.save()
47
+
48
+ return new Response(request.body);
49
+ }
50
+ }
@@ -0,0 +1,48 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { SimpleError } from "@simonbackx/simple-errors";
3
+ import { BalanceItem, Group, Member } from "@stamhoofd/models";
4
+ import { MemberBalanceItem } from "@stamhoofd/structures";
5
+
6
+ import { Context } from "../../../../helpers/Context";
7
+
8
+ type Params = { id: string };
9
+ type Query = undefined
10
+ type Body = undefined
11
+ type ResponseBody = MemberBalanceItem[]
12
+
13
+ export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ protected doesMatch(request: Request): [true, Params] | [false] {
15
+ if (request.method != "GET") {
16
+ return [false];
17
+ }
18
+
19
+ const params = Endpoint.parseParameters(request.url, "/organization/members/@id/balance", { id: String});
20
+
21
+ if (params) {
22
+ return [true, params as Params];
23
+ }
24
+ return [false];
25
+ }
26
+
27
+ async handle(request: DecodedRequest<Params, Query, Body>) {
28
+ const organization = await Context.setOrganizationScope();
29
+ await Context.authenticate()
30
+
31
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
32
+ throw Context.auth.error()
33
+ }
34
+
35
+ const member = (await Member.getWithRegistrations(request.params.id))
36
+
37
+ if (!member || !await Context.auth.hasFinancialMemberAccess(member)) {
38
+ throw Context.auth.notFoundOrNoAccess("Geen lid gevonden, of je hebt geen toegang tot dit lid")
39
+ }
40
+
41
+ // Get all balance items for this member or users
42
+ const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization.id, member.users.map(u => u.id), [member.id])
43
+
44
+ return new Response(
45
+ await BalanceItem.getMemberStructure(balanceItems)
46
+ );
47
+ }
48
+ }
@@ -0,0 +1,207 @@
1
+ import { AutoEncoder, Data, DateDecoder, Decoder, EnumDecoder, field, IntegerDecoder, StringDecoder } from "@simonbackx/simple-encoding";
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { Organization, Payment } from "@stamhoofd/models";
4
+ import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from "@stamhoofd/structures";
5
+
6
+ import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
7
+ import { Context } from "../../../../helpers/Context";
8
+
9
+ type Params = Record<string, never>;
10
+ type Body = undefined
11
+ type ResponseBody = PaymentGeneral[]
12
+
13
+ export class StringArrayDecoder<T> implements Decoder<T[]> {
14
+ decoder: Decoder<T>;
15
+
16
+ constructor(decoder: Decoder<T>) {
17
+ this.decoder = decoder;
18
+ }
19
+
20
+ decode(data: Data): T[] {
21
+ const strValue = data.string;
22
+
23
+ // Split on comma
24
+ const parts = strValue.split(",");
25
+ return parts
26
+ .map((v, index) => {
27
+ return data.clone({
28
+ data: v,
29
+ context: data.context,
30
+ field: data.addToCurrentField(index)
31
+ }).decode(this.decoder)
32
+ });
33
+ }
34
+ }
35
+
36
+ export class StringNullableDecoder<T> implements Decoder<T | null> {
37
+ decoder: Decoder<T>;
38
+
39
+ constructor(decoder: Decoder<T>) {
40
+ this.decoder = decoder;
41
+ }
42
+
43
+ decode(data: Data): T | null {
44
+ if (data.value === 'null') {
45
+ return null;
46
+ }
47
+
48
+ return data.decode(this.decoder);
49
+ }
50
+ }
51
+
52
+
53
+ class Query extends AutoEncoder {
54
+ /**
55
+ * Usage in combination with paidSince is special!
56
+ */
57
+ @field({ decoder: StringDecoder, optional: true })
58
+ afterId?: string
59
+
60
+ /**
61
+ * Return all payments that were paid after (and including) this date.
62
+ * Only returns orders **equal** to this date if afterId is not provided or if the id of those payments is also higher.
63
+ */
64
+ @field({ decoder: DateDecoder, optional: true })
65
+ paidSince?: Date
66
+
67
+ @field({ decoder: DateDecoder, optional: true })
68
+ paidBefore?: Date
69
+
70
+ @field({ decoder: IntegerDecoder, optional: true })
71
+ limit?: number
72
+
73
+ @field({ decoder: new StringArrayDecoder(new EnumDecoder(PaymentMethod)), optional: true })
74
+ methods?: PaymentMethod[]
75
+
76
+ @field({ decoder: new StringArrayDecoder(new StringNullableDecoder(new EnumDecoder(PaymentProvider))), optional: true })
77
+ providers?: (PaymentProvider|null)[]
78
+ }
79
+
80
+ export class GetPaymentsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
81
+ protected queryDecoder = Query;
82
+
83
+ protected doesMatch(request: Request): [true, Params] | [false] {
84
+ if (request.method != "GET") {
85
+ return [false];
86
+ }
87
+
88
+ const params = Endpoint.parseParameters(request.url, "/organization/payments", {});
89
+
90
+ if (params) {
91
+ return [true, params as Params];
92
+ }
93
+ return [false];
94
+ }
95
+
96
+ async handle(request: DecodedRequest<Params, Query, Body>) {
97
+ const organization = await Context.setOrganizationScope();
98
+ await Context.authenticate()
99
+
100
+ if (!await Context.auth.canManagePayments(organization.id)) {
101
+ throw Context.auth.error()
102
+ }
103
+
104
+ return new Response(
105
+ (await this.getPayments(organization, request.query))
106
+ );
107
+ }
108
+
109
+ async getPayments(organization: Organization, query: Query) {
110
+ const paidSince = query.paidSince ?? new Date(Date.now() - (24 * 60 * 60 * 1000 * 7 ))
111
+ paidSince.setMilliseconds(0)
112
+ const payments: Payment[] = []
113
+
114
+ if (query.afterId) {
115
+ // First return all payments with id > afterId and paidAt == paidSince
116
+ payments.push(...await Payment.where({
117
+ organizationId: organization.id,
118
+ paidAt: {
119
+ sign: '=',
120
+ value: paidSince
121
+ },
122
+ id: {
123
+ sign: '>',
124
+ value: query.afterId ?? ""
125
+ },
126
+ method: {
127
+ sign: 'IN',
128
+ value: query.methods ?? [PaymentMethod.Transfer]
129
+ },
130
+ provider: {
131
+ sign: 'IN',
132
+ value: query.providers ?? [null]
133
+ }
134
+ }, {
135
+ limit: query.limit ?? undefined,
136
+ sort: [{
137
+ column: "id",
138
+ direction: "ASC"
139
+ }]
140
+ }));
141
+ }
142
+
143
+ payments.push(...await Payment.where({
144
+ organizationId: organization.id,
145
+ paidAt: query.paidBefore ? [{
146
+ sign: query.afterId ? '>' : '>=',
147
+ value: paidSince
148
+ }, {
149
+ sign: '<=',
150
+ value: query.paidBefore
151
+ }] : {
152
+ sign: query.afterId ? '>' : '>=',
153
+ value: paidSince
154
+ },
155
+ method: {
156
+ sign: 'IN',
157
+ value: query.methods ?? [PaymentMethod.Transfer]
158
+ },
159
+ provider: {
160
+ sign: 'IN',
161
+ value: query.providers ?? [null]
162
+ }
163
+ }, {
164
+ limit: query.limit ? (query.limit - payments.length) : undefined,
165
+ sort: [{
166
+ column: "paidAt",
167
+ direction: "ASC"
168
+ },
169
+ {
170
+ column: "id",
171
+ direction: "ASC"
172
+ }]
173
+ }));
174
+
175
+
176
+ if (!query.paidSince && !query.methods && !query.providers) {
177
+ // Default behaviour is to return all not-paid transfer payments that are not yet paid
178
+
179
+ payments.push(...
180
+ await Payment.where({
181
+ organizationId: organization.id,
182
+ paidAt: null,
183
+ method: PaymentMethod.Transfer,
184
+ status: {
185
+ sign: '!=',
186
+ value: PaymentStatus.Failed
187
+ }
188
+ })
189
+ );
190
+
191
+ payments.push(...
192
+ await Payment.where({
193
+ organizationId: organization.id,
194
+ paidAt: null,
195
+ updatedAt: {
196
+ sign: '>',
197
+ value: new Date(Date.now() - (24 * 60 * 60 * 1000 * 7 ))
198
+ },
199
+ method: PaymentMethod.Transfer,
200
+ status: PaymentStatus.Failed
201
+ })
202
+ );
203
+ }
204
+
205
+ return await AuthenticatedStructures.paymentsGeneral(payments, true)
206
+ }
207
+ }