@universis/janitor 2.2.0 → 2.4.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.
@@ -0,0 +1,145 @@
1
+ import {Router} from 'express';
2
+ import cloneDeep from 'lodash/cloneDeep';
3
+ import MD5 from 'crypto-js/md5';
4
+ import { ValueFormatter } from '@themost/data';
5
+
6
+ /**
7
+ *
8
+ * @param {{profiles:Map<string, import('./outputCacheRouter').OutputCachingProfile>,elements: import('./outputCacheRouter').OutputCachingElement[]}} options
9
+ * @returns {import('express').Router}
10
+ */
11
+ function outputCache({profiles, elements}) {
12
+ const router = new Router();
13
+ // prepare regular expressions
14
+ const outputCacheElements = Array.isArray(elements) ? elements : [];
15
+ outputCacheElements.forEach((element) => {
16
+ if (!(element.expr instanceof RegExp)) {
17
+ element.expr = new RegExp('^' + element.resource, 'g');
18
+ }
19
+ });
20
+ router.use((req, res, next) => {
21
+ // only GET requests are cacheable
22
+ if (req.method !== 'GET') {
23
+ return next();
24
+ }
25
+ // if cache-control header is set to no-cache then skip caching
26
+ const cacheControl = req.headers['cache-control'];
27
+ if (cacheControl && cacheControl.includes('no-cache')) {
28
+ return next();
29
+ }
30
+ for (let element of outputCacheElements) {
31
+ if (element.expr.test(req.path)) {
32
+ let profile;
33
+ if (typeof element.profile === 'string') {
34
+ profile = profiles.get(element.profile);
35
+ } else if (typeof element.profile === 'object') {
36
+ profile = element.profile;
37
+ }
38
+ if (profile) {
39
+ // clone profile to avoid mutation
40
+ Object.assign(req, {
41
+ outputCaching: cloneDeep(profile)
42
+ });
43
+ }
44
+ return next();
45
+ }
46
+ }
47
+ return next();
48
+ });
49
+ router.use((req, res, next) => {
50
+ try {
51
+ const shouldCached = req.outputCaching && (req.outputCaching.location === 'any' || req.outputCaching.location === 'client');
52
+ if (!shouldCached) {
53
+ return next();
54
+ }
55
+ // determine cache location
56
+ let cacheLocation = req.outputCaching.location === 'any' ? 'public' : 'private';
57
+ if (req.headers.authorization && req.headers.authorization.length > 0) {
58
+ // if authorization header is present, we should not cache the response publicly
59
+ cacheLocation = 'private';
60
+ }
61
+ if (req.outputCaching.duration) {
62
+ res.setHeader('Cache-Control', `${cacheLocation}, max-age=${req.outputCaching.duration}`);
63
+ } else {
64
+ res.setHeader('Cache-Control', cacheLocation);
65
+ }
66
+ const {varyByContentEncoding, varyByHeader, varyByParam, location, duration} = req.outputCaching;
67
+ if (varyByContentEncoding) {
68
+ res.setHeader('Vary', 'Accept-Encoding');
69
+ }
70
+ const varyByReqHeader = varyByHeader || [];
71
+ if (varyByReqHeader.length) {
72
+ res.setHeader('Vary', varyByReqHeader.join(', '));
73
+ }
74
+ let varyByQueryParam = varyByParam || [];
75
+ if (varyByQueryParam.length === 1 && varyByQueryParam[0] === '*') {
76
+ varyByQueryParam = Object.keys(req.query).sort((a, b) => {
77
+ if (a < b) {
78
+ return -1;
79
+ }
80
+ if (a > b) {
81
+ return 1;
82
+ }
83
+ return 0;
84
+ });
85
+ }
86
+
87
+ const headers = varyByReqHeader.reduce((acc, val) => {
88
+ acc[val] = req.get(val);
89
+ return acc;
90
+ }, {});
91
+
92
+ const params = varyByQueryParam.reduce((acc, val) => {
93
+ if (req.query[val]) {
94
+ acc[val] = req.query[val];
95
+ }
96
+ return acc;
97
+ }, {});
98
+
99
+ // vary by query parameters is not supported by HTTP caching, but we can use it to create a cache key
100
+ const entry = {
101
+ path: req.path,
102
+ location: location,
103
+ contentEncoding: varyByContentEncoding,
104
+ headers,
105
+ params,
106
+ customParams: null,
107
+ duration: duration
108
+ };
109
+
110
+ (async () => {
111
+ const {varyByExpr} = req.outputCaching;
112
+ if (varyByExpr && typeof varyByExpr === 'object') {
113
+ // noinspection JSCheckFunctionSignatures
114
+ const formatter = new ValueFormatter(req.context, null, req);
115
+ entry.customParams = await formatter.format(varyByExpr);
116
+ }
117
+ return `W/"${MD5(JSON.stringify({
118
+ path: entry.path,
119
+ location: entry.location,
120
+ contentEncoding: entry.contentEncoding,
121
+ headers: entry.headers,
122
+ params: entry.params,
123
+ customParams: entry.customParams,
124
+ duration: entry.duration,
125
+ })).toString()}"`;
126
+ })().then((entityTag) => {
127
+ if (req.headers['if-none-match'] === entityTag) {
128
+ return res.status(304).end();
129
+ }
130
+ res.setHeader('ETag', entityTag);
131
+ res.setHeader('Date', new Date().toUTCString());
132
+ return next();
133
+ }).catch((err) => {
134
+ return next(err);
135
+ });
136
+ } catch (err) {
137
+ return next(err);
138
+ }
139
+ });
140
+ return router;
141
+ }
142
+
143
+ export {
144
+ outputCache
145
+ }
@@ -0,0 +1,96 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "profiles": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "array",
9
+ "items": [
10
+ {
11
+ "type": "string"
12
+ },
13
+ {
14
+ "$ref": "#/definitions/OutputCachingProfile"
15
+ }
16
+ ]
17
+ }
18
+ },
19
+ "elements": {
20
+ "type": "array",
21
+ "items": {
22
+ "$ref": "#/definitions/OutputCachingElement"
23
+ }
24
+ }
25
+ },
26
+ "required": [
27
+ "profiles",
28
+ "elements"
29
+ ],
30
+ "definitions": {
31
+ "OutputCachingProfile": {
32
+ "type": "object",
33
+ "properties": {
34
+ "location": {
35
+ "type": "string",
36
+ "enum": [
37
+ "any",
38
+ "none",
39
+ "server",
40
+ "client",
41
+ "serverAndClient"
42
+ ]
43
+ },
44
+ "duration": {
45
+ "type": "number"
46
+ },
47
+ "varyByContentEncoding": {
48
+ "type": "string"
49
+ },
50
+ "varyByHeader": {
51
+ "type": "array",
52
+ "items": {
53
+ "type": "string"
54
+ }
55
+ },
56
+ "varyByParam": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "string"
60
+ }
61
+ },
62
+ "varyByExpr": {
63
+ "type": "object"
64
+ }
65
+ },
66
+ "required": [
67
+ "location"
68
+ ]
69
+ },
70
+ "OutputCachingElement": {
71
+ "type": "object",
72
+ "properties": {
73
+ "resource": {
74
+ "type": "string"
75
+ },
76
+ "expr": {
77
+ "type": "object"
78
+ },
79
+ "profile": {
80
+ "oneOf": [
81
+ {
82
+ "$ref": "#/definitions/OutputCachingProfile"
83
+ },
84
+ {
85
+ "type": "string"
86
+ }
87
+ ]
88
+ }
89
+ },
90
+ "required": [
91
+ "resource",
92
+ "profile"
93
+ ]
94
+ }
95
+ }
96
+ }
@@ -1,2 +1,3 @@
1
1
  import { Handler } from 'express';
2
2
  export declare function validateScope(): Handler;
3
+ export declare function validateClientScope(): Handler;
@@ -1,26 +1,54 @@
1
1
  import { ScopeAccessConfiguration } from './ScopeAccessConfiguration';
2
2
  import {HttpForbiddenError} from '@themost/common';
3
+ import {VerifyUserClientScope} from './VerifyUserClientScope';
4
+
5
+ function validateClientScope() {
6
+ return function (req, res, next) {
7
+ try {
8
+ /**
9
+ * @type {VerifyUserClientScope}
10
+ */
11
+ const service = req.context.getApplication().getService(VerifyUserClientScope);
12
+ if (service == null) {
13
+ return next();
14
+ }
15
+ void service.verify(req).then(() => {
16
+ return next();
17
+ }).catch((reason) => {
18
+ return next(reason);
19
+ });
20
+ } catch (err) {
21
+ return next(err);
22
+ }
23
+ }
24
+ }
3
25
 
4
26
  function validateScope() {
5
27
  return (req, res, next) => {
6
- /**
7
- * @type {ScopeAccessConfiguration}
8
- */
9
- let scopeAccessConfiguration = req.context.getApplication().getConfiguration().getStrategy(ScopeAccessConfiguration);
10
- if (typeof scopeAccessConfiguration === 'undefined') {
11
- return next(new Error('Invalid application configuration. Scope access configuration strategy is missing or is in accessible.'));
12
- }
13
- scopeAccessConfiguration.verify(req).then(value => {
14
- if (value) {
15
- return next();
28
+ return validateClientScope(req, res, (err) => {
29
+ if (err) {
30
+ return next(err);
31
+ }
32
+ /**
33
+ * @type {ScopeAccessConfiguration}
34
+ */
35
+ let scopeAccessConfiguration = req.context.getApplication().getConfiguration().getStrategy(ScopeAccessConfiguration);
36
+ if (typeof scopeAccessConfiguration === 'undefined') {
37
+ return next(new Error('Invalid application configuration. Scope access configuration strategy is missing or is in accessible.'));
16
38
  }
17
- return next(new HttpForbiddenError('Access denied due to authorization scopes.'))
18
- }).catch(reason => {
19
- return next(reason);
39
+ scopeAccessConfiguration.verify(req).then(value => {
40
+ if (value) {
41
+ return next();
42
+ }
43
+ return next(new HttpForbiddenError('Access denied due to authorization scopes.'))
44
+ }).catch(reason => {
45
+ return next(reason);
46
+ });
20
47
  });
21
48
  };
22
49
  }
23
50
 
24
51
  export {
25
- validateScope
52
+ validateScope,
53
+ validateClientScope
26
54
  }