@stamhoofd/backend 2.97.3 → 2.98.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.98.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
46
46
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
47
47
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
48
|
-
"@stamhoofd/backend-i18n": "2.
|
|
49
|
-
"@stamhoofd/backend-middleware": "2.
|
|
50
|
-
"@stamhoofd/email": "2.
|
|
51
|
-
"@stamhoofd/models": "2.
|
|
52
|
-
"@stamhoofd/queues": "2.
|
|
53
|
-
"@stamhoofd/sql": "2.
|
|
54
|
-
"@stamhoofd/structures": "2.
|
|
55
|
-
"@stamhoofd/utility": "2.
|
|
48
|
+
"@stamhoofd/backend-i18n": "2.98.0",
|
|
49
|
+
"@stamhoofd/backend-middleware": "2.98.0",
|
|
50
|
+
"@stamhoofd/email": "2.98.0",
|
|
51
|
+
"@stamhoofd/models": "2.98.0",
|
|
52
|
+
"@stamhoofd/queues": "2.98.0",
|
|
53
|
+
"@stamhoofd/sql": "2.98.0",
|
|
54
|
+
"@stamhoofd/structures": "2.98.0",
|
|
55
|
+
"@stamhoofd/utility": "2.98.0",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
57
57
|
"axios": "^1.8.2",
|
|
58
58
|
"cookie": "^0.7.0",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"publishConfig": {
|
|
71
71
|
"access": "public"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "d15c1756a1e10788fa2736efcc6abbc2e3f365a6"
|
|
74
74
|
}
|
|
@@ -4,16 +4,20 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
4
4
|
import { EmailAddress } from '@stamhoofd/email';
|
|
5
5
|
import { Organization } from '@stamhoofd/models';
|
|
6
6
|
import { EmailAddressSettings, OrganizationSimple } from '@stamhoofd/structures';
|
|
7
|
+
import { Context } from '../../../helpers/Context';
|
|
7
8
|
|
|
8
9
|
type Params = Record<string, never>;
|
|
9
10
|
type Body = undefined;
|
|
10
11
|
|
|
11
12
|
class Query extends AutoEncoder {
|
|
12
|
-
@field({ decoder: StringDecoder })
|
|
13
|
-
id: string;
|
|
13
|
+
@field({ decoder: StringDecoder, nullable: true, optional: true })
|
|
14
|
+
id: string | null = null;
|
|
14
15
|
|
|
15
|
-
@field({ decoder: StringDecoder })
|
|
16
|
-
token: string;
|
|
16
|
+
@field({ decoder: StringDecoder, nullable: true, optional: true })
|
|
17
|
+
token: string | null = null;
|
|
18
|
+
|
|
19
|
+
@field({ decoder: StringDecoder, nullable: true, optional: true })
|
|
20
|
+
email: string | null = null;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
type ResponseBody = EmailAddressSettings;
|
|
@@ -35,19 +39,75 @@ export class GetEmailAddressEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
if (request.query.id) {
|
|
43
|
+
if (!request.query.token) {
|
|
44
|
+
throw new SimpleError({
|
|
45
|
+
code: 'missing_field',
|
|
46
|
+
message: 'Missing token',
|
|
47
|
+
field: 'token',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const email = await EmailAddress.getByID(request.query.id);
|
|
52
|
+
if (!email || email.token !== request.query.token || request.query.token.length < 10 || request.query.id.length < 10) {
|
|
53
|
+
throw new SimpleError({
|
|
54
|
+
code: 'invalid_fields',
|
|
55
|
+
message: 'Invalid token or id',
|
|
56
|
+
human: $t(`ceacb5a8-7777-4366-abcb-9dd90ffb832e`),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const organization = email.organizationId ? (await Organization.getByID(email.organizationId)) : undefined;
|
|
61
|
+
return new Response(EmailAddressSettings.create({
|
|
62
|
+
...email,
|
|
63
|
+
organization: organization ? OrganizationSimple.create(organization) : null,
|
|
64
|
+
}));
|
|
45
65
|
}
|
|
66
|
+
else {
|
|
67
|
+
if (!request.query.email) {
|
|
68
|
+
throw new SimpleError({
|
|
69
|
+
code: 'missing_field',
|
|
70
|
+
message: 'Missing email or id',
|
|
71
|
+
field: 'email',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const organization = await Context.setOptionalOrganizationScope();
|
|
76
|
+
await Context.authenticate();
|
|
77
|
+
|
|
78
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
79
|
+
throw Context.auth.error();
|
|
80
|
+
}
|
|
46
81
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
82
|
+
const query = EmailAddress.select().where(
|
|
83
|
+
'email', request.query.email,
|
|
84
|
+
);
|
|
85
|
+
if (organization) {
|
|
86
|
+
query.andWhere('organizationId', organization.id);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// No need
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const emails = await query.fetch();
|
|
93
|
+
|
|
94
|
+
if (emails.length === 0) {
|
|
95
|
+
throw new SimpleError({
|
|
96
|
+
code: 'not_found',
|
|
97
|
+
message: 'Email not found',
|
|
98
|
+
human: $t(`9ddb6616-f62d-4c91-82a9-e5cf398e4c4a`),
|
|
99
|
+
statusCode: 404,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new Response(EmailAddressSettings.create({
|
|
104
|
+
email: request.query.email,
|
|
105
|
+
unsubscribedAll: !!emails.find(e => e.unsubscribedAll),
|
|
106
|
+
unsubscribedMarketing: !!emails.find(e => e.unsubscribedMarketing),
|
|
107
|
+
hardBounce: !!emails.find(e => e.hardBounce),
|
|
108
|
+
markedAsSpam: !!emails.find(e => e.markedAsSpam),
|
|
109
|
+
organization: organization ? OrganizationSimple.create(organization) : null,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
52
112
|
}
|
|
53
113
|
}
|
|
@@ -2,22 +2,39 @@ import { AutoEncoder, BooleanDecoder, Decoder, field, StringDecoder } from '@sim
|
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
4
|
import { EmailAddress } from '@stamhoofd/email';
|
|
5
|
+
import { Context } from '../../../helpers/Context';
|
|
6
|
+
import { SESv2Client, DeleteSuppressedDestinationCommand } from '@aws-sdk/client-sesv2'; // ES Modules import
|
|
5
7
|
|
|
6
8
|
type Params = Record<string, never>;
|
|
7
9
|
type Query = undefined;
|
|
8
10
|
|
|
9
11
|
class Body extends AutoEncoder {
|
|
10
|
-
@field({ decoder: StringDecoder })
|
|
11
|
-
id: string;
|
|
12
|
+
@field({ decoder: StringDecoder, nullable: true, optional: true })
|
|
13
|
+
id: string | null = null;
|
|
12
14
|
|
|
13
|
-
@field({ decoder: StringDecoder })
|
|
14
|
-
token: string;
|
|
15
|
+
@field({ decoder: StringDecoder, nullable: true, optional: true })
|
|
16
|
+
token: string | null = null;
|
|
17
|
+
|
|
18
|
+
@field({ decoder: StringDecoder, nullable: true, optional: true })
|
|
19
|
+
email: string | null = null;
|
|
15
20
|
|
|
16
21
|
@field({ decoder: BooleanDecoder, optional: true })
|
|
17
22
|
unsubscribedMarketing?: boolean;
|
|
18
23
|
|
|
19
24
|
@field({ decoder: BooleanDecoder, optional: true })
|
|
20
25
|
unsubscribedAll?: boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set to false to unblock
|
|
29
|
+
*/
|
|
30
|
+
@field({ decoder: BooleanDecoder, optional: true })
|
|
31
|
+
hardBounce?: boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Set to false to unblock
|
|
35
|
+
*/
|
|
36
|
+
@field({ decoder: BooleanDecoder, optional: true })
|
|
37
|
+
markedAsSpam?: boolean;
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
type ResponseBody = undefined;
|
|
@@ -39,19 +56,90 @@ export class ManageEmailAddressEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
if (request.body.id) {
|
|
60
|
+
if (!request.body.token) {
|
|
61
|
+
throw new SimpleError({
|
|
62
|
+
code: 'missing_field',
|
|
63
|
+
message: 'Missing token',
|
|
64
|
+
field: 'token',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const email = await EmailAddress.getByID(request.body.id);
|
|
68
|
+
if (!email || email.token !== request.body.token || request.body.token.length < 10 || request.body.id.length < 10) {
|
|
69
|
+
throw new SimpleError({
|
|
70
|
+
code: 'invalid_fields',
|
|
71
|
+
message: 'Invalid token or id',
|
|
72
|
+
human: $t(`ceacb5a8-7777-4366-abcb-9dd90ffb832e`),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
email.unsubscribedAll = request.body.unsubscribedAll ?? email.unsubscribedAll;
|
|
77
|
+
email.unsubscribedMarketing = request.body.unsubscribedMarketing ?? email.unsubscribedMarketing;
|
|
78
|
+
|
|
79
|
+
await email.save();
|
|
80
|
+
return new Response(undefined);
|
|
49
81
|
}
|
|
82
|
+
else {
|
|
83
|
+
if (!request.body.email) {
|
|
84
|
+
throw new SimpleError({
|
|
85
|
+
code: 'missing_field',
|
|
86
|
+
message: 'Missing email or id',
|
|
87
|
+
field: 'email',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
50
90
|
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
const organization = await Context.setOptionalOrganizationScope();
|
|
92
|
+
await Context.authenticate();
|
|
53
93
|
|
|
54
|
-
|
|
55
|
-
|
|
94
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
95
|
+
throw Context.auth.error();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const query = EmailAddress.select().where(
|
|
99
|
+
'email', request.body.email,
|
|
100
|
+
);
|
|
101
|
+
if (organization) {
|
|
102
|
+
query.andWhere('organizationId', organization.id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const emails = await query.fetch();
|
|
106
|
+
|
|
107
|
+
if (emails.length === 0) {
|
|
108
|
+
throw new SimpleError({
|
|
109
|
+
code: 'not_found',
|
|
110
|
+
message: 'Email not found',
|
|
111
|
+
human: $t(`9ddb6616-f62d-4c91-82a9-e5cf398e4c4a`),
|
|
112
|
+
statusCode: 404,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
console.log('Updated email address settings', request.body);
|
|
116
|
+
const client = new SESv2Client({});
|
|
117
|
+
|
|
118
|
+
if (request.body.hardBounce === false || request.body.markedAsSpam === false) {
|
|
119
|
+
// Remove from AWS suppression list
|
|
120
|
+
// todo
|
|
121
|
+
try {
|
|
122
|
+
const input = {
|
|
123
|
+
EmailAddress: request.body.email,
|
|
124
|
+
};
|
|
125
|
+
const command = new DeleteSuppressedDestinationCommand(input);
|
|
126
|
+
const response = await client.send(command);
|
|
127
|
+
console.log('Removed from AWS suppression list', request.body.email, response);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error('Error removing from suppression list', request.body.email, error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const email of emails) {
|
|
135
|
+
email.unsubscribedAll = request.body.unsubscribedAll ?? email.unsubscribedAll;
|
|
136
|
+
email.unsubscribedMarketing = request.body.unsubscribedMarketing ?? email.unsubscribedMarketing;
|
|
137
|
+
email.hardBounce = request.body.hardBounce ?? email.hardBounce;
|
|
138
|
+
email.markedAsSpam = request.body.markedAsSpam ?? email.markedAsSpam;
|
|
139
|
+
await email.save();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return new Response(undefined);
|
|
143
|
+
}
|
|
56
144
|
}
|
|
57
145
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { Email, EmailRecipient } from '@stamhoofd/models';
|
|
3
|
+
import { EmailRecipient as EmailRecipientStruct, EmailStatus, PermissionLevel } 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 = EmailRecipientStruct;
|
|
12
|
+
|
|
13
|
+
export class RetryEmailRecipientEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
14
|
+
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
15
|
+
if (request.method !== 'POST') {
|
|
16
|
+
return [false];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const params = Endpoint.parseParameters(request.url, '/email-recipients/@id/retry', { 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.setOptionalOrganizationScope();
|
|
29
|
+
await Context.authenticate();
|
|
30
|
+
|
|
31
|
+
if (!await Context.auth.canReadEmails(organization)) {
|
|
32
|
+
// Fast fail before query
|
|
33
|
+
throw Context.auth.error();
|
|
34
|
+
}
|
|
35
|
+
const emailRecipient = await EmailRecipient.getByID(request.params.id);
|
|
36
|
+
if (!emailRecipient || emailRecipient.organizationId !== (organization?.id ?? null)) {
|
|
37
|
+
throw new SimpleError({
|
|
38
|
+
code: 'not_found',
|
|
39
|
+
message: 'Email recipient not found',
|
|
40
|
+
human: $t(`2e79bf4a-878b-43b0-902b-9d080c8b7fdf`),
|
|
41
|
+
statusCode: 404,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const model = await Email.getByID(emailRecipient.emailId);
|
|
46
|
+
if (!model || (model.organizationId !== (organization?.id ?? null))) {
|
|
47
|
+
throw new SimpleError({
|
|
48
|
+
code: 'not_found',
|
|
49
|
+
message: 'Email not found',
|
|
50
|
+
human: $t(`9ddb6616-f62d-4c91-82a9-e5cf398e4c4a`),
|
|
51
|
+
statusCode: 404,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!await Context.auth.canAccessEmail(model, PermissionLevel.Write)) {
|
|
56
|
+
throw Context.auth.error();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (model.status !== EmailStatus.Sent) {
|
|
60
|
+
throw new SimpleError({
|
|
61
|
+
code: 'not_sent',
|
|
62
|
+
message: 'Cant retry email that is not sent',
|
|
63
|
+
statusCode: 400,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (emailRecipient.sentAt) {
|
|
68
|
+
throw new SimpleError({
|
|
69
|
+
code: 'already_sent',
|
|
70
|
+
message: 'Cant retry email recipient that is already sent',
|
|
71
|
+
human: $t(`f3f837eb-d6f7-4c6e-9477-d566a03e09b1`),
|
|
72
|
+
statusCode: 400,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Retry
|
|
77
|
+
await model.resumeSending(emailRecipient.id);
|
|
78
|
+
|
|
79
|
+
await emailRecipient.refresh();
|
|
80
|
+
|
|
81
|
+
return new Response((await EmailRecipient.getStructures([emailRecipient]))[0]);
|
|
82
|
+
}
|
|
83
|
+
}
|