@stamhoofd/backend-middleware 2.1.1
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 +20 -0
- package/src/CORSMiddleware.ts +28 -0
- package/src/LogMiddleware.ts +127 -0
- package/src/VersionMiddleware.ts +84 -0
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stamhoofd/backend-middleware",
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"license": "UNLICENCED",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -b",
|
|
13
|
+
"build:full": "rm -rf ./dist && yarn build"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@simonbackx/simple-endpoints": "1.13.0",
|
|
17
|
+
"@simonbackx/simple-errors": "^1.4",
|
|
18
|
+
"@simonbackx/simple-logging": "^1.0.1"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EncodedResponse, Request, ResponseMiddleware } from "@simonbackx/simple-endpoints";
|
|
2
|
+
|
|
3
|
+
export const CORSMiddleware: ResponseMiddleware = {
|
|
4
|
+
handleResponse(request: Request, response: EncodedResponse) {
|
|
5
|
+
response.headers["Access-Control-Allow-Origin"] = request.headers.origin ?? "*"
|
|
6
|
+
response.headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS, PATCH, PUT, DELETE"
|
|
7
|
+
response.headers["Access-Control-Allow-Headers"] = request.headers["access-control-request-headers"] ?? "*";
|
|
8
|
+
response.headers["Access-Control-Max-Age"] = "86400"; // Cache 24h
|
|
9
|
+
|
|
10
|
+
if (request.method !== "OPTIONS") {
|
|
11
|
+
// Expose all headers
|
|
12
|
+
const exposeHeaders = Object.keys(response.headers).map(h => h.toLowerCase()).filter(h => !['content-length', 'cache-control', 'content-language', 'content-type', 'expires', 'last-modified', 'pragma'].includes(h)).join(", ");
|
|
13
|
+
if (exposeHeaders) {
|
|
14
|
+
response.headers["Access-Control-Expose-Headers"] = exposeHeaders
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Not needed
|
|
19
|
+
// response.headers["Access-Control-Allow-Credentials"] = "true";
|
|
20
|
+
|
|
21
|
+
// API is public
|
|
22
|
+
response.headers["Cross-Origin-Resource-Policy"] = "cross-origin"
|
|
23
|
+
|
|
24
|
+
if (request.headers.origin && !response.headers["Vary"]) {
|
|
25
|
+
response.headers["Vary"] = "Origin"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { EncodedResponse, Request, RequestMiddleware,ResponseMiddleware } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { isSimpleError, isSimpleErrors } from "@simonbackx/simple-errors";
|
|
3
|
+
import { logger, StyledText } from "@simonbackx/simple-logging";
|
|
4
|
+
let requestCounter = 0;
|
|
5
|
+
|
|
6
|
+
function logRequestDetails(request: Request) {
|
|
7
|
+
if (Object.keys(request.query).length) {
|
|
8
|
+
const json: any = {...request.query}
|
|
9
|
+
if (json && json.password) {
|
|
10
|
+
json.password = '*******'
|
|
11
|
+
}
|
|
12
|
+
logger.error(
|
|
13
|
+
...requestPrefix(request, 'query'),
|
|
14
|
+
"Request query was ",
|
|
15
|
+
json
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
request.body.then((body) => {
|
|
20
|
+
if (!body) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const json = JSON.parse(body)
|
|
25
|
+
if (Array.isArray(json) || Object.keys(json).length) {
|
|
26
|
+
if (json && json.password) {
|
|
27
|
+
json.password = '*******'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
logger.error(
|
|
31
|
+
...requestPrefix(request, 'body'),
|
|
32
|
+
"Request body was ",
|
|
33
|
+
json
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
logger.error(
|
|
38
|
+
...requestPrefix(request, 'body'),
|
|
39
|
+
"Request body was ",
|
|
40
|
+
body
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}).catch(console.error)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function requestOneLiner(request: Request): (StyledText | string)[] {
|
|
47
|
+
return [
|
|
48
|
+
new StyledText(request.method).addClass('request', 'method', request.method.toLowerCase()),
|
|
49
|
+
' ',
|
|
50
|
+
new StyledText(request.url).addClass('request', 'url'),
|
|
51
|
+
' (',
|
|
52
|
+
new StyledText(request.getIP()).addClass('request', 'ip'),
|
|
53
|
+
'@',
|
|
54
|
+
new StyledText(request.host).addClass('request', 'host'),
|
|
55
|
+
')'
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requestPrefix(request: Request, ...classes: string[]): (StyledText | string)[] {
|
|
60
|
+
return [
|
|
61
|
+
new StyledText(`[R${((request as any)._uniqueIndex as number).toString().padStart(4, "0")}] `).addClass('request', 'tag', ...classes),
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const LogMiddleware: ResponseMiddleware & RequestMiddleware = {
|
|
66
|
+
handleRequest(request: Request) {
|
|
67
|
+
(request as any)._uniqueIndex = requestCounter++
|
|
68
|
+
(request as any)._startTime = process.hrtime();
|
|
69
|
+
|
|
70
|
+
if (request.method == "OPTIONS") {
|
|
71
|
+
if (STAMHOOFD.environment === "development") {
|
|
72
|
+
logger.log(
|
|
73
|
+
...requestPrefix(request),
|
|
74
|
+
...requestOneLiner(request)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.log(
|
|
81
|
+
...requestPrefix(request),
|
|
82
|
+
...requestOneLiner(request)
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
wrapRun<T>(run: () => Promise<T>, request: Request) {
|
|
87
|
+
return logger.setContext({
|
|
88
|
+
prefixes: requestPrefix(request, 'output'),
|
|
89
|
+
tags: ['request', 'request-output']
|
|
90
|
+
}, run)
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
handleResponse(request: Request, response: EncodedResponse, error?: Error) {
|
|
94
|
+
const endTime = process.hrtime();
|
|
95
|
+
const startTime = (request as any)._startTime ?? endTime;
|
|
96
|
+
const timeInMs = Math.round((endTime[0] - startTime[0]) * 1000 + (endTime[1] - startTime[1]) / 1000000);
|
|
97
|
+
|
|
98
|
+
if (request.method !== "OPTIONS") {
|
|
99
|
+
logger.log(
|
|
100
|
+
...requestPrefix(request, 'time'),
|
|
101
|
+
response.status + " - Finished in "+timeInMs+"ms"
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (error) {
|
|
106
|
+
if (isSimpleError(error) || isSimpleErrors(error)) {
|
|
107
|
+
if (!error.hasCode("expired_access_token") && !error.hasCode('unknown_domain') && !error.hasCode('unknown_webshop')) {
|
|
108
|
+
logger.error(
|
|
109
|
+
...requestPrefix(request, 'error'),
|
|
110
|
+
"Request with error in response ",
|
|
111
|
+
new StyledText(error).addClass('request', 'error')
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
logRequestDetails(request)
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
logger.error(
|
|
118
|
+
...requestPrefix(request, 'error'),
|
|
119
|
+
"Request with internal error ",
|
|
120
|
+
new StyledText(error).addClass('request', 'error')
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
logRequestDetails(request)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { EncodedResponse, Request, RequestMiddleware, ResponseMiddleware } from "@simonbackx/simple-endpoints";
|
|
2
|
+
import { isSimpleError, isSimpleErrors, SimpleError } from "@simonbackx/simple-errors";
|
|
3
|
+
import { Version } from "@stamhoofd/structures";
|
|
4
|
+
|
|
5
|
+
export class VersionMiddleware implements RequestMiddleware, ResponseMiddleware {
|
|
6
|
+
minimumVersion: number | undefined
|
|
7
|
+
latestVersions: {android: number, ios: number, web: number}
|
|
8
|
+
|
|
9
|
+
constructor(options: {latestVersions: {android: number, ios: number, web: number}, minimumVersion?: number}) {
|
|
10
|
+
this.minimumVersion = options.minimumVersion
|
|
11
|
+
this.latestVersions = options.latestVersions
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
handleRequest(request: Request) {
|
|
15
|
+
if (!this.minimumVersion) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const platform = request.headers["x-platform"];
|
|
20
|
+
|
|
21
|
+
let version!: number;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
version = request.getVersion()
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if ((isSimpleError(e) || isSimpleErrors(e)) && e.hasCode('missing_version')) {
|
|
27
|
+
// Allow missing version on /openid/ path
|
|
28
|
+
if (request.url.startsWith("/openid/")) {
|
|
29
|
+
request.version = this.latestVersions.web
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*if (request.getIP() === '') {
|
|
37
|
+
throw new SimpleError({
|
|
38
|
+
code: "blocked",
|
|
39
|
+
statusCode: 400,
|
|
40
|
+
message: "Temporary blocked",
|
|
41
|
+
human: "Jouw verbinding is tijdelijk geblokkeerd. Gelieve contact op te nemen met hallo@stamhoofd.be"
|
|
42
|
+
})
|
|
43
|
+
}*/
|
|
44
|
+
|
|
45
|
+
if (version < this.minimumVersion) {
|
|
46
|
+
// WARNING: update caddy config for on demand certificates, because we don't want to throw errors over there!
|
|
47
|
+
if (platform === "web" || platform === undefined) {
|
|
48
|
+
throw new SimpleError({
|
|
49
|
+
code: "client_update_required",
|
|
50
|
+
statusCode: 400,
|
|
51
|
+
message: "Er is een noodzakelijke update beschikbaar. Herlaad de pagina en wis indien nodig de cache van jouw browser.",
|
|
52
|
+
human: "Er is een noodzakelijke update beschikbaar. Herlaad de pagina en wis indien nodig de cache van jouw browser."
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
throw new SimpleError({
|
|
56
|
+
code: "client_update_required",
|
|
57
|
+
statusCode: 400,
|
|
58
|
+
message: "Er is een noodzakelijke update beschikbaar. Update de app en probeer opnieuw!",
|
|
59
|
+
human: "Er is een noodzakelijke update beschikbaar. Update de app en probeer opnieuw!"
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
handleResponse(request: Request, response: EncodedResponse) {
|
|
66
|
+
const platform = request.headers["x-platform"];
|
|
67
|
+
|
|
68
|
+
if (platform === "android") {
|
|
69
|
+
response.headers["X-Platform-Latest-Version"] = this.latestVersions.android
|
|
70
|
+
}
|
|
71
|
+
if (platform === "ios") {
|
|
72
|
+
response.headers["X-Platform-Latest-Version"] = this.latestVersions.ios
|
|
73
|
+
}
|
|
74
|
+
if (platform === "web") {
|
|
75
|
+
response.headers["X-Platform-Latest-Version"] = this.latestVersions.web
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
response.headers["X-Version"] = Math.min(Version, request.getVersion())
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// No version provided or invalid version
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|