@stamhoofd/backend 2.2.0 → 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.
- package/.env.json +2 -2
- package/index.ts +3 -0
- package/package.json +4 -4
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +3 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +6 -3
- package/src/endpoints/auth/GetOtherUserEndpoint.ts +41 -0
- package/src/endpoints/auth/GetUserEndpoint.ts +6 -28
- package/src/endpoints/auth/PatchUserEndpoint.ts +25 -6
- package/src/endpoints/auth/SignupEndpoint.ts +2 -2
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +120 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +51 -0
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +108 -0
- package/src/endpoints/global/events/GetEventsEndpoint.ts +223 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +319 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +124 -48
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +86 -109
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +2 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +43 -25
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +26 -7
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +23 -22
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +136 -123
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
- package/src/helpers/AdminPermissionChecker.ts +54 -3
- package/src/helpers/AuthenticatedStructures.ts +88 -23
- package/src/helpers/Context.ts +4 -0
- package/src/helpers/EmailResumer.ts +17 -0
- package/src/helpers/MemberUserSyncer.ts +221 -0
- package/src/seeds/1722256498-group-update-occupancy.ts +52 -0
package/.env.json
CHANGED
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"rendererApi": "renderer.stamhoofd"
|
|
25
25
|
},
|
|
26
26
|
"translationNamespace": "digit",
|
|
27
|
-
"userMode": "
|
|
27
|
+
"userMode": "platform",
|
|
28
28
|
|
|
29
29
|
"PORT": 9091,
|
|
30
30
|
"DB_HOST": "127.0.0.1",
|
|
31
31
|
"DB_USER": "root",
|
|
32
32
|
"DB_PASS": "root",
|
|
33
|
-
"DB_DATABASE": "stamhoofd",
|
|
33
|
+
"DB_DATABASE": "ksa-stamhoofd",
|
|
34
34
|
|
|
35
35
|
"SMTP_HOST": "0.0.0.0",
|
|
36
36
|
"SMTP_USERNAME": "username",
|
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
|
+
"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": "
|
|
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": "
|
|
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": "
|
|
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
|
}
|
|
@@ -22,6 +22,9 @@ export const filterCompilers: SQLFilterDefinitions = {
|
|
|
22
22
|
name: createSQLExpressionFilterCompiler(
|
|
23
23
|
SQL.column('organizations', 'name')
|
|
24
24
|
),
|
|
25
|
+
active: createSQLExpressionFilterCompiler(
|
|
26
|
+
SQL.column('organizations', 'active')
|
|
27
|
+
),
|
|
25
28
|
city: createSQLExpressionFilterCompiler(
|
|
26
29
|
SQL.jsonValue(SQL.column('organizations', 'address'), '$.value.city'),
|
|
27
30
|
undefined,
|
|
@@ -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 =
|
|
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(
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
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 =
|
|
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.
|
|
50
|
-
|
|
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(
|
|
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
|
+
}
|