@stamhoofd/backend 2.85.4 → 2.87.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 +23 -23
- package/src/boot.ts +240 -0
- package/src/crons/clearExcelCache.test.ts +4 -1
- package/src/crons/delete-old-email-drafts.ts +37 -0
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +6 -2
- package/src/crons/index.ts +1 -1
- package/src/endpoints/global/files/UploadFile.ts +4 -34
- package/src/endpoints/global/members/GetMembersEndpoint.ts +1 -0
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +14 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +4 -0
- package/src/helpers/AdminPermissionChecker.ts +18 -8
- package/src/helpers/AuthenticatedStructures.ts +36 -21
- package/src/helpers/FlagMomentCleanup.ts +14 -2
- package/src/migrate.ts +65 -0
- package/src/seeds/0000000001-development-user.ts +49 -0
- package/src/seeds/1751445358-upload-email-attachments.ts +106 -0
- package/src/services/EventNotificationService.ts +1 -1
- package/src/sql-filters/members.ts +5 -1
- 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.87.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -9,24 +9,25 @@
|
|
|
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": {
|
|
25
|
-
"@types/cookie": "^0.
|
|
26
|
+
"@types/cookie": "^0.6.0",
|
|
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.87.0",
|
|
48
|
+
"@stamhoofd/backend-middleware": "2.87.0",
|
|
49
|
+
"@stamhoofd/email": "2.87.0",
|
|
50
|
+
"@stamhoofd/models": "2.87.0",
|
|
51
|
+
"@stamhoofd/queues": "2.87.0",
|
|
52
|
+
"@stamhoofd/sql": "2.87.0",
|
|
53
|
+
"@stamhoofd/structures": "2.87.0",
|
|
54
|
+
"@stamhoofd/utility": "2.87.0",
|
|
54
55
|
"archiver": "^7.0.1",
|
|
55
|
-
"axios": "1.
|
|
56
|
-
"cookie": "^0.
|
|
57
|
-
"formidable": "3.5.
|
|
56
|
+
"axios": "^1.8.2",
|
|
57
|
+
"cookie": "^0.7.0",
|
|
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": "372713e1e4b626d6b4ce4f3d148f14948528312e"
|
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registerCron } from '@stamhoofd/crons';
|
|
2
|
+
import { Email } from '@stamhoofd/models';
|
|
3
|
+
import { EmailStatus } from '@stamhoofd/structures';
|
|
4
|
+
|
|
5
|
+
let lastRunDate: number | null = null;
|
|
6
|
+
|
|
7
|
+
registerCron('deleteOldEmailDrafts', deleteOldEmailDrafts);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run every night at 5 AM.
|
|
11
|
+
*/
|
|
12
|
+
export async function deleteOldEmailDrafts() {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
|
|
15
|
+
if (now.getDate() === lastRunDate) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const hour = now.getHours();
|
|
20
|
+
|
|
21
|
+
// between 5 and 6 AM
|
|
22
|
+
if (hour !== 5 && STAMHOOFD.environment !== 'development') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Clear old email drafts older than 7 days
|
|
27
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
28
|
+
const result = await Email.delete()
|
|
29
|
+
.where('status', EmailStatus.Draft)
|
|
30
|
+
.where('createdAt', '<', sevenDaysAgo)
|
|
31
|
+
.where('updatedAt', '<', sevenDaysAgo)
|
|
32
|
+
.where('sentAt', null);
|
|
33
|
+
|
|
34
|
+
console.log(`Deleted ${result.affectedRows} old email drafts.`);
|
|
35
|
+
|
|
36
|
+
lastRunDate = now.getDate();
|
|
37
|
+
}
|