@things-factory/integration-base 8.0.0-alpha.8 → 8.0.0-beta.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/dist-server/controllers/scenario-controller.d.ts +2 -0
- package/dist-server/controllers/scenario-controller.js +16 -19
- package/dist-server/controllers/scenario-controller.js.map +1 -1
- package/dist-server/engine/connection-manager.js.map +1 -1
- package/dist-server/engine/connector/headless-connector.d.ts +23 -0
- package/dist-server/engine/connector/headless-connector.js +283 -0
- package/dist-server/engine/connector/headless-connector.js.map +1 -0
- package/dist-server/engine/connector/http-connector.js +1 -1
- package/dist-server/engine/connector/http-connector.js.map +1 -1
- package/dist-server/engine/connector/index.d.ts +1 -0
- package/dist-server/engine/connector/index.js +1 -0
- package/dist-server/engine/connector/index.js.map +1 -1
- package/dist-server/engine/connector/operato-connector.js +3 -4
- package/dist-server/engine/connector/operato-connector.js.map +1 -1
- package/dist-server/engine/index.d.ts +1 -0
- package/dist-server/engine/index.js +1 -0
- package/dist-server/engine/index.js.map +1 -1
- package/dist-server/engine/resource-pool/headless-pool.d.ts +1 -0
- package/dist-server/engine/resource-pool/headless-pool.js +121 -0
- package/dist-server/engine/resource-pool/headless-pool.js.map +1 -0
- package/dist-server/engine/resource-pool/index.d.ts +1 -0
- package/dist-server/engine/resource-pool/index.js +5 -0
- package/dist-server/engine/resource-pool/index.js.map +1 -0
- package/dist-server/engine/task/headless-post.js +19 -33
- package/dist-server/engine/task/headless-post.js.map +1 -1
- package/dist-server/engine/task/headless-scrap.js +20 -13
- package/dist-server/engine/task/headless-scrap.js.map +1 -1
- package/dist-server/engine/task/utils/headless-pool-for-scenario.js +1 -1
- package/dist-server/engine/task/utils/headless-pool-for-scenario.js.map +1 -1
- package/dist-server/service/payload-log/payload-log.js +7 -4
- package/dist-server/service/payload-log/payload-log.js.map +1 -1
- package/dist-server/service/scenario/scenario-query.d.ts +2 -1
- package/dist-server/service/scenario/scenario-query.js +10 -0
- package/dist-server/service/scenario/scenario-query.js.map +1 -1
- package/dist-server/service/scenario/scenario-type.d.ts +3 -3
- package/dist-server/service/scenario/scenario-type.js +7 -7
- package/dist-server/service/scenario/scenario-type.js.map +1 -1
- package/dist-server/service/scenario/scenario.d.ts +3 -1
- package/dist-server/service/scenario/scenario.js +9 -0
- package/dist-server/service/scenario/scenario.js.map +1 -1
- package/dist-server/service/scenario-instance/scenario-instance-type.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +12 -11
- package/server/controllers/scenario-controller.ts +27 -36
- package/server/engine/connection-manager.ts +1 -1
- package/server/engine/connector/headless-connector.ts +341 -0
- package/server/engine/connector/http-connector.ts +1 -1
- package/server/engine/connector/index.ts +1 -0
- package/server/engine/connector/operato-connector.ts +4 -7
- package/server/engine/index.ts +1 -0
- package/server/engine/resource-pool/headless-pool.ts +136 -0
- package/server/engine/resource-pool/index.ts +1 -0
- package/server/engine/task/headless-post.ts +21 -40
- package/server/engine/task/headless-scrap.ts +21 -18
- package/server/engine/task/utils/headless-pool-for-scenario.ts +1 -1
- package/server/service/payload-log/payload-log.ts +8 -4
- package/server/service/scenario/scenario-query.ts +6 -1
- package/server/service/scenario/scenario-type.ts +5 -5
- package/server/service/scenario/scenario.ts +15 -19
- package/server/service/scenario-instance/scenario-instance-type.ts +1 -1
- package/translations/en.json +12 -5
- package/translations/ja.json +12 -5
- package/translations/ko.json +12 -5
- package/translations/ms.json +12 -5
- package/translations/zh.json +12 -5
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@things-factory/integration-base",
|
3
|
-
"version": "8.0.0-
|
3
|
+
"version": "8.0.0-beta.0",
|
4
4
|
"main": "dist-server/index.js",
|
5
5
|
"browser": "client/index.js",
|
6
6
|
"things-factory": true,
|
@@ -26,23 +26,24 @@
|
|
26
26
|
},
|
27
27
|
"dependencies": {
|
28
28
|
"@apollo/client": "^3.6.9",
|
29
|
-
"@operato/moment-timezone-es": "^8.0.0-
|
30
|
-
"@things-factory/api": "^8.0.0-
|
31
|
-
"@things-factory/auth-base": "^8.0.0-
|
32
|
-
"@things-factory/cache-service": "^8.0.0-
|
33
|
-
"@things-factory/env": "^8.0.0-
|
34
|
-
"@things-factory/oauth2-client": "^8.0.0-
|
35
|
-
"@things-factory/scheduler-client": "^8.0.0-
|
36
|
-
"@things-factory/shell": "^8.0.0-
|
37
|
-
"@things-factory/utils": "^8.0.0-
|
29
|
+
"@operato/moment-timezone-es": "^8.0.0-beta",
|
30
|
+
"@things-factory/api": "^8.0.0-beta.0",
|
31
|
+
"@things-factory/auth-base": "^8.0.0-beta.0",
|
32
|
+
"@things-factory/cache-service": "^8.0.0-beta.0",
|
33
|
+
"@things-factory/env": "^8.0.0-beta.0",
|
34
|
+
"@things-factory/oauth2-client": "^8.0.0-beta.0",
|
35
|
+
"@things-factory/scheduler-client": "^8.0.0-beta.0",
|
36
|
+
"@things-factory/shell": "^8.0.0-beta.0",
|
37
|
+
"@things-factory/utils": "^8.0.0-beta.0",
|
38
38
|
"async-mqtt": "^2.5.0",
|
39
39
|
"chance": "^1.1.11",
|
40
40
|
"cross-fetch": "^3.0.4",
|
41
41
|
"ieee754": "^1.2.1",
|
42
42
|
"node-fetch": "^2.6.0",
|
43
43
|
"promise-socket": "^7.0.0",
|
44
|
+
"puppeteer": "^23.0.0",
|
44
45
|
"readline": "^1.3.0",
|
45
46
|
"ses": "^1.5.0"
|
46
47
|
},
|
47
|
-
"gitHead": "
|
48
|
+
"gitHead": "add6fb8224b2cb19cbea47bed6a5ecb0424c9a28"
|
48
49
|
}
|
@@ -1,14 +1,10 @@
|
|
1
1
|
import { getRepository, Domain, GraphqlLocalClient } from '@things-factory/shell'
|
2
|
-
import {
|
2
|
+
import { checkUserHasRole } from '@things-factory/auth-base'
|
3
3
|
import { cacheService } from '@things-factory/cache-service'
|
4
4
|
|
5
5
|
import { ScenarioEngine } from '../engine/scenario-engine'
|
6
6
|
import { Scenario } from '../service/scenario/scenario'
|
7
|
-
import {
|
8
|
-
ScenarioInstance,
|
9
|
-
ScenarioInstanceRunResult,
|
10
|
-
ScenarioInstanceStatus
|
11
|
-
} from '../service/scenario-instance/scenario-instance-type'
|
7
|
+
import { ScenarioInstance, ScenarioInstanceRunResult } from '../service/scenario-instance/scenario-instance-type'
|
12
8
|
import { Step } from '../service/step/step-type'
|
13
9
|
|
14
10
|
const debug = require('debug')('things-factory:integration-base:controller:run-scenario')
|
@@ -16,31 +12,48 @@ const debug = require('debug')('things-factory:integration-base:controller:run-s
|
|
16
12
|
async function findScenario(
|
17
13
|
scenarioName: string,
|
18
14
|
domain: Domain
|
19
|
-
): Promise<{
|
15
|
+
): Promise<{
|
16
|
+
id: string
|
17
|
+
ttl: number
|
18
|
+
name: string
|
19
|
+
steps: Step[]
|
20
|
+
domain: Domain
|
21
|
+
}> {
|
20
22
|
var repository = getRepository(Scenario)
|
21
23
|
|
22
24
|
var scenario = await repository.findOne({
|
23
25
|
where: { domain: { id: domain.id }, name: scenarioName },
|
24
|
-
relations: ['domain', 'steps', 'creator', 'updater']
|
26
|
+
relations: ['domain', 'steps', 'role', 'creator', 'updater']
|
25
27
|
})
|
26
28
|
|
27
29
|
if (!scenario && domain.parentId) {
|
28
30
|
scenario = await repository.findOne({
|
29
31
|
where: { domain: { id: domain.parentId }, name: scenarioName },
|
30
|
-
relations: ['domain', 'steps', 'creator', 'updater']
|
32
|
+
relations: ['domain', 'steps', 'role', 'creator', 'updater']
|
31
33
|
})
|
32
34
|
}
|
33
35
|
|
34
36
|
return scenario as any
|
35
37
|
}
|
36
38
|
|
39
|
+
export async function checkHasRole(scenario: Partial<Scenario>, context: ResolverContext): Promise<void> {
|
40
|
+
const { domain, user } = context.state
|
41
|
+
if (!(await checkUserHasRole(scenario.roleId, domain, user))) {
|
42
|
+
throw new Error(
|
43
|
+
context.t('error.scenario run unauthorized', {
|
44
|
+
scenario: scenario.name
|
45
|
+
})
|
46
|
+
)
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
37
50
|
export async function runScenario(
|
38
51
|
instanceName: string,
|
39
52
|
scenarioName: string,
|
40
53
|
variables: any,
|
41
54
|
context: ResolverContext
|
42
55
|
): Promise<ScenarioInstanceRunResult> {
|
43
|
-
const { domain, user, lng
|
56
|
+
const { domain, user, lng } = context.state
|
44
57
|
|
45
58
|
debug('runScenario', scenarioName, instanceName, variables)
|
46
59
|
|
@@ -54,19 +67,7 @@ export async function runScenario(
|
|
54
67
|
)
|
55
68
|
}
|
56
69
|
|
57
|
-
|
58
|
-
const { category, privilege } = scenario.privilege || {}
|
59
|
-
|
60
|
-
console.error(
|
61
|
-
`Unauthorized! ${category && privilege ? category + ':' + privilege + ' privilege' : 'ownership granted'} required`
|
62
|
-
)
|
63
|
-
|
64
|
-
throw new Error(
|
65
|
-
context.t('error.scenario run unauthorized', {
|
66
|
-
scenario: scenarioName
|
67
|
-
})
|
68
|
-
)
|
69
|
-
}
|
70
|
+
await checkHasRole(scenario, context)
|
70
71
|
|
71
72
|
if (scenario.ttl > 0) {
|
72
73
|
const cachedValue = await cacheService.getFromCache(scenario.id, { domain: domain.id, variables: variables || {} })
|
@@ -104,7 +105,7 @@ export async function startScenario(
|
|
104
105
|
variables: any,
|
105
106
|
context: ResolverContext
|
106
107
|
): Promise<ScenarioInstance> {
|
107
|
-
const { domain, user, lng
|
108
|
+
const { domain, user, lng } = context.state
|
108
109
|
|
109
110
|
debug('startScenario', instanceName, scenarioName, variables)
|
110
111
|
|
@@ -118,12 +119,7 @@ export async function startScenario(
|
|
118
119
|
)
|
119
120
|
}
|
120
121
|
|
121
|
-
|
122
|
-
const { category, privilege } = scenario.privilege || {}
|
123
|
-
throw new Error(
|
124
|
-
`Unauthorized! ${category && privilege ? category + ':' + privilege + ' privilege' : 'ownership granted'} required`
|
125
|
-
)
|
126
|
-
}
|
122
|
+
await checkHasRole(scenario, context)
|
127
123
|
|
128
124
|
instanceName = instanceName || scenarioName
|
129
125
|
return await ScenarioEngine.load(instanceName, scenario, { domain, user, lng, variables })
|
@@ -152,12 +148,7 @@ export async function stopScenario(
|
|
152
148
|
|
153
149
|
var scenario = await findScenario(scenarioInstance.scenarioName, domain)
|
154
150
|
|
155
|
-
|
156
|
-
const { category, privilege } = scenario.privilege || {}
|
157
|
-
throw new Error(
|
158
|
-
`Unauthorized! ${category && privilege ? category + ':' + privilege + ' privilege' : 'ownership granted'} required`
|
159
|
-
)
|
160
|
-
}
|
151
|
+
await checkHasRole(scenario, context)
|
161
152
|
|
162
153
|
await ScenarioEngine.unload(domain, instanceName)
|
163
154
|
|
@@ -24,7 +24,7 @@ function getSystemTimeZone() {
|
|
24
24
|
}
|
25
25
|
|
26
26
|
const SYSTEM_TZ = getSystemTimeZone()
|
27
|
-
const systemTimestamp = format((info, opts) => {
|
27
|
+
const systemTimestamp = format((info, opts: { tz?: string }) => {
|
28
28
|
if (opts.tz) info.timestamp = moment().tz(opts.tz).format()
|
29
29
|
return info
|
30
30
|
})
|
@@ -0,0 +1,341 @@
|
|
1
|
+
import { ConnectionManager } from '../connection-manager'
|
2
|
+
import { Connector } from '../types'
|
3
|
+
import { getHeadlessPool } from '../resource-pool/headless-pool'
|
4
|
+
import { Browser, Page } from 'puppeteer'
|
5
|
+
|
6
|
+
/*
|
7
|
+
Functionality of the headless-connector:
|
8
|
+
- Provides a mechanism to acquire an active session page.
|
9
|
+
- Performs login when necessary to obtain valid cookies.
|
10
|
+
- Applies these cookies to the page for session management.
|
11
|
+
- Pages are acquired from the `headlessPool`, which manages browser instances.
|
12
|
+
- During the login process, pages from the pool are used for actions like form filling and navigation.
|
13
|
+
- Valid cookies are saved after login and reused for subsequent page acquisitions.
|
14
|
+
- Users must explicitly release the page after use through the `releasePage` method.
|
15
|
+
- Released pages are returned to the `headlessPool` for reuse.
|
16
|
+
*/
|
17
|
+
|
18
|
+
export class HeadlessConnector implements Connector {
|
19
|
+
async ready(connectionConfigs) {
|
20
|
+
await Promise.all(connectionConfigs.map(this.connect.bind(this)))
|
21
|
+
ConnectionManager.logger.info('headless-connector connections are ready')
|
22
|
+
}
|
23
|
+
|
24
|
+
async connect(connection) {
|
25
|
+
const {
|
26
|
+
endpoint: uri = '1',
|
27
|
+
params: {
|
28
|
+
username = '',
|
29
|
+
password = '',
|
30
|
+
loginPath = '/login',
|
31
|
+
usernameSelector = '#username',
|
32
|
+
passwordSelector = '#password',
|
33
|
+
submitSelector = '#submit',
|
34
|
+
successSelector = null,
|
35
|
+
shadowDomSelectors = '', // Comma separated shadow DOM selectors
|
36
|
+
timeout = 15000, // Default timeout for operations
|
37
|
+
retries = 3 // Default number of retries for login or page actions
|
38
|
+
} = {}
|
39
|
+
} = connection
|
40
|
+
|
41
|
+
const loginInfo = {
|
42
|
+
loginRequired: Boolean(username), // Determine if login is required
|
43
|
+
username,
|
44
|
+
password,
|
45
|
+
loginPath,
|
46
|
+
timeout,
|
47
|
+
retries,
|
48
|
+
loginSelectors: {
|
49
|
+
usernameSelector,
|
50
|
+
passwordSelector,
|
51
|
+
submitSelector,
|
52
|
+
successSelector,
|
53
|
+
shadowDomSelectors: shadowDomSelectors
|
54
|
+
.split(',')
|
55
|
+
.map(selector => selector.trim())
|
56
|
+
.filter(Boolean)
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
async function acquireBrowser() {
|
61
|
+
try {
|
62
|
+
const pool = getHeadlessPool()
|
63
|
+
const browser = await pool.acquire()
|
64
|
+
return browser
|
65
|
+
} catch (error) {
|
66
|
+
ConnectionManager.logger.error('Failed to acquire browser:', error)
|
67
|
+
throw error
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
async function releaseBrowser(browser: Browser) {
|
72
|
+
try {
|
73
|
+
const pool = getHeadlessPool()
|
74
|
+
await pool.release(browser)
|
75
|
+
} catch (error) {
|
76
|
+
ConnectionManager.logger.error('Failed to release browser:', error)
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
ConnectionManager.addConnectionInstance(connection, {
|
81
|
+
endpoint: connection.endpoint,
|
82
|
+
params: connection.params,
|
83
|
+
acquireSessionPage: async () => {
|
84
|
+
const browser = await acquireBrowser()
|
85
|
+
let page
|
86
|
+
let cookies = connection.cookies
|
87
|
+
|
88
|
+
try {
|
89
|
+
page = await browser.newPage()
|
90
|
+
|
91
|
+
page.on('console', async msg => {
|
92
|
+
console.log(`[browser ${msg.type()}] ${msg.text()}`)
|
93
|
+
})
|
94
|
+
|
95
|
+
await page.setRequestInterception(true)
|
96
|
+
|
97
|
+
// page.on('request', request => {
|
98
|
+
// const resourceType = request.resourceType()
|
99
|
+
// if (resourceType === 'image' || resourceType === 'stylesheet' || resourceType === 'font') {
|
100
|
+
// request.abort() // 이미지, 스타일시트, 폰트 요청 차단
|
101
|
+
// } else {
|
102
|
+
// request.continue() // 나머지 요청은 진행
|
103
|
+
// }
|
104
|
+
// })
|
105
|
+
|
106
|
+
page.on('requestfailed', request => {
|
107
|
+
console.log('Request failed:')
|
108
|
+
console.log(`- URL: ${request.url()}`)
|
109
|
+
console.log(`- Method: ${request.method()}`)
|
110
|
+
console.log(`- Failure Text: ${request.failure()?.errorText}`)
|
111
|
+
console.log(`- Headers:`, request.headers())
|
112
|
+
|
113
|
+
// POST 데이터 (필요한 경우)
|
114
|
+
if (request.postData()) {
|
115
|
+
console.log(`- Post Data: ${request.postData()}`)
|
116
|
+
}
|
117
|
+
})
|
118
|
+
|
119
|
+
if (cookies && isCookieValid(cookies)) {
|
120
|
+
await this.applyCookiesAndVerifySession(page, cookies, loginInfo)
|
121
|
+
return page
|
122
|
+
}
|
123
|
+
|
124
|
+
if (loginInfo.loginRequired) {
|
125
|
+
await this.performLogin(page, uri, loginInfo)
|
126
|
+
cookies = await page.cookies()
|
127
|
+
connection.cookies = cookies
|
128
|
+
}
|
129
|
+
|
130
|
+
return page
|
131
|
+
} catch (error) {
|
132
|
+
ConnectionManager.logger.error('Failed to acquire session page:', error)
|
133
|
+
throw error
|
134
|
+
}
|
135
|
+
},
|
136
|
+
releasePage: async (page: Page) => {
|
137
|
+
try {
|
138
|
+
if (page) {
|
139
|
+
const browser = page.browser()
|
140
|
+
await page.close()
|
141
|
+
await releaseBrowser(browser)
|
142
|
+
}
|
143
|
+
} catch (error) {
|
144
|
+
ConnectionManager.logger.error('Failed to release page:', error)
|
145
|
+
}
|
146
|
+
},
|
147
|
+
acquireBrowser,
|
148
|
+
releaseBrowser
|
149
|
+
})
|
150
|
+
|
151
|
+
ConnectionManager.logger.info(
|
152
|
+
`headless-connector connection(${connection.name}:${connection.endpoint}) is connected`
|
153
|
+
)
|
154
|
+
}
|
155
|
+
|
156
|
+
async applyCookiesAndVerifySession(page, cookies, loginInfo) {
|
157
|
+
await page.setCookie(...cookies)
|
158
|
+
await page.reload({ waitUntil: 'networkidle2', timeout: loginInfo.timeout })
|
159
|
+
|
160
|
+
if (loginInfo.loginRequired && loginInfo.loginSelectors.successSelector) {
|
161
|
+
const success = await this.resolveShadowDom(
|
162
|
+
page,
|
163
|
+
loginInfo.loginSelectors.shadowDomSelectors,
|
164
|
+
loginInfo.loginSelectors.successSelector
|
165
|
+
)
|
166
|
+
if (!success) {
|
167
|
+
throw new Error('Session invalid, login required')
|
168
|
+
}
|
169
|
+
}
|
170
|
+
}
|
171
|
+
|
172
|
+
async performLogin(page, uri, loginInfo) {
|
173
|
+
for (let attempt = 1; attempt <= loginInfo.retries; attempt++) {
|
174
|
+
try {
|
175
|
+
await page.goto(`${uri}${loginInfo.loginPath}`, { waitUntil: 'networkidle2', timeout: loginInfo.timeout })
|
176
|
+
|
177
|
+
const usernameInput = await this.resolveShadowDom(
|
178
|
+
page,
|
179
|
+
loginInfo.loginSelectors.shadowDomSelectors,
|
180
|
+
loginInfo.loginSelectors.usernameSelector
|
181
|
+
)
|
182
|
+
const passwordInput = await this.resolveShadowDom(
|
183
|
+
page,
|
184
|
+
loginInfo.loginSelectors.shadowDomSelectors,
|
185
|
+
loginInfo.loginSelectors.passwordSelector
|
186
|
+
)
|
187
|
+
const submitButton = await this.resolveShadowDom(
|
188
|
+
page,
|
189
|
+
loginInfo.loginSelectors.shadowDomSelectors,
|
190
|
+
loginInfo.loginSelectors.submitSelector
|
191
|
+
)
|
192
|
+
|
193
|
+
if (!usernameInput || !passwordInput || !submitButton) {
|
194
|
+
throw new Error('Failed to locate input elements in shadow DOM')
|
195
|
+
}
|
196
|
+
|
197
|
+
await usernameInput.type(loginInfo.username)
|
198
|
+
await passwordInput.type(loginInfo.password)
|
199
|
+
|
200
|
+
// Capture the response of the form submission
|
201
|
+
const [response] = await Promise.all([
|
202
|
+
page.waitForNavigation({ waitUntil: 'networkidle2', timeout: loginInfo.timeout }),
|
203
|
+
submitButton.click()
|
204
|
+
])
|
205
|
+
|
206
|
+
// Check response status code
|
207
|
+
if (response) {
|
208
|
+
const status = response.status()
|
209
|
+
if (status >= 200 && status < 300) {
|
210
|
+
ConnectionManager.logger.info(`Login successful with status code: ${status}`)
|
211
|
+
return // Login successful
|
212
|
+
} else if (status >= 400) {
|
213
|
+
throw new Error(`Login failed with status code: ${status}`)
|
214
|
+
}
|
215
|
+
} else {
|
216
|
+
throw new Error('No response received during login')
|
217
|
+
}
|
218
|
+
|
219
|
+
if (loginInfo.loginSelectors.successSelector) {
|
220
|
+
const success = await this.resolveShadowDom(
|
221
|
+
page,
|
222
|
+
loginInfo.loginSelectors.shadowDomSelectors,
|
223
|
+
loginInfo.loginSelectors.successSelector
|
224
|
+
)
|
225
|
+
if (!success) {
|
226
|
+
throw new Error('Login failed: Success selector not found')
|
227
|
+
}
|
228
|
+
}
|
229
|
+
} catch (error) {
|
230
|
+
ConnectionManager.logger.warn(`Login attempt ${attempt} failed:`, error)
|
231
|
+
|
232
|
+
if (attempt === loginInfo.retries) {
|
233
|
+
throw new Error(`Login failed after ${loginInfo.retries} attempts`)
|
234
|
+
}
|
235
|
+
}
|
236
|
+
}
|
237
|
+
}
|
238
|
+
|
239
|
+
async resolveShadowDom(page, shadowSelectors, targetSelector) {
|
240
|
+
let context
|
241
|
+
|
242
|
+
if (!shadowSelectors || shadowSelectors.length === 0) {
|
243
|
+
// No Shadow DOM path; use document root as the context
|
244
|
+
context = page.mainFrame() // Puppeteer uses frames to represent document
|
245
|
+
return context.$(targetSelector) // Search directly in the document root
|
246
|
+
}
|
247
|
+
|
248
|
+
context = page // Start with the page as the context
|
249
|
+
for (const selector of shadowSelectors) {
|
250
|
+
const shadowHost = await context.$(selector)
|
251
|
+
if (!shadowHost) {
|
252
|
+
throw new Error(`Shadow host not found: ${selector}`)
|
253
|
+
}
|
254
|
+
context = await page.evaluateHandle(host => host.shadowRoot, shadowHost)
|
255
|
+
}
|
256
|
+
return context.evaluateHandle((shadowRoot, selector) => shadowRoot.querySelector(selector), targetSelector)
|
257
|
+
}
|
258
|
+
|
259
|
+
async disconnect(connection) {
|
260
|
+
const connectionInstance = ConnectionManager.getConnectionInstance(connection)
|
261
|
+
ConnectionManager.removeConnectionInstance(connection)
|
262
|
+
ConnectionManager.logger.info(`headless-connector connection(${connection.name}) is disconnected`)
|
263
|
+
}
|
264
|
+
|
265
|
+
get parameterSpec() {
|
266
|
+
return [
|
267
|
+
{
|
268
|
+
type: 'string',
|
269
|
+
name: 'username',
|
270
|
+
label: 'username'
|
271
|
+
},
|
272
|
+
{
|
273
|
+
type: 'password',
|
274
|
+
name: 'password',
|
275
|
+
label: 'password'
|
276
|
+
},
|
277
|
+
{
|
278
|
+
type: 'string',
|
279
|
+
name: 'loginPath',
|
280
|
+
label: 'login-path'
|
281
|
+
},
|
282
|
+
{
|
283
|
+
type: 'string',
|
284
|
+
name: 'usernameSelector',
|
285
|
+
label: 'username-selector'
|
286
|
+
},
|
287
|
+
{
|
288
|
+
type: 'string',
|
289
|
+
name: 'passwordSelector',
|
290
|
+
label: 'password-selector'
|
291
|
+
},
|
292
|
+
{
|
293
|
+
type: 'string',
|
294
|
+
name: 'submitSelector',
|
295
|
+
label: 'submit-selector'
|
296
|
+
},
|
297
|
+
{
|
298
|
+
type: 'string',
|
299
|
+
name: 'successSelector',
|
300
|
+
label: 'success-selector'
|
301
|
+
},
|
302
|
+
{
|
303
|
+
type: 'string',
|
304
|
+
name: 'shadowDomSelectors',
|
305
|
+
label: 'shadow-dom-selectors'
|
306
|
+
},
|
307
|
+
{
|
308
|
+
type: 'number',
|
309
|
+
name: 'timeout',
|
310
|
+
label: 'timeout',
|
311
|
+
value: 15000
|
312
|
+
},
|
313
|
+
{
|
314
|
+
type: 'number',
|
315
|
+
name: 'retries',
|
316
|
+
label: 'maximum-retries',
|
317
|
+
value: 3
|
318
|
+
}
|
319
|
+
]
|
320
|
+
}
|
321
|
+
|
322
|
+
get taskPrefixes() {
|
323
|
+
return ['headless']
|
324
|
+
}
|
325
|
+
|
326
|
+
get description() {
|
327
|
+
return 'Headless Pool Connector with login capabilities'
|
328
|
+
}
|
329
|
+
|
330
|
+
get help() {
|
331
|
+
return 'integration/connector/headless-connector'
|
332
|
+
}
|
333
|
+
}
|
334
|
+
|
335
|
+
ConnectionManager.registerConnector('headless-connector', new HeadlessConnector())
|
336
|
+
|
337
|
+
function isCookieValid(cookies) {
|
338
|
+
if (!cookies || cookies.length === 0) return false
|
339
|
+
const now = Date.now() / 1000 // Current time in seconds
|
340
|
+
return cookies.some(cookie => cookie.expires && cookie.expires > now)
|
341
|
+
}
|
@@ -17,7 +17,7 @@ import { Scenario } from '../../service/scenario/scenario'
|
|
17
17
|
import { ScenarioInstance } from '../../service/scenario-instance/scenario-instance-type'
|
18
18
|
|
19
19
|
import { getRepository, GraphqlLocalClient, Domain } from '@things-factory/shell'
|
20
|
-
import { User,
|
20
|
+
import { User, checkUserHasRole } from '@things-factory/auth-base'
|
21
21
|
|
22
22
|
const debug = require('debug')('things-factory:integration-base:operato-connector')
|
23
23
|
|
@@ -198,7 +198,7 @@ export class OperatoConnector implements Connector {
|
|
198
198
|
}
|
199
199
|
|
200
200
|
async runScenario(subscriptions: SubscriberData[], variables: any): Promise<ScenarioInstance> {
|
201
|
-
const { domain, user
|
201
|
+
const { domain, user } = this.context
|
202
202
|
const { tag } = variables
|
203
203
|
|
204
204
|
if (!tag) {
|
@@ -210,11 +210,8 @@ export class OperatoConnector implements Connector {
|
|
210
210
|
throw new Error(`scenario is not found - ${tag}`)
|
211
211
|
}
|
212
212
|
|
213
|
-
if (!(await
|
214
|
-
|
215
|
-
throw new Error(
|
216
|
-
`Unauthorized! ${category && privilege ? category + ':' + privilege + ' privilege' : 'ownership granted'} required`
|
217
|
-
)
|
213
|
+
if (!(await checkUserHasRole(scenario.roleId, domain, user))) {
|
214
|
+
throw new Error(`Unauthorized! ${scenario.name} doesn't have required role.`)
|
218
215
|
}
|
219
216
|
|
220
217
|
/* create a scenario instance */
|
package/server/engine/index.ts
CHANGED