@stamhoofd/backend 2.3.1 → 2.4.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 (31) hide show
  1. package/index.ts +3 -0
  2. package/package.json +4 -4
  3. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
  4. package/src/endpoints/auth/CreateAdminEndpoint.ts +6 -3
  5. package/src/endpoints/auth/GetOtherUserEndpoint.ts +41 -0
  6. package/src/endpoints/auth/GetUserEndpoint.ts +6 -28
  7. package/src/endpoints/auth/PatchUserEndpoint.ts +25 -6
  8. package/src/endpoints/auth/SignupEndpoint.ts +2 -2
  9. package/src/endpoints/global/email/CreateEmailEndpoint.ts +120 -0
  10. package/src/endpoints/global/email/GetEmailEndpoint.ts +51 -0
  11. package/src/endpoints/global/email/PatchEmailEndpoint.ts +108 -0
  12. package/src/endpoints/global/events/GetEventsEndpoint.ts +223 -0
  13. package/src/endpoints/global/events/PatchEventsEndpoint.ts +319 -0
  14. package/src/endpoints/global/members/GetMembersEndpoint.ts +124 -48
  15. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +86 -109
  16. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +2 -1
  17. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -0
  18. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -2
  19. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +1 -1
  20. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +43 -25
  21. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +26 -7
  22. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +23 -22
  23. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +136 -123
  24. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
  25. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
  26. package/src/helpers/AdminPermissionChecker.ts +54 -3
  27. package/src/helpers/AuthenticatedStructures.ts +88 -23
  28. package/src/helpers/Context.ts +4 -0
  29. package/src/helpers/EmailResumer.ts +17 -0
  30. package/src/helpers/MemberUserSyncer.ts +221 -0
  31. package/src/seeds/1722256498-group-update-occupancy.ts +52 -0
package/index.ts CHANGED
@@ -10,6 +10,7 @@ import { sleep } from "@stamhoofd/utility";
10
10
 
11
11
  import { areCronsRunning, crons, stopCronScheduling } from './src/crons';
12
12
  import { ContextMiddleware } from "./src/middleware/ContextMiddleware";
13
+ import { resumeEmails } from "./src/helpers/EmailResumer";
13
14
 
14
15
  process.on("unhandledRejection", (error: Error) => {
15
16
  console.error("unhandledRejection");
@@ -81,6 +82,8 @@ const start = async () => {
81
82
 
82
83
  routerServer.listen(STAMHOOFD.PORT ?? 9090);
83
84
 
85
+ resumeEmails().catch(console.error);
86
+
84
87
  if (routerServer.server) {
85
88
  // Default timeout is a bit too short
86
89
  routerServer.server.timeout = 61000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/cookie": "^0.5.1",
25
- "@types/luxon": "^2.0.8",
25
+ "@types/luxon": "3.4.2",
26
26
  "@types/mailparser": "3.4.4",
27
27
  "@types/mysql": "^2.15.20",
28
28
  "@types/node": "^18.11.17",
@@ -41,7 +41,7 @@
41
41
  "formidable": "3.5.1",
42
42
  "handlebars": "^4.7.7",
43
43
  "jsonwebtoken": "9.0.0",
44
- "luxon": "^2.2.0",
44
+ "luxon": "3.4.4",
45
45
  "mailparser": "3.7.0",
46
46
  "mockdate": "^3.0.2",
47
47
  "mysql": "^2.18.1",
@@ -50,5 +50,5 @@
50
50
  "postmark": "4.0.2",
51
51
  "stripe": "^11.5.0"
52
52
  },
53
- "gitHead": "9a2484194d078935d8b50c4aee790196d798435e"
53
+ "gitHead": "5313c328ca0e90544bf198bc6bebc90d7dced2aa"
54
54
  }
@@ -1,6 +1,6 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { SQL, SQLAlias, SQLSum, SQLCount, SQLDistinct, SQLSelectAs } from '@stamhoofd/sql';
3
- import { ChargeMembershipsSummary } from '@stamhoofd/structures';
3
+ import { ChargeMembershipsSummary, ChargeMembershipsTypeSummary } from '@stamhoofd/structures';
4
4
  import { Context } from '../../../helpers/Context';
5
5
 
6
6
 
@@ -81,8 +81,69 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
81
81
  memberships: memberships ?? 0,
82
82
  members: members ?? 0,
83
83
  price: price ?? 0,
84
- organizations: organizations ?? 0
84
+ organizations: organizations ?? 0,
85
+ membershipsPerType: await this.fetchPerType()
85
86
  })
86
87
  );
87
88
  }
89
+
90
+ async fetchPerType() {
91
+ const query = SQL
92
+ .select(
93
+ SQL.column('member_platform_memberships', 'membershipTypeId'),
94
+ new SQLSelectAs(
95
+ new SQLCount(
96
+ new SQLDistinct(
97
+ SQL.column('member_platform_memberships', 'id')
98
+ )
99
+ ),
100
+ new SQLAlias('data__memberships')
101
+ ),
102
+ new SQLSelectAs(
103
+ new SQLCount(
104
+ new SQLDistinct(
105
+ SQL.column('member_platform_memberships', 'memberId')
106
+ )
107
+ ),
108
+ new SQLAlias('data__members')
109
+ ),
110
+ new SQLSelectAs(
111
+ new SQLCount(
112
+ new SQLDistinct(
113
+ SQL.column('member_platform_memberships', 'organizationId')
114
+ )
115
+ ),
116
+ new SQLAlias('data__organizations')
117
+ ),
118
+ new SQLSelectAs(
119
+ new SQLSum(
120
+ SQL.column('member_platform_memberships', 'price')
121
+ ),
122
+ new SQLAlias('data__price')
123
+ )
124
+ )
125
+ .from(
126
+ SQL.table('member_platform_memberships')
127
+ );
128
+ query.where(SQL.column('invoiceId'), null)
129
+ query.andWhere(SQL.column('invoiceItemDetailId'), null)
130
+ query.groupBy(SQL.column('member_platform_memberships', 'membershipTypeId'));
131
+
132
+
133
+ const result = await query.fetch();
134
+ console.log(result);
135
+
136
+ const membershipsPerType = new Map<string, ChargeMembershipsTypeSummary>();
137
+
138
+ for (const row of result) {
139
+ membershipsPerType.set(row['member_platform_memberships']['membershipTypeId'] as string, ChargeMembershipsTypeSummary.create({
140
+ memberships: row['data']['memberships'] as number,
141
+ members: row['data']['members'] as number,
142
+ price: row['data']['price'] as number,
143
+ organizations: row['data']['organizations'] as number
144
+ }));
145
+ }
146
+
147
+ return membershipsPerType;
148
+ }
88
149
  }
@@ -3,14 +3,15 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
3
3
  import { SimpleError } from "@simonbackx/simple-errors";
4
4
  import { Email } from '@stamhoofd/email';
5
5
  import { PasswordToken, User } from '@stamhoofd/models';
6
- import { User as UserStruct,UserPermissions } from "@stamhoofd/structures";
6
+ import { User as UserStruct,UserPermissions, UserWithMembers } from "@stamhoofd/structures";
7
7
  import { Formatter } from '@stamhoofd/utility';
8
8
 
9
9
  import { Context } from '../../helpers/Context';
10
+ import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures';
10
11
  type Params = Record<string, never>;
11
12
  type Query = undefined;
12
13
  type Body = UserStruct
13
- type ResponseBody = UserStruct
14
+ type ResponseBody = UserWithMembers
14
15
 
15
16
  export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
16
17
  bodyDecoder = UserStruct as Decoder<UserStruct>
@@ -132,6 +133,8 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
132
133
  });
133
134
  }
134
135
 
135
- return new Response(UserStruct.create({...admin, hasAccount: admin.hasAccount()}));
136
+ return new Response(
137
+ await AuthenticatedStructures.userWithMembers(admin)
138
+ );
136
139
  }
137
140
  }
@@ -0,0 +1,41 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { UserWithMembers } from '@stamhoofd/structures';
3
+
4
+ import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures';
5
+ import { Context } from '../../helpers/Context';
6
+ import { User } from '@stamhoofd/models';
7
+
8
+ type Params = {id: string};
9
+ type Query = undefined;
10
+ type Body = undefined;
11
+ type ResponseBody = UserWithMembers;
12
+
13
+ export class GetUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+
15
+ protected doesMatch(request: Request): [true, Params] | [false] {
16
+ if (request.method != "GET") {
17
+ return [false];
18
+ }
19
+
20
+ const params = Endpoint.parseParameters(request.url, "/user/@id", {id: String});
21
+
22
+ if (params) {
23
+ return [true, params as Params];
24
+ }
25
+ return [false];
26
+ }
27
+
28
+ async handle(request: DecodedRequest<Params, Query, Body>) {
29
+ await Context.setOptionalOrganizationScope()
30
+ await Context.authenticate()
31
+
32
+ const user = await User.getByID(request.params.id)
33
+ if (!user || !(await Context.auth.canAccessUser(user)) ){
34
+ throw Context.auth.error()
35
+ }
36
+
37
+ return new Response(
38
+ await AuthenticatedStructures.userWithMembers(user)
39
+ );
40
+ }
41
+ }
@@ -1,12 +1,13 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { MyUser, User as UserStruct } from '@stamhoofd/structures';
2
+ import { UserWithMembers } from '@stamhoofd/structures';
3
3
 
4
+ import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures';
4
5
  import { Context } from '../../helpers/Context';
5
6
 
6
7
  type Params = Record<string, never>;
7
8
  type Query = undefined;
8
9
  type Body = undefined;
9
- type ResponseBody = UserStruct|MyUser;
10
+ type ResponseBody = UserWithMembers;
10
11
 
11
12
  export class GetUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
12
13
 
@@ -27,31 +28,8 @@ export class GetUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
27
28
  await Context.setOptionalOrganizationScope()
28
29
  const {user} = await Context.authenticate({allowWithoutAccount: true})
29
30
 
30
- if (request.request.getVersion() < 243) {
31
- // Password
32
- const st = MyUser.create({
33
- firstName: user.firstName,
34
- lastName: user.lastName,
35
- id: user.id,
36
- organizationId: user.organizationId,
37
- email: user.email,
38
- verified: user.verified,
39
- permissions: user.permissions,
40
- hasAccount: user.hasAccount()
41
- })
42
- return new Response(st);
43
- }
44
-
45
- const st = UserStruct.create({
46
- firstName: user.firstName,
47
- lastName: user.lastName,
48
- id: user.id,
49
- organizationId: user.organizationId,
50
- email: user.email,
51
- verified: user.verified,
52
- permissions: user.permissions,
53
- hasAccount: user.hasAccount()
54
- })
55
- return new Response(st);
31
+ return new Response(
32
+ await AuthenticatedStructures.userWithMembers(user)
33
+ );
56
34
  }
57
35
  }
@@ -1,15 +1,17 @@
1
1
  import { AutoEncoderPatchType, Decoder } 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, PasswordToken, Token, User } from '@stamhoofd/models';
5
- import { NewUser, PermissionLevel, SignupResponse, User as UserStruct,UserPermissions } from "@stamhoofd/structures";
4
+ import { EmailVerificationCode, Member, PasswordToken, Token, User } from '@stamhoofd/models';
5
+ import { NewUser, PermissionLevel, SignupResponse, UserPermissions, UserWithMembers } from "@stamhoofd/structures";
6
6
 
7
7
  import { Context } from '../../helpers/Context';
8
+ import { MemberUserSyncer } from '../../helpers/MemberUserSyncer';
9
+ import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures';
8
10
 
9
11
  type Params = { id: string };
10
12
  type Query = undefined;
11
13
  type Body = AutoEncoderPatchType<NewUser>
12
- type ResponseBody = UserStruct
14
+ type ResponseBody = UserWithMembers
13
15
 
14
16
  export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
15
17
  bodyDecoder = NewUser.patchType() as Decoder<AutoEncoderPatchType<NewUser>>
@@ -46,8 +48,23 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
46
48
  }
47
49
 
48
50
  if (await Context.auth.canEditUserName(editUser)) {
49
- editUser.firstName = request.body.firstName ?? editUser.firstName
50
- editUser.lastName = request.body.lastName ?? editUser.lastName
51
+ if (editUser.memberId) {
52
+ const member = await Member.getWithRegistrations(editUser.memberId)
53
+ if (member) {
54
+ member.details.firstName = request.body.firstName ?? member.details.firstName
55
+ member.details.lastName = request.body.lastName ?? member.details.lastName
56
+
57
+ editUser.firstName = member.details.firstName
58
+ editUser.lastName = member.details.lastName
59
+ await member.save()
60
+
61
+ // Also propage the name change to other users of the same member if needed
62
+ await MemberUserSyncer.onChangeMember(member)
63
+ }
64
+ } else {
65
+ editUser.firstName = request.body.firstName ?? editUser.firstName
66
+ editUser.lastName = request.body.lastName ?? editUser.lastName
67
+ }
51
68
  }
52
69
 
53
70
  if (request.body.permissions !== undefined) {
@@ -117,6 +134,8 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
117
134
  }
118
135
  }
119
136
 
120
- return new Response(UserStruct.create({...editUser, hasAccount: editUser.hasAccount()}));
137
+ return new Response(
138
+ await AuthenticatedStructures.userWithMembers(editUser)
139
+ );
121
140
  }
122
141
  }
@@ -34,10 +34,10 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
34
34
  const u = await User.getForRegister(organization?.id ?? null, request.body.email)
35
35
 
36
36
  // Don't optimize. Always run two queries atm.
37
- let user = await User.register(
37
+ let user = u ? undefined : (await User.register(
38
38
  organization,
39
39
  request.body
40
- );
40
+ ));
41
41
 
42
42
  let sendCode = true
43
43
 
@@ -0,0 +1,120 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
3
+ import { Email, EmailTemplate, RateLimiter } from '@stamhoofd/models';
4
+ import { EmailPreview, EmailStatus, Email as EmailStruct, Version, EmailTemplate as EmailTemplateStruct } from "@stamhoofd/structures";
5
+
6
+ import { Context } from '../../../helpers/Context';
7
+ import { SQL } from '@stamhoofd/sql';
8
+
9
+ type Params = Record<string, never>;
10
+ type Query = undefined;
11
+ type Body = EmailStruct
12
+ type ResponseBody = EmailPreview;
13
+
14
+ export const paidEmailRateLimiter = new RateLimiter({
15
+ limits: [
16
+ {
17
+ // Max 5.000 emails a day
18
+ limit: 5000,
19
+ duration: 24 * 60 * 1000 * 60
20
+ },
21
+ {
22
+ // 10.000 requests per week
23
+ limit: 10000,
24
+ duration: 24 * 60 * 1000 * 60 * 7
25
+ }
26
+ ]
27
+ });
28
+
29
+ export const freeEmailRateLimiter = new RateLimiter({
30
+ limits: [
31
+ {
32
+ // Max 100 a day
33
+ limit: 100,
34
+ duration: 24 * 60 * 1000 * 60
35
+ },
36
+ {
37
+ // Max 200 a week
38
+ limit: 200,
39
+ duration: 7 * 24 * 60 * 1000 * 60
40
+ }
41
+ ]
42
+ });
43
+
44
+ /**
45
+ * 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
46
+ */
47
+
48
+ export class CreateEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
49
+ bodyDecoder = EmailStruct as Decoder<EmailStruct>
50
+
51
+ protected doesMatch(request: Request): [true, Params] | [false] {
52
+ if (request.method != "POST") {
53
+ return [false];
54
+ }
55
+
56
+ const params = Endpoint.parseParameters(request.url, "/email", {});
57
+
58
+ if (params) {
59
+ return [true, params as Params];
60
+ }
61
+ return [false];
62
+ }
63
+
64
+ async handle(request: DecodedRequest<Params, Query, Body>) {
65
+ const organization = await Context.setOptionalOrganizationScope();
66
+ const {user} = await Context.authenticate()
67
+
68
+ if (!Context.auth.canSendEmails()) {
69
+ throw Context.auth.error()
70
+ }
71
+
72
+ const model = new Email();
73
+ model.userId = user.id;
74
+ model.organizationId = organization?.id ?? null;
75
+ model.recipientFilter = request.body.recipientFilter;
76
+
77
+ model.subject = request.body.subject;
78
+ model.html = request.body.html;
79
+ model.text = request.body.text;
80
+ model.json = request.body.json;
81
+ model.status = request.body.status;
82
+ model.attachments = request.body.attachments;
83
+ model.fromAddress = request.body.fromAddress;
84
+ model.fromName = request.body.fromName;
85
+
86
+ // Check default
87
+ if (JSON.stringify(model.json).length < 3 && model.recipientFilter.filters[0].type && EmailTemplateStruct.getDefaultForRecipient(model.recipientFilter.filters[0].type)) {
88
+ const type = EmailTemplateStruct.getDefaultForRecipient(model.recipientFilter.filters[0].type)
89
+
90
+ // Most specific template: for specific group
91
+ let templates = (await EmailTemplate.where({ type, organizationId: organization?.id ?? null, groupId: null }))
92
+
93
+ // Then default
94
+ if (templates.length == 0 && organization) {
95
+ templates = (await EmailTemplate.where({ type, organizationId: null, groupId: null }))
96
+ }
97
+
98
+ if (templates.length == 0) {
99
+ // No default
100
+ } else {
101
+ const defaultTemplate = templates[0]
102
+ model.html = defaultTemplate.html;
103
+ model.text = defaultTemplate.text;
104
+ model.subject = defaultTemplate.subject;
105
+ model.json = defaultTemplate.json;
106
+ }
107
+ }
108
+
109
+ await model.save();
110
+ await model.buildExampleRecipient()
111
+ model.updateCount();
112
+
113
+ if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
114
+ model.send().catch(console.error)
115
+ }
116
+
117
+
118
+ return new Response(await model.getPreviewStructure());
119
+ }
120
+ }
@@ -0,0 +1,51 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { Email } from '@stamhoofd/models';
3
+ import { EmailPreview } from "@stamhoofd/structures";
4
+
5
+ import { SimpleError } from '@simonbackx/simple-errors';
6
+ import { Context } from '../../../helpers/Context';
7
+
8
+ type Params = {id: string};
9
+ type Query = undefined;
10
+ type Body = undefined
11
+ type ResponseBody = EmailPreview;
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 GetEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
+ protected doesMatch(request: Request): [true, Params] | [false] {
19
+ if (request.method != "GET") {
20
+ return [false];
21
+ }
22
+
23
+ const params = Endpoint.parseParameters(request.url, "/email/@id", {id: String});
24
+
25
+ if (params) {
26
+ return [true, params as Params];
27
+ }
28
+ return [false];
29
+ }
30
+
31
+ async handle(request: DecodedRequest<Params, Query, Body>) {
32
+ const organization = await Context.setOptionalOrganizationScope();
33
+ const {user} = await Context.authenticate()
34
+
35
+ if (!Context.auth.canSendEmails()) {
36
+ throw Context.auth.error()
37
+ }
38
+
39
+ const model = await Email.getByID(request.params.id);
40
+ if (!model || model.userId !== user.id || (model.organizationId !== (organization?.id ?? null))) {
41
+ throw new SimpleError({
42
+ code: "not_found",
43
+ human: "Email not found",
44
+ message: 'Deze e-mail bestaat niet of is verwijderd',
45
+ statusCode: 404
46
+ })
47
+ }
48
+
49
+ return new Response(await model.getPreviewStructure());
50
+ }
51
+ }
@@ -0,0 +1,108 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
+ import { Email } from '@stamhoofd/models';
3
+ import { EmailPreview, EmailStatus, Email as EmailStruct } from "@stamhoofd/structures";
4
+
5
+ import { SimpleError } from '@simonbackx/simple-errors';
6
+ import { Context } from '../../../helpers/Context';
7
+ import { AutoEncoderPatchType, Decoder, patchObject } from "@simonbackx/simple-encoding";
8
+
9
+ type Params = {id: string};
10
+ type Query = undefined;
11
+ type Body = AutoEncoderPatchType<EmailStruct>
12
+ type ResponseBody = EmailPreview;
13
+
14
+ /**
15
+ * 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
16
+ */
17
+
18
+ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ bodyDecoder = EmailStruct.patchType() as Decoder<AutoEncoderPatchType<EmailStruct>>
20
+
21
+ protected doesMatch(request: Request): [true, Params] | [false] {
22
+ if (request.method != "PATCH") {
23
+ return [false];
24
+ }
25
+
26
+ const params = Endpoint.parseParameters(request.url, "/email/@id", {id: String});
27
+
28
+ if (params) {
29
+ return [true, params as Params];
30
+ }
31
+ return [false];
32
+ }
33
+
34
+ async handle(request: DecodedRequest<Params, Query, Body>) {
35
+ const organization = await Context.setOptionalOrganizationScope();
36
+ const {user} = await Context.authenticate()
37
+
38
+ if (!Context.auth.canSendEmails()) {
39
+ throw Context.auth.error()
40
+ }
41
+
42
+ const model = await Email.getByID(request.params.id);
43
+ if (!model || model.userId !== user.id || (model.organizationId !== (organization?.id ?? null))) {
44
+ throw new SimpleError({
45
+ code: "not_found",
46
+ human: "Email not found",
47
+ message: 'Deze e-mail bestaat niet of is verwijderd',
48
+ statusCode: 404
49
+ })
50
+ }
51
+
52
+ if (model.status !== EmailStatus.Draft) {
53
+ throw new SimpleError({
54
+ code: "not_draft",
55
+ human: "Email is not a draft",
56
+ message: 'Deze e-mail is al verzonden en kan niet meer aangepast worden',
57
+ statusCode: 400
58
+ })
59
+ }
60
+
61
+ let rebuild = false;
62
+
63
+ if (request.body.subject !== undefined) {
64
+ model.subject = request.body.subject;
65
+ }
66
+
67
+ if (request.body.html !== undefined) {
68
+ model.html = request.body.html;
69
+ }
70
+
71
+ if (request.body.text !== undefined) {
72
+ model.text = request.body.text;
73
+ }
74
+
75
+ if (request.body.json !== undefined) {
76
+ model.json = request.body.json;
77
+ }
78
+
79
+ if (request.body.fromAddress !== undefined) {
80
+ model.fromAddress = request.body.fromAddress;
81
+ }
82
+
83
+ if (request.body.fromName !== undefined) {
84
+ model.fromName = request.body.fromName;
85
+ }
86
+
87
+ if (request.body.recipientFilter) {
88
+ model.recipientFilter = patchObject(model.recipientFilter, request.body.recipientFilter);
89
+ rebuild = true;
90
+ }
91
+
92
+ await model.save();
93
+
94
+ if (rebuild) {
95
+ await model.buildExampleRecipient()
96
+ model.updateCount()
97
+
98
+ // Force null - because we have stale data
99
+ model.recipientCount = null
100
+ }
101
+
102
+ if (request.body.status === EmailStatus.Sending || request.body.status === EmailStatus.Sent) {
103
+ model.send().catch(console.error)
104
+ }
105
+
106
+ return new Response(await model.getPreviewStructure());
107
+ }
108
+ }