w-web-api 1.0.81 → 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/docs/WWebApi.html CHANGED
@@ -80,7 +80,7 @@
80
80
 
81
81
  <dt class="tag-source">Source:</dt>
82
82
  <dd class="tag-source"><ul class="dummy"><li>
83
- <a href="server_WWebApi.mjs.html">server/WWebApi.mjs</a>, <a href="server_WWebApi.mjs.html#line139">line 139</a>
83
+ <a href="server_WWebApi.mjs.html">server/WWebApi.mjs</a>, <a href="server_WWebApi.mjs.html#line141">line 141</a>
84
84
  </li></ul></dd>
85
85
 
86
86
 
@@ -991,6 +991,46 @@ instWWebApi.on('error', (err) => {
991
991
  </tr>
992
992
 
993
993
 
994
+
995
+ <tr>
996
+
997
+ <td class="name"><code>isAllowTarget</code></td>
998
+
999
+
1000
+ <td class="type">
1001
+
1002
+
1003
+ <span class="param-type">function</span>
1004
+
1005
+
1006
+
1007
+
1008
+ </td>
1009
+
1010
+
1011
+ <td class="attributes">
1012
+
1013
+ &lt;optional><br>
1014
+
1015
+
1016
+
1017
+
1018
+
1019
+ </td>
1020
+
1021
+
1022
+
1023
+ <td class="default">
1024
+
1025
+ <code>null</code>
1026
+
1027
+ </td>
1028
+
1029
+
1030
+ <td class="description last"><p>輸入測試分頁proxy之目標位址檢核函數,函數會傳入目標網址字串(首跳與轉址後每一跳皆會呼叫),回傳false即拒絕該請求(回err-key 'errReqTargetNotAllowed'),預設null(不限制)</p></td>
1031
+ </tr>
1032
+
1033
+
994
1034
  </tbody>
995
1035
  </table>
996
1036
 
@@ -1079,7 +1119,7 @@ instWWebApi.on('error', (err) => {
1079
1119
  <br class="clear">
1080
1120
 
1081
1121
  <footer>
1082
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Sep 05 2026 15:16:41 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1122
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 12:02:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1083
1123
  </footer>
1084
1124
 
1085
1125
  <script>prettyPrint();</script>
package/docs/index.html CHANGED
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Sep 05 2026 15:16:41 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 12:02:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
@@ -53,8 +53,9 @@ import map from 'lodash-es/map.js'
53
53
  import keys from 'lodash-es/keys.js'
54
54
  import j2o from 'wsemi/src/j2o.mjs'
55
55
  import iseobj from 'wsemi/src/iseobj.mjs'
56
+ import isobj from 'wsemi/src/isobj.mjs'
56
57
  import isestr from 'wsemi/src/isestr.mjs'
57
- import isearr from 'wsemi/src/isearr.mjs'
58
+ import isarr from 'wsemi/src/isarr.mjs'
58
59
  import ispint from 'wsemi/src/ispint.mjs'
59
60
  import isErr from 'wsemi/src/isErr.mjs'
60
61
  import isfun from 'wsemi/src/isfun.mjs'
@@ -103,6 +104,7 @@ import ds from '../src/schema/index.mjs'
103
104
  * @param {String} [opt.logFd='./logs'] 輸入log紀錄儲存位置字串(srLog 寫入與統計掃描共用),預設'./logs'
104
105
  * @param {String} [opt.logInterval='hr'] 輸入log紀錄檔案拆檔時長字串,可選'hr'或'day',預設'hr'
105
106
  * @param {Integer} [opt.logNumKeep=null] 輸入log檔保留數上限正整數,超出者自最舊刪除,未給採w-syslog預設(hr模式365*24、day模式365),預設null
107
+ * @param {Function} [opt.isAllowTarget=null] 輸入測試分頁proxy之目標位址檢核函數,函數會傳入目標網址字串(首跳與轉址後每一跳皆會呼叫),回傳false即拒絕該請求(回err-key 'errReqTargetNotAllowed'),預設null(不限制)
106
108
  * @returns {Object} 回傳物件,其內server為hapi伺服器實體,wsrv為w-converhp的伺服器事件物件,wsds為w-serv-webdata的伺服器事件物件,可監聽error事件
107
109
  * @example
108
110
  *
@@ -281,6 +283,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
281
283
  let showLanguage = get(opt, 'showLanguage', 'y')
282
284
  let language = get(opt, 'language', 'eng')
283
285
  let logFd = get(opt, 'logFd', './logs') //srLog 寫入 + staEvent 統計共用之 log 資料夾
286
+ let isAllowTarget = get(opt, 'isAllowTarget', null) //測試分頁 proxy 之目標檢核(含轉址每一跳),交 procProxy;改造前 procProxy 雖支援此 hook 但建構子未接線
284
287
 
285
288
 
286
289
  //kpLangExt
@@ -313,7 +316,30 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
313
316
  let srLog = srLogInit(opt)
314
317
 
315
318
  //maskTok:debug console.log 不印完整 token/authorization(憑證),僅前 4 碼 + ***,避免憑證落入 stdout
316
- let maskTok = (t) => isestr(t) ? (strleft(t, 4) + '***') : t
319
+ //陣列亦須逐元素遮罩:Hapi 對重複之 query 參數(?token=a&amp;token=b)給陣列,若只認字串則原樣印出完整憑證
320
+ //(複審實測 `req.query { token: [ 'ARRAYSECRET', 'x' ] }` 明文落 stdout);此處收斂後 maskQuery 與 token 兩行同時生效。
321
+ let maskTok = (t) => {
322
+ if (isarr(t)) {
323
+ return t.map(maskTok)
324
+ }
325
+ return isestr(t) ? (strleft(t, 4) + '***') : t
326
+ }
327
+
328
+ //maskQuery:印 req.query 整包時,把其中之憑證欄位遮罩後才輸出。
329
+ //why:query 內含 token,直接 console.log(query) 會讓完整憑證落入 stdout,等同繞過 maskTok(原本三處
330
+ //「先印整包 query、下一行才印遮罩後 token」即為此情形)。回傳淺拷貝,不動原物件。
331
+ let maskQuery = (q) => {
332
+ if (!isobj(q)) {
333
+ return q
334
+ }
335
+ let r = { ...q }
336
+ for (let k of Object.keys(r)) {
337
+ if (k.toLowerCase() === 'token') {
338
+ r[k] = maskTok(r[k])
339
+ }
340
+ }
341
+ return r
342
+ }
317
343
 
318
344
  //kpFunExt 邊界統一處理:成功記 info(供 staEvent 統計呼叫頻率);catch 後端錯誤 → srLog 該 err-key →
319
345
  //原樣 re-reject(前端依 key 查自己 lang 顯示)。event 統一加 'kpfun-' 前綴 → 專用事件、與 api/verifyConn 區分。
@@ -332,9 +358,24 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
332
358
  }
333
359
  }
334
360
 
361
+ //HTTP route 邊界統一處理(與 kpFunExt 之 wrapErrLog 同構):core 內任一 reject 之 err-key 皆於此記錄後原樣再拋。
362
+ //why:route 之錯誤路徑(token 無效、keyTable 無效、user 無效、payload 無效)原僅 console.log,不入 srLog,
363
+ //而統計頁與稽核讀的正是 srLog,導致認證失敗永不可見。逐點加 log 會使同一規則散落多處,故收斂於邊界。
364
+ let wrapRouteErrLog = (event, fn) => {
365
+ return async (...args) => {
366
+ try {
367
+ return await fn(...args)
368
+ }
369
+ catch (err) {
370
+ srLog.error({ event, err })
371
+ throw err
372
+ }
373
+ }
374
+ }
375
+
335
376
 
336
377
  //procProxy
337
- let { proxyRequest } = procProxy({ srLog })
378
+ let { proxyRequest } = procProxy({ srLog, isAllowTarget })
338
379
 
339
380
 
340
381
  //procApis
@@ -461,7 +502,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
461
502
 
462
503
  //check
463
504
  if (!iseobj(user)) {
464
- console.log(`token`, token)
505
+ console.log(`token`, maskTok(token))
465
506
  console.log(`can not find the user from token`)
466
507
  return Promise.reject('errUserNotFound')
467
508
  }
@@ -522,8 +563,9 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
522
563
  //rows
523
564
  let rows = get(payload, 'rows', [])
524
565
 
525
- //check
526
- if (!isearr(rows)) {
566
+ //check(陣列層:非空陣列即可;逐列檢核統一於 syncAndReplaceTabs,使 [null] 與 [null,null] 皆回同一 err-key errRowInvalid,
567
+ //不因 wsemi isearr「僅長度 1 檢查元素」而分岔)
568
+ if (!isarr(rows) || rows.length === 0) {
527
569
  console.log('payload', payload)
528
570
  console.log(`invalid rows in payload`)
529
571
  return Promise.reject('errRowsInvalid')
@@ -554,6 +596,12 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
554
596
  // }
555
597
 
556
598
 
599
+ //各資料表同步時之必填欄位(與前端編輯表單同步檢測對稱:apis 之 name/url 必填)
600
+ let kpRequiredFields = {
601
+ apis: ['name', 'url'],
602
+ }
603
+
604
+
557
605
  //syncAndReplaceTabs
558
606
  let syncAndReplaceTabs = async (userId, inp, keyTable) => {
559
607
 
@@ -573,13 +621,30 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
573
621
  let rows = get(inp, 'rows', [])
574
622
  // console.log('rows', rows)
575
623
 
576
- //check
577
- if (!isearr(rows)) {
624
+ //check(陣列層:須為非空陣列;逐列檢核見下)
625
+ if (!isarr(rows) || rows.length === 0) {
578
626
  console.log('inp', inp)
579
627
  console.log('keyTable', keyTable)
580
628
  return Promise.reject('errRowsInvalid')
581
629
  }
582
630
 
631
+ //逐列檢核:任一列無效即整批拒絕(回 err-key errRowInvalid),且一律在 delAll 之前,既有群組資料不動;
632
+ //列索引與欄位名入 srLog 供整合端查因(回呼叫端只給 key,同其他錯誤契約)。
633
+ //why:改造前只有陣列層 isearr(wsemi:僅長度 1 時檢查元素),[null,null]/[1,2] 可通過,下方 v.group= 即 TypeError,
634
+ //原始例外訊息經 pm2resolve 外洩給呼叫端;缺必填欄位之物件列則經 ltdtmapping 補空值後以空名稱進樹。
635
+ let rejectRow = (index, field) => {
636
+ srLog.error({ event: 'syncAndReplaceTabs', err: 'errRowInvalid', keyTable, group, index, field })
637
+ console.log(`[API]syncAndReplaceTabs/check rows[${index}]: invalid row (field: ${field})`)
638
+ return Promise.reject('errRowInvalid')
639
+ }
640
+
641
+ //逐列檢核 ①:每列須為物件(非物件列連 group 都無法寫入)
642
+ for (let k = 0; k &lt; rows.length; k++) {
643
+ if (!iseobj(rows[k])) {
644
+ return rejectRow(k, '(row)')
645
+ }
646
+ }
647
+
583
648
  //save from
584
649
  rows = map(rows, (v, k) => {
585
650
  // v.order = k + 1 //order大部分由外部給予, 不能複寫
@@ -606,11 +671,20 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
606
671
  })
607
672
  }
608
673
 
674
+ //逐列檢核 ②(b64 還原後):必填欄位須為非空字串(funNew/ltdtmapping 只補預設值、不驗證)
675
+ let ksRequired = get(kpRequiredFields, keyTable, [])
676
+ for (let k = 0; k &lt; rows.length; k++) {
677
+ let field = ksRequired.find((f) => !isestr(rows[k][f]))
678
+ if (isestr(field)) {
679
+ return rejectRow(k, field)
680
+ }
681
+ }
682
+
609
683
  //ltdtmapping
610
684
  rows = ltdtmapping(rows, ds[keyTable].keys)
611
685
  // console.log('ltdtmapping rows', rows)
612
686
 
613
- //delAll group
687
+ //delAll group(逐列檢核皆已通過,此後才動既有資料;delAll→insert 非原子屬已知限制 ADR-016)
614
688
  await woItems[keyTable].delAll({ group })
615
689
 
616
690
  //insert
@@ -673,7 +747,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
673
747
 
674
748
  //check
675
749
  if (!isestr(token)) {
676
- console.log('req.query', get(req, 'query'))
750
+ console.log('req.query', maskQuery(get(req, 'query')))
677
751
  console.log('token', maskTok(token))
678
752
  console.log('[API]getUserByToken/check token: invalid token')
679
753
  return Promise.reject('errTokenNoPermission')
@@ -694,7 +768,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
694
768
  }
695
769
 
696
770
  //pm2resolve core
697
- let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 會提取state進行判斷
771
+ let r = await pm2resolve(wrapRouteErrLog('api/getUserByToken', core))() //w-ui-loginout接收已預設格式用pm2resolve轉過, 會提取state進行判斷
698
772
  if (isErr(r.msg)) {
699
773
  r.msg = r.msg.message
700
774
  }
@@ -814,7 +888,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
814
888
 
815
889
  //check
816
890
  if (!isestr(token)) {
817
- console.log('req.query', get(req, 'query'))
891
+ console.log('req.query', maskQuery(get(req, 'query')))
818
892
  console.log('token', maskTok(token))
819
893
  console.log('[API]syncAndReplaceTabs/check token: invalid token')
820
894
  return Promise.reject('errTokenNoPermission')
@@ -825,7 +899,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
825
899
 
826
900
  //check
827
901
  if (!isestr(keyTable)) {
828
- console.log('req.query', get(req, 'query'))
902
+ console.log('req.query', maskQuery(get(req, 'query')))
829
903
  console.log('keyTable', keyTable)
830
904
  console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
831
905
  return Promise.reject('errKeyTableInvalid')
@@ -870,7 +944,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
870
944
  }
871
945
 
872
946
  //pm2resolve core
873
- let r = await pm2resolve(core)()
947
+ let r = await pm2resolve(wrapRouteErrLog('api/syncAndReplaceTabs', core))()
874
948
  if (isErr(r.msg)) {
875
949
  r.msg = r.msg.message
876
950
  }
@@ -910,6 +984,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
910
984
  console.log('apiType', apiType)
911
985
  console.log('authorization', maskTok(authorization))
912
986
  console.log('invalid token')
987
+ srLog.error({ event: 'verifyConn', err: 'errTokenNoPermission', apiType }) //認證失敗須入 log 供稽核/統計(CLAUDE.md「log 記錄要點」)
913
988
  return false
914
989
  }
915
990
 
@@ -917,9 +992,17 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
917
992
  //套件使用者之責——由其提供之 getUserByToken(可對非授權者回 {})/ verifyClientUser / verifyAppUser,
918
993
  //或 WServOrm 之 useCheckUser/useExcludeWhenNotAdmin 等鉤子決定;套件不對連線層強加特定授權政策(避免越界、
919
994
  //保留使用者彈性)。詳見 spec/設計要點與取捨.md「套件授權責任」。
920
- let b = await p.checkToken(token)
995
+ //checkToken 失敗會 throw(err-key),原本直接向上拋致認證失敗不入 log;此處攔截記錄後原樣再拋,行為不變。
996
+ let b
997
+ try {
998
+ b = await p.checkToken(token)
999
+ }
1000
+ catch (err) {
1001
+ srLog.error({ event: 'verifyConn', err, apiType })
1002
+ throw err
1003
+ }
921
1004
 
922
- srLog.info({ event: 'verifyConn', apiType }) //連線之 token 認證通過(checkToken 失敗會 throw 不到此)
1005
+ srLog.info({ event: 'verifyConn', apiType }) //連線之 token 認證通過
923
1006
  return b
924
1007
  },
925
1008
  getUserIdByToken: async (token) => { //可使用async或sync函數
@@ -969,7 +1052,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
969
1052
  // },
970
1053
 
971
1054
  },
972
- fnTableTags: 'tableTags-web-api.json',
1055
+ fpTableTags: 'tableTags-web-api.json', //w-serv-hapi 1.0.87 起正名 fpTableTags(同 sso / perm); 舊名 fnTableTags 於 1.0.86 傳到 w-serv-webdata 時不被讀取, 實際一直寫預設 tableTags.json
973
1056
  })
974
1057
 
975
1058
 
@@ -992,7 +1075,7 @@ export default WWebApi
992
1075
  <br class="clear">
993
1076
 
994
1077
  <footer>
995
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Sep 05 2026 15:16:41 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1078
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 12:02:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
996
1079
  </footer>
997
1080
 
998
1081
  <script>prettyPrint();</script>
@@ -174,7 +174,7 @@ export default {
174
174
  <br class="clear">
175
175
 
176
176
  <footer>
177
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Sep 05 2026 15:16:41 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
177
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Sep 13 2026 12:02:14 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
178
178
  </footer>
179
179
 
180
180
  <script>prettyPrint();</script>
package/package.json CHANGED
@@ -1,19 +1,18 @@
1
1
  {
2
2
  "name": "w-web-api",
3
- "version": "1.0.81",
3
+ "version": "1.0.83",
4
4
  "main": "dist/w-web-api.umd.js",
5
5
  "dependencies": {
6
- "axios": "^1.18.1",
7
6
  "json5": "^2.2.3",
8
- "w-component-vue": "^2.5.11",
9
- "w-data-collector": "^1.0.35",
10
- "w-echarts-vue": "^1.0.9",
11
- "w-md2html": "^1.0.49",
12
- "w-serv-hapi": "^1.0.85",
13
- "w-serv-orm": "^1.0.52",
14
- "w-syslog": "^1.0.24",
15
- "w-ui-loginout": "^1.0.23",
16
- "wsemi": "^1.8.84"
7
+ "w-component-vue": "^2.5.12",
8
+ "w-data-collector": "^1.0.36",
9
+ "w-echarts-vue": "^1.0.10",
10
+ "w-md2html": "^1.0.50",
11
+ "w-serv-hapi": "^1.0.87",
12
+ "w-serv-orm": "^1.0.53",
13
+ "w-syslog": "^1.0.25",
14
+ "w-ui-loginout": "^1.0.24",
15
+ "wsemi": "^1.8.92"
17
16
  },
18
17
  "devDependencies": {
19
18
  "@vue/cli-plugin-babel": "^5.0.8",
@@ -28,18 +27,15 @@
28
27
  "eslint-plugin-vue": "^9.10.0",
29
28
  "vue": "^2.7.16",
30
29
  "vuex": "^3.6.2",
31
- "w-orm-lmdb": "^1.0.29",
32
- "w-package-tools": "^1.1.15",
30
+ "w-orm-lmdb": "^1.0.30",
31
+ "w-package-tools": "^1.1.16",
33
32
  "w-package-tools-e2e": "^1.0.0",
34
- "w-screenctl": "^1.0.17"
33
+ "w-screenctl": "^1.0.18"
35
34
  },
36
35
  "scripts": {
37
36
  "serve": "vue-cli-service serve",
38
37
  "build": "vue-cli-service build",
39
- "test:unit": "node server/staLogs/test_staEvent.mjs && mocha test/unit-*.test.mjs --timeout 300000",
40
- "test:apiunit": "node server/staLogs/test_staEvent.mjs && mocha test/unit-*.test.mjs test/api-*.test.mjs --timeout 300000",
41
- "test:e2e": "node test/run-e2e-isolated.mjs",
42
- "test": "npm run test:apiunit && npm run test:e2e",
38
+ "test": "mocha --no-parallel \"test/*.test.mjs\" --timeout 180000",
43
39
  "lint": "vue-cli-service lint",
44
40
  "deploy": "gh-pages -d docs"
45
41
  },
@@ -6,8 +6,9 @@ import map from 'lodash-es/map.js'
6
6
  import keys from 'lodash-es/keys.js'
7
7
  import j2o from 'wsemi/src/j2o.mjs'
8
8
  import iseobj from 'wsemi/src/iseobj.mjs'
9
+ import isobj from 'wsemi/src/isobj.mjs'
9
10
  import isestr from 'wsemi/src/isestr.mjs'
10
- import isearr from 'wsemi/src/isearr.mjs'
11
+ import isarr from 'wsemi/src/isarr.mjs'
11
12
  import ispint from 'wsemi/src/ispint.mjs'
12
13
  import isErr from 'wsemi/src/isErr.mjs'
13
14
  import isfun from 'wsemi/src/isfun.mjs'
@@ -56,6 +57,7 @@ import ds from '../src/schema/index.mjs'
56
57
  * @param {String} [opt.logFd='./logs'] 輸入log紀錄儲存位置字串(srLog 寫入與統計掃描共用),預設'./logs'
57
58
  * @param {String} [opt.logInterval='hr'] 輸入log紀錄檔案拆檔時長字串,可選'hr'或'day',預設'hr'
58
59
  * @param {Integer} [opt.logNumKeep=null] 輸入log檔保留數上限正整數,超出者自最舊刪除,未給採w-syslog預設(hr模式365*24、day模式365),預設null
60
+ * @param {Function} [opt.isAllowTarget=null] 輸入測試分頁proxy之目標位址檢核函數,函數會傳入目標網址字串(首跳與轉址後每一跳皆會呼叫),回傳false即拒絕該請求(回err-key 'errReqTargetNotAllowed'),預設null(不限制)
59
61
  * @returns {Object} 回傳物件,其內server為hapi伺服器實體,wsrv為w-converhp的伺服器事件物件,wsds為w-serv-webdata的伺服器事件物件,可監聽error事件
60
62
  * @example
61
63
  *
@@ -234,6 +236,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
234
236
  let showLanguage = get(opt, 'showLanguage', 'y')
235
237
  let language = get(opt, 'language', 'eng')
236
238
  let logFd = get(opt, 'logFd', './logs') //srLog 寫入 + staEvent 統計共用之 log 資料夾
239
+ let isAllowTarget = get(opt, 'isAllowTarget', null) //測試分頁 proxy 之目標檢核(含轉址每一跳),交 procProxy;改造前 procProxy 雖支援此 hook 但建構子未接線
237
240
 
238
241
 
239
242
  //kpLangExt
@@ -266,7 +269,30 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
266
269
  let srLog = srLogInit(opt)
267
270
 
268
271
  //maskTok:debug console.log 不印完整 token/authorization(憑證),僅前 4 碼 + ***,避免憑證落入 stdout
269
- let maskTok = (t) => isestr(t) ? (strleft(t, 4) + '***') : t
272
+ //陣列亦須逐元素遮罩:Hapi 對重複之 query 參數(?token=a&token=b)給陣列,若只認字串則原樣印出完整憑證
273
+ //(複審實測 `req.query { token: [ 'ARRAYSECRET', 'x' ] }` 明文落 stdout);此處收斂後 maskQuery 與 token 兩行同時生效。
274
+ let maskTok = (t) => {
275
+ if (isarr(t)) {
276
+ return t.map(maskTok)
277
+ }
278
+ return isestr(t) ? (strleft(t, 4) + '***') : t
279
+ }
280
+
281
+ //maskQuery:印 req.query 整包時,把其中之憑證欄位遮罩後才輸出。
282
+ //why:query 內含 token,直接 console.log(query) 會讓完整憑證落入 stdout,等同繞過 maskTok(原本三處
283
+ //「先印整包 query、下一行才印遮罩後 token」即為此情形)。回傳淺拷貝,不動原物件。
284
+ let maskQuery = (q) => {
285
+ if (!isobj(q)) {
286
+ return q
287
+ }
288
+ let r = { ...q }
289
+ for (let k of Object.keys(r)) {
290
+ if (k.toLowerCase() === 'token') {
291
+ r[k] = maskTok(r[k])
292
+ }
293
+ }
294
+ return r
295
+ }
270
296
 
271
297
  //kpFunExt 邊界統一處理:成功記 info(供 staEvent 統計呼叫頻率);catch 後端錯誤 → srLog 該 err-key →
272
298
  //原樣 re-reject(前端依 key 查自己 lang 顯示)。event 統一加 'kpfun-' 前綴 → 專用事件、與 api/verifyConn 區分。
@@ -285,9 +311,24 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
285
311
  }
286
312
  }
287
313
 
314
+ //HTTP route 邊界統一處理(與 kpFunExt 之 wrapErrLog 同構):core 內任一 reject 之 err-key 皆於此記錄後原樣再拋。
315
+ //why:route 之錯誤路徑(token 無效、keyTable 無效、user 無效、payload 無效)原僅 console.log,不入 srLog,
316
+ //而統計頁與稽核讀的正是 srLog,導致認證失敗永不可見。逐點加 log 會使同一規則散落多處,故收斂於邊界。
317
+ let wrapRouteErrLog = (event, fn) => {
318
+ return async (...args) => {
319
+ try {
320
+ return await fn(...args)
321
+ }
322
+ catch (err) {
323
+ srLog.error({ event, err })
324
+ throw err
325
+ }
326
+ }
327
+ }
328
+
288
329
 
289
330
  //procProxy
290
- let { proxyRequest } = procProxy({ srLog })
331
+ let { proxyRequest } = procProxy({ srLog, isAllowTarget })
291
332
 
292
333
 
293
334
  //procApis
@@ -414,7 +455,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
414
455
 
415
456
  //check
416
457
  if (!iseobj(user)) {
417
- console.log(`token`, token)
458
+ console.log(`token`, maskTok(token))
418
459
  console.log(`can not find the user from token`)
419
460
  return Promise.reject('errUserNotFound')
420
461
  }
@@ -475,8 +516,9 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
475
516
  //rows
476
517
  let rows = get(payload, 'rows', [])
477
518
 
478
- //check
479
- if (!isearr(rows)) {
519
+ //check(陣列層:非空陣列即可;逐列檢核統一於 syncAndReplaceTabs,使 [null] 與 [null,null] 皆回同一 err-key errRowInvalid,
520
+ //不因 wsemi isearr「僅長度 1 檢查元素」而分岔)
521
+ if (!isarr(rows) || rows.length === 0) {
480
522
  console.log('payload', payload)
481
523
  console.log(`invalid rows in payload`)
482
524
  return Promise.reject('errRowsInvalid')
@@ -507,6 +549,12 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
507
549
  // }
508
550
 
509
551
 
552
+ //各資料表同步時之必填欄位(與前端編輯表單同步檢測對稱:apis 之 name/url 必填)
553
+ let kpRequiredFields = {
554
+ apis: ['name', 'url'],
555
+ }
556
+
557
+
510
558
  //syncAndReplaceTabs
511
559
  let syncAndReplaceTabs = async (userId, inp, keyTable) => {
512
560
 
@@ -526,13 +574,30 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
526
574
  let rows = get(inp, 'rows', [])
527
575
  // console.log('rows', rows)
528
576
 
529
- //check
530
- if (!isearr(rows)) {
577
+ //check(陣列層:須為非空陣列;逐列檢核見下)
578
+ if (!isarr(rows) || rows.length === 0) {
531
579
  console.log('inp', inp)
532
580
  console.log('keyTable', keyTable)
533
581
  return Promise.reject('errRowsInvalid')
534
582
  }
535
583
 
584
+ //逐列檢核:任一列無效即整批拒絕(回 err-key errRowInvalid),且一律在 delAll 之前,既有群組資料不動;
585
+ //列索引與欄位名入 srLog 供整合端查因(回呼叫端只給 key,同其他錯誤契約)。
586
+ //why:改造前只有陣列層 isearr(wsemi:僅長度 1 時檢查元素),[null,null]/[1,2] 可通過,下方 v.group= 即 TypeError,
587
+ //原始例外訊息經 pm2resolve 外洩給呼叫端;缺必填欄位之物件列則經 ltdtmapping 補空值後以空名稱進樹。
588
+ let rejectRow = (index, field) => {
589
+ srLog.error({ event: 'syncAndReplaceTabs', err: 'errRowInvalid', keyTable, group, index, field })
590
+ console.log(`[API]syncAndReplaceTabs/check rows[${index}]: invalid row (field: ${field})`)
591
+ return Promise.reject('errRowInvalid')
592
+ }
593
+
594
+ //逐列檢核 ①:每列須為物件(非物件列連 group 都無法寫入)
595
+ for (let k = 0; k < rows.length; k++) {
596
+ if (!iseobj(rows[k])) {
597
+ return rejectRow(k, '(row)')
598
+ }
599
+ }
600
+
536
601
  //save from
537
602
  rows = map(rows, (v, k) => {
538
603
  // v.order = k + 1 //order大部分由外部給予, 不能複寫
@@ -559,11 +624,20 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
559
624
  })
560
625
  }
561
626
 
627
+ //逐列檢核 ②(b64 還原後):必填欄位須為非空字串(funNew/ltdtmapping 只補預設值、不驗證)
628
+ let ksRequired = get(kpRequiredFields, keyTable, [])
629
+ for (let k = 0; k < rows.length; k++) {
630
+ let field = ksRequired.find((f) => !isestr(rows[k][f]))
631
+ if (isestr(field)) {
632
+ return rejectRow(k, field)
633
+ }
634
+ }
635
+
562
636
  //ltdtmapping
563
637
  rows = ltdtmapping(rows, ds[keyTable].keys)
564
638
  // console.log('ltdtmapping rows', rows)
565
639
 
566
- //delAll group
640
+ //delAll group(逐列檢核皆已通過,此後才動既有資料;delAll→insert 非原子屬已知限制 ADR-016)
567
641
  await woItems[keyTable].delAll({ group })
568
642
 
569
643
  //insert
@@ -626,7 +700,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
626
700
 
627
701
  //check
628
702
  if (!isestr(token)) {
629
- console.log('req.query', get(req, 'query'))
703
+ console.log('req.query', maskQuery(get(req, 'query')))
630
704
  console.log('token', maskTok(token))
631
705
  console.log('[API]getUserByToken/check token: invalid token')
632
706
  return Promise.reject('errTokenNoPermission')
@@ -647,7 +721,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
647
721
  }
648
722
 
649
723
  //pm2resolve core
650
- let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 會提取state進行判斷
724
+ let r = await pm2resolve(wrapRouteErrLog('api/getUserByToken', core))() //w-ui-loginout接收已預設格式用pm2resolve轉過, 會提取state進行判斷
651
725
  if (isErr(r.msg)) {
652
726
  r.msg = r.msg.message
653
727
  }
@@ -767,7 +841,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
767
841
 
768
842
  //check
769
843
  if (!isestr(token)) {
770
- console.log('req.query', get(req, 'query'))
844
+ console.log('req.query', maskQuery(get(req, 'query')))
771
845
  console.log('token', maskTok(token))
772
846
  console.log('[API]syncAndReplaceTabs/check token: invalid token')
773
847
  return Promise.reject('errTokenNoPermission')
@@ -778,7 +852,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
778
852
 
779
853
  //check
780
854
  if (!isestr(keyTable)) {
781
- console.log('req.query', get(req, 'query'))
855
+ console.log('req.query', maskQuery(get(req, 'query')))
782
856
  console.log('keyTable', keyTable)
783
857
  console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
784
858
  return Promise.reject('errKeyTableInvalid')
@@ -823,7 +897,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
823
897
  }
824
898
 
825
899
  //pm2resolve core
826
- let r = await pm2resolve(core)()
900
+ let r = await pm2resolve(wrapRouteErrLog('api/syncAndReplaceTabs', core))()
827
901
  if (isErr(r.msg)) {
828
902
  r.msg = r.msg.message
829
903
  }
@@ -863,6 +937,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
863
937
  console.log('apiType', apiType)
864
938
  console.log('authorization', maskTok(authorization))
865
939
  console.log('invalid token')
940
+ srLog.error({ event: 'verifyConn', err: 'errTokenNoPermission', apiType }) //認證失敗須入 log 供稽核/統計(CLAUDE.md「log 記錄要點」)
866
941
  return false
867
942
  }
868
943
 
@@ -870,9 +945,17 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
870
945
  //套件使用者之責——由其提供之 getUserByToken(可對非授權者回 {})/ verifyClientUser / verifyAppUser,
871
946
  //或 WServOrm 之 useCheckUser/useExcludeWhenNotAdmin 等鉤子決定;套件不對連線層強加特定授權政策(避免越界、
872
947
  //保留使用者彈性)。詳見 spec/設計要點與取捨.md「套件授權責任」。
873
- let b = await p.checkToken(token)
948
+ //checkToken 失敗會 throw(err-key),原本直接向上拋致認證失敗不入 log;此處攔截記錄後原樣再拋,行為不變。
949
+ let b
950
+ try {
951
+ b = await p.checkToken(token)
952
+ }
953
+ catch (err) {
954
+ srLog.error({ event: 'verifyConn', err, apiType })
955
+ throw err
956
+ }
874
957
 
875
- srLog.info({ event: 'verifyConn', apiType }) //連線之 token 認證通過(checkToken 失敗會 throw 不到此)
958
+ srLog.info({ event: 'verifyConn', apiType }) //連線之 token 認證通過
876
959
  return b
877
960
  },
878
961
  getUserIdByToken: async (token) => { //可使用async或sync函數
@@ -922,7 +1005,7 @@ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser,
922
1005
  // },
923
1006
 
924
1007
  },
925
- fnTableTags: 'tableTags-web-api.json',
1008
+ fpTableTags: 'tableTags-web-api.json', //w-serv-hapi 1.0.87 起正名 fpTableTags(同 sso / perm); 舊名 fnTableTags 於 1.0.86 傳到 w-serv-webdata 時不被讀取, 實際一直寫預設 tableTags.json
926
1009
  })
927
1010
 
928
1011