w-web-perm 1.1.4 → 1.1.6

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.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/css/app.6918ec45.css +1 -0
  3. package/dist/css/{chunk-vendors.2bcfce08.css → chunk-vendors.3c580e72.css} +1 -1
  4. package/dist/index.tmp +2 -2
  5. package/dist/js/app-legacy.5f15a58f.js +1 -0
  6. package/dist/js/app.5f15a58f.js +1 -0
  7. package/dist/js/{chunk-vendors-legacy.fc37f076.js → chunk-vendors-legacy.38f515f9.js} +16 -16
  8. package/dist/js/{chunk-vendors.fc37f076.js → chunk-vendors.38f515f9.js} +16 -16
  9. package/dist/w-web-perm.umd.js +2 -2
  10. package/dist/w-web-perm.umd.js.map +1 -1
  11. package/docs/WWebPerm.html +7 -7
  12. package/docs/global.html +2068 -111
  13. package/docs/index.html +2 -2
  14. package/docs/server_WWebPerm.mjs.html +63 -31
  15. package/docs/server_lockSave.mjs.html +115 -0
  16. package/docs/server_maskLog.mjs.html +176 -0
  17. package/docs/server_procCore.mjs.html +31 -5
  18. package/docs/server_wrapInjected.mjs.html +163 -0
  19. package/docs/src_fetchJson.mjs.html +31 -5
  20. package/docs/src_js_legendFit.mjs.html +225 -0
  21. package/docs/src_plugins_reuseDeletedIds.mjs.html +136 -0
  22. package/docs/src_plugins_submitGuard.mjs.html +163 -0
  23. package/package.json +13 -13
  24. package/public/index.html +2 -0
  25. package/server/WWebPerm.mjs +61 -29
  26. package/server/lockSave.mjs +43 -0
  27. package/server/maskLog.mjs +104 -0
  28. package/server/procCore.mjs +29 -3
  29. package/server/procLang.mjs +8 -0
  30. package/server/procStaInfor.mjs +32 -14
  31. package/server/wrapInjected.mjs +91 -0
  32. package/src/components/Common/CheckYes.vue +6 -0
  33. package/src/components/Common/CheckYesNo.vue +6 -0
  34. package/src/components/LayoutContentGrups.vue +30 -21
  35. package/src/components/LayoutContentPemis.vue +30 -21
  36. package/src/components/LayoutContentStaInfor.vue +74 -5
  37. package/src/components/LayoutContentTargets.vue +36 -22
  38. package/src/components/LayoutContentUsers.vue +31 -21
  39. package/src/components/RelationChips.vue +11 -31
  40. package/src/components/VeCgrups.vue +8 -1
  41. package/src/components/VeCpemis.vue +8 -1
  42. package/src/components/VeCrules.vue +8 -1
  43. package/src/components/VeGrupBlngUsers.vue +37 -31
  44. package/src/components/VePemiBlngGrups.vue +37 -31
  45. package/src/fetchJson.mjs +28 -2
  46. package/src/getPerm.mjs +9 -6
  47. package/src/getPermUserInfor.mjs +12 -9
  48. package/src/js/legendFit.mjs +153 -0
  49. package/src/plugins/mUI.mjs +20 -0
  50. package/src/plugins/reuseDeletedIds.mjs +64 -0
  51. package/src/plugins/submitGuard.mjs +91 -0
  52. package/srv.mjs +20 -1
  53. package/dist/css/app.506ce4d8.css +0 -1
  54. package/dist/js/app-legacy.8e615442.js +0 -1
  55. package/dist/js/app.8e615442.js +0 -1
  56. package/tableTags-web-perm.json +0 -1
@@ -0,0 +1,136 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>src/plugins/reuseDeletedIds.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">src/plugins/reuseDeletedIds.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import map from 'lodash-es/map.js'
50
+ import each from 'lodash-es/each.js'
51
+
52
+
53
+ /**
54
+ * 新增列沿用資料庫既有 id 時之正規化 (標的清單專用, 其 id 為可就地改寫之標的路徑; ADR-025)
55
+ *
56
+ * 同一次儲存中刪除(或把 id 改掉)某既有列, 又新增一列並把 id 改成該既有列之 id, 淨效果為「修改該列」:
57
+ * 該新增列改為一般列送出(_isNew 設 false), 建立者 / 建立時間沿用資料庫之值(修改者 / 修改時間由後端寫入).
58
+ * 否則後端之「新增列已存在即拒絕」(saveNewRowExists, 擋同一包重送) 會把它當重送擋下, 且重新整理後重做仍被擋;
59
+ * 亦避免以前端佔位字({待自動給予})覆寫建立者 / 建立時間.
60
+ * 只處理 id 於 rows 內唯一者; 重複 id 不處理, 由後端 ckKey 回 saveRowFieldDuplicate.
61
+ * 使用者 / 群組 / 權限清單之 id 為系統產生且不可編輯, 新增列不會與既有列同 id, 不需此處理.
62
+ *
63
+ * @param {Array} rows 輸入欲送出之列陣列(清單之 opt.rows), 不修改
64
+ * @param {Array} rowsDb 輸入資料庫之列陣列(前端 store 之同表資料)
65
+ * @returns {Array} 回傳正規化後之新陣列
66
+ */
67
+ function reuseDeletedIds(rows, rowsDb) {
68
+
69
+ //kpDb, kpCount: 以 Map 存放, 不以 lodash get 取值(標的 id 為路徑, 可能含「.」而被當成巢狀路徑)
70
+ let kpDb = new Map()
71
+ each(rowsDb, (r) => {
72
+ kpDb.set(get(r, 'id', ''), r)
73
+ })
74
+ let kpCount = new Map()
75
+ each(rows, (r) => {
76
+ let id = get(r, 'id', '')
77
+ kpCount.set(id, (kpCount.get(id) || 0) + 1)
78
+ })
79
+
80
+ let rs = map(rows, (r) => {
81
+ let id = get(r, 'id', '')
82
+
83
+ //非新增列
84
+ if (get(r, '_isNew') !== true) {
85
+ return r
86
+ }
87
+
88
+ //資料庫無此 id: 真正之新增
89
+ if (!kpDb.has(id)) {
90
+ return r
91
+ }
92
+ let rDb = kpDb.get(id)
93
+
94
+ //rows 內重複 id: 不處理
95
+ if (kpCount.get(id) !== 1) {
96
+ return r
97
+ }
98
+
99
+ return {
100
+ ...r,
101
+ _isNew: false,
102
+ userId: get(rDb, 'userId', ''),
103
+ timeCreate: get(rDb, 'timeCreate', ''),
104
+ }
105
+ })
106
+
107
+ return rs
108
+ }
109
+
110
+
111
+ export default reuseDeletedIds
112
+ </code></pre>
113
+ </article>
114
+ </section>
115
+
116
+
117
+
118
+
119
+
120
+
121
+ </div>
122
+
123
+ <br class="clear">
124
+
125
+ <footer>
126
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
127
+ </footer>
128
+
129
+ <script>prettyPrint();</script>
130
+ <script src="scripts/polyfill.js"></script>
131
+ <script src="scripts/linenumber.js"></script>
132
+
133
+
134
+
135
+ </body>
136
+ </html>
@@ -0,0 +1,163 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>src/plugins/submitGuard.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createLegendMeasurer">createLegendMeasurer</a></li><li><a href="global.html#createLockSave">createLockSave</a></li><li><a href="global.html#decideLegendType">decideLegendType</a></li><li><a href="global.html#describeUpstream">describeUpstream</a></li><li><a href="global.html#fetchJson">fetchJson</a></li><li><a href="global.html#genLegend">genLegend</a></li><li><a href="global.html#maskQuery">maskQuery</a></li><li><a href="global.html#maskTok">maskTok</a></li><li><a href="global.html#maskUrl">maskUrl</a></li><li><a href="global.html#mergePickKeysOnly">mergePickKeysOnly</a></li><li><a href="global.html#reuseDeletedIds">reuseDeletedIds</a></li><li><a href="global.html#submitGuard">submitGuard</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">src/plugins/submitGuard.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>import get from 'lodash-es/get.js'
49
+ import each from 'lodash-es/each.js'
50
+ import isfun from 'wsemi/src/isfun.mjs'
51
+ import haskey from 'wsemi/src/haskey.mjs'
52
+
53
+
54
+ /**
55
+ * 送出類操作之前端雙擊防護核心 (與 Vue / store 無關, 供單元測試; 由 mUI 之 runSubmit 與 updateLoading 使用, ADR-025)
56
+ *
57
+ * - run(key, fn, opt): 同 key 之送出流程進行中(自觸發起至流程結束, 含結果訊息框開啟期間)再觸發即略過.
58
+ * 按鈕之滑鼠與鍵盤 Enter、輸入框之 Enter 等入口共用同一狀態.
59
+ * opt.pm 為 WButtonChip / WButtonCircle 之 promiseUnlock 鎖(click 事件之 msg.pm), 於 releaseBtnLocks()(全頁 loading 關閉, 即請求結束)
60
+ * 或流程結束時釋放; 被略過之觸發亦立即釋放其 pm, 否則該按鈕永久鎖住.
61
+ * fn 收到 unlock 函數: 流程需先開確認框者於開框前呼叫, 按鈕鎖只涵蓋請求期間(確認框背後之按鈕不顯示載入圖示), 重入仍由流程狀態擋.
62
+ * opt.hold 為函數, 輸入流程結果, 回傳 true 時保持占位與按鈕鎖(如登入成功轉址, 頁面即將離開).
63
+ * - releaseBtnLocks(): 釋放進行中流程所登記之按鈕鎖, 流程狀態不變(結果訊息框開啟期間之重入仍擋).
64
+ * - isRunning(key): 查詢 key 是否進行中.
65
+ *
66
+ * 為何需要: 全頁 loading(WDialog)不搶焦點, 只擋滑鼠, 鍵盤事件仍送達背後之按鈕與輸入框;
67
+ * 按鈕鎖若於 handler 第一行即解除, 鍵盤連按照樣送出(2026-09-29 實測).
68
+ *
69
+ * @returns {Object} 回傳 { run, releaseBtnLocks, isRunning }
70
+ */
71
+ function submitGuard() {
72
+
73
+ //kpRunning, 進行中流程: key → { unlock }
74
+ let kpRunning = {}
75
+
76
+ let run = (key, fn, opt = {}) => {
77
+
78
+ //unlock, 釋放本次觸發之按鈕鎖; pm.resolve 重複呼叫無作用
79
+ let pm = get(opt, 'pm', null)
80
+ let unlock = () => {
81
+ if (pm &amp;&amp; isfun(pm.resolve)) {
82
+ pm.resolve()
83
+ }
84
+ }
85
+
86
+ //同 key 進行中: 略過, 並立即釋放本次觸發之按鈕鎖
87
+ if (haskey(kpRunning, key)) {
88
+ unlock()
89
+ return Promise.resolve(null)
90
+ }
91
+
92
+ //占位
93
+ kpRunning[key] = { unlock }
94
+
95
+ //hold
96
+ let hold = get(opt, 'hold', null)
97
+
98
+ //end
99
+ let end = () => {
100
+ unlock()
101
+ delete kpRunning[key]
102
+ }
103
+
104
+ return Promise.resolve()
105
+ .then(() => {
106
+ return fn(unlock)
107
+ })
108
+ .then((r) => {
109
+ if (isfun(hold) &amp;&amp; hold(r) === true) {
110
+ return r //保持占位與按鈕鎖
111
+ }
112
+ end()
113
+ return r
114
+ }, (err) => {
115
+ end()
116
+ return Promise.reject(err)
117
+ })
118
+ }
119
+
120
+ let releaseBtnLocks = () => {
121
+ each(kpRunning, (v) => {
122
+ v.unlock()
123
+ })
124
+ }
125
+
126
+ let isRunning = (key) => {
127
+ return haskey(kpRunning, key)
128
+ }
129
+
130
+ return {
131
+ run,
132
+ releaseBtnLocks,
133
+ isRunning,
134
+ }
135
+ }
136
+
137
+
138
+ export default submitGuard
139
+ </code></pre>
140
+ </article>
141
+ </section>
142
+
143
+
144
+
145
+
146
+
147
+
148
+ </div>
149
+
150
+ <br class="clear">
151
+
152
+ <footer>
153
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 02 2026 20:53:56 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
154
+ </footer>
155
+
156
+ <script>prettyPrint();</script>
157
+ <script src="scripts/polyfill.js"></script>
158
+ <script src="scripts/linenumber.js"></script>
159
+
160
+
161
+
162
+ </body>
163
+ </html>
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "w-web-perm",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "main": "dist/w-web-perm.umd.js",
5
5
  "dependencies": {
6
6
  "json5": "^2.2.3",
7
- "w-aggrid-vue": "^2.1.4",
8
- "w-component-vue": "^2.5.21",
9
- "w-data-collector": "^1.0.40",
10
- "w-echarts-vue": "^1.0.14",
11
- "w-serv-hapi": "^1.1.2",
12
- "w-serv-orm": "^1.1.3",
13
- "w-syslog": "^1.0.29",
14
- "w-ui-loginout": "^1.0.28",
15
- "wsemi": "^1.9.3"
7
+ "w-aggrid-vue": "^2.1.5",
8
+ "w-component-vue": "^2.5.26",
9
+ "w-data-collector": "^1.0.41",
10
+ "w-echarts-vue": "^1.0.16",
11
+ "w-serv-hapi": "^1.1.3",
12
+ "w-serv-orm": "^1.1.4",
13
+ "w-syslog": "^1.0.30",
14
+ "w-ui-loginout": "^1.0.29",
15
+ "wsemi": "^1.9.9"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@vue/cli-plugin-babel": "^5.0.8",
@@ -27,10 +27,10 @@
27
27
  "eslint-plugin-vue": "^9.10.0",
28
28
  "vue": "^2.7.16",
29
29
  "vuex": "^3.6.2",
30
- "w-orm-lmdb": "^1.0.34",
30
+ "w-orm-lmdb": "^1.0.35",
31
31
  "w-package-tools": "^1.1.16",
32
- "w-package-tools-e2e": "^1.0.0",
33
- "w-screenctl": "^1.0.22"
32
+ "w-package-tools-e2e": "^1.0.6",
33
+ "w-screenctl": "^1.0.23"
34
34
  },
35
35
  "scripts": {
36
36
  "serve": "vue-cli-service serve",
package/public/index.html CHANGED
@@ -4,6 +4,8 @@
4
4
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
6
6
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
+ <!-- referrer: 本站以 ?token= 開啟(w-ui-loginout 讀完不清網址), 瀏覽器預設會把含權杖之完整網址放進同源請求之 Referer, 進而寫入後端 log 與反向代理 access log; strict-origin 使請求只帶 origin(見 spec/設計要點與取捨.md ADR-023) -->
8
+ <meta name="referrer" content="strict-origin">
7
9
  <title></title>
8
10
 
9
11
  <!-- mdi font -->
@@ -23,14 +23,18 @@ import replace from 'wsemi/src/replace.mjs'
23
23
  import strdelleft from 'wsemi/src/strdelleft.mjs'
24
24
  import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
25
25
  import pmKeyMutex from 'wsemi/src/pmKeyMutex.mjs'
26
+ import cacheSt from 'wsemi/src/cacheSt.mjs'
26
27
  import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
27
28
  import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
28
29
  import ds from '../src/schema/index.mjs'
29
30
  import { getUserRules } from '../src/plugins/mShare.mjs'
30
31
  import procCore from './procCore.mjs'
32
+ import createLockSave from './lockSave.mjs'
31
33
  import procLang from './procLang.mjs'
32
34
  import srLogInit from './srLog.mjs'
33
35
  import procStaInfor from './procStaInfor.mjs'
36
+ import { maskTok, maskQuery, maskUrl } from './maskLog.mjs'
37
+ import wrapInjected from './wrapInjected.mjs'
34
38
 
35
39
 
36
40
  /**
@@ -40,9 +44,9 @@ import procStaInfor from './procStaInfor.mjs'
40
44
  * @param {Function} WOrm 輸入資料庫ORM函數
41
45
  * @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
42
46
  * @param {String} db 輸入資料庫名稱字串
43
- * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫
44
- * @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件(perm users 表之列)與來源字串from,通過此函數識別後回傳布林值(可為Promise),允許使用者回傳true,反之回傳false。from 值域: 'getUserByToken'(登入)、'getPerm'(api/getPerm)、以及資料通道之 apiType 'main'(RPC 與 ORM 執行)、'upload-controller'、'upload-slice'、'download-get-filename'、'download-get-file'、'download';資料通道與登入採同一授權強度(反查 perm users 且 isActive='y' 後再經本函數)
45
- * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
47
+ * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫。函數拋錯或reject時視同查無使用者(回自家錯誤key,錯誤原文不回前端、不入log,只於srLog記warn)
48
+ * @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件(perm users 表之列)與來源字串from,通過此函數識別後回傳布林值(可為Promise),允許使用者回傳true,反之回傳false。from 值域: 'getUserByToken'(登入)、'getPerm'(api/getPerm)、以及資料通道之 apiType 'main'(RPC 與 ORM 執行)、'upload-controller'、'upload-slice'、'download-get-filename'、'download-get-file'、'download';資料通道與登入採同一授權強度(反查 perm users 且 isActive='y' 後再經本函數);函數拋錯或reject時視同回傳false
49
+ * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false;函數拋錯或reject時視同回傳false
46
50
  * @param {Object} [opt={}] 輸入設定物件,預設{}
47
51
  * @param {Integer} [opt.serverPort=11006] 輸入伺服器通訊port,預設11006
48
52
  * @param {Boolean} [opt.useCheckUser=false] 輸入是否檢查使用者資訊布林值,預設false
@@ -110,7 +114,7 @@ import procStaInfor from './procStaInfor.mjs'
110
114
  * isAdmin: 'y',
111
115
  * }
112
116
  * }
113
- * console.log('invalid token', token)
117
+ * console.log('invalid token') //勿印權杖值
114
118
  * console.log('於生產環境時得加入SSO等驗證token機制')
115
119
  * return {}
116
120
  * }
@@ -274,6 +278,27 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
274
278
  let srLog = srLogInit(opt)
275
279
 
276
280
 
281
+ //注入函數一律經包裝(W 契約, spec/設計要點與取捨.md ADR-023), 以包裝版取代三個參數
282
+ //why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
283
+ //部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址.
284
+ //包裝後失敗視同否定結果(getUserByToken → null 等同查無, verify* → false 等同無權限), 走各呼叫點既有分支回自家 key;
285
+ //上游原文不回前端、不入 log, 只於此記 warn(白名單 key, 或型別與 Error.name). 同一次失敗 log 2~3 筆: 此處 warn(因) + 呼叫點 error(果).
286
+ //以下取代後, 本函數內除此三行外不得再直接使用原始參數(盤點見 CLAUDE_rulebook.md「權杖不外洩」, 測試 unit-wrapInjected WRAP-SITE-*)
287
+ let logInjectFail = (name) => {
288
+ return (d) => {
289
+ srLog.warn({ event: `inject-${name}-fail`, ...d })
290
+ }
291
+ }
292
+ getUserByToken = wrapInjected(getUserByToken, { failValue: null, onFail: logInjectFail('getUserByToken') })
293
+ verifyClientUser = wrapInjected(verifyClientUser, { failValue: false, onFail: logInjectFail('verifyClientUser') })
294
+ verifyAppUser = wrapInjected(verifyAppUser, { failValue: false, onFail: logInjectFail('verifyAppUser') })
295
+ //getUserById: 第四個注入函數(選填; useCheckUser=true 時由 WServOrm 於 execute 內呼叫, 原 reject 原文經 pm2resolve 回前端), 有給才包裝;
296
+ //失敗 → null 等同查無, 由 WServOrm 既有之查無分支拒絕(fail-closed). 須於 WServOrm 建構前取代(建構時即取走此函數)
297
+ if (isfun(getUserById)) {
298
+ getUserById = wrapInjected(getUserById, { failValue: null, onFail: logInjectFail('getUserById') })
299
+ }
300
+
301
+
277
302
  //WServOrm
278
303
  let optWServOrm = {
279
304
  useCheckUser,
@@ -297,8 +322,14 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
297
322
  let kmx = pmKeyMutex()
298
323
 
299
324
 
325
+ //lockSave: 後台整表寫入(updateTabItems)之雙擊防護(後端, 見 lockSave.mjs, ADR-025): 以「updateTabItems:{表名}:{操作者 id}」於 cacheSt 原子占位,
326
+ //同一操作者之同表寫入處理中再送出即 reject 'saveInProgress'(不排隊); 包在 kmx 之外層, 不同操作者之並行仍由 kmx 序列化(ADR-012)
327
+ let cst = cacheSt()
328
+ let lockSave = createLockSave(cst)
329
+
330
+
300
331
  //procCore: 整表批次 diff 寫入邏輯 (updateTabItems + updateTargets/Pemis/Grups/Users)
301
- let pc = procCore(woItems, procOrm, { srLog, kmx })
332
+ let pc = procCore(woItems, procOrm, { srLog, kmx, lockSave })
302
333
 
303
334
 
304
335
  //procStaInfor: 統計資訊, fdLog 對齊 srLog 使用同一目錄(讀 settings.logFd, fallback 邏輯同 srLog.mjs); mock 由 settings.staEventMock 注入(e2e 統計圖穩定用)
@@ -396,7 +427,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
396
427
 
397
428
  //check
398
429
  if (!iseobj(userSelf)) {
399
- console.log(`token`, token)
430
+ console.log(`token`, maskTok(token))
400
431
  srLog.error({ event: 'getTokenUser-error', msg: 'cannotFindUserFromToken' })
401
432
  return Promise.reject(`cannotFindUserFromToken`)
402
433
  }
@@ -478,7 +509,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
478
509
 
479
510
  //check
480
511
  if (!iseobj(user)) {
481
- console.log(`token`, token)
512
+ console.log(`token`, maskTok(token))
482
513
  srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'cannotFindUserFromToken' })
483
514
  return Promise.reject(`cannotFindUserFromToken`)
484
515
  }
@@ -750,8 +781,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
750
781
 
751
782
  //check
752
783
  if (!isestr(token)) {
753
- console.log('req.query', get(req, 'query'))
754
- console.log('token', token)
784
+ console.log('req.query', maskQuery(get(req, 'query')))
785
+ console.log('token', maskTok(token))
755
786
  console.log('[API]getUserByToken/check token: invalid token')
756
787
  srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
757
788
  return Promise.reject(`tokenNoPermission`)
@@ -763,7 +794,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
763
794
 
764
795
  //check
765
796
  if (!iseobj(user)) {
766
- console.log('token', token)
797
+ console.log('token', maskTok(token))
767
798
  console.log('[API]getUserByToken/check user: invalid user')
768
799
  srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
769
800
  return Promise.reject(`tokenNoPermission`)
@@ -800,8 +831,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
800
831
 
801
832
  //check
802
833
  if (!isestr(token)) {
803
- console.log('req.query', get(req, 'query'))
804
- console.log('token', token)
834
+ console.log('req.query', maskQuery(get(req, 'query')))
835
+ console.log('token', maskTok(token))
805
836
  console.log('[API]getPerm/check token: invalid token')
806
837
  srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
807
838
  return Promise.reject(`tokenNoPermission`)
@@ -813,7 +844,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
813
844
 
814
845
  //check
815
846
  if (!iseobj(userSelf)) {
816
- console.log('token', token)
847
+ console.log('token', maskTok(token))
817
848
  console.log('[API]getPerm/check userSelf: invalid userSelf')
818
849
  srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
819
850
  return Promise.reject(`tokenNoPermission`)
@@ -825,7 +856,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
825
856
 
826
857
  //check
827
858
  if (!isestr(userIdSelf)) {
828
- console.log('token', token)
859
+ console.log('token', maskTok(token))
829
860
  console.log('userSelf', userSelf)
830
861
  console.log('[API]getPerm/check userIdSelf: invalid userIdSelf')
831
862
  srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
@@ -838,7 +869,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
838
869
 
839
870
  //check
840
871
  if (!iseobj(userWithRulesSelf)) {
841
- console.log('token', token)
872
+ console.log('token', maskTok(token))
842
873
  console.log('userSelf', userSelf)
843
874
  console.log('userIdSelf', userIdSelf)
844
875
  console.log('[API]getPerm/check userWithRulesSelf: invalid userWithRulesSelf')
@@ -877,7 +908,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
877
908
 
878
909
  //check
879
910
  if (!isestr(tokenSelf)) {
880
- console.log('req.query', get(req, 'query'))
911
+ console.log('req.query', maskQuery(get(req, 'query')))
881
912
  console.log('[API]getPermUserInfor/check tokenSelf: invalid tokenSelf')
882
913
  srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
883
914
  return Promise.reject(`tokenNoPermission`)
@@ -889,8 +920,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
889
920
 
890
921
  //check
891
922
  if (!iseobj(userSelf)) {
892
- console.log('req.query', get(req, 'query'))
893
- console.log('tokenSelf', tokenSelf)
923
+ console.log('req.query', maskQuery(get(req, 'query')))
924
+ console.log('tokenSelf', maskTok(tokenSelf))
894
925
  console.log('[API]getPermUserInfor/check userSelf: invalid userSelf')
895
926
  srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
896
927
  return Promise.reject(`tokenNoPermission`)
@@ -902,8 +933,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
902
933
 
903
934
  //check
904
935
  if (!isestr(userIdFind)) {
905
- console.log('req.query', get(req, 'query'))
906
- console.log('tokenSelf', tokenSelf)
936
+ console.log('req.query', maskQuery(get(req, 'query')))
937
+ console.log('tokenSelf', maskTok(tokenSelf))
907
938
  console.log('[API]getPermUserInfor/check userIdFind: invalid userIdFind')
908
939
  srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
909
940
  return Promise.reject(`tokenNoPermission`)
@@ -952,8 +983,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
952
983
 
953
984
  //check
954
985
  if (!isestr(token)) {
955
- console.log('req.query', get(req, 'query'))
956
- console.log('token', token)
986
+ console.log('req.query', maskQuery(get(req, 'query')))
987
+ console.log('token', maskTok(token))
957
988
  console.log('[API]syncAndReplaceTabs/check token: invalid token')
958
989
  srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
959
990
  return Promise.reject(`tokenNoPermission`)
@@ -964,7 +995,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
964
995
 
965
996
  //check
966
997
  if (!isestr(keyTable)) {
967
- console.log('req.query', get(req, 'query'))
998
+ console.log('req.query', maskQuery(get(req, 'query')))
968
999
  console.log('keyTable', keyTable)
969
1000
  console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
970
1001
  srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
@@ -984,7 +1015,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
984
1015
 
985
1016
  //check user
986
1017
  if (!iseobj(user)) {
987
- console.log('token', token)
1018
+ console.log('token', maskTok(token))
988
1019
  console.log('[API]syncAndReplaceTabs/check user: invalid user')
989
1020
  srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
990
1021
  return Promise.reject(`tokenNoPermission`)
@@ -995,7 +1026,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
995
1026
 
996
1027
  //check userId
997
1028
  if (!isestr(userId)) {
998
- console.log('token', token)
1029
+ console.log('token', maskTok(token))
999
1030
  console.log('user', user)
1000
1031
  console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
1001
1032
  srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
@@ -1052,8 +1083,9 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1052
1083
  //check
1053
1084
  if (!isestr(token)) {
1054
1085
  console.log('apiType', apiType)
1055
- console.log('authorization', authorization)
1086
+ console.log('authorization', maskTok(authorization))
1056
1087
  console.log('invalid token')
1088
+ srLog.error({ event: 'verifyConn-error', ok: false, apiType, msg: 'tokenNoPermission' }) //認證失敗須入 log 供稽核/統計(CLAUDE.md「log 記錄要點」; 同 w-web-api、w-web-task); 只記 key, 不記 authorization 原值
1057
1089
  return false
1058
1090
  }
1059
1091
 
@@ -1078,8 +1110,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1078
1110
  //origin
1079
1111
  let origin = get(headers, 'origin', '')
1080
1112
 
1081
- //referer
1082
- let referer = get(headers, 'referer', '')
1113
+ //referer, 只記 origin + pathname(ADR-023): 前端以 ?token= 開站(w-ui-loginout 讀完不清網址), 同源請求之 Referer 預設帶完整網址, 原樣記錄即把使用者權杖寫入 srLog
1114
+ let referer = maskUrl(get(headers, 'referer', ''))
1083
1115
 
1084
1116
  //info, 失敗時 msg 為 err key(cannotFindUserFromToken / cannotGetUserFromPerm / userNoPermission 等), 供資安稽核與統計
1085
1117
  if (b) {
@@ -1096,7 +1128,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
1096
1128
  let user = await getUserByToken(token)
1097
1129
  let userId = get(user, 'id', '')
1098
1130
  if (!isestr(userId)) {
1099
- console.log('token', token)
1131
+ console.log('token', maskTok(token))
1100
1132
  console.log('userId', userId)
1101
1133
  srLog.error({ event: 'getUserIdByToken-error', msg: 'cannotFindUserId' })
1102
1134
  return Promise.reject(`cannotFindUserId`)
@@ -0,0 +1,43 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isestr from 'wsemi/src/isestr.mjs'
3
+
4
+
5
+ /**
6
+ * 有副作用之寫入路徑(如清單儲存 updateUsersList / updateTokensList / updateIpsList)之雙擊防護(後端, ADR-025)
7
+ *
8
+ * 以「操作:操作者 id」於 wsemi cacheSt 原子占位執行 fn, 同一操作者之同一操作處理中再送出即 reject 錯誤 key
9
+ * (預設 'saveInProgress', 可以 opt.errKey 指定; 皆為 i18n key, 見 procLang); 不同操作者、不同操作互不影響;
10
+ * fn 之 reject 原樣 bubble; fn 結束(含失敗)即釋放.
11
+ * 第 2 次不排隊執行: 其內容與第 1 次相同, 排隊後再寫一次會把第 1 次剛建立之列當修改覆寫
12
+ * (依序重送另由各專案之資料層檢查擋, 如 SSO procCore.updateTabItems 之新增列檢查回 'saveNewRowExists').
13
+ * operatorId 可為操作者 id 或「操作者 id + 列識別」(只擋同一列之連發, 同批其他列不受影響).
14
+ *
15
+ * @param {Object} cst 輸入 wsemi cacheSt 實例
16
+ * @returns {Function} 回傳 async (op, operatorId, fn, opt = {}) => fn 之結果, opt.errKey 為占位衝突時 reject 之 key
17
+ */
18
+ function createLockSave(cst) {
19
+
20
+ let lockSave = async (op, operatorId, fn, opt = {}) => {
21
+
22
+ //errKey
23
+ let errKey = get(opt, 'errKey', '')
24
+ if (!isestr(errKey)) {
25
+ errKey = 'saveInProgress'
26
+ }
27
+
28
+ let key = `${op}:${operatorId}`
29
+ return await cst.setWithFree([key], fn)
30
+ .catch((err) => {
31
+ //remap setWithFree 之占位衝突 → i18n key; fn 內之 reject 原樣 bubble
32
+ if (err === `setWithFree: key in use: ${key}`) {
33
+ return Promise.reject(errKey)
34
+ }
35
+ return Promise.reject(err)
36
+ })
37
+ }
38
+
39
+ return lockSave
40
+ }
41
+
42
+
43
+ export default createLockSave