@stamhoofd/backend 2.91.0 → 2.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -10
- package/src/audit-logs/EmailLogger.ts +4 -4
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +29 -5
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +207 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +67 -22
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
- package/src/helpers/AdminPermissionChecker.ts +81 -5
- package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
- package/src/seeds/1755181288-remove-duplicate-members.ts +145 -0
- package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
- package/src/services/uitpas/UitpasService.ts +71 -2
- package/src/services/uitpas/checkUitpasNumbers.ts +1 -0
- package/src/sql-filters/emails.ts +65 -0
- package/src/sql-sorters/emails.ts +47 -0
|
@@ -3,7 +3,7 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Email } from '@stamhoofd/email';
|
|
6
|
-
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode
|
|
6
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
|
|
7
7
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
8
|
import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderData, OrderResponse, Order as OrderStruct, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, TranslatedString, Version, WebshopAuthType, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
9
9
|
import { Formatter } from '@stamhoofd/utility';
|
|
@@ -133,72 +133,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
133
133
|
request.body.validate(webshopStruct, organization.meta, request.i18n, false, Context.user?.getStructure());
|
|
134
134
|
request.body.update(webshopStruct);
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
const articlesWithUitpasSocialTariff = request.body.cart.items.filter(item => item.productPrice.uitpasBaseProductPriceId !== null);
|
|
138
|
-
for (const item of articlesWithUitpasSocialTariff) {
|
|
139
|
-
const uitpasNumbersOnly = item.uitpasNumbers.map(p => p.uitpasNumber);
|
|
140
|
-
|
|
141
|
-
// verify the amount of UiTPAS numbers
|
|
142
|
-
if (uitpasNumbersOnly.length !== item.amount) {
|
|
143
|
-
throw new SimpleError({
|
|
144
|
-
code: 'amount_of_uitpas_numbers_mismatch',
|
|
145
|
-
message: 'The number of UiTPAS numbers and items with UiTPAS social tariff does not match',
|
|
146
|
-
human: $t('6140c642-69b2-43d6-80ba-2af4915c5837'),
|
|
147
|
-
field: 'cart.items.uitpasNumbers',
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// verify the UiTPAS numbers are unique (within the order)
|
|
152
|
-
if (uitpasNumbersOnly.length !== Formatter.uniqueArray(uitpasNumbersOnly).length) {
|
|
153
|
-
throw new SimpleError({
|
|
154
|
-
code: 'duplicate_uitpas_numbers',
|
|
155
|
-
message: 'Duplicate uitpas numbers used',
|
|
156
|
-
human: $t('d9ec27f3-dafa-41e8-bcfb-9da564a4a675'),
|
|
157
|
-
field: 'cart.items.uitpasNumbers',
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// verify the UiTPAS numbers are not already used for this product
|
|
162
|
-
const hasBeenUsed = await WebshopUitpasNumber.areUitpasNumbersUsed(webshop.id, item.product.id, uitpasNumbersOnly);
|
|
163
|
-
if (hasBeenUsed) {
|
|
164
|
-
throw new SimpleError({
|
|
165
|
-
code: 'uitpas_number_already_used',
|
|
166
|
-
message: 'One or more uitpas numbers are already used',
|
|
167
|
-
human: $t('1ef059c2-e758-4cfa-bc2b-16a581029450'),
|
|
168
|
-
field: 'cart.items.uitpasNumbers',
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// verify the UiTPAS numbers are valid for social tariff (static check + API call to UiTPAS)
|
|
173
|
-
if (item.product.uitpasEvent) {
|
|
174
|
-
const basePrice = item.product.prices.find(p => p.id === item.productPrice.uitpasBaseProductPriceId)?.price ?? 0;
|
|
175
|
-
const reducedPrices = await UitpasService.getSocialTariffForUitpasNumbers(organization.id, uitpasNumbersOnly, basePrice, item.product.uitpasEvent.url);
|
|
176
|
-
const expectedReducedPrices = item.uitpasNumbers;
|
|
177
|
-
if (reducedPrices.length < expectedReducedPrices.length) {
|
|
178
|
-
// should not happen
|
|
179
|
-
throw new SimpleError({
|
|
180
|
-
code: 'uitpas_social_tariff_price_mismatch',
|
|
181
|
-
message: 'UiTPAS wrong number of prices retruned',
|
|
182
|
-
human: $t('2d1983fa-2224-422f-9ea0-fdae77cb4914'),
|
|
183
|
-
field: 'cart.items.uitpasNumbers',
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
for (let i = 0; i < expectedReducedPrices.length; i++) {
|
|
187
|
-
if (reducedPrices[i].price !== expectedReducedPrices[i].price) {
|
|
188
|
-
throw new SimpleError({
|
|
189
|
-
code: 'uitpas_social_tariff_price_mismatch',
|
|
190
|
-
message: 'UiTPAS social tariff have a different price',
|
|
191
|
-
human: $t('2f4b9572-4b9c-42e0-91f1-b0984624d225', { correctPrice: Formatter.price(reducedPrices[i].price), orderPrice: Formatter.price(expectedReducedPrices[i].price) }),
|
|
192
|
-
field: 'uitpasNumbers.' + i.toString(),
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
item.uitpasNumbers[i].uitpasTariffId = reducedPrices[i].uitpasTariffId;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
await UitpasService.checkUitpasNumbers(uitpasNumbersOnly); // Throws if invalid
|
|
200
|
-
}
|
|
201
|
-
}
|
|
136
|
+
request.body.cart = await UitpasService.validateCart(organization.id, webshop.id, request.body.cart);
|
|
202
137
|
|
|
203
138
|
const order = new Order().setRelation(Order.webshop, webshop);
|
|
204
139
|
order.data = request.body; // TODO: validate
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
-
import { BalanceItem, CachedBalance, Document, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
|
|
3
|
+
import { BalanceItem, CachedBalance, Document, Email, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
|
|
4
4
|
import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordSettings, ResourcePermissions } from '@stamhoofd/structures';
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { MemberRecordStore } from '../services/MemberRecordStore';
|
|
@@ -709,7 +709,7 @@ export class AdminPermissionChecker {
|
|
|
709
709
|
}
|
|
710
710
|
|
|
711
711
|
if (!template.organizationId) {
|
|
712
|
-
return this.hasPlatformFullAccess();
|
|
712
|
+
return this.hasPlatformFullAccess() || !!this.platformPermissions?.hasAccessRight(AccessRight.ManageEmailTemplates);
|
|
713
713
|
}
|
|
714
714
|
|
|
715
715
|
if (await this.hasFullAccess(template.organizationId)) {
|
|
@@ -734,6 +734,11 @@ export class AdminPermissionChecker {
|
|
|
734
734
|
return true;
|
|
735
735
|
}
|
|
736
736
|
|
|
737
|
+
const o = await this.getOrganizationPermissions(template.organizationId);
|
|
738
|
+
if (o && o.hasAccessRight(AccessRight.ManageEmailTemplates)) {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
|
|
737
742
|
return false;
|
|
738
743
|
}
|
|
739
744
|
|
|
@@ -845,8 +850,79 @@ export class AdminPermissionChecker {
|
|
|
845
850
|
return this.hasSomeAccess(organizationId);
|
|
846
851
|
}
|
|
847
852
|
|
|
848
|
-
canSendEmails() {
|
|
849
|
-
|
|
853
|
+
async canSendEmails(organizationId: Organization | string | null) {
|
|
854
|
+
if (organizationId) {
|
|
855
|
+
return (await this.getOrganizationPermissions(organizationId))?.hasAccessRightForSomeResourceOfType(PermissionsResourceType.Senders, AccessRight.SendMessages) ?? false;
|
|
856
|
+
}
|
|
857
|
+
return this.platformPermissions?.hasAccessRightForSomeResourceOfType(PermissionsResourceType.Senders, AccessRight.SendMessages) ?? false;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Fast check if this user can read at least one email in the system.
|
|
862
|
+
*/
|
|
863
|
+
async canReadEmails(organizationId: Organization | string | null) {
|
|
864
|
+
if (await this.canSendEmails(organizationId)) {
|
|
865
|
+
// A user can reads its own emails, so they can read.
|
|
866
|
+
return true;
|
|
867
|
+
}
|
|
868
|
+
if (organizationId) {
|
|
869
|
+
return (await this.getOrganizationPermissions(organizationId))?.hasAccessForSomeResourceOfType(PermissionsResourceType.Senders, PermissionLevel.Read) ?? false;
|
|
870
|
+
}
|
|
871
|
+
return this.platformPermissions?.hasAccessForSomeResourceOfType(PermissionsResourceType.Senders, PermissionLevel.Read) ?? false;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async canReadAllEmails(organizationId: Organization | string | null, senderId = ''): Promise<boolean> {
|
|
875
|
+
if (organizationId) {
|
|
876
|
+
return (await this.getOrganizationPermissions(organizationId))?.hasResourceAccess(PermissionsResourceType.Senders, senderId, PermissionLevel.Read) ?? false;
|
|
877
|
+
}
|
|
878
|
+
return this.platformPermissions?.hasResourceAccess(PermissionsResourceType.Senders, senderId, PermissionLevel.Read) ?? false;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async canAccessEmail(email: Email, level: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
|
|
882
|
+
if (!this.checkScope(email.organizationId)) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (email.userId === this.user.id) {
|
|
887
|
+
// User can always read their own emails
|
|
888
|
+
// Note; for sending we'll always need to use 'canSendEmailsFrom' externally
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (email.organizationId) {
|
|
893
|
+
const organizationPermissions = await this.getOrganizationPermissions(email.organizationId);
|
|
894
|
+
if (!organizationPermissions) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
if (!email.senderId) {
|
|
898
|
+
return organizationPermissions.hasResourceAccess(PermissionsResourceType.Senders, '', level);
|
|
899
|
+
}
|
|
900
|
+
return organizationPermissions.hasResourceAccess(PermissionsResourceType.Senders, email.senderId, level);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Platform email
|
|
904
|
+
const platformPermissions = this.platformPermissions;
|
|
905
|
+
if (!platformPermissions) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
if (!email.senderId) {
|
|
909
|
+
return platformPermissions.hasResourceAccess(PermissionsResourceType.Senders, '', level);
|
|
910
|
+
}
|
|
911
|
+
return platformPermissions.hasResourceAccess(PermissionsResourceType.Senders, email.senderId, level);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async canSendEmail(email: Email): Promise<boolean> {
|
|
915
|
+
if (email.senderId) {
|
|
916
|
+
return await this.canSendEmailsFrom(email.organizationId, email.senderId);
|
|
917
|
+
}
|
|
918
|
+
return await this.canSendEmails(email.organizationId);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
async canSendEmailsFrom(organizationId: Organization | string | null, senderId: string): Promise<boolean> {
|
|
922
|
+
if (organizationId) {
|
|
923
|
+
return (await this.getOrganizationPermissions(organizationId))?.hasResourceAccessRight(PermissionsResourceType.Senders, senderId, AccessRight.SendMessages) ?? false;
|
|
924
|
+
}
|
|
925
|
+
return this.platformPermissions?.hasResourceAccessRight(PermissionsResourceType.Senders, senderId, AccessRight.SendMessages) ?? false;
|
|
850
926
|
}
|
|
851
927
|
|
|
852
928
|
async canReadEmailTemplates(organizationId: string) {
|
|
@@ -909,7 +985,7 @@ export class AdminPermissionChecker {
|
|
|
909
985
|
*/
|
|
910
986
|
async hasSomeAccess(organizationOrId: string | Organization): Promise<boolean> {
|
|
911
987
|
const organizationPermissions = await this.getOrganizationPermissions(organizationOrId);
|
|
912
|
-
return !!organizationPermissions;
|
|
988
|
+
return !!organizationPermissions && !organizationPermissions.isEmpty;
|
|
913
989
|
}
|
|
914
990
|
|
|
915
991
|
async canManageAdmins(organizationId: string) {
|