@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 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
+ }