@vario-software/vario-app-framework-backend 2025.37.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/api/Api.js +449 -0
- package/api/ErpApi.js +94 -0
- package/api/helpers/fetch.js +10 -0
- package/api/helpers/gateway.js +19 -0
- package/api/helpers/getResponseStream.js +17 -0
- package/api/helpers/redirectRequest.js +13 -0
- package/api/helpers/vql.js +61 -0
- package/api/modules/eav.js +83 -0
- package/api/modules/migration.js +95 -0
- package/api/modules/textEnum.js +149 -0
- package/api/modules/webhook.js +44 -0
- package/app.js +115 -0
- package/modules/accessToken.js +43 -0
- package/modules/baseUrlCache.js +53 -0
- package/modules/offlineToken.js +44 -0
- package/package.json +23 -0
- package/setup/appAuthentication.js +45 -0
- package/setup/context.js +37 -0
- package/setup/exception.js +49 -0
- package/utils/context.js +82 -0
- package/utils/httpError.js +16 -0
- package/utils/keycloak.js +57 -0
- package/utils/licenses.js +59 -0
- package/utils/logger.js +29 -0
- package/utils/migrator.js +320 -0
- package/utils/permission.js +34 -0
- package/utils/promiseSingletonMap.js +30 -0
- package/utils/token.js +83 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const Eav = class
|
|
2
|
+
{
|
|
3
|
+
constructor(ApiAdapter)
|
|
4
|
+
{
|
|
5
|
+
this.ApiAdapter = ApiAdapter;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
getGroup = async function(groupKey)
|
|
9
|
+
{
|
|
10
|
+
const { data: eavGroup } = await this.ApiAdapter.fetch(`/cmn/eav-groups/by-key/${groupKey}`, {
|
|
11
|
+
method: 'GET',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
return eavGroup;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
setGroup = async function(eavGroup)
|
|
18
|
+
{
|
|
19
|
+
const existingGroup = await this.getGroupIdByKey(eavGroup.key);
|
|
20
|
+
|
|
21
|
+
if (existingGroup)
|
|
22
|
+
{
|
|
23
|
+
eavGroup.id = existingGroup;
|
|
24
|
+
|
|
25
|
+
return eavGroup;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
eavGroup = await this.ApiAdapter.fetch('/cmn/eav-groups', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
body: JSON.stringify(eavGroup),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return eavGroup.data;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
changeGroup = async function (groupKey, callback)
|
|
37
|
+
{
|
|
38
|
+
let { data: eavGroup } = await this.ApiAdapter.fetch(`/cmn/eav-groups/by-key/${groupKey}`, {
|
|
39
|
+
method: 'GET',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
eavGroup = callback(eavGroup);
|
|
43
|
+
|
|
44
|
+
eavGroup = await this.ApiAdapter.fetch(`/cmn/eav-groups/${eavGroup.id}`, {
|
|
45
|
+
method: 'PUT',
|
|
46
|
+
body: JSON.stringify(eavGroup),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return eavGroup.data;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
deleteGroup = async function (groupKey)
|
|
53
|
+
{
|
|
54
|
+
const { data: eavGroup } = await this.ApiAdapter.fetch(`/cmn/eav-groups/by-key/${groupKey}`);
|
|
55
|
+
|
|
56
|
+
await this.ApiAdapter.fetch(`/cmn/eav-groups/${eavGroup.id}/remove-data`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body: JSON.stringify({ entities: eavGroup.entities }),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await this.ApiAdapter.fetch(`/cmn/eav-groups/${eavGroup.id}`, {
|
|
62
|
+
method: 'DELETE',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
getGroupIdByKey = async function(groupKey)
|
|
69
|
+
{
|
|
70
|
+
try
|
|
71
|
+
{
|
|
72
|
+
const { data: eavGroup } = await this.ApiAdapter.fetch(`/cmn/eav-groups/by-key/${groupKey}`);
|
|
73
|
+
|
|
74
|
+
return eavGroup.id;
|
|
75
|
+
}
|
|
76
|
+
catch
|
|
77
|
+
{
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
module.exports = Eav;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const { getApp } = require('#backend/utils/context.js');
|
|
2
|
+
|
|
3
|
+
const Migration = class
|
|
4
|
+
{
|
|
5
|
+
constructor(ApiAdapter)
|
|
6
|
+
{
|
|
7
|
+
this.ApiAdapter = ApiAdapter;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get = async function(identifier)
|
|
11
|
+
{
|
|
12
|
+
const { data } = await this.getAll({ limit: 1, identifier });
|
|
13
|
+
|
|
14
|
+
return data[0];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
getAll = async function({ offset = 0, limit = 10, identifier })
|
|
18
|
+
{
|
|
19
|
+
const app = getApp();
|
|
20
|
+
|
|
21
|
+
const { data, moreElements, nextOffset } = await this.ApiAdapter.vql({
|
|
22
|
+
statement: `
|
|
23
|
+
SELECT id,
|
|
24
|
+
identifier
|
|
25
|
+
FROM system.queryAppMigrations
|
|
26
|
+
WHERE id NOTNULL
|
|
27
|
+
AND appIdentifier = '${app.client.appIdentifier}'
|
|
28
|
+
${identifier ? `AND identifier = '${identifier}'` : ''}
|
|
29
|
+
`,
|
|
30
|
+
offset,
|
|
31
|
+
limit,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
data,
|
|
36
|
+
moreElements,
|
|
37
|
+
nextOffset,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async getNote(key)
|
|
42
|
+
{
|
|
43
|
+
const app = getApp();
|
|
44
|
+
|
|
45
|
+
const { data } = await this.ApiAdapter.vql({
|
|
46
|
+
statement: `
|
|
47
|
+
SELECT note
|
|
48
|
+
FROM system.queryAppMigrations
|
|
49
|
+
WHERE appIdentifier = '${app.client.appIdentifier}'
|
|
50
|
+
AND identifier = '${key}'
|
|
51
|
+
`,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let note;
|
|
55
|
+
|
|
56
|
+
try
|
|
57
|
+
{
|
|
58
|
+
note = JSON.parse(data[0]?.note);
|
|
59
|
+
}
|
|
60
|
+
catch (error)
|
|
61
|
+
{
|
|
62
|
+
note = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return note;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
set = async function(identifier, note)
|
|
69
|
+
{
|
|
70
|
+
const app = getApp();
|
|
71
|
+
|
|
72
|
+
const { data: response } = await this.ApiAdapter.fetch('/cmn/system/app-migration', {
|
|
73
|
+
body: {
|
|
74
|
+
appIdentifier: app.client.appIdentifier,
|
|
75
|
+
identifier,
|
|
76
|
+
executedAt: new Date().toISOString(),
|
|
77
|
+
note,
|
|
78
|
+
},
|
|
79
|
+
method: 'post',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return response;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
delete = async function(id)
|
|
86
|
+
{
|
|
87
|
+
const { data: response } = await this.ApiAdapter.fetch(`/cmn/system/app-migration/${id}`, {
|
|
88
|
+
method: 'delete',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return response;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
module.exports = Migration;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const TextEnum = class
|
|
2
|
+
{
|
|
3
|
+
constructor(ApiAdapter)
|
|
4
|
+
{
|
|
5
|
+
this.ApiAdapter = ApiAdapter;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
get = async function(key)
|
|
9
|
+
{
|
|
10
|
+
const { data } = await this.getAll({ limit: 1, key });
|
|
11
|
+
|
|
12
|
+
return data[0];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
getAll = async function({ offset = 0, limit = 10, key })
|
|
16
|
+
{
|
|
17
|
+
const { data, moreElements, nextOffset } = await this.ApiAdapter.vql({
|
|
18
|
+
statement: `
|
|
19
|
+
SELECT
|
|
20
|
+
id,
|
|
21
|
+
label,
|
|
22
|
+
key,
|
|
23
|
+
textEnums.id,
|
|
24
|
+
textEnums.label
|
|
25
|
+
FROM masterdata.query-textenum-group
|
|
26
|
+
${key ? `WHERE key = '${key}'` : ''}
|
|
27
|
+
`,
|
|
28
|
+
offset,
|
|
29
|
+
limit,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
data,
|
|
34
|
+
moreElements,
|
|
35
|
+
nextOffset,
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
setGroup = async function(textEnumGroupTemplate)
|
|
40
|
+
{
|
|
41
|
+
let textEnumGroup = await this.getEnumGroup(textEnumGroupTemplate.key);
|
|
42
|
+
|
|
43
|
+
if (textEnumGroup)
|
|
44
|
+
{
|
|
45
|
+
await this.removeEnums(textEnumGroup);
|
|
46
|
+
}
|
|
47
|
+
else
|
|
48
|
+
{
|
|
49
|
+
textEnumGroup = await this.createGroup(textEnumGroupTemplate);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return textEnumGroup;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
setEnums = async function(textEnumGroupKey, textEnums)
|
|
56
|
+
{
|
|
57
|
+
await this.createEnums(
|
|
58
|
+
textEnumGroupKey,
|
|
59
|
+
textEnums,
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
createGroup = async function(textEnumGroup)
|
|
64
|
+
{
|
|
65
|
+
const response = await this.ApiAdapter.fetch(
|
|
66
|
+
'/cmn/masterdata/text-enum-groups',
|
|
67
|
+
{
|
|
68
|
+
body: JSON.stringify(textEnumGroup),
|
|
69
|
+
method: 'POST',
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return response.data;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
getAllEnums = async function({ offset = 0, limit = 10, groupId })
|
|
76
|
+
{
|
|
77
|
+
const { data, moreElements, nextOffset } = await this.ApiAdapter.vql({
|
|
78
|
+
statement: `
|
|
79
|
+
SELECT
|
|
80
|
+
id,
|
|
81
|
+
entry
|
|
82
|
+
FROM masterdata.query-textenum
|
|
83
|
+
${groupId ? `WHERE group.id = '${groupId}'` : ''}
|
|
84
|
+
`,
|
|
85
|
+
offset,
|
|
86
|
+
limit,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
data,
|
|
91
|
+
moreElements,
|
|
92
|
+
nextOffset,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
removeEnums = async function(textEnumGroup)
|
|
97
|
+
{
|
|
98
|
+
const textEnums = await this.getEnumsByGroup(textEnumGroup);
|
|
99
|
+
|
|
100
|
+
if (!textEnums)
|
|
101
|
+
{
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await textEnums.reduce(async (previousPromise, textEnum) =>
|
|
106
|
+
{
|
|
107
|
+
await previousPromise;
|
|
108
|
+
|
|
109
|
+
return this.removeEnum(textEnum);
|
|
110
|
+
}, Promise.resolve());
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
removeEnum = async function(textEnum)
|
|
116
|
+
{
|
|
117
|
+
return this.ApiAdapter.fetch(
|
|
118
|
+
`/cmn/masterdata/text-enums/${textEnum.id}`,
|
|
119
|
+
{
|
|
120
|
+
method: 'DELETE',
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
createEnums = async function(customGroupKey, textEnums)
|
|
126
|
+
{
|
|
127
|
+
await textEnums.reduce(async (previousPromise, textEnum) =>
|
|
128
|
+
{
|
|
129
|
+
await previousPromise;
|
|
130
|
+
|
|
131
|
+
return this.createEnum(textEnum);
|
|
132
|
+
}, Promise.resolve());
|
|
133
|
+
|
|
134
|
+
return true;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
createEnum = async function(textEnum)
|
|
138
|
+
{
|
|
139
|
+
await this.ApiAdapter.fetch(
|
|
140
|
+
'/cmn/masterdata/text-enums',
|
|
141
|
+
{
|
|
142
|
+
body: JSON.stringify(textEnum),
|
|
143
|
+
method: 'POST',
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
module.exports = TextEnum;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { getRequest } = require('#backend/utils/context.js');
|
|
2
|
+
const { getApp } = require('#backend/utils/context.js');
|
|
3
|
+
|
|
4
|
+
const TextEnum = class
|
|
5
|
+
{
|
|
6
|
+
constructor(ApiAdapter)
|
|
7
|
+
{
|
|
8
|
+
this.ApiAdapter = ApiAdapter;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
register = async function(destinationQueue, url)
|
|
12
|
+
{
|
|
13
|
+
const apiUrl = `${process.env.WEBHOOK_HOST ?? `https://${getRequest().get('host')}`}`;
|
|
14
|
+
|
|
15
|
+
const app = getApp();
|
|
16
|
+
|
|
17
|
+
await this.ApiAdapter.fetch('/cmn/system/app-message-webhook/register', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
url: `${apiUrl}${url}`,
|
|
21
|
+
destinationQueue,
|
|
22
|
+
appIdentifier: app.client.appIdentifier,
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
deregister = async function(destinationQueue, url)
|
|
28
|
+
{
|
|
29
|
+
const apiUrl = `${process.env.WEBHOOK_HOST ?? `https://${getRequest().get('host')}`}`;
|
|
30
|
+
|
|
31
|
+
const app = getApp();
|
|
32
|
+
|
|
33
|
+
await this.ApiAdapter.fetch('/cmn/system/app-message-webhook/deregister', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
url: `${apiUrl}${url}`,
|
|
37
|
+
destinationQueue,
|
|
38
|
+
appIdentifier: app.client.appIdentifier,
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
module.exports = TextEnum;
|
package/app.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const bodyParser = require('body-parser');
|
|
3
|
+
const cors = require('cors');
|
|
4
|
+
const { createProxyMiddleware } = require('http-proxy-middleware');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const ErpApi = require('#backend/api/ErpApi.js');
|
|
7
|
+
const appAuthentication = require('#backend/setup/appAuthentication.js');
|
|
8
|
+
const setupContext = require('#backend/setup/context.js');
|
|
9
|
+
const setupException = require('#backend/setup/exception.js');
|
|
10
|
+
const { log } = require('#backend/utils/logger.js');
|
|
11
|
+
const OfflineToken = require('#backend/modules/offlineToken.js');
|
|
12
|
+
const AccessToken = require('#backend/modules/accessToken.js');
|
|
13
|
+
const BaseUrlCache = require('#backend/modules/baseUrlCache.js');
|
|
14
|
+
|
|
15
|
+
const VarioCloudApp = class
|
|
16
|
+
{
|
|
17
|
+
constructor(client, options = {})
|
|
18
|
+
{
|
|
19
|
+
this.express = express();
|
|
20
|
+
this.port = '8080';
|
|
21
|
+
this.uiPath = null;
|
|
22
|
+
this.uiPrefix = '/ui';
|
|
23
|
+
|
|
24
|
+
this.version = 'latest';
|
|
25
|
+
|
|
26
|
+
this.client = client;
|
|
27
|
+
|
|
28
|
+
this.log = options.log ?? log;
|
|
29
|
+
this.offlineToken = options.offlineToken ?? new OfflineToken(this);
|
|
30
|
+
this.accessToken = options.accessToken ?? new AccessToken(this);
|
|
31
|
+
this.baseUrlCache = options.baseUrlCache ?? new BaseUrlCache(this);
|
|
32
|
+
|
|
33
|
+
this.erp = ErpApi;
|
|
34
|
+
|
|
35
|
+
this.express.disable('x-powered-by');
|
|
36
|
+
|
|
37
|
+
this.express.use(cors());
|
|
38
|
+
|
|
39
|
+
this.express.use(bodyParser.json(options.bodyParser));
|
|
40
|
+
this.express.use(bodyParser.raw({ type: 'application/octet-stream', limit: 100 * 1024 * 1024 }));
|
|
41
|
+
|
|
42
|
+
this.apiServer = express.Router();
|
|
43
|
+
|
|
44
|
+
this.apiServer.use(setupContext(this));
|
|
45
|
+
this.apiServer.use(appAuthentication);
|
|
46
|
+
|
|
47
|
+
this.express.use(options.apiPrefix ?? '/api', this.apiServer);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
start()
|
|
51
|
+
{
|
|
52
|
+
validateClient(this.client);
|
|
53
|
+
|
|
54
|
+
if (this.uiPath)
|
|
55
|
+
{
|
|
56
|
+
this.uiServer = express.Router();
|
|
57
|
+
|
|
58
|
+
if (this.uiPath.startsWith('http'))
|
|
59
|
+
{
|
|
60
|
+
this.uiServer.use(
|
|
61
|
+
createProxyMiddleware({
|
|
62
|
+
target: `${this.uiPath}/ui`,
|
|
63
|
+
changeOrigin: true,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
{
|
|
69
|
+
this.uiServer.use(express.static(this.uiPath));
|
|
70
|
+
|
|
71
|
+
// For SPA-Routing
|
|
72
|
+
this.uiServer.get('/*', (req, res) =>
|
|
73
|
+
{
|
|
74
|
+
res.sendFile(path.resolve(this.uiPath, 'index.html'));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.express.use(this.uiPrefix, this.uiServer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.express.use(setupException(this));
|
|
82
|
+
|
|
83
|
+
return new Promise((resolve, reject) =>
|
|
84
|
+
{
|
|
85
|
+
this.express.listen(this.port, error =>
|
|
86
|
+
{
|
|
87
|
+
if (error)
|
|
88
|
+
{
|
|
89
|
+
reject(error);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
resolve(this);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function validateClient(client)
|
|
100
|
+
{
|
|
101
|
+
if (!client)
|
|
102
|
+
{
|
|
103
|
+
throw new Error('client config missing');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const missingProps = ['clientId', 'clientSecret', 'appIdentifier']
|
|
107
|
+
.filter(prop => !client[prop]);
|
|
108
|
+
|
|
109
|
+
if (missingProps.length)
|
|
110
|
+
{
|
|
111
|
+
throw new Error(`client config is missing: ${missingProps.join()}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = VarioCloudApp;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class AccessToken
|
|
2
|
+
{
|
|
3
|
+
#cache = {};
|
|
4
|
+
|
|
5
|
+
async init()
|
|
6
|
+
{
|
|
7
|
+
return Promise.resolve();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async get(tenant)
|
|
11
|
+
{
|
|
12
|
+
const tokenData = this.#cache[tenant];
|
|
13
|
+
|
|
14
|
+
if (!tokenData)
|
|
15
|
+
{
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Date.now() >= tokenData.expiresAt)
|
|
20
|
+
{
|
|
21
|
+
await this.delete(tenant);
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return tokenData.accessToken;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async set(tenant, accessToken, expiresAt)
|
|
30
|
+
{
|
|
31
|
+
this.#cache[tenant] = {
|
|
32
|
+
accessToken,
|
|
33
|
+
expiresAt,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async delete(tenant)
|
|
38
|
+
{
|
|
39
|
+
delete this.#cache[tenant];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = AccessToken;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { getTenant } = require('#backend/utils/context.js');
|
|
2
|
+
const { validateOfflineToken } = require('#backend/utils/token.js');
|
|
3
|
+
|
|
4
|
+
class BaseUrlCache
|
|
5
|
+
{
|
|
6
|
+
#cache = {};
|
|
7
|
+
|
|
8
|
+
constructor(app)
|
|
9
|
+
{
|
|
10
|
+
this.app = app;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async init()
|
|
14
|
+
{
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async get()
|
|
19
|
+
{
|
|
20
|
+
const tenant = getTenant();
|
|
21
|
+
|
|
22
|
+
if (this.#cache[tenant])
|
|
23
|
+
{
|
|
24
|
+
return this.#cache[tenant];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const offlineToken = await this.app.offlineToken.get(tenant);
|
|
28
|
+
const { iss } = await validateOfflineToken(offlineToken);
|
|
29
|
+
|
|
30
|
+
const domain = iss.replace('https://sso.', '').split('/')[0];
|
|
31
|
+
const baseUrl = `https://${tenant}.${domain}`;
|
|
32
|
+
|
|
33
|
+
await this.set(baseUrl);
|
|
34
|
+
|
|
35
|
+
return baseUrl;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async set(value)
|
|
39
|
+
{
|
|
40
|
+
const tenant = getTenant();
|
|
41
|
+
|
|
42
|
+
this.#cache[tenant] = value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async delete()
|
|
46
|
+
{
|
|
47
|
+
const tenant = getTenant();
|
|
48
|
+
|
|
49
|
+
delete this.#cache[tenant];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = BaseUrlCache;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
class OfflineToken
|
|
2
|
+
{
|
|
3
|
+
constructor(app, filename = 'offlineToken.db')
|
|
4
|
+
{
|
|
5
|
+
this.app = app;
|
|
6
|
+
this.filename = filename;
|
|
7
|
+
this.database = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async init()
|
|
11
|
+
{
|
|
12
|
+
// eslint-disable-next-line v-custom-rules/no-await-on-import
|
|
13
|
+
const { JSONFilePreset } = await import('lowdb/node');
|
|
14
|
+
|
|
15
|
+
this.database = await JSONFilePreset(this.filename, {});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async get(tenant)
|
|
19
|
+
{
|
|
20
|
+
return this.database.data[tenant];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async set(tenant, offlineToken)
|
|
24
|
+
{
|
|
25
|
+
this.app.accessToken.delete(tenant);
|
|
26
|
+
|
|
27
|
+
return this.database.update(tokens =>
|
|
28
|
+
{
|
|
29
|
+
tokens[tenant] = offlineToken;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async delete(tenant)
|
|
34
|
+
{
|
|
35
|
+
this.app.accessToken.delete(tenant);
|
|
36
|
+
|
|
37
|
+
return this.database.update(tokens =>
|
|
38
|
+
{
|
|
39
|
+
delete tokens[tenant];
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = OfflineToken;
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vario-software/vario-app-framework-backend",
|
|
3
|
+
"version": "2025.37.0",
|
|
4
|
+
"repository": "https://github.com/vario-software/vario-app-framework",
|
|
5
|
+
"author": "VARIO Software AG",
|
|
6
|
+
"homepage": "https://www.vario.ag",
|
|
7
|
+
"description": "The VARIO App Framework makes it easy and quick to set up a VARIO Cloud App in Node.js by handling API communication, authentication and providing useful built-in utilities.",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"publishConfig": { "access": "public" },
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"body-parser": "2.2.0",
|
|
12
|
+
"cors": "^2.8.5",
|
|
13
|
+
"express": "4.21.2",
|
|
14
|
+
"follow-redirects": "1.15.11",
|
|
15
|
+
"http-proxy-middleware": "3.0.5",
|
|
16
|
+
"jose": "6.1.0",
|
|
17
|
+
"lodash": "4.17.21",
|
|
18
|
+
"lowdb": "^7.0.1"
|
|
19
|
+
},
|
|
20
|
+
"imports": {
|
|
21
|
+
"#backend/*": "./*"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { getContext, getRequestId } = require('#backend/utils/context.js');
|
|
2
|
+
const { validateAppToken } = require('#backend/utils/token.js');
|
|
3
|
+
|
|
4
|
+
function appAuthentication(req, res, next)
|
|
5
|
+
{
|
|
6
|
+
const authorizationHeader = req.get('Authorization');
|
|
7
|
+
|
|
8
|
+
// Read appToken from Authorization-Header
|
|
9
|
+
let token = authorizationHeader?.replace('Bearer ', '');
|
|
10
|
+
|
|
11
|
+
if (['/api/install', '/api/uninstall'].includes(req.path))
|
|
12
|
+
{
|
|
13
|
+
/* if the app has no ui we need to
|
|
14
|
+
extract the appToken from the query */
|
|
15
|
+
if (req.method === 'GET')
|
|
16
|
+
{
|
|
17
|
+
token = req.query.appToken;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
validateAppToken(token)
|
|
22
|
+
.then(accessToken =>
|
|
23
|
+
{
|
|
24
|
+
const context = getContext();
|
|
25
|
+
|
|
26
|
+
context.appToken = token;
|
|
27
|
+
context.accessToken = accessToken;
|
|
28
|
+
|
|
29
|
+
next();
|
|
30
|
+
})
|
|
31
|
+
.catch(error =>
|
|
32
|
+
{
|
|
33
|
+
console.log({
|
|
34
|
+
message: 'Auth failed',
|
|
35
|
+
requestId: getRequestId(),
|
|
36
|
+
token,
|
|
37
|
+
error,
|
|
38
|
+
requestPath: req.path,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
res.status(401).end();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = appAuthentication;
|