@adaptivestone/framework 4.9.2 → 4.11.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/CHANGELOG.md +11 -0
- package/config/http.js +1 -1
- package/package.json +3 -3
- package/services/http/HttpServer.js +15 -7
- package/services/http/middleware/Cors.js +44 -0
- package/services/http/middleware/Cors.test.js +135 -0
- package/services/http/middleware/GetUserByToken.js +1 -1
- package/services/http/middleware/GetUserByToken.test.js +108 -0
- package/services/http/middleware/StaticFiles.js +60 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
### 4.11.0
|
|
2
|
+
|
|
3
|
+
[NEW] Cors middleware
|
|
4
|
+
[BREAKING] This is a potencial breaking change as we switched from cors external package to internal middleware. From API nothing was changed. This is a potencial breaking changes, but it should keep working as it
|
|
5
|
+
|
|
6
|
+
### 4.10.0
|
|
7
|
+
|
|
8
|
+
[UPDATE] deps update
|
|
9
|
+
[NEW] Static file middleware
|
|
10
|
+
[BREAKING] This is a potencial breaking change as we switched from express.static to internal middleware that provide less features but faster. From API nothing was changed
|
|
11
|
+
|
|
1
12
|
### 4.9.2
|
|
2
13
|
|
|
3
14
|
[UPDATE] deps update
|
package/config/http.js
CHANGED
|
@@ -2,7 +2,7 @@ module.exports = {
|
|
|
2
2
|
port: process.env.HTTP_PORT || 3300,
|
|
3
3
|
hostname: process.env.HTTP_HOST || '0.0.0.0',
|
|
4
4
|
// if you want to use 'all' domains please copy this file to your app
|
|
5
|
-
// and set "corsDomains:
|
|
5
|
+
// and set "corsDomains: [/./]
|
|
6
6
|
corsDomains: ['http://localhost:3000'],
|
|
7
7
|
myDomain: process.env.HTTP_DOMAIN || 'http://localhost:3300',
|
|
8
8
|
siteDomain: process.env.FRONT_DOMAIN || 'http://localhost:3000',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaptivestone/framework",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.11.0",
|
|
4
4
|
"description": "Adaptive stone node js framework",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"engines": {
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"author": "Andrey Logunov",
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"cors": "^2.8.5",
|
|
32
31
|
"deepmerge": "^4.2.2",
|
|
33
32
|
"dotenv": "^16.0.0",
|
|
34
33
|
"express": "^4.17.1",
|
|
@@ -38,6 +37,7 @@
|
|
|
38
37
|
"i18next-chained-backend": "^4.0.0",
|
|
39
38
|
"i18next-fs-backend": "^2.0.0",
|
|
40
39
|
"juice": "^9.0.0",
|
|
40
|
+
"mime": "^3.0.0",
|
|
41
41
|
"minimist": "^1.2.5",
|
|
42
42
|
"mongoose": "^7.0.0",
|
|
43
43
|
"nodemailer": "^6.6.3",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"eslint-plugin-prettier": "^5.0.0",
|
|
59
59
|
"eslint-plugin-vitest": "^0.3.1",
|
|
60
60
|
"husky": "^8.0.0",
|
|
61
|
-
"lint-staged": "^
|
|
61
|
+
"lint-staged": "^15.0.0",
|
|
62
62
|
"mongodb-memory-server": "^9.0.0",
|
|
63
63
|
"nodemon": "^3.0.1",
|
|
64
64
|
"prettier": "^3.0.0",
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const http = require('node:http');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const express = require('express');
|
|
4
|
-
const cors = require('cors');
|
|
5
4
|
|
|
6
5
|
const RequestLoggerMiddleware = require('./middleware/RequestLogger');
|
|
7
6
|
const I18nMiddleware = require('./middleware/I18n');
|
|
8
7
|
const PrepareAppInfoMiddleware = require('./middleware/PrepareAppInfo');
|
|
9
8
|
const RequestParserMiddleware = require('./middleware/RequestParser');
|
|
9
|
+
const StaticFilesMiddleware = require('./middleware/StaticFiles');
|
|
10
|
+
const Cors = require('./middleware/Cors');
|
|
10
11
|
|
|
11
12
|
const Base = require('../../modules/Base');
|
|
12
13
|
|
|
@@ -30,12 +31,19 @@ class HttpServer extends Base {
|
|
|
30
31
|
|
|
31
32
|
const httpConfig = this.app.getConfig('http');
|
|
32
33
|
this.express.use(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
this.express.use(
|
|
34
|
+
new Cors(this.app, {
|
|
35
|
+
origins: httpConfig.corsDomains,
|
|
36
|
+
}).getMiddleware(),
|
|
37
|
+
);
|
|
38
|
+
// todo whitelist
|
|
39
|
+
this.express.use(
|
|
40
|
+
new StaticFilesMiddleware(this.app, {
|
|
41
|
+
folders: [
|
|
42
|
+
this.app.foldersConfig.public,
|
|
43
|
+
path.join(__dirname, '../../public/files'),
|
|
44
|
+
],
|
|
45
|
+
}).getMiddleware(),
|
|
46
|
+
);
|
|
39
47
|
|
|
40
48
|
this.express.use(new RequestParserMiddleware(this.app).getMiddleware());
|
|
41
49
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const AbstractMiddleware = require('./AbstractMiddleware');
|
|
2
|
+
|
|
3
|
+
class Cors extends AbstractMiddleware {
|
|
4
|
+
constructor(app, params) {
|
|
5
|
+
super(app);
|
|
6
|
+
this.params = params;
|
|
7
|
+
if (!Array.isArray(params?.origins) || !params.origins.length) {
|
|
8
|
+
throw new Error('Cors inited without origin config');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static get description() {
|
|
13
|
+
return 'Add CORS headers to request';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async middleware(req, res, next) {
|
|
17
|
+
if (req.method !== 'OPTIONS') {
|
|
18
|
+
// only get supported
|
|
19
|
+
return next();
|
|
20
|
+
}
|
|
21
|
+
for (const host of this.params.origins) {
|
|
22
|
+
if (
|
|
23
|
+
(typeof host === 'string' && req.headers.origin === host) ||
|
|
24
|
+
(host instanceof RegExp && host.test(req.headers.origin))
|
|
25
|
+
) {
|
|
26
|
+
res.set('Access-Control-Allow-Origin', req.headers.origin);
|
|
27
|
+
res.set(
|
|
28
|
+
'Access-Control-Allow-Methods',
|
|
29
|
+
'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
30
|
+
);
|
|
31
|
+
res.set('Vary', 'Origin, Access-Control-Request-Headers');
|
|
32
|
+
|
|
33
|
+
const allowedHeaders = req.headers['access-control-request-headers'];
|
|
34
|
+
if (allowedHeaders) {
|
|
35
|
+
res.set('Access-Control-Allow-Headers', allowedHeaders);
|
|
36
|
+
}
|
|
37
|
+
return res.end();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return next();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = Cors;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import Cors from './Cors';
|
|
3
|
+
|
|
4
|
+
describe('cors middleware methods', () => {
|
|
5
|
+
it('have description fields', async () => {
|
|
6
|
+
expect.assertions(1);
|
|
7
|
+
const middleware = new Cors(global.server.app, { origins: ['something'] });
|
|
8
|
+
expect(middleware.constructor.description).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should throw without origns', async () => {
|
|
12
|
+
expect.assertions(1);
|
|
13
|
+
expect(() => new Cors(global.server.app)).toThrow();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should throw with empty options', async () => {
|
|
17
|
+
expect.assertions(1);
|
|
18
|
+
expect(() => new Cors(global.server.app, {})).toThrow();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should throw with empty origins', async () => {
|
|
22
|
+
expect.assertions(1);
|
|
23
|
+
expect(() => new Cors(global.server.app, { origins: [] })).toThrow();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should throw with empty origins not array', async () => {
|
|
27
|
+
expect.assertions(1);
|
|
28
|
+
expect(() => new Cors(global.server.app, { origins: 'origins' })).toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('non options should be ignored', async () => {
|
|
32
|
+
expect.assertions(1);
|
|
33
|
+
let isCalled = false;
|
|
34
|
+
const nextFunction = () => {
|
|
35
|
+
isCalled = true;
|
|
36
|
+
};
|
|
37
|
+
const req = {
|
|
38
|
+
method: 'GET',
|
|
39
|
+
};
|
|
40
|
+
const middleware = new Cors(global.server.app, {
|
|
41
|
+
origins: ['something'],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
45
|
+
expect(isCalled).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('host the not match origin', async () => {
|
|
49
|
+
expect.assertions(1);
|
|
50
|
+
let isCalled = false;
|
|
51
|
+
const nextFunction = () => {
|
|
52
|
+
isCalled = true;
|
|
53
|
+
};
|
|
54
|
+
const req = {
|
|
55
|
+
method: 'OPTIONS',
|
|
56
|
+
headers: { origin: 'http://anotherDomain.com' },
|
|
57
|
+
};
|
|
58
|
+
const middleware = new Cors(global.server.app, {
|
|
59
|
+
origins: ['https://localhost'],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
63
|
+
expect(isCalled).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('string domain match', async () => {
|
|
67
|
+
expect.assertions(5);
|
|
68
|
+
let isEndCalled = false;
|
|
69
|
+
const map = new Map();
|
|
70
|
+
const req = {
|
|
71
|
+
method: 'OPTIONS',
|
|
72
|
+
headers: {
|
|
73
|
+
origin: 'https://localhost',
|
|
74
|
+
'access-control-request-headers': 'someAccessControlRequestHeaders',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const res = {
|
|
78
|
+
set: (key, val) => {
|
|
79
|
+
map.set(key, val);
|
|
80
|
+
},
|
|
81
|
+
end: () => {
|
|
82
|
+
isEndCalled = true;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const middleware = new Cors(global.server.app, {
|
|
86
|
+
origins: ['https://localhost'],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await middleware.middleware(req, res);
|
|
90
|
+
expect(isEndCalled).toBeTruthy();
|
|
91
|
+
expect(map.get('Vary')).toBe('Origin, Access-Control-Request-Headers');
|
|
92
|
+
expect(map.get('Access-Control-Allow-Headers')).toBe(
|
|
93
|
+
'someAccessControlRequestHeaders',
|
|
94
|
+
);
|
|
95
|
+
expect(map.get('Access-Control-Allow-Origin')).toBe('https://localhost');
|
|
96
|
+
expect(map.get('Access-Control-Allow-Methods')).toBe(
|
|
97
|
+
'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('regexp domain match', async () => {
|
|
102
|
+
expect.assertions(5);
|
|
103
|
+
let isEndCalled = false;
|
|
104
|
+
const map = new Map();
|
|
105
|
+
const req = {
|
|
106
|
+
method: 'OPTIONS',
|
|
107
|
+
headers: {
|
|
108
|
+
origin: 'https://localhost',
|
|
109
|
+
'access-control-request-headers': 'someAccessControlRequestHeaders',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
const res = {
|
|
113
|
+
set: (key, val) => {
|
|
114
|
+
map.set(key, val);
|
|
115
|
+
},
|
|
116
|
+
end: () => {
|
|
117
|
+
isEndCalled = true;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const middleware = new Cors(global.server.app, {
|
|
121
|
+
origins: [/./],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await middleware.middleware(req, res);
|
|
125
|
+
expect(isEndCalled).toBeTruthy();
|
|
126
|
+
expect(map.get('Vary')).toBe('Origin, Access-Control-Request-Headers');
|
|
127
|
+
expect(map.get('Access-Control-Allow-Headers')).toBe(
|
|
128
|
+
'someAccessControlRequestHeaders',
|
|
129
|
+
);
|
|
130
|
+
expect(map.get('Access-Control-Allow-Origin')).toBe('https://localhost');
|
|
131
|
+
expect(map.get('Access-Control-Allow-Methods')).toBe(
|
|
132
|
+
'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -23,7 +23,7 @@ class GetUserByToken extends AbstractMiddleware {
|
|
|
23
23
|
}
|
|
24
24
|
let { token } = req.body;
|
|
25
25
|
this.logger.verbose(
|
|
26
|
-
`GetUserByToken token in BODY ${token}. Token
|
|
26
|
+
`GetUserByToken token in BODY ${token}. Token in Authorization header ${req.get(
|
|
27
27
|
'Authorization',
|
|
28
28
|
)}`,
|
|
29
29
|
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import GetUserByToken from './GetUserByToken';
|
|
3
|
+
|
|
4
|
+
describe('getUserByToken middleware methods', () => {
|
|
5
|
+
it('have description fields', async () => {
|
|
6
|
+
expect.assertions(1);
|
|
7
|
+
const middleware = new GetUserByToken(global.server.app);
|
|
8
|
+
expect(middleware.constructor.description).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('have description usedAuthParameters', async () => {
|
|
12
|
+
expect.assertions(2);
|
|
13
|
+
const middleware = new GetUserByToken(global.server.app);
|
|
14
|
+
const params = middleware.usedAuthParameters;
|
|
15
|
+
expect(params).toHaveLength(1);
|
|
16
|
+
expect(params[0].name).toBe('Authorization');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should not called twice', async () => {
|
|
20
|
+
expect.assertions(1);
|
|
21
|
+
const middleware = new GetUserByToken(global.server.app);
|
|
22
|
+
let isCalled = false;
|
|
23
|
+
const nextFunction = () => {
|
|
24
|
+
isCalled = true;
|
|
25
|
+
};
|
|
26
|
+
const req = {
|
|
27
|
+
appInfo: {
|
|
28
|
+
user: {},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
32
|
+
expect(isCalled).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should not getuser without token', async () => {
|
|
36
|
+
expect.assertions(1);
|
|
37
|
+
const middleware = new GetUserByToken(global.server.app);
|
|
38
|
+
let isCalled = false;
|
|
39
|
+
const nextFunction = () => {
|
|
40
|
+
isCalled = true;
|
|
41
|
+
};
|
|
42
|
+
const req = {
|
|
43
|
+
appInfo: {},
|
|
44
|
+
body: {},
|
|
45
|
+
get: () => {},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
49
|
+
expect(isCalled).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should not getuser with a wrong token', async () => {
|
|
53
|
+
expect.assertions(2);
|
|
54
|
+
const middleware = new GetUserByToken(global.server.app);
|
|
55
|
+
let isCalled = false;
|
|
56
|
+
const nextFunction = () => {
|
|
57
|
+
isCalled = true;
|
|
58
|
+
};
|
|
59
|
+
const req = {
|
|
60
|
+
appInfo: {},
|
|
61
|
+
body: {
|
|
62
|
+
token: 'fake',
|
|
63
|
+
},
|
|
64
|
+
get: () => {},
|
|
65
|
+
};
|
|
66
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
67
|
+
expect(isCalled).toBeTruthy();
|
|
68
|
+
expect(req.appInfo.user).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should not getuser with a good token in body', async () => {
|
|
72
|
+
expect.assertions(2);
|
|
73
|
+
const middleware = new GetUserByToken(global.server.app);
|
|
74
|
+
let isCalled = false;
|
|
75
|
+
const nextFunction = () => {
|
|
76
|
+
isCalled = true;
|
|
77
|
+
};
|
|
78
|
+
const req = {
|
|
79
|
+
appInfo: {},
|
|
80
|
+
body: {
|
|
81
|
+
token: global.authToken.token,
|
|
82
|
+
},
|
|
83
|
+
get: () => {},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
87
|
+
expect(isCalled).toBeTruthy();
|
|
88
|
+
expect(req.appInfo.user).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should not getuser with a good token in header', async () => {
|
|
92
|
+
expect.assertions(2);
|
|
93
|
+
const middleware = new GetUserByToken(global.server.app);
|
|
94
|
+
let isCalled = false;
|
|
95
|
+
const nextFunction = () => {
|
|
96
|
+
isCalled = true;
|
|
97
|
+
};
|
|
98
|
+
const req = {
|
|
99
|
+
appInfo: {},
|
|
100
|
+
body: {},
|
|
101
|
+
get: () => global.authToken.token,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await middleware.middleware(req, {}, nextFunction);
|
|
105
|
+
expect(isCalled).toBeTruthy();
|
|
106
|
+
expect(req.appInfo.user).toBeDefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const fsPromises = require('node:fs/promises');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const mime = require('mime');
|
|
5
|
+
|
|
6
|
+
const AbstractMiddleware = require('./AbstractMiddleware');
|
|
7
|
+
/**
|
|
8
|
+
* Middleware for static files
|
|
9
|
+
*/
|
|
10
|
+
class StaticFiles extends AbstractMiddleware {
|
|
11
|
+
constructor(app, params) {
|
|
12
|
+
super(app);
|
|
13
|
+
this.params = params;
|
|
14
|
+
if (!params || !params.folders || !params.folders.length) {
|
|
15
|
+
throw new Error('StaticFiles inited without folders config');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static get description() {
|
|
20
|
+
return 'Static file server middleware. Host you static files from public foolder. Mostly for dev.';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async middleware(req, res, next) {
|
|
24
|
+
if (req.method !== 'GET') {
|
|
25
|
+
// only get supported
|
|
26
|
+
return next();
|
|
27
|
+
}
|
|
28
|
+
const { folders } = this.params;
|
|
29
|
+
|
|
30
|
+
const promises = [];
|
|
31
|
+
|
|
32
|
+
for (const f of folders) {
|
|
33
|
+
const filePath = path.join(f, req.url);
|
|
34
|
+
promises.push(
|
|
35
|
+
fsPromises
|
|
36
|
+
.stat(filePath)
|
|
37
|
+
.catch(() => {
|
|
38
|
+
// nothing there, file just not exists
|
|
39
|
+
})
|
|
40
|
+
.then((stats) => ({ stats, file: filePath })),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fileStats = await Promise.all(promises);
|
|
45
|
+
|
|
46
|
+
for (const fileStat of fileStats) {
|
|
47
|
+
if (fileStat.stats && fileStat.stats.isFile()) {
|
|
48
|
+
const contentType = mime.getType(fileStat.file);
|
|
49
|
+
const fileStream = fs.createReadStream(fileStat.file);
|
|
50
|
+
res.set('Content-Type', contentType);
|
|
51
|
+
fileStream.pipe(res);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return next();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = StaticFiles;
|