@stamhoofd/backend 2.85.4 → 2.86.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/.nvmrc +1 -1
- package/index.ts +9 -231
- package/migrations.ts +11 -33
- package/package.json +21 -21
- package/src/boot.ts +240 -0
- package/src/crons/clearExcelCache.test.ts +4 -1
- package/src/crons/index.ts +0 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +4 -0
- package/src/migrate.ts +65 -0
- package/src/seeds/0000000001-development-user.ts +49 -0
- package/src/crons/postmark.ts +0 -223
package/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
22
|
package/index.ts
CHANGED
|
@@ -1,237 +1,15 @@
|
|
|
1
1
|
import backendEnv from '@stamhoofd/backend-env';
|
|
2
|
-
backendEnv.load({ service: 'api' });
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
|
|
7
|
-
import { Email } from '@stamhoofd/email';
|
|
8
|
-
import { loadLogger } from '@stamhoofd/logging';
|
|
9
|
-
import { Version } from '@stamhoofd/structures';
|
|
10
|
-
import { sleep } from '@stamhoofd/utility';
|
|
11
|
-
|
|
12
|
-
import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
|
|
13
|
-
import { Platform } from '@stamhoofd/models';
|
|
14
|
-
import { resumeEmails } from './src/helpers/EmailResumer';
|
|
15
|
-
import { GlobalHelper } from './src/helpers/GlobalHelper';
|
|
16
|
-
import { SetupStepUpdater } from './src/helpers/SetupStepUpdater';
|
|
17
|
-
import { ContextMiddleware } from './src/middleware/ContextMiddleware';
|
|
18
|
-
import { AuditLogService } from './src/services/AuditLogService';
|
|
19
|
-
import { BalanceItemService } from './src/services/BalanceItemService';
|
|
20
|
-
import { DocumentService } from './src/services/DocumentService';
|
|
21
|
-
import { FileSignService } from './src/services/FileSignService';
|
|
22
|
-
import { PlatformMembershipService } from './src/services/PlatformMembershipService';
|
|
23
|
-
import { UniqueUserService } from './src/services/UniqueUserService';
|
|
24
|
-
import { QueueHandler } from '@stamhoofd/queues';
|
|
25
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
26
|
-
|
|
27
|
-
process.on('unhandledRejection', (error: Error) => {
|
|
28
|
-
console.error('unhandledRejection');
|
|
29
|
-
console.error(error.message, error.stack);
|
|
3
|
+
backendEnv.load({ service: 'api' }).catch((error) => {
|
|
4
|
+
console.error('Failed to load environment:', error);
|
|
30
5
|
process.exit(1);
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Set timezone
|
|
37
|
-
process.env.TZ = 'UTC';
|
|
38
|
-
|
|
39
|
-
// Quick check
|
|
40
|
-
if (new Date().getTimezoneOffset() !== 0) {
|
|
41
|
-
throw new Error('Process should always run in UTC timezone');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const seeds = async () => {
|
|
45
|
-
try {
|
|
46
|
-
// Internal
|
|
47
|
-
await AuditLogService.disable(async () => {
|
|
48
|
-
await Migration.runAll(__dirname + '/src/seeds');
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
console.error('Failed to run seeds:');
|
|
53
|
-
console.error(e);
|
|
6
|
+
}).then(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'development') {
|
|
8
|
+
const { run } = await import('./src/migrate');
|
|
9
|
+
await run();
|
|
54
10
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const start = async () => {
|
|
59
|
-
console.log('Running server at v' + Version);
|
|
60
|
-
loadLogger();
|
|
61
|
-
|
|
62
|
-
await GlobalHelper.load();
|
|
63
|
-
await UniqueUserService.check();
|
|
64
|
-
|
|
65
|
-
// Init platform shared struct: otherwise permissions won't work with missing responsibilities
|
|
66
|
-
console.log('Loading platform...');
|
|
67
|
-
await Platform.loadCaches();
|
|
68
|
-
|
|
69
|
-
const router = new Router();
|
|
70
|
-
|
|
71
|
-
console.log('Loading endpoints...');
|
|
72
|
-
|
|
73
|
-
// Note: we should load endpoints one by once to have a reliable order of url matching
|
|
74
|
-
await router.loadAllEndpoints(__dirname + '/src/endpoints/global/*');
|
|
75
|
-
await router.loadAllEndpoints(__dirname + '/src/endpoints/admin/*');
|
|
76
|
-
await router.loadAllEndpoints(__dirname + '/src/endpoints/auth');
|
|
77
|
-
await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/dashboard/*');
|
|
78
|
-
await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/registration');
|
|
79
|
-
await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/webshops');
|
|
80
|
-
await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/shared');
|
|
81
|
-
await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/shared/*');
|
|
82
|
-
|
|
83
|
-
router.endpoints.push(new CORSPreflightEndpoint());
|
|
84
|
-
|
|
85
|
-
console.log('Creating router...');
|
|
86
|
-
const routerServer = new RouterServer(router);
|
|
87
|
-
routerServer.verbose = false;
|
|
88
|
-
|
|
89
|
-
// Log requests and errors
|
|
90
|
-
routerServer.addRequestMiddleware(LogMiddleware);
|
|
91
|
-
routerServer.addResponseMiddleware(LogMiddleware);
|
|
92
|
-
|
|
93
|
-
routerServer.addResponseMiddleware(FileSignService);
|
|
94
|
-
routerServer.addRequestMiddleware(FileSignService);
|
|
95
|
-
|
|
96
|
-
// Contexts
|
|
97
|
-
routerServer.addRequestMiddleware(ContextMiddleware);
|
|
98
|
-
|
|
99
|
-
// Add version headers and minimum version
|
|
100
|
-
const versionMiddleware = new VersionMiddleware({
|
|
101
|
-
latestVersions: {
|
|
102
|
-
android: STAMHOOFD.LATEST_ANDROID_VERSION,
|
|
103
|
-
ios: STAMHOOFD.LATEST_IOS_VERSION,
|
|
104
|
-
web: Version,
|
|
105
|
-
},
|
|
106
|
-
minimumVersion: 331,
|
|
107
|
-
});
|
|
108
|
-
routerServer.addRequestMiddleware(versionMiddleware);
|
|
109
|
-
routerServer.addResponseMiddleware(versionMiddleware);
|
|
110
|
-
|
|
111
|
-
// Add CORS headers
|
|
112
|
-
routerServer.addResponseMiddleware(CORSMiddleware);
|
|
113
|
-
|
|
114
|
-
console.log('Loading loaders...');
|
|
115
|
-
|
|
116
|
-
// Register Excel loaders
|
|
117
|
-
await import('./src/excel-loaders');
|
|
118
|
-
|
|
119
|
-
// Register Email Recipient loaders
|
|
120
|
-
await import('./src/email-recipient-loaders/members');
|
|
121
|
-
await import('./src/email-recipient-loaders/registrations');
|
|
122
|
-
await import('./src/email-recipient-loaders/orders');
|
|
123
|
-
await import('./src/email-recipient-loaders/receivable-balances');
|
|
124
|
-
await import('./src/excel-loaders/registrations');
|
|
125
|
-
|
|
126
|
-
console.log('Opening port...');
|
|
127
|
-
routerServer.listen(STAMHOOFD.PORT ?? 9090);
|
|
128
|
-
|
|
129
|
-
const hrend = process.hrtime(bootTime);
|
|
130
|
-
console.log('🟢 HTTP server started in ' + Math.ceil(hrend[0] * 1000 + hrend[1] / 1000000) + 'ms');
|
|
131
|
-
|
|
132
|
-
resumeEmails().catch(console.error);
|
|
133
|
-
|
|
134
|
-
if (routerServer.server) {
|
|
135
|
-
// Default timeout is a bit too short
|
|
136
|
-
routerServer.server.timeout = 61000;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
let shuttingDown = false;
|
|
140
|
-
const shutdown = async () => {
|
|
141
|
-
if (shuttingDown) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
shuttingDown = true;
|
|
145
|
-
console.log('Shutting down...');
|
|
146
|
-
// Disable keep alive
|
|
147
|
-
routerServer.defaultHeaders = Object.assign(routerServer.defaultHeaders, { Connection: 'close' });
|
|
148
|
-
if (routerServer.server) {
|
|
149
|
-
routerServer.server.headersTimeout = 5000;
|
|
150
|
-
routerServer.server.keepAliveTimeout = 1;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
stopCrons();
|
|
154
|
-
|
|
155
|
-
if (STAMHOOFD.environment === 'development') {
|
|
156
|
-
setTimeout(() => {
|
|
157
|
-
console.error('Forcing exit after 5 seconds');
|
|
158
|
-
process.exit(1);
|
|
159
|
-
}, 5000);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
await routerServer.close();
|
|
164
|
-
console.log('HTTP server stopped');
|
|
165
|
-
}
|
|
166
|
-
catch (err) {
|
|
167
|
-
console.error('Failed to stop HTTP server:');
|
|
168
|
-
console.error(err);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
await BalanceItemService.flushAll();
|
|
172
|
-
await waitForCrons();
|
|
173
|
-
QueueHandler.abortAll(
|
|
174
|
-
new SimpleError({
|
|
175
|
-
code: 'SHUTDOWN',
|
|
176
|
-
message: 'Shutting down',
|
|
177
|
-
statusCode: 503,
|
|
178
|
-
}),
|
|
179
|
-
);
|
|
180
|
-
await QueueHandler.awaitAll();
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
while (Email.currentQueue.length > 0) {
|
|
184
|
-
console.log(`${Email.currentQueue.length} emails still in queue. Waiting 500ms...`);
|
|
185
|
-
await sleep(500);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
console.error('Failed to wait for emails to finish:');
|
|
190
|
-
console.error(err);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
await Database.end();
|
|
195
|
-
console.log('MySQL connections closed');
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
console.error('Failed to close MySQL connection:');
|
|
199
|
-
console.error(err);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Should not be needed, but added for security as sometimes a promise hangs somewhere
|
|
203
|
-
process.exit(0);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
process.on('SIGTERM', () => {
|
|
207
|
-
console.info('SIGTERM signal received.');
|
|
208
|
-
shutdown().catch((e) => {
|
|
209
|
-
console.error(e);
|
|
210
|
-
process.exit(1);
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
process.on('SIGINT', () => {
|
|
215
|
-
console.info('SIGINT signal received.');
|
|
216
|
-
shutdown().catch((e) => {
|
|
217
|
-
console.error(e);
|
|
218
|
-
process.exit(1);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// Register crons
|
|
223
|
-
await import('./src/crons');
|
|
224
|
-
|
|
225
|
-
AuditLogService.listen();
|
|
226
|
-
PlatformMembershipService.listen();
|
|
227
|
-
DocumentService.listen();
|
|
228
|
-
SetupStepUpdater.listen();
|
|
229
|
-
|
|
230
|
-
startCrons();
|
|
231
|
-
seeds().catch(console.error);
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
start().catch((error) => {
|
|
235
|
-
console.error('unhandledRejection', error);
|
|
11
|
+
await import('./src/boot');
|
|
12
|
+
}).catch((error) => {
|
|
13
|
+
console.error('Failed to start the API:', error);
|
|
236
14
|
process.exit(1);
|
|
237
15
|
});
|
package/migrations.ts
CHANGED
|
@@ -1,35 +1,13 @@
|
|
|
1
1
|
import backendEnv from '@stamhoofd/backend-env';
|
|
2
|
-
backendEnv.load();
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (new Date().getTimezoneOffset() !== 0) {
|
|
16
|
-
throw new Error('Process should always run in UTC timezone');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const start = async () => {
|
|
20
|
-
// External migrations
|
|
21
|
-
await Migration.runAll(path.dirname(modelsPath) + '/migrations');
|
|
22
|
-
await Migration.runAll(path.dirname(emailPath) + '/migrations');
|
|
23
|
-
|
|
24
|
-
// Internal
|
|
25
|
-
await Migration.runAll(__dirname + '/src/migrations');
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
start()
|
|
29
|
-
.catch((error) => {
|
|
30
|
-
console.error('unhandledRejection', error);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
})
|
|
33
|
-
.finally(() => {
|
|
34
|
-
process.exit();
|
|
35
|
-
});
|
|
3
|
+
backendEnv.load({ service: 'api' }).catch((error) => {
|
|
4
|
+
console.error('Failed to load environment:', error);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
}).then(async () => {
|
|
7
|
+
const { run } = await import('./src/migrate');
|
|
8
|
+
await run();
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}).catch((error) => {
|
|
11
|
+
console.error('Failed to run migrations:', error);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.86.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -9,16 +9,17 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "UNLICENCED",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"dev": "
|
|
13
|
-
"dev:
|
|
12
|
+
"dev": "wait-on ../../shared/middleware/dist/index.js && concurrently -r 'yarn -s build --watch --preserveWatchOutput' \"yarn -s dev:watch\"",
|
|
13
|
+
"dev:watch": "wait-on ./dist/index.js && nodemon --quiet --inspect=5858 --watch dist --watch '../../../shared/*/dist/' --watch '../../shared/*/dist/' --ext .ts,.json,.sql,.js --delay 2000ms --exec 'node --enable-source-maps ./dist/index.js' --signal SIGTERM",
|
|
14
|
+
"dev:backend": "yarn -s dev",
|
|
14
15
|
"build": "rm -rf ./dist/src/migrations && rm -rf ./dist/src/seeds && tsc -b",
|
|
15
|
-
"build:full": "yarn clear && yarn build",
|
|
16
|
+
"build:full": "yarn -s clear && yarn -s build",
|
|
16
17
|
"clear": "rm -rf ./dist",
|
|
17
|
-
"start": "yarn build && node --enable-source-maps ./dist/index.js",
|
|
18
|
+
"start": "yarn -s build && node --enable-source-maps ./dist/index.js",
|
|
18
19
|
"test": "jest --runInBand",
|
|
19
20
|
"test:build": "yarn -s build:full && yarn -s test",
|
|
20
|
-
"test:reset": "yarn build:full && jest --runInBand",
|
|
21
|
-
"migrations": "yarn build:full && node ./dist/migrations.js",
|
|
21
|
+
"test:reset": "yarn -s build:full && jest --runInBand",
|
|
22
|
+
"migrations": "yarn -s build:full && node ./dist/migrations.js",
|
|
22
23
|
"lint": "eslint"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"@types/luxon": "3.4.2",
|
|
27
28
|
"@types/mailparser": "3.4.4",
|
|
28
29
|
"@types/mysql": "^2.15.20",
|
|
29
|
-
"@types/node": "^
|
|
30
|
+
"@types/node": "^22",
|
|
30
31
|
"nock": "^13.5.1",
|
|
31
32
|
"qs": "^6.11.2",
|
|
32
33
|
"sinon": "^18.0.0"
|
|
@@ -39,22 +40,22 @@
|
|
|
39
40
|
"@aws-sdk/s3-request-presigner": "3.823.0",
|
|
40
41
|
"@bwip-js/node": "^4.5.1",
|
|
41
42
|
"@mollie/api-client": "3.7.0",
|
|
42
|
-
"@simonbackx/simple-database": "1.
|
|
43
|
+
"@simonbackx/simple-database": "1.33.0",
|
|
43
44
|
"@simonbackx/simple-encoding": "2.22.0",
|
|
44
45
|
"@simonbackx/simple-endpoints": "1.20.1",
|
|
45
46
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
46
|
-
"@stamhoofd/backend-i18n": "2.
|
|
47
|
-
"@stamhoofd/backend-middleware": "2.
|
|
48
|
-
"@stamhoofd/email": "2.
|
|
49
|
-
"@stamhoofd/models": "2.
|
|
50
|
-
"@stamhoofd/queues": "2.
|
|
51
|
-
"@stamhoofd/sql": "2.
|
|
52
|
-
"@stamhoofd/structures": "2.
|
|
53
|
-
"@stamhoofd/utility": "2.
|
|
47
|
+
"@stamhoofd/backend-i18n": "2.86.0",
|
|
48
|
+
"@stamhoofd/backend-middleware": "2.86.0",
|
|
49
|
+
"@stamhoofd/email": "2.86.0",
|
|
50
|
+
"@stamhoofd/models": "2.86.0",
|
|
51
|
+
"@stamhoofd/queues": "2.86.0",
|
|
52
|
+
"@stamhoofd/sql": "2.86.0",
|
|
53
|
+
"@stamhoofd/structures": "2.86.0",
|
|
54
|
+
"@stamhoofd/utility": "2.86.0",
|
|
54
55
|
"archiver": "^7.0.1",
|
|
55
|
-
"axios": "1.
|
|
56
|
+
"axios": "^1.8.2",
|
|
56
57
|
"cookie": "^0.5.0",
|
|
57
|
-
"formidable": "3.5.
|
|
58
|
+
"formidable": "3.5.4",
|
|
58
59
|
"handlebars": "^4.7.7",
|
|
59
60
|
"jsonwebtoken": "9.0.0",
|
|
60
61
|
"luxon": "3.4.4",
|
|
@@ -63,11 +64,10 @@
|
|
|
63
64
|
"mysql2": "^3.14.1",
|
|
64
65
|
"node-rsa": "1.1.1",
|
|
65
66
|
"openid-client": "^5.4.0",
|
|
66
|
-
"postmark": "^4.0.5",
|
|
67
67
|
"stripe": "^16.6.0"
|
|
68
68
|
},
|
|
69
69
|
"publishConfig": {
|
|
70
70
|
"access": "public"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "e7db435d283a81bee4f7d26c2ccc61ee1df0524c"
|
|
73
73
|
}
|
package/src/boot.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Column, Database, Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
|
|
4
|
+
import { Email } from '@stamhoofd/email';
|
|
5
|
+
import { loadLogger } from '@stamhoofd/logging';
|
|
6
|
+
import { Version } from '@stamhoofd/structures';
|
|
7
|
+
import { sleep } from '@stamhoofd/utility';
|
|
8
|
+
|
|
9
|
+
import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
|
|
10
|
+
import { Platform } from '@stamhoofd/models';
|
|
11
|
+
import { resumeEmails } from './helpers/EmailResumer';
|
|
12
|
+
import { GlobalHelper } from './helpers/GlobalHelper';
|
|
13
|
+
import { SetupStepUpdater } from './helpers/SetupStepUpdater';
|
|
14
|
+
import { ContextMiddleware } from './middleware/ContextMiddleware';
|
|
15
|
+
import { AuditLogService } from './services/AuditLogService';
|
|
16
|
+
import { BalanceItemService } from './services/BalanceItemService';
|
|
17
|
+
import { DocumentService } from './services/DocumentService';
|
|
18
|
+
import { FileSignService } from './services/FileSignService';
|
|
19
|
+
import { PlatformMembershipService } from './services/PlatformMembershipService';
|
|
20
|
+
import { UniqueUserService } from './services/UniqueUserService';
|
|
21
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
22
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
23
|
+
|
|
24
|
+
process.on('unhandledRejection', (error: Error) => {
|
|
25
|
+
console.error('unhandledRejection');
|
|
26
|
+
console.error(error.message, error.stack);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Set version of saved structures
|
|
31
|
+
Column.setJSONVersion(Version);
|
|
32
|
+
|
|
33
|
+
// Set timezone
|
|
34
|
+
process.env.TZ = 'UTC';
|
|
35
|
+
|
|
36
|
+
// Quick check
|
|
37
|
+
if (new Date().getTimezoneOffset() !== 0) {
|
|
38
|
+
throw new Error('Process should always run in UTC timezone');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const seeds = async () => {
|
|
42
|
+
try {
|
|
43
|
+
// Internal
|
|
44
|
+
await AuditLogService.disable(async () => {
|
|
45
|
+
await Migration.runAll(__dirname + '/seeds');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
console.error('Failed to run seeds:');
|
|
50
|
+
console.error(e);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const bootTime = process.hrtime();
|
|
54
|
+
|
|
55
|
+
function productionLog(message: string) {
|
|
56
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
57
|
+
console.log(message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const start = async () => {
|
|
62
|
+
productionLog('Running server at v' + Version);
|
|
63
|
+
loadLogger();
|
|
64
|
+
|
|
65
|
+
await GlobalHelper.load();
|
|
66
|
+
await UniqueUserService.check();
|
|
67
|
+
|
|
68
|
+
// Init platform shared struct: otherwise permissions won't work with missing responsibilities
|
|
69
|
+
productionLog('Loading platform...');
|
|
70
|
+
await Platform.loadCaches();
|
|
71
|
+
|
|
72
|
+
const router = new Router();
|
|
73
|
+
|
|
74
|
+
productionLog('Loading endpoints...');
|
|
75
|
+
|
|
76
|
+
// Note: we should load endpoints one by once to have a reliable order of url matching
|
|
77
|
+
await router.loadAllEndpoints(__dirname + '/endpoints/global/*');
|
|
78
|
+
await router.loadAllEndpoints(__dirname + '/endpoints/admin/*');
|
|
79
|
+
await router.loadAllEndpoints(__dirname + '/endpoints/auth');
|
|
80
|
+
await router.loadAllEndpoints(__dirname + '/endpoints/organization/dashboard/*');
|
|
81
|
+
await router.loadAllEndpoints(__dirname + '/endpoints/organization/registration');
|
|
82
|
+
await router.loadAllEndpoints(__dirname + '/endpoints/organization/webshops');
|
|
83
|
+
await router.loadAllEndpoints(__dirname + '/endpoints/organization/shared');
|
|
84
|
+
await router.loadAllEndpoints(__dirname + '/endpoints/organization/shared/*');
|
|
85
|
+
|
|
86
|
+
router.endpoints.push(new CORSPreflightEndpoint());
|
|
87
|
+
|
|
88
|
+
productionLog('Creating router...');
|
|
89
|
+
const routerServer = new RouterServer(router);
|
|
90
|
+
routerServer.verbose = false;
|
|
91
|
+
|
|
92
|
+
// Log requests and errors
|
|
93
|
+
routerServer.addRequestMiddleware(LogMiddleware);
|
|
94
|
+
routerServer.addResponseMiddleware(LogMiddleware);
|
|
95
|
+
|
|
96
|
+
routerServer.addResponseMiddleware(FileSignService);
|
|
97
|
+
routerServer.addRequestMiddleware(FileSignService);
|
|
98
|
+
|
|
99
|
+
// Contexts
|
|
100
|
+
routerServer.addRequestMiddleware(ContextMiddleware);
|
|
101
|
+
|
|
102
|
+
// Add version headers and minimum version
|
|
103
|
+
const versionMiddleware = new VersionMiddleware({
|
|
104
|
+
latestVersions: {
|
|
105
|
+
android: STAMHOOFD.LATEST_ANDROID_VERSION,
|
|
106
|
+
ios: STAMHOOFD.LATEST_IOS_VERSION,
|
|
107
|
+
web: Version,
|
|
108
|
+
},
|
|
109
|
+
minimumVersion: 331,
|
|
110
|
+
});
|
|
111
|
+
routerServer.addRequestMiddleware(versionMiddleware);
|
|
112
|
+
routerServer.addResponseMiddleware(versionMiddleware);
|
|
113
|
+
|
|
114
|
+
// Add CORS headers
|
|
115
|
+
routerServer.addResponseMiddleware(CORSMiddleware);
|
|
116
|
+
|
|
117
|
+
productionLog('Loading loaders...');
|
|
118
|
+
|
|
119
|
+
// Register Excel loaders
|
|
120
|
+
await import('./excel-loaders');
|
|
121
|
+
|
|
122
|
+
// Register Email Recipient loaders
|
|
123
|
+
await import('./email-recipient-loaders/members');
|
|
124
|
+
await import('./email-recipient-loaders/registrations');
|
|
125
|
+
await import('./email-recipient-loaders/orders');
|
|
126
|
+
await import('./email-recipient-loaders/receivable-balances');
|
|
127
|
+
await import('./excel-loaders/registrations');
|
|
128
|
+
|
|
129
|
+
productionLog('Opening port...');
|
|
130
|
+
routerServer.listen(STAMHOOFD.PORT ?? 9090);
|
|
131
|
+
|
|
132
|
+
const hrend = process.hrtime(bootTime);
|
|
133
|
+
productionLog('🟢 HTTP server started in ' + Math.ceil(hrend[0] * 1000 + hrend[1] / 1000000) + 'ms');
|
|
134
|
+
|
|
135
|
+
resumeEmails().catch(console.error);
|
|
136
|
+
|
|
137
|
+
if (routerServer.server) {
|
|
138
|
+
// Default timeout is a bit too short
|
|
139
|
+
routerServer.server.timeout = 61000;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let shuttingDown = false;
|
|
143
|
+
const shutdown = async () => {
|
|
144
|
+
if (shuttingDown) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
shuttingDown = true;
|
|
148
|
+
productionLog('Shutting down...');
|
|
149
|
+
// Disable keep alive
|
|
150
|
+
routerServer.defaultHeaders = Object.assign(routerServer.defaultHeaders, { Connection: 'close' });
|
|
151
|
+
if (routerServer.server) {
|
|
152
|
+
routerServer.server.headersTimeout = 5000;
|
|
153
|
+
routerServer.server.keepAliveTimeout = 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
stopCrons();
|
|
157
|
+
|
|
158
|
+
if (STAMHOOFD.environment === 'development') {
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
console.error('Forcing exit after 5 seconds');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}, 5000);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await routerServer.close();
|
|
167
|
+
productionLog('HTTP server stopped');
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
console.error('Failed to stop HTTP server:');
|
|
171
|
+
console.error(err);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await BalanceItemService.flushAll();
|
|
175
|
+
await waitForCrons();
|
|
176
|
+
QueueHandler.abortAll(
|
|
177
|
+
new SimpleError({
|
|
178
|
+
code: 'SHUTDOWN',
|
|
179
|
+
message: 'Shutting down',
|
|
180
|
+
statusCode: 503,
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
await QueueHandler.awaitAll();
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
while (Email.currentQueue.length > 0) {
|
|
187
|
+
console.log(`${Email.currentQueue.length} emails still in queue. Waiting 500ms...`);
|
|
188
|
+
await sleep(500);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
console.error('Failed to wait for emails to finish:');
|
|
193
|
+
console.error(err);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await Database.end();
|
|
198
|
+
productionLog('MySQL connections closed');
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
console.error('Failed to close MySQL connection:');
|
|
202
|
+
console.error(err);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Should not be needed, but added for security as sometimes a promise hangs somewhere
|
|
206
|
+
process.exit(0);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
process.on('SIGTERM', () => {
|
|
210
|
+
productionLog('SIGTERM signal received.');
|
|
211
|
+
shutdown().catch((e) => {
|
|
212
|
+
console.error(e);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
process.on('SIGINT', () => {
|
|
218
|
+
productionLog('SIGINT signal received.');
|
|
219
|
+
shutdown().catch((e) => {
|
|
220
|
+
console.error(e);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Register crons
|
|
226
|
+
await import('./crons');
|
|
227
|
+
|
|
228
|
+
AuditLogService.listen();
|
|
229
|
+
PlatformMembershipService.listen();
|
|
230
|
+
DocumentService.listen();
|
|
231
|
+
SetupStepUpdater.listen();
|
|
232
|
+
|
|
233
|
+
startCrons();
|
|
234
|
+
seeds().catch(console.error);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
start().catch((error) => {
|
|
238
|
+
console.error('unhandledRejection', error);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
});
|
|
@@ -131,7 +131,10 @@ describe('clearExcelCacheHelper', () => {
|
|
|
131
131
|
dir.isDirectory = () => true;
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
-
fsMock.readdir.mockReturnValue(
|
|
134
|
+
fsMock.readdir.mockReturnValue(
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
136
|
+
Promise.resolve([...directories, file1]) as any,
|
|
137
|
+
);
|
|
135
138
|
// #endregion
|
|
136
139
|
|
|
137
140
|
// act
|
package/src/crons/index.ts
CHANGED
|
@@ -356,7 +356,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
const maximumStart = 1000 * 60 * 60 * 24 * 31 * 2; // 2 months in advance
|
|
359
|
-
if (period.startDate > new Date(Date.now() + maximumStart)) {
|
|
359
|
+
if (period.startDate > new Date(Date.now() + maximumStart) && STAMHOOFD.environment !== 'development') {
|
|
360
360
|
throw new SimpleError({
|
|
361
361
|
code: 'invalid_field',
|
|
362
362
|
message: 'The period you want to set has not started yet',
|
|
@@ -77,6 +77,10 @@ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
77
77
|
expressData = {};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
if (type === 'express') {
|
|
81
|
+
expressData.controller = undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
80
84
|
// Create a new Stripe account
|
|
81
85
|
const stripe = StripeHelper.getInstance();
|
|
82
86
|
const account = await stripe.accounts.create({
|
package/src/migrate.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { Column, DatabaseInstance, Migration } from '@simonbackx/simple-database';
|
|
4
|
+
import { Version } from '@stamhoofd/structures';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
Column.setJSONVersion(Version);
|
|
8
|
+
process.env.TZ = 'UTC';
|
|
9
|
+
|
|
10
|
+
const emailPath = require.resolve('@stamhoofd/email');
|
|
11
|
+
const modelsPath = require.resolve('@stamhoofd/models');
|
|
12
|
+
|
|
13
|
+
// Validate UTC timezone
|
|
14
|
+
if (new Date().getTimezoneOffset() !== 0) {
|
|
15
|
+
throw new Error('Process should always run in UTC timezone');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const start = async () => {
|
|
19
|
+
if (!STAMHOOFD.DB_DATABASE) {
|
|
20
|
+
throw new Error('STAMHOOFD.DB_DATABASE is not set');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let killSignalReceived = false;
|
|
24
|
+
const handler = () => {
|
|
25
|
+
// Ignore
|
|
26
|
+
console.error('Ignoring SIGTERM signal during migration');
|
|
27
|
+
killSignalReceived = true;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
process.on('SIGTERM', handler);
|
|
31
|
+
process.on('SIGINT', handler);
|
|
32
|
+
|
|
33
|
+
// Create database if not exists
|
|
34
|
+
const query = 'CREATE DATABASE IF NOT EXISTS `' + STAMHOOFD.DB_DATABASE + '` DEFAULT CHARACTER SET = `utf8mb4` DEFAULT COLLATE = `utf8mb4_0900_ai_ci`';
|
|
35
|
+
|
|
36
|
+
// Create a non-focused datbase instance to run the query without selecting a database
|
|
37
|
+
const globalDatabase = new DatabaseInstance({
|
|
38
|
+
database: null,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await globalDatabase.statement(query);
|
|
42
|
+
|
|
43
|
+
// External migrations
|
|
44
|
+
await Migration.runAll(path.dirname(modelsPath) + '/migrations');
|
|
45
|
+
await Migration.runAll(path.dirname(emailPath) + '/migrations');
|
|
46
|
+
|
|
47
|
+
// Internal
|
|
48
|
+
await Migration.runAll(__dirname + '/src/migrations');
|
|
49
|
+
|
|
50
|
+
if (killSignalReceived) {
|
|
51
|
+
console.error(chalk.red('Killing process due to received signal during migration'));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
process.off('SIGTERM', handler);
|
|
56
|
+
process.off('SIGINT', handler);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export async function run() {
|
|
60
|
+
await start()
|
|
61
|
+
.catch((error) => {
|
|
62
|
+
console.error('unhandledRejection', error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { User } from '@stamhoofd/models';
|
|
3
|
+
import { NewUser, PermissionLevel, Permissions, UserPermissions } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
export default new Migration(async () => {
|
|
6
|
+
if (STAMHOOFD.environment !== 'development') {
|
|
7
|
+
console.log('Skipped');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Check if total users is 0
|
|
12
|
+
const totalUsers = await User.select().count();
|
|
13
|
+
if (totalUsers > 0) {
|
|
14
|
+
console.log('Skipped, users already exist');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Create a development user with platform level admin access
|
|
19
|
+
const user = await User.register(
|
|
20
|
+
null, // No organization
|
|
21
|
+
NewUser.create({
|
|
22
|
+
id: '00000000-0000-4000-8000-000000000000',
|
|
23
|
+
firstName: 'Stamhoofd',
|
|
24
|
+
lastName: 'Development',
|
|
25
|
+
email: 'hallo@stamhoofd.be',
|
|
26
|
+
password: 'stamhoofd',
|
|
27
|
+
}),
|
|
28
|
+
{ allowPlatform: true },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (!user) {
|
|
32
|
+
throw new Error('Failed to create development user');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
user.verified = true;
|
|
36
|
+
user.permissions = UserPermissions.create({
|
|
37
|
+
globalPermissions: Permissions.create({
|
|
38
|
+
level: PermissionLevel.Full,
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
await user.save();
|
|
42
|
+
|
|
43
|
+
console.log('Created a new development user:');
|
|
44
|
+
console.log('E-mail:', user.email);
|
|
45
|
+
console.log('Password: stamhoofd');
|
|
46
|
+
|
|
47
|
+
// Do something here
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
});
|
package/src/crons/postmark.ts
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { Email, EmailAddress } from '@stamhoofd/email';
|
|
2
|
-
import { AuditLog, Organization } from '@stamhoofd/models';
|
|
3
|
-
import { DateTime } from 'luxon';
|
|
4
|
-
|
|
5
|
-
import { registerCron } from '@stamhoofd/crons';
|
|
6
|
-
import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType } from '@stamhoofd/structures';
|
|
7
|
-
|
|
8
|
-
// Importing postmark returns undefined (this is a bug, so we need to use require)
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
10
|
-
const postmark = require('postmark') as typeof import('postmark');
|
|
11
|
-
|
|
12
|
-
let lastPostmarkCheck: Date | null = null;
|
|
13
|
-
let lastPostmarkIds: Set<number> = new Set();
|
|
14
|
-
|
|
15
|
-
registerCron('checkPostmarkBounces', checkPostmarkBounces);
|
|
16
|
-
|
|
17
|
-
async function saveLog({ email, organization, type, subType, id, response, subject, sender }: { id: number; sender: string; email: string; response: string; subject: string;organization: Organization | undefined; type: AuditLogType; subType?: string }) {
|
|
18
|
-
const log = new AuditLog();
|
|
19
|
-
log.organizationId = organization?.id ?? null;
|
|
20
|
-
log.externalId = 'postmark-bounce-' + id.toString();
|
|
21
|
-
log.type = type;
|
|
22
|
-
log.objectId = email;
|
|
23
|
-
log.source = AuditLogSource.System;
|
|
24
|
-
log.replacements = new Map([
|
|
25
|
-
['e', AuditLogReplacement.create({
|
|
26
|
-
value: email || '',
|
|
27
|
-
type: AuditLogReplacementType.EmailAddress,
|
|
28
|
-
})],
|
|
29
|
-
['subType', AuditLogReplacement.key(subType || 'unknown')],
|
|
30
|
-
['response', AuditLogReplacement.longText(response)],
|
|
31
|
-
['sender', AuditLogReplacement.create({
|
|
32
|
-
value: sender,
|
|
33
|
-
type: AuditLogReplacementType.EmailAddress,
|
|
34
|
-
})],
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
if (subject) {
|
|
38
|
-
log.replacements.set('subject', AuditLogReplacement.string(subject));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check if we already logged this bounce
|
|
42
|
-
const existing = await AuditLog.select().where('externalId', log.externalId).first(false);
|
|
43
|
-
if (existing) {
|
|
44
|
-
console.log('Already logged this bounce, skipping');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
await log.save();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function checkPostmarkBounces() {
|
|
52
|
-
if (STAMHOOFD.environment !== 'production') {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const token = STAMHOOFD.POSTMARK_SERVER_TOKEN;
|
|
57
|
-
if (!token) {
|
|
58
|
-
console.log('No postmark token, skipping postmark bounces');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const fromDate = (lastPostmarkCheck ?? new Date(new Date().getTime() - 24 * 60 * 60 * 1000 * 2));
|
|
62
|
-
const ET = DateTime.fromJSDate(fromDate).setZone('EST').toISO({ includeOffset: false });
|
|
63
|
-
|
|
64
|
-
if (!ET) {
|
|
65
|
-
console.error('Could not convert date to EST:', fromDate);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
console.log('Checking bounces from Postmark since', fromDate, ET);
|
|
69
|
-
const client = new postmark.ServerClient(token);
|
|
70
|
-
|
|
71
|
-
const toDate = DateTime.now().setZone('EST').toISO({ includeOffset: false });
|
|
72
|
-
|
|
73
|
-
if (!toDate) {
|
|
74
|
-
console.error('Could not convert date to EST:', new Date());
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let offset = 0;
|
|
79
|
-
let total = 1;
|
|
80
|
-
const count = 500;
|
|
81
|
-
|
|
82
|
-
// Sadly the postmark api returns bounces in the wrong order, to make them easier fetchable so we need to fetch them all in one go every time
|
|
83
|
-
while (offset < total && offset <= 10000 - count) {
|
|
84
|
-
const bounces = await client.getBounces({
|
|
85
|
-
fromDate: ET,
|
|
86
|
-
toDate,
|
|
87
|
-
count,
|
|
88
|
-
offset,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
if (bounces.TotalCount === 0) {
|
|
92
|
-
console.log('No Postmark bounces at this time');
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
total = bounces.TotalCount;
|
|
97
|
-
|
|
98
|
-
console.log('Found', bounces.TotalCount, 'bounces from Postmark');
|
|
99
|
-
|
|
100
|
-
let lastId: number | null = null;
|
|
101
|
-
const idList = new Set<number>();
|
|
102
|
-
let newEventCount = 0;
|
|
103
|
-
|
|
104
|
-
for (const bounce of bounces.Bounces) {
|
|
105
|
-
idList.add(bounce.ID);
|
|
106
|
-
if (lastPostmarkIds.has(bounce.ID)) {
|
|
107
|
-
lastId = bounce.ID;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
newEventCount += 1;
|
|
111
|
-
|
|
112
|
-
// Try to get the organization, if possible, else default to global blocking: "null", which is not visible for an organization, but it is applied
|
|
113
|
-
const source = bounce.From;
|
|
114
|
-
const organization = source ? await Organization.getByEmail(source) : undefined;
|
|
115
|
-
console.log(bounce);
|
|
116
|
-
|
|
117
|
-
if (bounce.Type === 'SpamComplaint' || bounce.Type === 'SpamNotification' || bounce.Type === 'VirusNotification') {
|
|
118
|
-
console.log('Postmark ' + bounce.Type + ' for: ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
119
|
-
const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null);
|
|
120
|
-
emailAddress.markedAsSpam = true;
|
|
121
|
-
await emailAddress.save();
|
|
122
|
-
|
|
123
|
-
if (bounce.Type === 'VirusNotification') {
|
|
124
|
-
await saveLog({
|
|
125
|
-
email: bounce.Email,
|
|
126
|
-
organization,
|
|
127
|
-
type: AuditLogType.EmailAddressFraudComplaint,
|
|
128
|
-
subType: bounce.Type,
|
|
129
|
-
response: bounce.Details,
|
|
130
|
-
id: bounce.ID,
|
|
131
|
-
subject: bounce.Subject,
|
|
132
|
-
sender: bounce.From,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
await saveLog({
|
|
137
|
-
email: bounce.Email,
|
|
138
|
-
organization,
|
|
139
|
-
type: AuditLogType.EmailAddressMarkedAsSpam,
|
|
140
|
-
subType: bounce.Type,
|
|
141
|
-
response: bounce.Details,
|
|
142
|
-
id: bounce.ID,
|
|
143
|
-
subject: bounce.Subject,
|
|
144
|
-
sender: bounce.From,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
else if (bounce.Inactive) {
|
|
149
|
-
// Block for everyone, but not visible
|
|
150
|
-
console.log('Postmark ' + bounce.Type + ' for: ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
151
|
-
const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null);
|
|
152
|
-
emailAddress.hardBounce = true;
|
|
153
|
-
await emailAddress.save();
|
|
154
|
-
await saveLog({
|
|
155
|
-
email: bounce.Email,
|
|
156
|
-
organization,
|
|
157
|
-
type: AuditLogType.EmailAddressHardBounced,
|
|
158
|
-
subType: bounce.Type,
|
|
159
|
-
response: bounce.Details,
|
|
160
|
-
id: bounce.ID,
|
|
161
|
-
subject: bounce.Subject,
|
|
162
|
-
sender: bounce.From,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
if (bounce.Type === 'SMTPApiError' && bounce.Details.startsWith("ErrorCode: '406'")) {
|
|
167
|
-
console.log('Email on Postmark suppression list: ' + bounce.Type + ': ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
168
|
-
|
|
169
|
-
// We've sent a message to an email that is blocked by Postmark
|
|
170
|
-
await saveLog({
|
|
171
|
-
email: bounce.Email,
|
|
172
|
-
organization,
|
|
173
|
-
type: AuditLogType.EmailAddressHardBounced,
|
|
174
|
-
subType: 'ExternalSuppressionList',
|
|
175
|
-
response: bounce.Details,
|
|
176
|
-
id: bounce.ID,
|
|
177
|
-
subject: '', // bounce.Subject is not correct here for some reason
|
|
178
|
-
sender: bounce.From,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
if (bounce.Type === 'SMTPApiError') {
|
|
183
|
-
// Log internally
|
|
184
|
-
Email.sendWebmaster({
|
|
185
|
-
subject: 'Received an SMTPApiError from Postmark',
|
|
186
|
-
text: 'We received an SMTPApiError for an e-mail from the organization: ' + organization?.name + '. Please check and adjust if needed.\n' + JSON.stringify(bounce, undefined, 4),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
console.log('Unhandled Postmark ' + bounce.Type + ': ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
191
|
-
|
|
192
|
-
await saveLog({
|
|
193
|
-
email: bounce.Email,
|
|
194
|
-
organization,
|
|
195
|
-
type: AuditLogType.EmailAddressSoftBounced,
|
|
196
|
-
subType: bounce.Type,
|
|
197
|
-
response: bounce.Details,
|
|
198
|
-
id: bounce.ID,
|
|
199
|
-
subject: bounce.Subject,
|
|
200
|
-
sender: bounce.From,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const bouncedAt = new Date(bounce.BouncedAt);
|
|
207
|
-
lastPostmarkCheck = lastPostmarkCheck ? new Date(Math.max(bouncedAt.getTime(), lastPostmarkCheck.getTime())) : bouncedAt;
|
|
208
|
-
|
|
209
|
-
lastId = bounce.ID;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (lastId && newEventCount === 0) {
|
|
213
|
-
console.log('Postmark has no new bounces');
|
|
214
|
-
// Increase timestamp by one second to avoid refetching it every time
|
|
215
|
-
if (lastPostmarkCheck) {
|
|
216
|
-
lastPostmarkCheck = new Date(lastPostmarkCheck.getTime() + 1000);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
lastPostmarkIds = idList;
|
|
220
|
-
|
|
221
|
-
offset += bounces.Bounces.length;
|
|
222
|
-
}
|
|
223
|
-
}
|