w-web-api 1.0.82 → 1.0.83
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/css/{app.9fbdb016.css → app.6da12a32.css} +1 -1
- package/dist/index.tmp +1 -1
- package/dist/js/app-legacy.bc36c734.js +1 -0
- package/dist/js/app.bc36c734.js +1 -0
- package/dist/js/{chunk-vendors.de0404f6.js → chunk-vendors-legacy.a68e060c.js} +3 -3
- package/dist/js/{chunk-vendors-legacy.de0404f6.js → chunk-vendors.a68e060c.js} +3 -3
- package/dist/w-web-api.umd.js +2 -2
- package/dist/w-web-api.umd.js.map +1 -1
- package/docs/WWebApi.html +42 -2
- package/docs/index.html +1 -1
- package/docs/server_WWebApi.mjs.html +101 -18
- package/docs/src_components_Common_CheckYesNo.vue.html +1 -1
- package/package.json +5 -9
- package/server/WWebApi.mjs +100 -17
- package/server/procApis.mjs +3 -1
- package/server/procLang.mjs +12 -0
- package/server/procProxy.mjs +183 -41
- package/server/provideTabs.mjs +9 -8
- package/server/staLogs/staLogsCore.mjs +441 -420
- package/src/components/LayoutContent.vue +25 -20
- package/src/components/LayoutContentEdit.vue +42 -13
- package/src/components/LayoutContentStats.vue +81 -22
- package/src/components/LayoutContentTest.vue +63 -91
- package/src/main.js +3 -1
- package/src/plugins/mShare.mjs +105 -0
- package/src/store/mutations.mjs +1 -0
- package/srv.mjs +9 -4
- package/tableTags-web-api.json +1 -0
- package/dist/js/app-legacy.075a8cf9.js +0 -1
- package/dist/js/app.075a8cf9.js +0 -1
- package/server/staLogs/test_staEvent.mjs +0 -214
- package/tableTags.json +0 -1
package/server/procLang.mjs
CHANGED
|
@@ -33,6 +33,10 @@ let kpLang = {
|
|
|
33
33
|
eng: 'OK',
|
|
34
34
|
cht: '確認',
|
|
35
35
|
},
|
|
36
|
+
treeRootAll: {
|
|
37
|
+
eng: 'All',
|
|
38
|
+
cht: '全部',
|
|
39
|
+
},
|
|
36
40
|
no: {
|
|
37
41
|
eng: 'No',
|
|
38
42
|
cht: '取消',
|
|
@@ -529,6 +533,10 @@ let kpLang = {
|
|
|
529
533
|
eng: 'Invalid rows',
|
|
530
534
|
cht: '資料列無效',
|
|
531
535
|
},
|
|
536
|
+
errRowInvalid: { //syncAndReplaceTabs 逐列檢核:某列非物件或缺必填欄位(列索引與欄位名入 srLog)
|
|
537
|
+
eng: 'Invalid row data (each row must be an object with name and url)',
|
|
538
|
+
cht: '列資料無效(每列須為物件且 name 與 url 必填)',
|
|
539
|
+
},
|
|
532
540
|
errTokenNoPermission: {
|
|
533
541
|
eng: 'Token does not have permission',
|
|
534
542
|
cht: 'token 沒有權限',
|
|
@@ -537,6 +545,10 @@ let kpLang = {
|
|
|
537
545
|
eng: 'Invalid table',
|
|
538
546
|
cht: '資料表無效',
|
|
539
547
|
},
|
|
548
|
+
errTimeLengthInvalid: {
|
|
549
|
+
eng: 'Invalid time length',
|
|
550
|
+
cht: '時間長度無效',
|
|
551
|
+
},
|
|
540
552
|
|
|
541
553
|
}
|
|
542
554
|
|
package/server/procProxy.mjs
CHANGED
|
@@ -1,18 +1,68 @@
|
|
|
1
1
|
import get from 'lodash-es/get.js'
|
|
2
2
|
import isestr from 'wsemi/src/isestr.mjs'
|
|
3
3
|
import isobj from 'wsemi/src/isobj.mjs'
|
|
4
|
-
import
|
|
4
|
+
import isarr from 'wsemi/src/isarr.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
//proxy 不轉送之標頭(小寫比對):host / content-length 由連線層決定;其餘為 RFC 7230 §6.1 hop-by-hop,
|
|
8
|
+
//轉送即錯(Node fetch 對 transfer-encoding / keep-alive 直接拋錯)。其他標頭一律原樣轉送(標頭表為唯一真理源)。
|
|
9
|
+
let ksHeaderStrip = ['host', 'content-length', 'connection', 'keep-alive', 'transfer-encoding', 'te', 'trailer', 'upgrade', 'proxy-connection']
|
|
10
|
+
|
|
11
|
+
//轉址:狀態碼(RFC 9110 §15.4)、跳數上限(同瀏覽器與 undici 預設)、方法改為 GET 時須移除之請求內容標頭(Fetch 標準 §4.4)
|
|
12
|
+
let ksRedirectStatus = [301, 302, 303, 307, 308]
|
|
13
|
+
let MAX_REDIRECTS = 20
|
|
14
|
+
let ksBodyHeader = ['content-encoding', 'content-language', 'content-location', 'content-type']
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
//toPairs:headers/query 之輸入 → 有序鍵值對 [[name, value], ...]。
|
|
18
|
+
//接受兩種形狀:①物件(向後相容其他呼叫端;值為陣列即同鍵多值)②有序鍵值對陣列(本前端請求建構器:重複鍵各自保留)。
|
|
19
|
+
//其餘 → []。值以 String() 轉字串(同改造前)。
|
|
20
|
+
function toPairs(v) {
|
|
21
|
+
let pairs = []
|
|
22
|
+
if (isarr(v)) {
|
|
23
|
+
for (let p of v) {
|
|
24
|
+
if (isarr(p) && isestr(p[0])) {
|
|
25
|
+
pairs.push([p[0], String(p[1])])
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return pairs
|
|
29
|
+
}
|
|
30
|
+
if (isobj(v)) {
|
|
31
|
+
for (let k of Object.keys(v)) {
|
|
32
|
+
let vs = isarr(v[k]) ? v[k] : [v[k]]
|
|
33
|
+
for (let x of vs) {
|
|
34
|
+
pairs.push([k, String(x)])
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return pairs
|
|
39
|
+
}
|
|
5
40
|
|
|
6
41
|
|
|
7
42
|
function procProxy(opt = {}) {
|
|
8
43
|
|
|
9
|
-
//srLog(後端 log;錯誤一律 log err-key,
|
|
44
|
+
//srLog(後端 log;錯誤一律 log err-key,fetch 例外另 log 真實訊息供排查)
|
|
10
45
|
let srLog = get(opt, 'srLog', null)
|
|
11
46
|
|
|
12
|
-
//isAllowTarget
|
|
47
|
+
//isAllowTarget:目標位址檢核函數(回 false 即拒絕);首跳與轉址之每一跳皆檢核
|
|
13
48
|
let isAllowTarget = get(opt, 'isAllowTarget', null)
|
|
49
|
+
let isTargetAllowed = (url) => {
|
|
50
|
+
if (typeof isAllowTarget !== 'function') {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
return isAllowTarget(url) !== false
|
|
54
|
+
}
|
|
14
55
|
|
|
15
56
|
|
|
57
|
+
//proxyRequest:以 Node 內建 fetch 代打目標(繞過瀏覽器 CORS)。契約(見 spec/流程_測試API.md「規則摘要」):
|
|
58
|
+
//- 請求內容原文送出:字串 body 以 TextEncoder 轉位元組原封送達,不解析、不驗證、不補 Content-Type
|
|
59
|
+
// (fetch 對字串 body 會自動補 text/plain,位元組 body 則不補;同 Scalar api-client 作法)。
|
|
60
|
+
//- 物件/陣列 body(非本前端之其他呼叫端)JSON 序列化送出,無 Content-Type 時補 application/json。
|
|
61
|
+
//- headers/query 接受物件或有序鍵值對;僅剝除 ksHeaderStrip;重複之 query 鍵各自 append(?id=1&id=2),
|
|
62
|
+
// 重複之標頭由 Headers 依 Fetch 標準合併(', ';cookie 為 '; ');4xx/5xx 視為有回應(不 reject)。
|
|
63
|
+
//- 轉址手動跟隨:每一跳重新檢核 isAllowTarget(fetch 之 redirect:'follow' 無逐跳 hook,合法首跳可 302 到不允許之位址繞過檢核);
|
|
64
|
+
// 方法/body 改寫依 Fetch 標準 §4.4(301/302 之 POST 與 303 之非 GET/HEAD 改 GET 並丟 body 與 content-* 標頭;307/308 保留);
|
|
65
|
+
// 跨來源(origin 不同)剝除 Authorization;上限 20 跳;3xx 無 Location 視為最終回應;整條鏈共用同一 timeout。
|
|
16
66
|
let proxyRequest = async (inp = {}) => {
|
|
17
67
|
|
|
18
68
|
//method
|
|
@@ -31,39 +81,54 @@ function procProxy(opt = {}) {
|
|
|
31
81
|
return Promise.reject('errReqUrlInvalid')
|
|
32
82
|
}
|
|
33
83
|
|
|
34
|
-
//isAllowTarget check
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
if (allowed === false) {
|
|
38
|
-
return Promise.reject('errReqTargetNotAllowed')
|
|
39
|
-
}
|
|
84
|
+
//isAllowTarget check(首跳)
|
|
85
|
+
if (!isTargetAllowed(url)) {
|
|
86
|
+
return Promise.reject('errReqTargetNotAllowed')
|
|
40
87
|
}
|
|
41
88
|
|
|
42
|
-
//headers
|
|
43
|
-
let headers =
|
|
44
|
-
|
|
45
|
-
headers
|
|
89
|
+
//headers(剝除 ksHeaderStrip,其餘原樣;Headers 對重複鍵依 Fetch 標準合併)
|
|
90
|
+
let headers = new Headers()
|
|
91
|
+
try {
|
|
92
|
+
for (let [k, v] of toPairs(get(inp, 'headers', {}))) {
|
|
93
|
+
if (ksHeaderStrip.includes(k.toLowerCase())) {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
headers.append(k, v)
|
|
97
|
+
}
|
|
46
98
|
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
//非法標頭名/值(Headers 拋 TypeError):log 真實訊息,回前端只給 err-key
|
|
101
|
+
if (srLog) {
|
|
102
|
+
srLog.error({ event: 'proxyRequest-headers', msg: get(err, 'message', String(err)) })
|
|
103
|
+
}
|
|
104
|
+
return Promise.reject('errProxyRequestFailed')
|
|
105
|
+
}
|
|
106
|
+
let hasContentType = headers.has('content-type')
|
|
47
107
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
params = query
|
|
108
|
+
//query:併入 URL(保留 url 既有 query;重複鍵各自 append)
|
|
109
|
+
let u
|
|
110
|
+
try {
|
|
111
|
+
u = new URL(url)
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return Promise.reject('errReqUrlInvalid')
|
|
115
|
+
}
|
|
116
|
+
for (let [k, v] of toPairs(get(inp, 'query', null))) {
|
|
117
|
+
u.searchParams.append(k, v)
|
|
59
118
|
}
|
|
60
119
|
|
|
61
|
-
//body
|
|
120
|
+
//body(GET/HEAD 不帶;fetch 對 GET/HEAD 帶 body 會拋錯)
|
|
62
121
|
let body = get(inp, 'body', undefined)
|
|
63
|
-
let data
|
|
122
|
+
let data
|
|
64
123
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
65
|
-
if (body
|
|
66
|
-
data = body
|
|
124
|
+
if (isestr(body)) {
|
|
125
|
+
data = new TextEncoder().encode(body) //原文位元組,不補 Content-Type
|
|
126
|
+
}
|
|
127
|
+
else if (isobj(body) || isarr(body)) {
|
|
128
|
+
data = new TextEncoder().encode(JSON.stringify(body))
|
|
129
|
+
if (!hasContentType) {
|
|
130
|
+
headers.set('Content-Type', 'application/json')
|
|
131
|
+
}
|
|
67
132
|
}
|
|
68
133
|
}
|
|
69
134
|
|
|
@@ -73,19 +138,88 @@ function procProxy(opt = {}) {
|
|
|
73
138
|
timeout = 30000
|
|
74
139
|
}
|
|
75
140
|
|
|
76
|
-
//send
|
|
141
|
+
//send(手動跟隨轉址,規則見上方契約)
|
|
77
142
|
let r
|
|
78
143
|
let t0 = Date.now()
|
|
79
144
|
try {
|
|
80
|
-
let
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
let signal = AbortSignal.timeout(timeout) //整條轉址鏈共用同一 timeout
|
|
146
|
+
let curUrl = u
|
|
147
|
+
let curMethod = method
|
|
148
|
+
let curBody = data
|
|
149
|
+
let hops = 0
|
|
150
|
+
let res
|
|
151
|
+
for (;;) {
|
|
152
|
+
res = await fetch(curUrl, {
|
|
153
|
+
method: curMethod,
|
|
154
|
+
headers,
|
|
155
|
+
body: curBody,
|
|
156
|
+
signal,
|
|
157
|
+
redirect: 'manual',
|
|
158
|
+
})
|
|
159
|
+
if (!ksRedirectStatus.includes(res.status)) {
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
let loc = res.headers.get('location')
|
|
163
|
+
if (loc === null) {
|
|
164
|
+
break //3xx 無 Location:視為最終回應(同 fetch 行為)
|
|
165
|
+
}
|
|
166
|
+
if (res.body) {
|
|
167
|
+
await res.body.cancel() //釋放此跳之連線
|
|
168
|
+
}
|
|
169
|
+
hops += 1
|
|
170
|
+
if (hops > MAX_REDIRECTS) {
|
|
171
|
+
throw Object.assign(new Error(`too many redirects (> ${MAX_REDIRECTS}), last: ${curUrl.href}`), { errKey: 'errProxyRequestFailed' })
|
|
172
|
+
}
|
|
173
|
+
let nextUrl
|
|
174
|
+
try {
|
|
175
|
+
nextUrl = new URL(loc, curUrl)
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
throw Object.assign(new Error(`invalid redirect location: ${loc}`), { errKey: 'errProxyRequestFailed' })
|
|
179
|
+
}
|
|
180
|
+
if (nextUrl.protocol !== 'http:' && nextUrl.protocol !== 'https:') {
|
|
181
|
+
throw Object.assign(new Error(`unsupported redirect protocol: ${nextUrl.href}`), { errKey: 'errProxyRequestFailed' })
|
|
182
|
+
}
|
|
183
|
+
if (!isTargetAllowed(nextUrl.href)) {
|
|
184
|
+
throw Object.assign(new Error(`redirect target not allowed: ${nextUrl.href}`), { errKey: 'errReqTargetNotAllowed' })
|
|
185
|
+
}
|
|
186
|
+
//方法/body 改寫(Fetch 標準 §4.4)
|
|
187
|
+
let toGet = (res.status === 303 && curMethod !== 'GET' && curMethod !== 'HEAD') || ((res.status === 301 || res.status === 302) && curMethod === 'POST')
|
|
188
|
+
if (toGet) {
|
|
189
|
+
curMethod = 'GET'
|
|
190
|
+
curBody = undefined
|
|
191
|
+
for (let h of ksBodyHeader) {
|
|
192
|
+
headers.delete(h)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//跨來源轉址剝除 Authorization(Fetch 標準:CORS non-wildcard request-header name)
|
|
196
|
+
if (nextUrl.origin !== curUrl.origin) {
|
|
197
|
+
headers.delete('authorization')
|
|
198
|
+
}
|
|
199
|
+
curUrl = nextUrl
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//text → 可解析為 JSON 者回物件,否則原文(對齊改造前 axios 之 silent JSON parsing)
|
|
203
|
+
let text = await res.text()
|
|
204
|
+
let resData = text
|
|
205
|
+
if (isestr(text)) {
|
|
206
|
+
try {
|
|
207
|
+
resData = JSON.parse(text)
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
resData = text
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//headers → 物件;set-cookie 多值以陣列呈現
|
|
215
|
+
let resHeaders = {}
|
|
216
|
+
res.headers.forEach((v, k) => {
|
|
217
|
+
resHeaders[k] = v
|
|
88
218
|
})
|
|
219
|
+
let cookies = res.headers.getSetCookie()
|
|
220
|
+
if (cookies.length > 0) {
|
|
221
|
+
resHeaders['set-cookie'] = cookies
|
|
222
|
+
}
|
|
89
223
|
|
|
90
224
|
let durationMs = Date.now() - t0
|
|
91
225
|
|
|
@@ -93,16 +227,24 @@ function procProxy(opt = {}) {
|
|
|
93
227
|
r = {
|
|
94
228
|
status: res.status,
|
|
95
229
|
statusText: res.statusText,
|
|
96
|
-
headers:
|
|
97
|
-
data:
|
|
230
|
+
headers: resHeaders,
|
|
231
|
+
data: resData,
|
|
98
232
|
durationMs,
|
|
99
233
|
}
|
|
100
234
|
|
|
101
235
|
}
|
|
102
236
|
catch (err) {
|
|
103
|
-
|
|
237
|
+
//轉址鏈之刻意拒絕(帶 errKey):log 細節後回該 err-key
|
|
238
|
+
let errKey = get(err, 'errKey', '')
|
|
239
|
+
if (isestr(errKey)) {
|
|
240
|
+
if (srLog) {
|
|
241
|
+
srLog.error({ event: 'proxyRequest-redirect', err: errKey, msg: get(err, 'message', '') })
|
|
242
|
+
}
|
|
243
|
+
return Promise.reject(errKey)
|
|
244
|
+
}
|
|
245
|
+
//fetch 例外(timeout/DNS/連線拒絕/受限標頭):log 真實訊息與 cause 供排查,但回前端只給 err-key(前端依 lang 顯示)
|
|
104
246
|
if (srLog) {
|
|
105
|
-
srLog.error({ event: 'proxyRequest-
|
|
247
|
+
srLog.error({ event: 'proxyRequest-fetch', msg: get(err, 'message', String(err)), cause: get(err, 'cause.message', '') })
|
|
106
248
|
}
|
|
107
249
|
return Promise.reject('errProxyRequestFailed')
|
|
108
250
|
}
|
package/server/provideTabs.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import axios from 'axios'
|
|
2
1
|
import get from 'lodash-es/get.js'
|
|
3
2
|
import genPm from 'wsemi/src/genPm.mjs'
|
|
4
3
|
import haskey from 'wsemi/src/haskey.mjs'
|
|
@@ -57,16 +56,18 @@ async function provideTabs(url, keyTable, group, rows) {
|
|
|
57
56
|
}
|
|
58
57
|
// console.log('rin', rin)
|
|
59
58
|
|
|
60
|
-
//axios
|
|
61
|
-
await
|
|
62
|
-
method: '
|
|
63
|
-
url,
|
|
59
|
+
//fetch(Node 內建;改造前為 axios,非 2xx 由 axios 拋錯落 catch → 此處以 res.ok 判定維持同一語意)
|
|
60
|
+
await fetch(url, {
|
|
61
|
+
method: 'POST',
|
|
64
62
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
65
|
-
|
|
63
|
+
body: JSON.stringify(rin),
|
|
66
64
|
})
|
|
67
|
-
.then((res) => {
|
|
65
|
+
.then(async (res) => {
|
|
68
66
|
// console.log('then', res)
|
|
69
|
-
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
return Promise.reject(new Error(`Request failed with status code ${res.status}`)) //同 axios 之訊息格式與 Error 型別
|
|
69
|
+
}
|
|
70
|
+
let data = await res.json()
|
|
70
71
|
let state = get(data, 'state')
|
|
71
72
|
let msg = get(data, 'msg', '')
|
|
72
73
|
if (state === 'success') {
|