w-web-perm 1.1.7 → 1.1.9

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 (51) hide show
  1. package/README.md +14 -4
  2. package/dist/index.tmp +1 -1
  3. package/dist/js/app-legacy.ecb7ce08.js +1 -0
  4. package/dist/js/app.ecb7ce08.js +1 -0
  5. package/dist/js/{chunk-vendors-legacy.00f159e8.js → chunk-vendors-legacy.da785a99.js} +3 -3
  6. package/dist/js/{chunk-vendors.00f159e8.js → chunk-vendors.da785a99.js} +3 -3
  7. package/dist/w-web-perm.umd.js +2 -2
  8. package/dist/w-web-perm.umd.js.map +1 -1
  9. package/docs/WWebPerm.html +78 -128
  10. package/docs/global.html +1280 -39
  11. package/docs/index.html +2 -2
  12. package/docs/server_WWebPerm.mjs.html +1247 -1196
  13. package/docs/server_dataChannel.mjs.html +131 -0
  14. package/docs/server_lockSave.mjs.html +2 -2
  15. package/docs/server_maskLog.mjs.html +2 -2
  16. package/docs/server_procCore.mjs.html +53 -11
  17. package/docs/server_staLogs_resolveShellWorker.mjs.html +180 -0
  18. package/docs/server_validateRows.mjs.html +184 -0
  19. package/docs/server_wrapInjected.mjs.html +2 -2
  20. package/docs/src_fetchJson.mjs.html +2 -2
  21. package/docs/src_js_legendFit.mjs.html +2 -2
  22. package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
  23. package/docs/src_plugins_submitGuard.mjs.html +2 -2
  24. package/g_backupDb.mjs +4 -0
  25. package/g_initialData.mjs +1 -0
  26. package/g_initialTestData.mjs +1 -0
  27. package/g_mOrm.mjs +2 -7
  28. package/g_provideGrups.mjs +1 -0
  29. package/g_providePemis.mjs +1 -0
  30. package/g_provideTargets.mjs +1 -0
  31. package/g_provideUsers.mjs +1 -0
  32. package/package.json +13 -13
  33. package/server/WWebPerm.mjs +1245 -1194
  34. package/server/dataChannel.mjs +59 -0
  35. package/server/procCore.mjs +51 -9
  36. package/server/procLang.mjs +13 -3
  37. package/server/staLogs/resolveShellWorker.mjs +108 -0
  38. package/server/staLogs/staLogsCore.callWorker.mjs +29 -9
  39. package/server/validateRows.mjs +112 -0
  40. package/src/components/LayoutContentGrups.vue +8 -8
  41. package/src/components/LayoutContentPemis.vue +8 -8
  42. package/src/components/LayoutContentTargets.vue +2 -2
  43. package/src/components/LayoutContentUsers.vue +4 -4
  44. package/src/components/VeCgrups.vue +3 -3
  45. package/src/components/VeCpemis.vue +3 -3
  46. package/src/components/VeCrules.vue +3 -3
  47. package/src/components/VeGrupBlngUsers.vue +6 -6
  48. package/src/components/VePemiBlngGrups.vue +6 -6
  49. package/srv.mjs +0 -4
  50. package/dist/js/app-legacy.6f82d8bd.js +0 -1
  51. package/dist/js/app.6f82d8bd.js +0 -1
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
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>
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#createAuthorizeExec">createAuthorizeExec</a></li><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#resolveShellWorker">resolveShellWorker</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#validateRowsBeforeWrite">validateRowsBeforeWrite</a></li><li><a href="global.html#wrapInjected">wrapInjected</a></li><li><a href="global.html#wrapOperOrm">wrapOperOrm</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -45,1200 +45,1251 @@
45
45
 
46
46
  <section>
47
47
  <article>
48
- <pre class="prettyprint source linenums"><code>import path from 'path'
49
- import fs from 'fs'
50
- import get from 'lodash-es/get.js'
51
- import map from 'lodash-es/map.js'
52
- import keys from 'lodash-es/keys.js'
53
- import join from 'lodash-es/join.js'
54
- import iseobj from 'wsemi/src/iseobj.mjs'
55
- import isestr from 'wsemi/src/isestr.mjs'
56
- import ispint from 'wsemi/src/ispint.mjs'
57
- import isearr from 'wsemi/src/isearr.mjs'
58
- import isErr from 'wsemi/src/isErr.mjs'
59
- import isfun from 'wsemi/src/isfun.mjs'
60
- import ispm from 'wsemi/src/ispm.mjs'
61
- import cint from 'wsemi/src/cint.mjs'
62
- import j2o from 'wsemi/src/j2o.mjs'
63
- import strleft from 'wsemi/src/strleft.mjs'
64
- import strright from 'wsemi/src/strright.mjs'
65
- import strdelright from 'wsemi/src/strdelright.mjs'
66
- import pm2resolve from 'wsemi/src/pm2resolve.mjs'
67
- import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
68
- import fsIsFile from 'wsemi/src/fsIsFile.mjs'
69
- import replace from 'wsemi/src/replace.mjs'
70
- import strdelleft from 'wsemi/src/strdelleft.mjs'
71
- import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
72
- import pmKeyMutex from 'wsemi/src/pmKeyMutex.mjs'
73
- import cacheSt from 'wsemi/src/cacheSt.mjs'
74
- import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
75
- import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
76
- import ds from '../src/schema/index.mjs'
77
- import { getUserRules } from '../src/plugins/mShare.mjs'
78
- import procCore from './procCore.mjs'
79
- import createLockSave from './lockSave.mjs'
80
- import procLang from './procLang.mjs'
81
- import srLogInit from './srLog.mjs'
82
- import procStaInfor from './procStaInfor.mjs'
83
- import { maskTok, maskQuery, maskUrl } from './maskLog.mjs'
84
- import wrapInjected from './wrapInjected.mjs'
85
-
86
-
87
- /**
88
- * 權限伺服器
89
- *
90
- * @class
91
- * @param {Function} WOrm 輸入資料庫ORM函數
92
- * @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
93
- * @param {String} db 輸入資料庫名稱字串
94
- * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫。函數拋錯或reject時視同查無使用者(回自家錯誤key,錯誤原文不回前端、不入log,只於srLog記warn)
95
- * @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
96
- * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false;函數拋錯或reject時視同回傳false
97
- * @param {Object} [opt={}] 輸入設定物件,預設{}
98
- * @param {Integer} [opt.serverPort=11006] 輸入伺服器通訊port,預設11006
99
- * @param {Boolean} [opt.useCheckUser=false] 輸入是否檢查使用者資訊布林值,預設false
100
- * @param {Function} [opt.getUserById=null] 輸入當useCheckUser=true時依照使用者ID取得使用者資訊物件函數,預設null
101
- * @param {Boolean} [opt.useExcludeWhenNotAdmin=false] 輸入使用ORM的select方法時是否自動刪除數據內isActive欄位之布林值,預設false
102
- * @param {Object} [opt.webName={}] 輸入站台名稱物件,至少包含語系eng與cht鍵的名稱,預設{}
103
- * @param {Object} [opt.webDescription={}] 輸入站台描述物件,至少包含語系eng與cht鍵的名稱,預設{}
104
- * @param {String} [opt.webLogo=''] 輸入站台logo字串,採base64格式,預設''
105
- * @param {String} [opt.subfolder=''] 輸入站台所在子目錄字串,提供站台位於內網採反向代理進行服務時,故需支援位於子目錄情形,預設''
106
- * @param {String} [opt.urlRedirect=''] 輸入錯誤時自動轉址字串,提供站台例如無法登入或驗證失敗時須自動轉址,預設''
107
- * @param {String} [opt.mappingBy='email'] 輸入外部系統識別使用者token後所提供之資料物件,與權限系統之使用者資料物件,兩者間查找之對應欄位,可選'id'、'email'、'name',預設'email'
108
- * @param {String} [opt.logFd='./logs'] 輸入log紀錄儲存位置字串,預設'./logs'
109
- * @param {String} [opt.logInterval='hr'] 輸入log紀錄檔案拆檔時長字串,可選'hr'或'day',預設'hr'
110
- * @param {Integer} [opt.logNumKeep=null] 輸入log檔保留數上限正整數,超出者自最舊刪除,未給採w-syslog預設(hr模式365*24、day模式365),預設null
111
- * @returns {Object} 回傳物件,其內server為hapi伺服器實體,wsrv為w-converhp的伺服器事件物件,wsds為w-serv-webdata的伺服器事件物件,可監聽error事件
112
- * @example
113
- *
114
- * import WOrm from 'w-orm-lmdb/src/WOrmLmdb.mjs'
115
- * import WWebPerm from './server/WWebPerm.mjs'
116
- * import getSettings from './g_getSettings.mjs'
117
- *
118
- *
119
- * //st
120
- * let st = getSettings()
121
- *
122
- * let url = st.dbUrl
123
- * let db = st.dbName
124
- * let opt = {
125
- *
126
- * useCheckUser: false,
127
- * getUserById: null,
128
- * useExcludeWhenNotAdmin: false,
129
- *
130
- * serverPort: 11006,
131
- * subfolder: '', //mperm
132
- * urlRedirect: 'https://www.google.com/', //本機測試時得先編譯, 再瀏覽: http://localhost:11006/
133
- *
134
- * webName: {
135
- * 'eng': 'Permission Service',
136
- * 'cht': '權限管理系統',
137
- * },
138
- * webDescription: {
139
- * 'eng': 'A web service package for user permissions and management targets.',
140
- * 'cht': 'A web service package for user permissions and management targets.',
141
- * },
142
- * webLogo: 'data:image/svg+xml;base64,...',
143
- *
144
- * }
145
- *
146
- * let getUserByToken = async (token) => {
147
- * // return {} //測試無法登入
148
- * if (token === '{token-for-application}') { //提供外部應用系統作為存取使用者
149
- * return {
150
- * id: 'id-for-application',
151
- * name: 'application',
152
- * email: 'application@example.com',
153
- * isAdmin: 'y',
154
- * }
155
- * }
156
- * if (token === 'sys') { //開發階段w-ui-loginout自動給予browser使用者(且位於localhost)的token為sys
157
- * return {
158
- * id: 'id-for-admin',
159
- * name: 'tester',
160
- * email: 'admin@example.com', //mappingBy為email, 開發階段時會使用email找到所建置之使用者資料
161
- * isAdmin: 'y',
162
- * }
163
- * }
164
- * console.log('invalid token') //勿印權杖值
165
- * console.log('於生產環境時得加入SSO等驗證token機制')
166
- * return {}
167
- * }
168
- *
169
- * let verifyClientUser = (user, from) => {
170
- * console.log('verifyClientUser/user', user)
171
- * // return false //測試無法登入
172
- * console.log('於生產環境時得加入限制瀏覽器使用者身份機制')
173
- * return user.isAdmin === 'y' //測試僅系統管理者使用
174
- * }
175
- *
176
- * let verifyAppUser = (user, from) => {
177
- * console.log('verifyAppUser/user', user)
178
- * // return false //測試無法登入
179
- * console.log('於生產環境時得加入限制應用程式使用者身份機制')
180
- * return user.isAdmin === 'y' //測試僅系統管理者使用
181
- * }
182
- *
183
- * //WWebPerm
184
- * let instWWebPerm = WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt)
185
- *
186
- * instWWebPerm.on('error', (err) => {
187
- * console.log(err)
188
- * })
189
- *
190
- */
191
- function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt = {}) {
192
- let instWServHapiServer = null
193
-
194
-
195
- //check WOrm
196
- if (!isfun(WOrm)) {
197
- console.log('invalid WOrm', WOrm)
198
- throw new Error('invalid WOrm')
199
- }
200
-
201
-
202
- //check url
203
- if (!isestr(url)) {
204
- console.log('invalid url', url)
205
- throw new Error('invalid url')
206
- }
207
-
208
-
209
- //check db
210
- if (!isestr(db)) {
211
- console.log('invalid db', db)
212
- throw new Error('invalid db')
213
- }
214
-
215
-
216
- //check getUserByToken
217
- if (!isfun(getUserByToken)) {
218
- console.log('invalid getUserByToken', getUserByToken)
219
- throw new Error('invalid getUserByToken')
220
- }
221
-
222
-
223
- //check verifyClientUser
224
- if (!isfun(verifyClientUser)) {
225
- console.log('invalid verifyClientUser', verifyClientUser)
226
- throw new Error('invalid verifyClientUser')
227
- }
228
-
229
-
230
- //check verifyAppUser
231
- if (!isfun(verifyAppUser)) {
232
- console.log('invalid verifyAppUser', verifyAppUser)
233
- throw new Error('invalid verifyAppUser')
234
- }
235
-
236
-
237
- //serverPort
238
- let serverPort = get(opt, 'serverPort')
239
- if (!ispint(serverPort)) {
240
- serverPort = 11006
241
- }
242
- serverPort = cint(serverPort)
243
-
244
-
245
- //useCheckUser
246
- let useCheckUser = get(opt, 'useCheckUser', false)
247
-
248
-
249
- //getUserById
250
- let getUserById = get(opt, 'getUserById', null)
251
-
252
-
253
- //useExcludeWhenNotAdmin
254
- let useExcludeWhenNotAdmin = get(opt, 'useExcludeWhenNotAdmin', false)
255
-
256
-
257
- //webName
258
- let webName = get(opt, 'webName', {})
259
-
260
-
261
- //webDescription
262
- let webDescription = get(opt, 'webDescription', {})
263
-
264
-
265
- //webLogo
266
- let webLogo = get(opt, 'webLogo', '')
267
-
268
-
269
- //subfolder
270
- let subfolder = get(opt, 'subfolder', '')
271
- if (isestr(subfolder)) {
272
- if (strright(subfolder, 1) === '/') { //右邊不需要給「/」
273
- subfolder = strdelright(subfolder, 1)
274
- }
275
- if (strleft(subfolder, 1) !== '/') { //左邊需要給「/」
276
- subfolder = `/${subfolder}`
277
- }
278
- }
279
-
280
-
281
- //urlRedirect
282
- let urlRedirect = get(opt, 'urlRedirect', '')
283
- // console.log('urlRedirect', urlRedirect)
284
-
285
-
286
- //params
287
- let showLanguage = get(opt, 'showLanguage', 'n')
288
- let language = get(opt, 'language', 'eng')
289
- let showModeEditTargets = get(opt, 'showModeEditTargets', 'n')
290
- let showModeEditPemis = get(opt, 'showModeEditPemis', 'n')
291
- let showModeEditGrups = get(opt, 'showModeEditGrups', 'n')
292
- let showModeEditUsers = get(opt, 'showModeEditUsers', 'n')
293
- let modeEditTargets = get(opt, 'modeEditTargets', 'n')
294
- let modeEditPemis = get(opt, 'modeEditPemis', 'n')
295
- let modeEditGrups = get(opt, 'modeEditGrups', 'n')
296
- let modeEditUsers = get(opt, 'modeEditUsers', 'n')
297
- let widthTargetId = get(opt, 'widthTargetId', '')
298
- let widthTargetDescription = get(opt, 'widthTargetDescription', '')
299
- let widthPemisName = get(opt, 'widthPemisName', '')
300
- let widthPemisDescription = get(opt, 'widthPemisDescription', '')
301
- let widthGrupsName = get(opt, 'widthGrupsName', '')
302
- let widthGrupsDescription = get(opt, 'widthGrupsDescription', '')
303
- let widthUsersName = get(opt, 'widthUsersName', '')
304
- let widthUsersEmail = get(opt, 'widthUsersEmail', '')
305
- let widthUsersDescription = get(opt, 'widthUsersDescription', '')
306
-
307
-
308
- //mappingBy
309
- let mappingBy = get(opt, 'mappingBy', '')
310
- if (mappingBy !== 'id' &amp;&amp; mappingBy !== 'email' &amp;&amp; mappingBy !== 'name') {
311
- mappingBy = 'id'
312
- }
313
- // console.log('mappingBy', mappingBy)
314
-
315
-
316
- //kpLangExt
317
- let kpLangExt = get(opt, 'kpLangExt', null)
318
-
319
-
320
- //kpLang
321
- let kpLang = procLang({ kpLangExt, webName, webDescription })
322
-
323
-
324
- //srLog
325
- let srLog = srLogInit(opt)
326
-
327
-
328
- //注入函數一律經包裝(W 契約, spec/設計要點與取捨.md ADR-023), 以包裝版取代三個參數
329
- //why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
330
- //部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址.
331
- //包裝後失敗視同否定結果(getUserByToken → null 等同查無, verify* → false 等同無權限), 走各呼叫點既有分支回自家 key;
332
- //上游原文不回前端、不入 log, 只於此記 warn(白名單 key, 或型別與 Error.name). 同一次失敗 log 2~3 筆: 此處 warn(因) + 呼叫點 error(果).
333
- //以下取代後, 本函數內除此三行外不得再直接使用原始參數(盤點見 CLAUDE_rulebook.md「權杖不外洩」, 測試 unit-wrapInjected WRAP-SITE-*)
334
- let logInjectFail = (name) => {
335
- return (d) => {
336
- srLog.warn({ event: `inject-${name}-fail`, ...d })
337
- }
338
- }
339
- getUserByToken = wrapInjected(getUserByToken, { failValue: null, onFail: logInjectFail('getUserByToken') })
340
- verifyClientUser = wrapInjected(verifyClientUser, { failValue: false, onFail: logInjectFail('verifyClientUser') })
341
- verifyAppUser = wrapInjected(verifyAppUser, { failValue: false, onFail: logInjectFail('verifyAppUser') })
342
- //getUserById: 第四個注入函數(選填; useCheckUser=true 時由 WServOrm 於 execute 內呼叫, 原 reject 原文經 pm2resolve 回前端), 有給才包裝;
343
- //失敗 → null 等同查無, 由 WServOrm 既有之查無分支拒絕(fail-closed). 須於 WServOrm 建構前取代(建構時即取走此函數)
344
- if (isfun(getUserById)) {
345
- getUserById = wrapInjected(getUserById, { failValue: null, onFail: logInjectFail('getUserById') })
346
- }
347
-
348
-
349
- //WServOrm
350
- let optWServOrm = {
351
- useCheckUser,
352
- getUserById,
353
- useExcludeWhenNotAdmin,
354
- }
355
- let wp = {}
356
- try {
357
- wp = WServOrm(ds, WOrm, url, db, optWServOrm)
358
- }
359
- catch (err) {
360
- console.log(err)
361
- srLog.error({ event: 'WServOrm-init-error', msg: (err &amp;&amp; err.message) || 'WServOrmInitFailed' })
362
- throw err //fail-fast: ORM 初始化失敗直接終止啟動, 不以 wp={} 繼續組裝出「port 起得來但 woItems/procOrm 為 undefined、CRUD 全 crash」的半死伺服器
363
- }
364
- let { woItems, procOrm } = wp
365
-
366
-
367
- //pmKeyMutex: per-key in-memory mutex, 同 key 序列化、不同 key 並行.
368
- //perm 為「整表批次 diff」寫入, key 粒度為 {op}:{keyTable}, 防同表並行 race (lost update).
369
- let kmx = pmKeyMutex()
370
-
371
-
372
- //lockSave: 後台整表寫入(updateTabItems)之雙擊防護(後端, 見 lockSave.mjs, ADR-025): 以「updateTabItems:{表名}:{操作者 id}」於 cacheSt 原子占位,
373
- //同一操作者之同表寫入處理中再送出即 reject 'saveInProgress'(不排隊); 包在 kmx 之外層, 不同操作者之並行仍由 kmx 序列化(ADR-012)
374
- let cst = cacheSt()
375
- let lockSave = createLockSave(cst)
376
-
377
-
378
- //procCore: 整表批次 diff 寫入邏輯 (updateTabItems + updateTargets/Pemis/Grups/Users)
379
- let pc = procCore(woItems, procOrm, { srLog, kmx, lockSave })
380
-
381
-
382
- //procStaInfor: 統計資訊, fdLog 對齊 srLog 使用同一目錄(讀 settings.logFd, fallback 邏輯同 srLog.mjs); mock 由 settings.staEventMock 注入(e2e 統計圖穩定用)
383
- let logFd = get(opt, 'logFd', '')
384
- if (!isestr(logFd)) {
385
- logFd = './logs'
386
- }
387
- let psi = procStaInfor({ fdLog: logFd, mock: get(opt, 'staEventMock', false), srLog })
388
-
389
-
390
- //getWebInfor
391
- let getWebInfor = (userId) => {
392
-
393
- //info
394
- srLog.info({ event: 'getWebInfor-success', userId })
395
-
396
- return {
397
-
398
- // webName, //已併入kpLang
399
- // webDescription, //已併入kpLang
400
- webLogo,
401
- // urlRedirect, //登入失敗就需要轉址, 故須通過html模板取代提供, 無法用api提供
402
-
403
- showLanguage,
404
- language,
405
- kpLang,
406
-
407
- showModeEditTargets,
408
- showModeEditPemis,
409
- showModeEditGrups,
410
- showModeEditUsers,
411
- modeEditTargets,
412
- modeEditPemis,
413
- modeEditGrups,
414
- modeEditUsers,
415
-
416
- widthTargetId,
417
- widthTargetDescription,
418
- widthPemisName,
419
- widthPemisDescription,
420
- widthGrupsName,
421
- widthGrupsDescription,
422
- widthUsersName,
423
- widthUsersEmail,
424
- widthUsersDescription,
425
-
426
- }
427
- }
428
-
429
-
430
- //checkUser
431
- let checkUser = async(user) => {
432
- let id = get(user, 'id', '')
433
- if (!isestr(id)) {
434
- console.log('user', user)
435
- srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserId' })
436
- return Promise.reject(`cannotGetUserId`)
437
- }
438
- let email = get(user, 'email', '')
439
- if (!isestr(email)) {
440
- console.log('user', user)
441
- srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserEmail' })
442
- return Promise.reject(`cannotGetUserEmail`)
443
- }
444
- let name = get(user, 'name', '')
445
- if (!isestr(name)) {
446
- console.log('user', user)
447
- srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserName' })
448
- return Promise.reject(`cannotGetUserName`)
449
- }
450
- let isAdmin = get(user, 'isAdmin', '')
451
- if (isAdmin !== 'y' &amp;&amp; isAdmin !== 'n') {
452
- console.log('user', user)
453
- console.log('user.isAdmin is not y or n', user.isAdmin)
454
- srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserRole' })
455
- return Promise.reject(`cannotGetUserRole`)
456
- }
457
- return true
458
- }
459
-
460
-
461
- //getTokenUser
462
- let getTokenUser = async(token) => {
463
- //基於token查找使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
464
-
465
- //userSelf
466
- let userSelf = null
467
- if (isestr(token)) {
468
- userSelf = getUserByToken(token)
469
- if (ispm(userSelf)) {
470
- userSelf = await userSelf
471
- }
472
- }
473
- // console.log('userSelf', userSelf)
474
-
475
- //check
476
- if (!iseobj(userSelf)) {
477
- console.log(`token`, maskTok(token))
478
- srLog.error({ event: 'getTokenUser-error', msg: 'cannotFindUserFromToken' })
479
- return Promise.reject(`cannotFindUserFromToken`)
480
- }
481
-
482
- //check userSelf
483
- await checkUser(userSelf)
484
-
485
- //須反查perm內users, 提供正規化屬性
486
- let vSelf = get(userSelf, mappingBy, '')
487
- // console.log('vSelf', vSelf)
488
-
489
- //check
490
- if (!isestr(vSelf)) {
491
- console.log('userSelf', userSelf)
492
- console.log('mappingBy', mappingBy)
493
- srLog.error({ event: 'getTokenUser-error', msg: 'cannotGetUserProp', mappingBy })
494
- return Promise.reject(`cannotGetUserProp`)
495
- }
496
-
497
- //userFind
498
- let userFind = await woItems.users.select({ [mappingBy]: vSelf, isActive: 'y' })
499
- userFind = get(userFind, 0, null)
500
- // console.log('userFind', userFind)
501
-
502
- //check
503
- if (!iseobj(userFind)) {
504
- console.log('userSelf', userSelf)
505
- console.log('mappingBy', mappingBy)
506
- srLog.error({ event: 'getTokenUser-error', msg: 'cannotGetUserFromPerm', mappingBy })
507
- return Promise.reject(`cannotGetUserFromPerm`)
508
- }
509
-
510
- //isAdmin 權威為 perm users 表之欄位值, 不以 token(getUserByToken)宣稱之 isAdmin 複寫。
511
- //(2026-07-11 稽核 A-C1 裁決(b): 原此處有一段恆為 no-op 之「複寫isAdmin」死碼
512
- // (userFind.isAdmin = isAdminSelf, 把自己指派給自己), 與 JSDoc 舊文「輸入'y'時會複寫」互斥;
513
- // 依業主裁決採保守解: 刪除死碼、文件對齊實際行為, 不啟用 token→admin 提權路徑。)
514
-
515
- return userFind
516
- }
517
-
518
-
519
- //getAndVerifyClientUser
520
- let getAndVerifyClientUser = async (token, from = '') => {
521
- //基於token查找client使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
522
-
523
- //getTokenUser
524
- let user = await getTokenUser(token)
525
-
526
- //verifyClientUser
527
- let b = verifyClientUser(user, from)
528
- if (ispm(b)) {
529
- b = await b
530
- }
531
-
532
- //check
533
- if (!b) {
534
- console.log('user', user)
535
- srLog.error({ event: 'getAndVerifyClientUser-error', msg: 'userNoPermission', from })
536
- return Promise.reject(`userNoPermission`)
537
- }
538
-
539
- return user
540
- }
541
-
542
-
543
- //getAndVerifyAppUser
544
- let getAndVerifyAppUser = async (token, from = '') => {
545
- //基於token查找app使用者, 會基於外部getUserByToken進行查找, 故只要getUserByToken提供使用者即可, 可支援getUserByToken給予針對應用程式所產生之虛擬使用者
546
-
547
- //user
548
- let user = null
549
- if (isestr(token)) {
550
- user = getUserByToken(token)
551
- if (ispm(user)) {
552
- user = await user
553
- }
554
- }
555
- // console.log('user', user)
556
-
557
- //check
558
- if (!iseobj(user)) {
559
- console.log(`token`, maskTok(token))
560
- srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'cannotFindUserFromToken' })
561
- return Promise.reject(`cannotFindUserFromToken`)
562
- }
563
-
564
- //verifyAppUser
565
- let b = verifyAppUser(user, from)
566
- if (ispm(b)) {
567
- b = await b
568
- }
569
-
570
- //check
571
- if (!b) {
572
- console.log('user', user)
573
- srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'userNoPermission', from })
574
- return Promise.reject(`userNoPermission`)
575
- }
576
-
577
- return user
578
- }
579
-
580
-
581
- //p
582
- let p = {
583
- //checkToken, 資料通道(/api/main 之 RPC 與 ORM 執行、上傳、下載)之連線驗證, 與 api/getUserByToken(登入)採同一授權強度:
584
- //外部 getUserByToken → 欄位齊備 → 反查 perm users(mappingBy)且 isActive='y' → verifyClientUser(user, from)
585
- //why: 2026-09-07 前僅驗外部 token 欄位齊備(checkUser), 不查 perm users 與 isActive, 亦不經 verifyClientUser,
586
- //致不在 perm users 表之外部使用者、被停用(isActive='n')之使用者仍可經資料通道讀寫全部權限資料(實測 A3/A4, 見 tmp/probe-report.json);
587
- //改為與登入入口對稱, 使 UI 之「是否有效」與部署層 verifyClientUser 政策對資料通道亦生效.
588
- //from 為 w-converhp 之 apiType('main' / 'upload-controller' / 'upload-slice' / 'download-get-filename' / 'download-get-file' / 'download'),
589
- //供部署層 verifyClientUser(user, from) 區分入口.
590
- checkToken: async(token, from = '') => {
591
-
592
- //getAndVerifyClientUser, reject 時 key: cannotFindUserFromToken / cannotGetUserFromPerm / userNoPermission 等
593
- await getAndVerifyClientUser(token, from)
594
-
595
- return true
596
- },
597
- }
598
-
599
-
600
- //parsePayload
601
- let parsePayload = async (req) => {
602
-
603
- //payload
604
- let payload = get(req, 'payload')
605
-
606
- //to obj
607
- if (isestr(payload)) {
608
- payload = j2o(payload)
609
- }
610
-
611
- //check
612
- if (!iseobj(payload)) {
613
- console.log('payload', payload)
614
- srLog.error({ event: 'parsePayload-error', msg: 'invalidPayload' })
615
- return Promise.reject(`invalidPayload`)
616
- }
617
-
618
- //from
619
- let from = get(payload, 'from', '')
620
-
621
- //check
622
- if (!isestr(from)) {
623
- console.log('payload', payload)
624
- srLog.error({ event: 'parsePayload-error', msg: 'invalidFromInPayload' })
625
- return Promise.reject(`invalidFromInPayload`)
626
- }
627
-
628
- //rows
629
- let rows = get(payload, 'rows', [])
630
-
631
- //check
632
- if (!isearr(rows)) {
633
- console.log('payload', payload)
634
- srLog.error({ event: 'parsePayload-error', msg: 'invalidRowsInPayload' })
635
- return Promise.reject(`invalidRowsInPayload`)
636
- }
637
-
638
- //inp
639
- let inp = {
640
- from,
641
- rows,
642
- }
643
-
644
- return inp
645
- }
646
-
647
-
648
- //syncAndReplaceTabs
649
- let syncAndReplaceTabs = async (userId, inp, keyTable) => {
650
-
651
- //序列化同表之並行整表 delAll+insert: 防 lost update (同 keyTable 序列化, 不同表並行)
652
- return await kmx('syncAndReplaceTabs:' + keyTable, async () => {
653
-
654
- //from
655
- let from = get(inp, 'from', '')
656
- // console.log('from', from)
657
-
658
- //check
659
- if (!isestr(from)) {
660
- console.log('inp', inp)
661
- srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidFrom', keyTable })
662
- return Promise.reject(`invalidFrom`)
663
- }
664
-
665
- //rows
666
- let rows = get(inp, 'rows', [])
667
- // console.log(keyTable, rows)
668
-
669
- //check
670
- if (!isearr(rows)) {
671
- console.log('inp', inp)
672
- console.log('keyTable', keyTable)
673
- srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', keyTable })
674
- return Promise.reject(`invalidRows`)
675
- }
676
-
677
- //save from
678
- rows = map(rows, (v, k) => {
679
- // v.order = k + 1 //order大部分由外部給予, 不能複寫
680
- v.from = from
681
- return v
682
- })
683
-
684
- //ltdtmapping
685
- rows = ltdtmapping(rows, ds[keyTable].keys)
686
- // console.log('ltdtmapping rows', rows)
687
-
688
- //delAll from
689
- await woItems[keyTable].delAll({ from })
690
-
691
- //insert
692
- // let r = await woItems[keyTable].insert(rows)
693
- let r = await procOrm(userId, keyTable, 'insert', rows) //須使用procOrm才有辦法自動給予相關欄位
694
- // console.log('r', r)
695
-
696
- return r
697
-
698
- })
699
- }
700
-
701
-
702
- //getGenUserByKV
703
- let getGenUserByKV = async(keyUser, valueUser) => {
704
-
705
- //userFind
706
- let userFind = await woItems.users.select({ [keyUser]: valueUser, isActive: 'y' })
707
- userFind = get(userFind, 0, null)
708
-
709
- //check
710
- if (!iseobj(userFind)) {
711
- return null
712
- }
713
-
714
- return userFind
715
- }
716
-
717
-
718
- //getGenUserByUserId
719
- let getGenUserByUserId = async(userId) => {
720
-
721
- //userFind
722
- let userFind = await getGenUserByKV('id', userId)
723
-
724
- //check
725
- if (!iseobj(userFind)) {
726
- return null
727
- }
728
-
729
- return userFind
730
- }
731
-
732
-
733
- //getGenUserAndRulesByUserId
734
- let getGenUserAndRulesByUserId = async (userId) => {
735
-
736
- //userFind
737
- let userFind = await getGenUserByUserId(userId)
738
- // console.log('userFind', userFind)
739
-
740
- //grups
741
- let grups = await woItems.grups.select()
742
- // console.log('grups', grups)
743
-
744
- //pemis
745
- let pemis = await woItems.pemis.select()
746
- // console.log('pemis', pemis)
747
-
748
- //targets
749
- let targets = await woItems.targets.select()
750
- // console.log('targets', targets)
751
-
752
- //getUserRules
753
- let kur = getUserRules(userFind, grups, pemis, targets)
754
- // console.log('getUserRules', kur)
755
-
756
- //check
757
- if (!iseobj(kur)) {
758
- console.log('userId', userId)
759
- srLog.error({ event: 'getGenUserAndRulesByUserId-error', msg: 'cannotGetUserRules', userId })
760
- return Promise.reject(`cannotGetUserRules`)
761
- }
762
-
763
- //grupsNames
764
- let grupsNames = map(get(kur, 'grups', []), 'name') //kur.grups僅會提供isActive='y', 故grupsNames為有效權限群組名稱
765
- grupsNames = join(grupsNames, ';')
766
-
767
- return {
768
- user: {
769
- ...userFind,
770
- grupsNames,
771
- },
772
- // grups: get(kur, 'grups', []),
773
- rules: get(kur, 'rules', []),
774
- }
775
- }
776
-
777
-
778
- //pathStaticFiles
779
- let pathStaticFiles = 'dist'
780
- let npmPathStaticFiles = './node_modules/w-web-perm/dist'
781
- if (fsIsFolder(npmPathStaticFiles)) {
782
- pathStaticFiles = npmPathStaticFiles
783
- }
784
- // console.log('pathStaticFiles', pathStaticFiles)
785
-
786
-
787
- //subfolder
788
- let fnEntryIn = 'index.tmp'
789
- let fnEntryOut = 'index.html'
790
- try {
791
- let fpEntryIn = path.resolve(pathStaticFiles, fnEntryIn)
792
- if (!fsIsFile(fpEntryIn)) {
793
- fpEntryIn = path.resolve(pathStaticFiles, fnEntryOut) //本機開發另使用html替代tmp
794
- }
795
- if (!fsIsFile(fpEntryIn)) {
796
- console.log('fpEntryIn', fpEntryIn)
797
- throw new Error(`invalid fpEntryIn`)
798
- }
799
- let fpEntryOut = path.resolve(pathStaticFiles, fnEntryOut)
800
- let c = fs.readFileSync(fpEntryIn, 'utf8')
801
- c = replace(c, '/mperm/', '{sfd}/') //方法同genEntry
802
- c = replace(c, '{sfd}', subfolder)
803
- c = replace(c, '{urlRedirect}', urlRedirect)
804
- c = replace(c, '{language}', language)
805
- fs.writeFileSync(fpEntryOut, c, 'utf8')
806
- }
807
- catch (err) {
808
- console.log(err)
809
- console.log(`can not generate ${fnEntryOut}`)
810
- }
811
-
812
-
813
- //apis
814
- let apis = [
815
-
816
- {
817
- method: 'GET',
818
- path: '/api/getUserByToken', //未登入主頁時需先檢測token, getUserByToken為w-ui-loginout預設值, 若要更改兩邊須同時修改
819
- handler: async function (req, res) {
820
- // console.log('getUserByToken', req)
821
-
822
- async function core() {
823
- //提供對象為browser使用者, 須先檢測token是否有效, 確認後回傳該使用者之資訊物件
824
-
825
- //token
826
- let token = get(req, 'query.token', '')
827
- // console.log('token', token)
828
-
829
- //check
830
- if (!isestr(token)) {
831
- console.log('req.query', maskQuery(get(req, 'query')))
832
- console.log('token', maskTok(token))
833
- console.log('[API]getUserByToken/check token: invalid token')
834
- srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
835
- return Promise.reject(`tokenNoPermission`)
836
- }
837
-
838
- //getAndVerifyClientUser
839
- let user = await getAndVerifyClientUser(token, 'getUserByToken')
840
- // console.log('user', user)
841
-
842
- //check
843
- if (!iseobj(user)) {
844
- console.log('token', maskTok(token))
845
- console.log('[API]getUserByToken/check user: invalid user')
846
- srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
847
- return Promise.reject(`tokenNoPermission`)
848
- }
849
-
850
- //info
851
- srLog.info({ event: 'api/getUserByToken-success', userId: get(user, 'id', '') })
852
-
853
- return user
854
- }
855
-
856
- //pm2resolve core
857
- let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 須另提取state進行判斷
858
- if (isErr(r.msg)) {
859
- r.msg = r.msg.message
860
- }
861
- // console.log('getUserByToken', r)
862
-
863
- return r
864
- },
865
- },
866
- {
867
- method: 'GET',
868
- path: '/api/getPerm',
869
- handler: async function (req, res) {
870
- // console.log('getPerm', req)
871
-
872
- async function core() {
873
- //提供對象為node與browser端, 須為client使用者, 須先檢測token是否有效, 再依照token取得使用者資訊物件, 再由其userId查詢回傳該使用者之權限資訊物件
874
-
875
- //token
876
- let token = get(req, 'query.token', '')
877
- // console.log('token', token)
878
-
879
- //check
880
- if (!isestr(token)) {
881
- console.log('req.query', maskQuery(get(req, 'query')))
882
- console.log('token', maskTok(token))
883
- console.log('[API]getPerm/check token: invalid token')
884
- srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
885
- return Promise.reject(`tokenNoPermission`)
886
- }
887
-
888
- //getAndVerifyClientUser, 發起token的所屬使用者, 驗證僅對client使用者, 不得使用app使用者
889
- let userSelf = await getAndVerifyClientUser(token, 'getPerm')
890
- // console.log('userSelf', userSelf)
891
-
892
- //check
893
- if (!iseobj(userSelf)) {
894
- console.log('token', maskTok(token))
895
- console.log('[API]getPerm/check userSelf: invalid userSelf')
896
- srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
897
- return Promise.reject(`tokenNoPermission`)
898
- }
899
-
900
- //userIdSelf, client使用者的userId
901
- let userIdSelf = get(userSelf, 'id', '')
902
- // console.log('userIdSelf', userIdSelf)
903
-
904
- //check
905
- if (!isestr(userIdSelf)) {
906
- console.log('token', maskTok(token))
907
- console.log('userSelf', userSelf)
908
- console.log('[API]getPerm/check userIdSelf: invalid userIdSelf')
909
- srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
910
- return Promise.reject(`tokenNoPermission`)
911
- }
912
-
913
- //getGenUserAndRulesByUserId, 查找到的client使用者與權限
914
- let userWithRulesSelf = await getGenUserAndRulesByUserId(userIdSelf)
915
- // console.log('getGenUserAndRulesByUserId', r)
916
-
917
- //check
918
- if (!iseobj(userWithRulesSelf)) {
919
- console.log('token', maskTok(token))
920
- console.log('userSelf', userSelf)
921
- console.log('userIdSelf', userIdSelf)
922
- console.log('[API]getPerm/check userWithRulesSelf: invalid userWithRulesSelf')
923
- srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
924
- return Promise.reject(`tokenNoPermission`)
925
- }
926
-
927
- //info
928
- srLog.info({ event: 'api/getPerm-success', userId: userIdSelf })
929
-
930
- return userWithRulesSelf
931
- }
932
-
933
- //pm2resolve core
934
- let r = await pm2resolve(core)()
935
- if (isErr(r.msg)) {
936
- r.msg = r.msg.message
937
- }
938
- // console.log('getPerm r', r)
939
-
940
- return r
941
- },
942
- },
943
- {
944
- method: 'GET',
945
- path: '/api/getPermUserInfor',
946
- handler: async function (req, res) {
947
- // console.log('getPermUserInfor', req)
948
-
949
- async function core() {
950
- //提供對象為node端, 須為app使用者, 須先檢測token是否有效, 再由其userId查詢回傳該使用者權限資訊物件
951
-
952
- //tokenSelf
953
- let tokenSelf = get(req, 'query.token', '')
954
- // console.log('token', token)
955
-
956
- //check
957
- if (!isestr(tokenSelf)) {
958
- console.log('req.query', maskQuery(get(req, 'query')))
959
- console.log('[API]getPermUserInfor/check tokenSelf: invalid tokenSelf')
960
- srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
961
- return Promise.reject(`tokenNoPermission`)
962
- }
963
-
964
- //getAndVerifyAppUser, 發起token的所屬使用者, 驗證須為app使用者
965
- let userSelf = await getAndVerifyAppUser(tokenSelf, 'getPermUserInfor')
966
- // console.log('userSelf', userSelf)
967
-
968
- //check
969
- if (!iseobj(userSelf)) {
970
- console.log('req.query', maskQuery(get(req, 'query')))
971
- console.log('tokenSelf', maskTok(tokenSelf))
972
- console.log('[API]getPermUserInfor/check userSelf: invalid userSelf')
973
- srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
974
- return Promise.reject(`tokenNoPermission`)
975
- }
976
-
977
- //userIdFind, 欲查找使用者的userId
978
- let userIdFind = get(req, 'query.userId', '')
979
- // console.log('userIdFind', userIdFind)
980
-
981
- //check
982
- if (!isestr(userIdFind)) {
983
- console.log('req.query', maskQuery(get(req, 'query')))
984
- console.log('tokenSelf', maskTok(tokenSelf))
985
- console.log('[API]getPermUserInfor/check userIdFind: invalid userIdFind')
986
- srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
987
- return Promise.reject(`tokenNoPermission`)
988
- }
989
-
990
- //getGenUserAndRulesByUserId, 查找到的使用者與權限
991
- let userWithRulesFind = await getGenUserAndRulesByUserId(userIdFind)
992
- // console.log('getGenUserAndRulesByUserId', userWithRulesFind)
993
-
994
- //check
995
- if (!iseobj(userWithRulesFind)) {
996
- console.log('userIdFind', userIdFind)
997
- console.log('[API]getPermUserInfor/check userWithRulesFind: invalid userWithRulesFind')
998
- srLog.error({ event: 'api/getPermUserInfor-error', msg: 'userIdNoPermrules' })
999
- return Promise.reject(`userIdNoPermrules`)
1000
- }
1001
-
1002
- //info
1003
- srLog.info({ event: 'api/getPermUserInfor-success', userId: userIdFind })
1004
-
1005
- return userWithRulesFind
1006
- }
1007
-
1008
- //pm2resolve core
1009
- let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 須另提取state進行判斷
1010
- if (isErr(r.msg)) {
1011
- r.msg = r.msg.message
1012
- }
1013
- // console.log('getPermUserInfor', r)
1014
-
1015
- return r
1016
- },
1017
- },
1018
- {
1019
- method: 'POST',
1020
- path: '/syncAndReplaceTabs',
1021
- handler: async function (req, res) {
1022
- // console.log('syncAndReplaceTabs', req)
1023
- // console.log('syncAndReplaceTabs payload', req.payload)
1024
-
1025
- async function core() {
1026
- //提供對象為node使用者, 須為系統管理者, 須先檢測token是否有效, 再進行數據變更
1027
-
1028
- //token
1029
- let token = get(req, 'query.token', '')
1030
-
1031
- //check
1032
- if (!isestr(token)) {
1033
- console.log('req.query', maskQuery(get(req, 'query')))
1034
- console.log('token', maskTok(token))
1035
- console.log('[API]syncAndReplaceTabs/check token: invalid token')
1036
- srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1037
- return Promise.reject(`tokenNoPermission`)
1038
- }
1039
-
1040
- //keyTable
1041
- let keyTable = get(req, 'query.keyTable', '')
1042
-
1043
- //check
1044
- if (!isestr(keyTable)) {
1045
- console.log('req.query', maskQuery(get(req, 'query')))
1046
- console.log('keyTable', keyTable)
1047
- console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
1048
- srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1049
- return Promise.reject(`tokenNoPermission`)
1050
- }
1051
-
1052
- //check keyTable
1053
- let allowedTables = ['targets', 'pemis', 'grups', 'users']
1054
- if (!allowedTables.includes(keyTable)) {
1055
- console.log('[API]syncAndReplaceTabs/check keyTable: unexpected keyTable', keyTable)
1056
- srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'invalidKeyTable', keyTable })
1057
- return Promise.reject(`invalidKeyTable`)
1058
- }
1059
-
1060
- //getAndVerifyAppUser
1061
- let user = await getAndVerifyAppUser(token, 'syncAndReplaceTabs')
1062
-
1063
- //check user
1064
- if (!iseobj(user)) {
1065
- console.log('token', maskTok(token))
1066
- console.log('[API]syncAndReplaceTabs/check user: invalid user')
1067
- srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1068
- return Promise.reject(`tokenNoPermission`)
1069
- }
1070
-
1071
- //userId
1072
- let userId = get(user, 'id', '')
1073
-
1074
- //check userId
1075
- if (!isestr(userId)) {
1076
- console.log('token', maskTok(token))
1077
- console.log('user', user)
1078
- console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
1079
- srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1080
- return Promise.reject(`tokenNoPermission`)
1081
- }
1082
-
1083
- //parsePayload
1084
- let inp = await parsePayload(req)
1085
-
1086
- //syncAndReplaceTabs
1087
- let r = await syncAndReplaceTabs(userId, inp, keyTable)
1088
-
1089
- //info
1090
- srLog.info({ event: 'api/syncAndReplaceTabs-success', userId, keyTable })
1091
-
1092
- return r
1093
- }
1094
-
1095
- //pm2resolve core
1096
- let r = await pm2resolve(core)()
1097
- if (isErr(r.msg)) {
1098
- r.msg = r.msg.message
1099
- }
1100
- // console.log('syncAndReplaceTabs', r)
1101
-
1102
- return r
1103
- },
1104
- },
1105
- ]
1106
-
1107
-
1108
- //tableNamesExec, tableNamesSync
1109
- let tableNamesExec = keys(ds)
1110
- // let tableNamesSync = filter(tableNamesExec, (v) => {
1111
- // return strright(v, 5) !== 'Items'//不同步數據
1112
- // })
1113
- let tableNamesSync = tableNamesExec
1114
-
1115
-
1116
- //WServHapiServer
1117
- instWServHapiServer = new WServHapiServer({
1118
- port: serverPort,
1119
- pathStaticFiles,
1120
- apiName: 'api',
1121
- apis,
1122
- tokenType: 'Bearer',
1123
- verifyConn: async ({ apiType, authorization, query, headers, req }) => {
1124
- // console.log('verifyConn', `apiType[${apiType}]`, `authorization[${authorization}]`)
1125
-
1126
- //token
1127
- let token = strdelleft(authorization, 7) //刪除Bearer
1128
- // console.log('token', token)
1129
-
1130
- //check
1131
- if (!isestr(token)) {
1132
- console.log('apiType', apiType)
1133
- console.log('authorization', maskTok(authorization))
1134
- console.log('invalid token')
1135
- srLog.error({ event: 'verifyConn-error', ok: false, apiType, msg: 'tokenNoPermission' }) //認證失敗須入 log 供稽核/統計(CLAUDE.md「log 記錄要點」; 同 w-web-api、w-web-task); 只記 key, 不記 authorization 原值
1136
- return false
1137
- }
1138
-
1139
- //checkToken, 失敗一律回 false(傳輸層回 permission denied); reject 之 err key 記於 verifyConn log 之 msg
1140
- let b = false
1141
- let msg = ''
1142
- await p.checkToken(token, apiType)
1143
- .then(() => {
1144
- b = true
1145
- })
1146
- .catch((err) => {
1147
- msg = (typeof err === 'string') ? err : ((err &amp;&amp; err.message) || 'checkTokenFail')
1148
- })
1149
- // console.log('b', b)
1150
-
1151
- //ip, 對齊sso慣例x-forwarded-for取第1個, fallback到req remoteAddress, 取不到則略過
1152
- let ip = get(headers, 'x-forwarded-for', '').split(',')[0].trim()
1153
- if (!isestr(ip)) {
1154
- ip = get(req, 'info.remoteAddress', '') || get(req, 'socket.remoteAddress', '') || get(req, 'connection.remoteAddress', '')
1155
- }
1156
-
1157
- //origin
1158
- let origin = get(headers, 'origin', '')
1159
-
1160
- //referer, 只記 origin + pathname(ADR-023): 前端以 ?token= 開站(w-ui-loginout 讀完不清網址), 同源請求之 Referer 預設帶完整網址, 原樣記錄即把使用者權杖寫入 srLog
1161
- let referer = maskUrl(get(headers, 'referer', ''))
1162
-
1163
- //info, 失敗時 msg 為 err key(cannotFindUserFromToken / cannotGetUserFromPerm / userNoPermission 等), 供資安稽核與統計
1164
- if (b) {
1165
- srLog.info({ event: 'verifyConn', ok: b, apiType, ip, origin, referer })
1166
- }
1167
- else {
1168
- srLog.error({ event: 'verifyConn-error', ok: b, apiType, ip, origin, referer, msg })
1169
- }
1170
-
1171
- return b
1172
- },
1173
- getUserIdByToken: async (token) => { //可使用async或sync函數
1174
- // console.log('getUserIdByToken', token)
1175
- let user = await getUserByToken(token)
1176
- let userId = get(user, 'id', '')
1177
- if (!isestr(userId)) {
1178
- console.log('token', maskTok(token))
1179
- console.log('userId', userId)
1180
- srLog.error({ event: 'getUserIdByToken-error', msg: 'cannotFindUserId' })
1181
- return Promise.reject(`cannotFindUserId`)
1182
- }
1183
- return userId
1184
- },
1185
- corsOrigins: ['*'],
1186
- useDbOrm: true,
1187
- kpOrm: woItems,
1188
- operOrm: procOrm, //procOrm的輸入為: userId, tableName, methodName, input
1189
- tableNamesExec,
1190
- methodsExec: ['select', 'insert', 'save', 'del', 'delAll'], //mix需於procOrm內註冊以提供
1191
- tableNamesSync,
1192
- kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數
1193
-
1194
- getWebInfor,
1195
-
1196
- updateTargets: pc.updateTargets,
1197
- updatePemis: pc.updatePemis,
1198
- updateGrups: pc.updateGrups,
1199
- //updateUsers, modeEditUsers='for:grups' 時 API 與 UI 一致只採納既有使用者之 cgrups 變更, 身分欄/新增/刪除/順序以資料庫為準
1200
- updateUsers: (userId, rows) => {
1201
- let opt = (modeEditUsers === 'for:grups') ? { pickKeysOnly: ['cgrups'] } : {}
1202
- return pc.updateUsers(userId, rows, opt)
1203
- },
1204
-
1205
- getStaEvent: async (userId, timeLength, timeInterval) => {
1206
- try {
1207
- let rs = await psi.getStaEvent(userId, timeLength, timeInterval)
1208
- srLog.info({ event: 'getStaEvent-success', userId })
1209
- return rs
1210
- }
1211
- catch (err) {
1212
- srLog.error({ event: 'getStaEvent-error', userId, msg: (typeof err === 'string') ? err : ((err &amp;&amp; err.message) || 'cannotGetStaEvent') })
1213
- throw err
1214
- }
1215
- },
1216
-
1217
- getStaEventTable: async (userId) => {
1218
- try {
1219
- let rs = await psi.getStaEventTable(userId)
1220
- srLog.info({ event: 'getStaEventTable-success', userId })
1221
- return rs
1222
- }
1223
- catch (err) {
1224
- srLog.error({ event: 'getStaEventTable-error', userId, msg: (typeof err === 'string') ? err : ((err &amp;&amp; err.message) || 'cannotGetStaEventTable') })
1225
- throw err
1226
- }
1227
- },
1228
-
1229
- },
1230
- //fpTableTags: 各資料表時間戳檔案(供前端判定是否需重新同步)。
1231
- //w-serv-hapi 1.0.87 起之正式名稱; 舊名 fnTableTags 於該版之前傳下去等同未給(w-serv-webdata 只讀 fpTableTags),
1232
- //故 1.0.86 及之前實際寫的是預設之 ./tableTags.json, 升版後改用本檔名。
1233
- fpTableTags: 'tableTags-web-perm.json',
1234
- })
1235
-
1236
-
1237
- return instWServHapiServer
1238
- }
1239
-
1240
-
1241
- export default WWebPerm
48
+ <pre class="prettyprint source linenums"><code>import path from 'path'
49
+ import fs from 'fs'
50
+ import get from 'lodash-es/get.js'
51
+ import map from 'lodash-es/map.js'
52
+ import keys from 'lodash-es/keys.js'
53
+ import join from 'lodash-es/join.js'
54
+ import iseobj from 'wsemi/src/iseobj.mjs'
55
+ import isestr from 'wsemi/src/isestr.mjs'
56
+ import ispint from 'wsemi/src/ispint.mjs'
57
+ import isearr from 'wsemi/src/isearr.mjs'
58
+ import isErr from 'wsemi/src/isErr.mjs'
59
+ import isfun from 'wsemi/src/isfun.mjs'
60
+ import ispm from 'wsemi/src/ispm.mjs'
61
+ import cint from 'wsemi/src/cint.mjs'
62
+ import j2o from 'wsemi/src/j2o.mjs'
63
+ import strleft from 'wsemi/src/strleft.mjs'
64
+ import strright from 'wsemi/src/strright.mjs'
65
+ import strdelright from 'wsemi/src/strdelright.mjs'
66
+ import pm2resolve from 'wsemi/src/pm2resolve.mjs'
67
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
68
+ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
69
+ import replace from 'wsemi/src/replace.mjs'
70
+ import strdelleft from 'wsemi/src/strdelleft.mjs'
71
+ import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
72
+ import pmKeyMutex from 'wsemi/src/pmKeyMutex.mjs'
73
+ import cacheSt from 'wsemi/src/cacheSt.mjs'
74
+ import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
75
+ import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
76
+ import checkOptRemoved from 'w-serv-orm/src/checkOptRemoved.mjs'
77
+ import ds from '../src/schema/index.mjs'
78
+ import { getUserRules } from '../src/plugins/mShare.mjs'
79
+ import procCore, { toOutKey } from './procCore.mjs'
80
+ import createLockSave from './lockSave.mjs'
81
+ import procLang from './procLang.mjs'
82
+ import srLogInit from './srLog.mjs'
83
+ import procStaInfor from './procStaInfor.mjs'
84
+ import { maskTok, maskQuery, maskUrl } from './maskLog.mjs'
85
+ import wrapInjected from './wrapInjected.mjs'
86
+ import validateRowsBeforeWrite from './validateRows.mjs'
87
+ import { createAuthorizeExec, wrapOperOrm } from './dataChannel.mjs'
88
+
89
+
90
+ /**
91
+ * 權限伺服器
92
+ *
93
+ * @class
94
+ * @param {Function} WOrm 輸入資料庫ORM函數
95
+ * @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
96
+ * @param {String} db 輸入資料庫名稱字串
97
+ * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫。函數拋錯或reject時視同查無使用者(回自家錯誤key,錯誤原文不回前端、不入log,只於srLog記warn)
98
+ * @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
99
+ * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false;函數拋錯或reject時視同回傳false
100
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
101
+ * @param {Integer} [opt.serverPort=11006] 輸入伺服器通訊port,預設11006。註:原opt.useCheckUser、opt.getUserById、opt.useExcludeWhenNotAdmin已隨w-serv-orm 1.1.9移除,給予undefined、null、false以外之值時建構即拋錯(錯誤訊息出自w-serv-orm之checkOptRemoved,並記srLog);資料通道之使用者授權由verifyConn(verifyClientUser)負責、表操作只開放select,刪除該三鍵即可,無須替代設定
102
+ * @param {Object} [opt.webName={}] 輸入站台名稱物件,至少包含語系eng與cht鍵的名稱,預設{}
103
+ * @param {Object} [opt.webDescription={}] 輸入站台描述物件,至少包含語系eng與cht鍵的名稱,預設{}
104
+ * @param {String} [opt.webLogo=''] 輸入站台logo字串,採base64格式,預設''
105
+ * @param {String} [opt.subfolder=''] 輸入站台所在子目錄字串,提供站台位於內網採反向代理進行服務時,故需支援位於子目錄情形,預設''
106
+ * @param {String} [opt.urlRedirect=''] 輸入錯誤時自動轉址字串,提供站台例如無法登入或驗證失敗時須自動轉址,預設''
107
+ * @param {String} [opt.mappingBy='id'] 輸入外部系統識別使用者token後所提供之資料物件,與權限系統之使用者資料物件,兩者間查找之對應欄位,可選'id'、'email'、'name',未給或非此三者時為'id',預設'id'
108
+ * @param {String} [opt.logFd='./logs'] 輸入log紀錄儲存位置字串,預設'./logs'
109
+ * @param {String} [opt.logInterval='hr'] 輸入log紀錄檔案拆檔時長字串,可選'hr'或'day',預設'hr'
110
+ * @param {Integer} [opt.logNumKeep=null] 輸入log檔保留數上限正整數,超出者自最舊刪除,未給採w-syslog預設(hr模式365*24、day模式365),預設null
111
+ * @returns {Object} 回傳w-serv-hapi之WServHapiServer事件物件,可監聽error事件;另提供ready函數(回傳promise,啟動成功resolve為hapi伺服器實體,失敗如埠被占用時reject)、stop函數(釋放資料同步等下游資源後停止伺服器)、getServer函數與getInstWConverServer函數。資料通道之表操作只開放select(新增、修改、刪除一律經updateTargets等函數);設定錯誤(含已移除之opt鍵)於建構時同步拋出並記srLog
112
+ * @example
113
+ *
114
+ * import WOrm from 'w-orm-lmdb/src/WOrmLmdb.mjs'
115
+ * import WWebPerm from './server/WWebPerm.mjs'
116
+ * import getSettings from './g_getSettings.mjs'
117
+ *
118
+ *
119
+ * //st
120
+ * let st = getSettings()
121
+ *
122
+ * let url = st.dbUrl
123
+ * let db = st.dbName
124
+ * let opt = {
125
+ *
126
+ * serverPort: 11006,
127
+ * subfolder: '', //mperm
128
+ * urlRedirect: 'https://www.google.com/', //本機測試時得先編譯, 再瀏覽: http://localhost:11006/
129
+ *
130
+ * webName: {
131
+ * 'eng': 'Permission Service',
132
+ * 'cht': '權限管理系統',
133
+ * },
134
+ * webDescription: {
135
+ * 'eng': 'A web service package for user permissions and management targets.',
136
+ * 'cht': 'A web service package for user permissions and management targets.',
137
+ * },
138
+ * webLogo: 'data:image/svg+xml;base64,...',
139
+ *
140
+ * }
141
+ *
142
+ * let getUserByToken = async (token) => {
143
+ * // return {} //測試無法登入
144
+ * if (token === '{token-for-application}') { //提供外部應用系統作為存取使用者
145
+ * return {
146
+ * id: 'id-for-application',
147
+ * name: 'application',
148
+ * email: 'application@example.com',
149
+ * isAdmin: 'y',
150
+ * }
151
+ * }
152
+ * if (token === 'sys') { //開發階段w-ui-loginout自動給予browser使用者(且位於localhost)的token為sys
153
+ * return {
154
+ * id: 'id-for-admin',
155
+ * name: 'tester',
156
+ * email: 'admin@example.com', //mappingBy為email, 開發階段時會使用email找到所建置之使用者資料
157
+ * isAdmin: 'y',
158
+ * }
159
+ * }
160
+ * console.log('invalid token') //勿印權杖值
161
+ * console.log('於生產環境時得加入SSO等驗證token機制')
162
+ * return {}
163
+ * }
164
+ *
165
+ * let verifyClientUser = (user, from) => {
166
+ * console.log('verifyClientUser/user', user)
167
+ * // return false //測試無法登入
168
+ * console.log('於生產環境時得加入限制瀏覽器使用者身份機制')
169
+ * return user.isAdmin === 'y' //測試僅系統管理者使用
170
+ * }
171
+ *
172
+ * let verifyAppUser = (user, from) => {
173
+ * console.log('verifyAppUser/user', user)
174
+ * // return false //測試無法登入
175
+ * console.log('於生產環境時得加入限制應用程式使用者身份機制')
176
+ * return user.isAdmin === 'y' //測試僅系統管理者使用
177
+ * }
178
+ *
179
+ * //WWebPerm
180
+ * let instWWebPerm = WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt)
181
+ *
182
+ * instWWebPerm.on('error', (err) => {
183
+ * console.log(err)
184
+ * })
185
+ *
186
+ */
187
+ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt = {}) {
188
+ let instWServHapiServer = null
189
+
190
+
191
+ //check WOrm
192
+ if (!isfun(WOrm)) {
193
+ console.log('invalid WOrm', WOrm)
194
+ throw new Error('invalid WOrm')
195
+ }
196
+
197
+
198
+ //check url
199
+ if (!isestr(url)) {
200
+ console.log('invalid url', url)
201
+ throw new Error('invalid url')
202
+ }
203
+
204
+
205
+ //check db
206
+ if (!isestr(db)) {
207
+ console.log('invalid db', db)
208
+ throw new Error('invalid db')
209
+ }
210
+
211
+
212
+ //check getUserByToken
213
+ if (!isfun(getUserByToken)) {
214
+ console.log('invalid getUserByToken', getUserByToken)
215
+ throw new Error('invalid getUserByToken')
216
+ }
217
+
218
+
219
+ //check verifyClientUser
220
+ if (!isfun(verifyClientUser)) {
221
+ console.log('invalid verifyClientUser', verifyClientUser)
222
+ throw new Error('invalid verifyClientUser')
223
+ }
224
+
225
+
226
+ //check verifyAppUser
227
+ if (!isfun(verifyAppUser)) {
228
+ console.log('invalid verifyAppUser', verifyAppUser)
229
+ throw new Error('invalid verifyAppUser')
230
+ }
231
+
232
+
233
+ //serverPort
234
+ let serverPort = get(opt, 'serverPort')
235
+ if (!ispint(serverPort)) {
236
+ serverPort = 11006
237
+ }
238
+ serverPort = cint(serverPort)
239
+
240
+
241
+ //webName
242
+ let webName = get(opt, 'webName', {})
243
+
244
+
245
+ //webDescription
246
+ let webDescription = get(opt, 'webDescription', {})
247
+
248
+
249
+ //webLogo
250
+ let webLogo = get(opt, 'webLogo', '')
251
+
252
+
253
+ //subfolder
254
+ let subfolder = get(opt, 'subfolder', '')
255
+ if (isestr(subfolder)) {
256
+ if (strright(subfolder, 1) === '/') { //右邊不需要給「/」
257
+ subfolder = strdelright(subfolder, 1)
258
+ }
259
+ if (strleft(subfolder, 1) !== '/') { //左邊需要給「/」
260
+ subfolder = `/${subfolder}`
261
+ }
262
+ }
263
+
264
+
265
+ //urlRedirect
266
+ let urlRedirect = get(opt, 'urlRedirect', '')
267
+ // console.log('urlRedirect', urlRedirect)
268
+
269
+
270
+ //params
271
+ let showLanguage = get(opt, 'showLanguage', 'n')
272
+ let language = get(opt, 'language', 'eng')
273
+ let showModeEditTargets = get(opt, 'showModeEditTargets', 'n')
274
+ let showModeEditPemis = get(opt, 'showModeEditPemis', 'n')
275
+ let showModeEditGrups = get(opt, 'showModeEditGrups', 'n')
276
+ let showModeEditUsers = get(opt, 'showModeEditUsers', 'n')
277
+ let modeEditTargets = get(opt, 'modeEditTargets', 'n')
278
+ let modeEditPemis = get(opt, 'modeEditPemis', 'n')
279
+ let modeEditGrups = get(opt, 'modeEditGrups', 'n')
280
+ let modeEditUsers = get(opt, 'modeEditUsers', 'n')
281
+ let widthTargetId = get(opt, 'widthTargetId', '')
282
+ let widthTargetDescription = get(opt, 'widthTargetDescription', '')
283
+ let widthPemisName = get(opt, 'widthPemisName', '')
284
+ let widthPemisDescription = get(opt, 'widthPemisDescription', '')
285
+ let widthGrupsName = get(opt, 'widthGrupsName', '')
286
+ let widthGrupsDescription = get(opt, 'widthGrupsDescription', '')
287
+ let widthUsersName = get(opt, 'widthUsersName', '')
288
+ let widthUsersEmail = get(opt, 'widthUsersEmail', '')
289
+ let widthUsersDescription = get(opt, 'widthUsersDescription', '')
290
+
291
+
292
+ //mappingBy
293
+ let mappingBy = get(opt, 'mappingBy', '')
294
+ if (mappingBy !== 'id' &amp;&amp; mappingBy !== 'email' &amp;&amp; mappingBy !== 'name') {
295
+ mappingBy = 'id'
296
+ }
297
+ // console.log('mappingBy', mappingBy)
298
+
299
+
300
+ //kpLangExt
301
+ let kpLangExt = get(opt, 'kpLangExt', null)
302
+
303
+
304
+ //kpLang
305
+ let kpLang = procLang({ kpLangExt, webName, webDescription })
306
+
307
+
308
+ //srLog
309
+ let srLog = srLogInit(opt)
310
+
311
+
312
+ //w-serv-orm 1.1.9 已移除之設定鍵(useCheckUser / getUserById / useExcludeWhenNotAdmin): 給予 undefined、null、false 以外之值即拋錯(fail-fast, ADR-026; 規則帳本 R-ORM-03),
313
+ //判準與訊息單一來源在 w-serv-orm 之 checkOptRemoved; 於 srLogInit 之後檢查以便記 key, 並先於 WServOrm 建構(不開啟資料庫).
314
+ //why fail-fast(而非 w-web-sso ADR-083 之忽略+警告): 本套件過去把三鍵透傳給 w-serv-orm 且實際生效(含依使用者過濾 select 結果),
315
+ //靜默忽略會無聲失去部署方以為仍在之可見範圍保證; sso 過去即已忽略三鍵, 其「忽略並警告」係延續自身歷史, 兩者不必一致.
316
+ try {
317
+ checkOptRemoved(opt)
318
+ }
319
+ catch (err) {
320
+ srLog.error({ event: 'WWebPerm-init-error', msg: err.message })
321
+ throw err
322
+ }
323
+
324
+
325
+ //注入函數一律經包裝(W 契約, spec/設計要點與取捨.md ADR-023), 以包裝版取代三個參數
326
+ //why: 注入函數拋錯或 reject 時原會被 await 原樣上拋, 其原文經 pm2resolve 回前端、經 verifyConn 之 msg 入 srLog、經 error 事件印出;
327
+ //部署方常以 w-web-sso 對外 helper(≤1.1.4)包裝 getUserByToken, 其 reject 字串含已代入系統介接權杖與使用者權杖之完整網址.
328
+ //包裝後失敗視同否定結果(getUserByToken → null 等同查無, verify* → false 等同無權限), 走各呼叫點既有分支回自家 key;
329
+ //上游原文不回前端、不入 log, 只於此記 warn(白名單 key, 或型別與 Error.name). 同一次失敗 log 2~3 筆: 此處 warn(因) + 呼叫點 error(果).
330
+ //以下取代後, 本函數內除此三行外不得再直接使用原始參數(盤點見 CLAUDE_rulebook.md「權杖不外洩」, 測試 unit-wrapInjected WRAP-SITE-*)
331
+ let logInjectFail = (name) => {
332
+ return (d) => {
333
+ srLog.warn({ event: `inject-${name}-fail`, ...d })
334
+ }
335
+ }
336
+ getUserByToken = wrapInjected(getUserByToken, { failValue: null, onFail: logInjectFail('getUserByToken') })
337
+ verifyClientUser = wrapInjected(verifyClientUser, { failValue: false, onFail: logInjectFail('verifyClientUser') })
338
+ verifyAppUser = wrapInjected(verifyAppUser, { failValue: false, onFail: logInjectFail('verifyAppUser') })
339
+
340
+
341
+ //WServOrm, w-serv-orm 1.1.10 起無設定鍵(不帶 opt)
342
+ let wp = {}
343
+ try {
344
+ wp = WServOrm(ds, WOrm, url, db)
345
+ }
346
+ catch (err) {
347
+ console.log(err)
348
+ srLog.error({ event: 'WServOrm-init-error', msg: (err &amp;&amp; err.message) || 'WServOrmInitFailed' })
349
+ throw err //fail-fast: ORM 初始化失敗直接終止啟動, 不以 wp={} 繼續組裝出「port 起得來但 woItems/procOrm 為 undefined、CRUD 全 crash」的半死伺服器
350
+ }
351
+ let { woItems, procOrm } = wp
352
+
353
+
354
+ //pmKeyMutex: per-key in-memory mutex, 同 key 序列化、不同 key 並行.
355
+ //perm 為「整表批次 diff」寫入, key 粒度為 {op}:{keyTable}, 防同表並行 race (lost update).
356
+ let kmx = pmKeyMutex()
357
+
358
+
359
+ //lockSave: 後台整表寫入(updateTabItems)之雙擊防護(後端, 見 lockSave.mjs, ADR-025): 以「updateTabItems:{表名}:{操作者 id}」於 cacheSt 原子占位,
360
+ //同一操作者之同表寫入處理中再送出即 reject 'saveInProgress'(不排隊); 包在 kmx 之外層, 不同操作者之並行仍由 kmx 序列化(ADR-012)
361
+ let cst = cacheSt()
362
+ let lockSave = createLockSave(cst)
363
+
364
+
365
+ //procCore: 整表批次 diff 寫入邏輯 (updateTabItems + updateTargets/Pemis/Grups/Users)
366
+ let pc = procCore(woItems, procOrm, { srLog, kmx, lockSave })
367
+
368
+
369
+ //procStaInfor: 統計資訊, fdLog 對齊 srLog 使用同一目錄(讀 settings.logFd, fallback 邏輯同 srLog.mjs); mock 由 settings.staEventMock 注入(e2e 統計圖穩定用)
370
+ let logFd = get(opt, 'logFd', '')
371
+ if (!isestr(logFd)) {
372
+ logFd = './logs'
373
+ }
374
+ let psi = procStaInfor({ fdLog: logFd, mock: get(opt, 'staEventMock', false), srLog })
375
+
376
+
377
+ //getWebInfor
378
+ let getWebInfor = (userId) => {
379
+
380
+ //info
381
+ srLog.info({ event: 'getWebInfor-success', userId })
382
+
383
+ return {
384
+
385
+ // webName, //已併入kpLang
386
+ // webDescription, //已併入kpLang
387
+ webLogo,
388
+ // urlRedirect, //登入失敗就需要轉址, 故須通過html模板取代提供, 無法用api提供
389
+
390
+ showLanguage,
391
+ language,
392
+ kpLang,
393
+
394
+ showModeEditTargets,
395
+ showModeEditPemis,
396
+ showModeEditGrups,
397
+ showModeEditUsers,
398
+ modeEditTargets,
399
+ modeEditPemis,
400
+ modeEditGrups,
401
+ modeEditUsers,
402
+
403
+ widthTargetId,
404
+ widthTargetDescription,
405
+ widthPemisName,
406
+ widthPemisDescription,
407
+ widthGrupsName,
408
+ widthGrupsDescription,
409
+ widthUsersName,
410
+ widthUsersEmail,
411
+ widthUsersDescription,
412
+
413
+ }
414
+ }
415
+
416
+
417
+ //checkUser
418
+ let checkUser = async(user) => {
419
+ let id = get(user, 'id', '')
420
+ if (!isestr(id)) {
421
+ console.log('user', user)
422
+ srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserId' })
423
+ return Promise.reject(`cannotGetUserId`)
424
+ }
425
+ let email = get(user, 'email', '')
426
+ if (!isestr(email)) {
427
+ console.log('user', user)
428
+ srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserEmail' })
429
+ return Promise.reject(`cannotGetUserEmail`)
430
+ }
431
+ let name = get(user, 'name', '')
432
+ if (!isestr(name)) {
433
+ console.log('user', user)
434
+ srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserName' })
435
+ return Promise.reject(`cannotGetUserName`)
436
+ }
437
+ let isAdmin = get(user, 'isAdmin', '')
438
+ if (isAdmin !== 'y' &amp;&amp; isAdmin !== 'n') {
439
+ console.log('user', user)
440
+ console.log('user.isAdmin is not y or n', user.isAdmin)
441
+ srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserRole' })
442
+ return Promise.reject(`cannotGetUserRole`)
443
+ }
444
+ return true
445
+ }
446
+
447
+
448
+ //getTokenUser
449
+ let getTokenUser = async(token) => {
450
+ //基於token查找使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
451
+
452
+ //userSelf
453
+ let userSelf = null
454
+ if (isestr(token)) {
455
+ userSelf = getUserByToken(token)
456
+ if (ispm(userSelf)) {
457
+ userSelf = await userSelf
458
+ }
459
+ }
460
+ // console.log('userSelf', userSelf)
461
+
462
+ //check
463
+ if (!iseobj(userSelf)) {
464
+ console.log(`token`, maskTok(token))
465
+ srLog.error({ event: 'getTokenUser-error', msg: 'cannotFindUserFromToken' })
466
+ return Promise.reject(`cannotFindUserFromToken`)
467
+ }
468
+
469
+ //check userSelf
470
+ await checkUser(userSelf)
471
+
472
+ //須反查perm內users, 提供正規化屬性
473
+ let vSelf = get(userSelf, mappingBy, '')
474
+ // console.log('vSelf', vSelf)
475
+
476
+ //check
477
+ if (!isestr(vSelf)) {
478
+ console.log('userSelf', userSelf)
479
+ console.log('mappingBy', mappingBy)
480
+ srLog.error({ event: 'getTokenUser-error', msg: 'cannotGetUserProp', mappingBy })
481
+ return Promise.reject(`cannotGetUserProp`)
482
+ }
483
+
484
+ //userFind
485
+ let userFind = await woItems.users.select({ [mappingBy]: vSelf, isActive: 'y' })
486
+ userFind = get(userFind, 0, null)
487
+ // console.log('userFind', userFind)
488
+
489
+ //check
490
+ if (!iseobj(userFind)) {
491
+ console.log('userSelf', userSelf)
492
+ console.log('mappingBy', mappingBy)
493
+ srLog.error({ event: 'getTokenUser-error', msg: 'cannotGetUserFromPerm', mappingBy })
494
+ return Promise.reject(`cannotGetUserFromPerm`)
495
+ }
496
+
497
+ //isAdmin 權威為 perm users 表之欄位值, 不以 token(getUserByToken)宣稱之 isAdmin 複寫。
498
+ //(2026-07-11 稽核 A-C1 裁決(b): 原此處有一段恆為 no-op 之「複寫isAdmin」死碼
499
+ // (userFind.isAdmin = isAdminSelf, 把自己指派給自己), 與 JSDoc 舊文「輸入'y'時會複寫」互斥;
500
+ // 依業主裁決採保守解: 刪除死碼、文件對齊實際行為, 不啟用 token→admin 提權路徑。)
501
+
502
+ return userFind
503
+ }
504
+
505
+
506
+ //getAndVerifyClientUser
507
+ let getAndVerifyClientUser = async (token, from = '') => {
508
+ //基於token查找client使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
509
+
510
+ //getTokenUser
511
+ let user = await getTokenUser(token)
512
+
513
+ //verifyClientUser
514
+ let b = verifyClientUser(user, from)
515
+ if (ispm(b)) {
516
+ b = await b
517
+ }
518
+
519
+ //check
520
+ if (!b) {
521
+ console.log('user', user)
522
+ srLog.error({ event: 'getAndVerifyClientUser-error', msg: 'userNoPermission', from })
523
+ return Promise.reject(`userNoPermission`)
524
+ }
525
+
526
+ return user
527
+ }
528
+
529
+
530
+ //getAndVerifyAppUser
531
+ let getAndVerifyAppUser = async (token, from = '') => {
532
+ //基於token查找app使用者, 會基於外部getUserByToken進行查找, 故只要getUserByToken提供使用者即可, 可支援getUserByToken給予針對應用程式所產生之虛擬使用者
533
+
534
+ //user
535
+ let user = null
536
+ if (isestr(token)) {
537
+ user = getUserByToken(token)
538
+ if (ispm(user)) {
539
+ user = await user
540
+ }
541
+ }
542
+ // console.log('user', user)
543
+
544
+ //check
545
+ if (!iseobj(user)) {
546
+ console.log(`token`, maskTok(token))
547
+ srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'cannotFindUserFromToken' })
548
+ return Promise.reject(`cannotFindUserFromToken`)
549
+ }
550
+
551
+ //verifyAppUser
552
+ let b = verifyAppUser(user, from)
553
+ if (ispm(b)) {
554
+ b = await b
555
+ }
556
+
557
+ //check
558
+ if (!b) {
559
+ console.log('user', user)
560
+ srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'userNoPermission', from })
561
+ return Promise.reject(`userNoPermission`)
562
+ }
563
+
564
+ return user
565
+ }
566
+
567
+
568
+ //p
569
+ let p = {
570
+ //checkToken, 資料通道(/api/main 之 RPC 與 ORM 執行、上傳、下載)之連線驗證, 與 api/getUserByToken(登入)採同一授權強度:
571
+ //外部 getUserByToken → 欄位齊備 → 反查 perm users(mappingBy)且 isActive='y' → verifyClientUser(user, from)
572
+ //why: 2026-09-07 前僅驗外部 token 欄位齊備(checkUser), 不查 perm users 與 isActive, 亦不經 verifyClientUser,
573
+ //致不在 perm users 表之外部使用者、被停用(isActive='n')之使用者仍可經資料通道讀寫全部權限資料(實測 A3/A4, 見 tmp/probe-report.json);
574
+ //改為與登入入口對稱, 使 UI 之「是否有效」與部署層 verifyClientUser 政策對資料通道亦生效.
575
+ //from 為 w-converhp 之 apiType('main' / 'upload-controller' / 'upload-slice' / 'download-get-filename' / 'download-get-file' / 'download'),
576
+ //供部署層 verifyClientUser(user, from) 區分入口.
577
+ checkToken: async(token, from = '') => {
578
+
579
+ //getAndVerifyClientUser, reject 時 key: cannotFindUserFromToken / cannotGetUserFromPerm / userNoPermission 等
580
+ await getAndVerifyClientUser(token, from)
581
+
582
+ return true
583
+ },
584
+ }
585
+
586
+
587
+ //parsePayload
588
+ let parsePayload = async (req) => {
589
+
590
+ //payload
591
+ let payload = get(req, 'payload')
592
+
593
+ //to obj
594
+ if (isestr(payload)) {
595
+ payload = j2o(payload)
596
+ }
597
+
598
+ //check
599
+ if (!iseobj(payload)) {
600
+ console.log('payload', payload)
601
+ srLog.error({ event: 'parsePayload-error', msg: 'invalidPayload' })
602
+ return Promise.reject(`invalidPayload`)
603
+ }
604
+
605
+ //from
606
+ let from = get(payload, 'from', '')
607
+
608
+ //check
609
+ if (!isestr(from)) {
610
+ console.log('payload', payload)
611
+ srLog.error({ event: 'parsePayload-error', msg: 'invalidFromInPayload' })
612
+ return Promise.reject(`invalidFromInPayload`)
613
+ }
614
+
615
+ //rows
616
+ let rows = get(payload, 'rows', [])
617
+
618
+ //check
619
+ if (!isearr(rows)) {
620
+ console.log('payload', payload)
621
+ srLog.error({ event: 'parsePayload-error', msg: 'invalidRowsInPayload' })
622
+ return Promise.reject(`invalidRowsInPayload`)
623
+ }
624
+
625
+ //inp
626
+ let inp = {
627
+ from,
628
+ rows,
629
+ }
630
+
631
+ return inp
632
+ }
633
+
634
+
635
+ //syncAndReplaceTabs
636
+ let syncAndReplaceTabs = async (userId, inp, keyTable) => {
637
+
638
+ //序列化同表之並行整表 delAll+insert: 防 lost update (同 keyTable 序列化, 不同表並行)
639
+ return await kmx('syncAndReplaceTabs:' + keyTable, async () => {
640
+
641
+ //from
642
+ let from = get(inp, 'from', '')
643
+ // console.log('from', from)
644
+
645
+ //check
646
+ if (!isestr(from)) {
647
+ console.log('inp', inp)
648
+ srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidFrom', keyTable })
649
+ return Promise.reject(`invalidFrom`)
650
+ }
651
+
652
+ //rows
653
+ let rows = get(inp, 'rows', [])
654
+ // console.log(keyTable, rows)
655
+
656
+ //check
657
+ if (!isearr(rows)) {
658
+ console.log('inp', inp)
659
+ console.log('keyTable', keyTable)
660
+ srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', keyTable })
661
+ return Promise.reject(`invalidRows`)
662
+ }
663
+
664
+ //save from
665
+ rows = map(rows, (v, k) => {
666
+ // v.order = k + 1 //order大部分由外部給予, 不能複寫
667
+ v.from = from
668
+ return v
669
+ })
670
+
671
+ //ltdtmapping
672
+ rows = ltdtmapping(rows, ds[keyTable].keys)
673
+ // console.log('ltdtmapping rows', rows)
674
+
675
+ //寫入前驗證(規則帳本 R-ORM-01, ADR-026): 於 delAll 之前擋下可預判之寫入失敗, 該 from 之既有列不變(ADR-008 防誤清源).
676
+ //why: 先 delAll 再 insert, 而 w-serv-orm 1.1.8 起對「會被 clearXSS 改寫之主鍵」整批拒絕、LMDB 對 > 1978 B 之主鍵整批拒絕,
677
+ //修正前遇之即該 from 之既有列全數清空(2026-10-14 V4/V7 實測). 主鍵問題 → invalidRows; 任一字串欄位含標記 → htmlMarkupNotAllowed
678
+ let ve = validateRowsBeforeWrite(rows)
679
+ if (ve !== null) {
680
+ let key = (ve.reason === 'htmlMarkup') ? 'htmlMarkupNotAllowed' : 'invalidRows'
681
+ srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, reason: ve.reason, keyTable, from, row: ve.k, field: ve.field })
682
+ return Promise.reject(key)
683
+ }
684
+
685
+ //跨 from 同鍵(規則帳本 R-SYNC-01): 本批有給之主鍵若已屬其他 from, 整批拒絕且不進 delAll(本 from 與他 from 之既有列皆不變).
686
+ //why: insert 對已存在之主鍵略過而整批仍回 success, 該列不會寫入本 from(靜默遺失); w-serv-orm 1.1.11 起數字主鍵存為其字串形式(原被換成隨機主鍵),
687
+ //以流水號為主鍵之各應用即互撞(2026-10-14 w-serv-orm 實測). 此處直呼 woItems(不經 procOrm 之轉型), 查詢條件一律用 String(id)(R-ORM-04);
688
+ //同 from 之既有列不算衝突(隨後由 delAll 重寫); 本段與 delAll/insert 同在 kmx 內(同表之並行同步已序列化)
689
+ let keyOfId = (v) => {
690
+ let id = get(v, 'id')
691
+ return (isestr(id) || (typeof id === 'number' &amp;&amp; Number.isFinite(id))) ? String(id) : null
692
+ }
693
+ let sids = rows.map(keyOfId).filter((s) => s !== null)
694
+ if (sids.length > 0) {
695
+ let rsExist = null
696
+ try {
697
+ rsExist = await woItems[keyTable].select({ id: { $in: sids } })
698
+ }
699
+ catch (err) {
700
+ let raw = (typeof err === 'string') ? err : ((err &amp;&amp; err.message) || String(err))
701
+ let key = toOutKey(err, 'anUnexpectedErrorOccurred') //尚未寫入任何資料, 不回 saveWriteFail
702
+ srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, raw, keyTable, from, step: 'select' })
703
+ return Promise.reject(key)
704
+ }
705
+ let fromById = new Map()
706
+ for (let v of rsExist) {
707
+ fromById.set(String(v.id), v.from)
708
+ }
709
+ let k = rows.findIndex((v) => {
710
+ let s = keyOfId(v)
711
+ return s !== null &amp;&amp; fromById.has(s) &amp;&amp; fromById.get(s) !== from
712
+ })
713
+ if (k >= 0) {
714
+ srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', reason: 'idOtherFrom', keyTable, from, row: k, field: 'id' })
715
+ return Promise.reject('invalidRows')
716
+ }
717
+ }
718
+
719
+ //delAll from, insert; ORM/I/O 失敗之原文只入 srLog, 外部應用收 saveWriteFail(規則帳本 R-ORM-02)
720
+ let r = null
721
+ try {
722
+ await woItems[keyTable].delAll({ from })
723
+ r = await procOrm(userId, keyTable, 'insert', rows) //須使用procOrm才有辦法自動給予相關欄位
724
+ }
725
+ catch (err) {
726
+ let raw = (typeof err === 'string') ? err : ((err &amp;&amp; err.message) || String(err))
727
+ let key = toOutKey(err)
728
+ srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, raw, keyTable, from })
729
+ return Promise.reject(key)
730
+ }
731
+ // console.log('r', r)
732
+
733
+ return r
734
+
735
+ })
736
+ }
737
+
738
+
739
+ //getGenUserByKV
740
+ let getGenUserByKV = async(keyUser, valueUser) => {
741
+
742
+ //userFind
743
+ let userFind = await woItems.users.select({ [keyUser]: valueUser, isActive: 'y' })
744
+ userFind = get(userFind, 0, null)
745
+
746
+ //check
747
+ if (!iseobj(userFind)) {
748
+ return null
749
+ }
750
+
751
+ return userFind
752
+ }
753
+
754
+
755
+ //getGenUserByUserId
756
+ let getGenUserByUserId = async(userId) => {
757
+
758
+ //userFind
759
+ let userFind = await getGenUserByKV('id', userId)
760
+
761
+ //check
762
+ if (!iseobj(userFind)) {
763
+ return null
764
+ }
765
+
766
+ return userFind
767
+ }
768
+
769
+
770
+ //getGenUserAndRulesByUserId
771
+ let getGenUserAndRulesByUserId = async (userId) => {
772
+
773
+ //userFind
774
+ let userFind = await getGenUserByUserId(userId)
775
+ // console.log('userFind', userFind)
776
+
777
+ //grups
778
+ let grups = await woItems.grups.select()
779
+ // console.log('grups', grups)
780
+
781
+ //pemis
782
+ let pemis = await woItems.pemis.select()
783
+ // console.log('pemis', pemis)
784
+
785
+ //targets
786
+ let targets = await woItems.targets.select()
787
+ // console.log('targets', targets)
788
+
789
+ //getUserRules
790
+ let kur = getUserRules(userFind, grups, pemis, targets)
791
+ // console.log('getUserRules', kur)
792
+
793
+ //check
794
+ if (!iseobj(kur)) {
795
+ console.log('userId', userId)
796
+ srLog.error({ event: 'getGenUserAndRulesByUserId-error', msg: 'cannotGetUserRules', userId })
797
+ return Promise.reject(`cannotGetUserRules`)
798
+ }
799
+
800
+ //grupsNames
801
+ let grupsNames = map(get(kur, 'grups', []), 'name') //kur.grups僅會提供isActive='y', 故grupsNames為有效權限群組名稱
802
+ grupsNames = join(grupsNames, ';')
803
+
804
+ return {
805
+ user: {
806
+ ...userFind,
807
+ grupsNames,
808
+ },
809
+ // grups: get(kur, 'grups', []),
810
+ rules: get(kur, 'rules', []),
811
+ }
812
+ }
813
+
814
+
815
+ //pathStaticFiles
816
+ let pathStaticFiles = 'dist'
817
+ let npmPathStaticFiles = './node_modules/w-web-perm/dist'
818
+ if (fsIsFolder(npmPathStaticFiles)) {
819
+ pathStaticFiles = npmPathStaticFiles
820
+ }
821
+ // console.log('pathStaticFiles', pathStaticFiles)
822
+
823
+
824
+ //subfolder
825
+ let fnEntryIn = 'index.tmp'
826
+ let fnEntryOut = 'index.html'
827
+ try {
828
+ let fpEntryIn = path.resolve(pathStaticFiles, fnEntryIn)
829
+ if (!fsIsFile(fpEntryIn)) {
830
+ fpEntryIn = path.resolve(pathStaticFiles, fnEntryOut) //本機開發另使用html替代tmp
831
+ }
832
+ if (!fsIsFile(fpEntryIn)) {
833
+ console.log('fpEntryIn', fpEntryIn)
834
+ throw new Error(`invalid fpEntryIn`)
835
+ }
836
+ let fpEntryOut = path.resolve(pathStaticFiles, fnEntryOut)
837
+ let c = fs.readFileSync(fpEntryIn, 'utf8')
838
+ c = replace(c, '/mperm/', '{sfd}/') //方法同genEntry
839
+ c = replace(c, '{sfd}', subfolder)
840
+ c = replace(c, '{urlRedirect}', urlRedirect)
841
+ c = replace(c, '{language}', language)
842
+ fs.writeFileSync(fpEntryOut, c, 'utf8')
843
+ }
844
+ catch (err) {
845
+ console.log(err)
846
+ console.log(`can not generate ${fnEntryOut}`)
847
+ }
848
+
849
+
850
+ //apis
851
+ let apis = [
852
+
853
+ {
854
+ method: 'GET',
855
+ path: '/api/getUserByToken', //未登入主頁時需先檢測token, getUserByToken為w-ui-loginout預設值, 若要更改兩邊須同時修改
856
+ handler: async function (req, res) {
857
+ // console.log('getUserByToken', req)
858
+
859
+ async function core() {
860
+ //提供對象為browser使用者, 須先檢測token是否有效, 確認後回傳該使用者之資訊物件
861
+
862
+ //token
863
+ let token = get(req, 'query.token', '')
864
+ // console.log('token', token)
865
+
866
+ //check
867
+ if (!isestr(token)) {
868
+ console.log('req.query', maskQuery(get(req, 'query')))
869
+ console.log('token', maskTok(token))
870
+ console.log('[API]getUserByToken/check token: invalid token')
871
+ srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
872
+ return Promise.reject(`tokenNoPermission`)
873
+ }
874
+
875
+ //getAndVerifyClientUser
876
+ let user = await getAndVerifyClientUser(token, 'getUserByToken')
877
+ // console.log('user', user)
878
+
879
+ //check
880
+ if (!iseobj(user)) {
881
+ console.log('token', maskTok(token))
882
+ console.log('[API]getUserByToken/check user: invalid user')
883
+ srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
884
+ return Promise.reject(`tokenNoPermission`)
885
+ }
886
+
887
+ //info
888
+ srLog.info({ event: 'api/getUserByToken-success', userId: get(user, 'id', '') })
889
+
890
+ return user
891
+ }
892
+
893
+ //pm2resolve core
894
+ let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 須另提取state進行判斷
895
+ if (isErr(r.msg)) {
896
+ r.msg = r.msg.message
897
+ }
898
+ // console.log('getUserByToken', r)
899
+
900
+ return r
901
+ },
902
+ },
903
+ {
904
+ method: 'GET',
905
+ path: '/api/getPerm',
906
+ handler: async function (req, res) {
907
+ // console.log('getPerm', req)
908
+
909
+ async function core() {
910
+ //提供對象為node與browser端, 須為client使用者, 須先檢測token是否有效, 再依照token取得使用者資訊物件, 再由其userId查詢回傳該使用者之權限資訊物件
911
+
912
+ //token
913
+ let token = get(req, 'query.token', '')
914
+ // console.log('token', token)
915
+
916
+ //check
917
+ if (!isestr(token)) {
918
+ console.log('req.query', maskQuery(get(req, 'query')))
919
+ console.log('token', maskTok(token))
920
+ console.log('[API]getPerm/check token: invalid token')
921
+ srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
922
+ return Promise.reject(`tokenNoPermission`)
923
+ }
924
+
925
+ //getAndVerifyClientUser, 發起token的所屬使用者, 驗證僅對client使用者, 不得使用app使用者
926
+ let userSelf = await getAndVerifyClientUser(token, 'getPerm')
927
+ // console.log('userSelf', userSelf)
928
+
929
+ //check
930
+ if (!iseobj(userSelf)) {
931
+ console.log('token', maskTok(token))
932
+ console.log('[API]getPerm/check userSelf: invalid userSelf')
933
+ srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
934
+ return Promise.reject(`tokenNoPermission`)
935
+ }
936
+
937
+ //userIdSelf, client使用者的userId
938
+ let userIdSelf = get(userSelf, 'id', '')
939
+ // console.log('userIdSelf', userIdSelf)
940
+
941
+ //check
942
+ if (!isestr(userIdSelf)) {
943
+ console.log('token', maskTok(token))
944
+ console.log('userSelf', userSelf)
945
+ console.log('[API]getPerm/check userIdSelf: invalid userIdSelf')
946
+ srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
947
+ return Promise.reject(`tokenNoPermission`)
948
+ }
949
+
950
+ //getGenUserAndRulesByUserId, 查找到的client使用者與權限
951
+ let userWithRulesSelf = await getGenUserAndRulesByUserId(userIdSelf)
952
+ // console.log('getGenUserAndRulesByUserId', r)
953
+
954
+ //check
955
+ if (!iseobj(userWithRulesSelf)) {
956
+ console.log('token', maskTok(token))
957
+ console.log('userSelf', userSelf)
958
+ console.log('userIdSelf', userIdSelf)
959
+ console.log('[API]getPerm/check userWithRulesSelf: invalid userWithRulesSelf')
960
+ srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
961
+ return Promise.reject(`tokenNoPermission`)
962
+ }
963
+
964
+ //info
965
+ srLog.info({ event: 'api/getPerm-success', userId: userIdSelf })
966
+
967
+ return userWithRulesSelf
968
+ }
969
+
970
+ //pm2resolve core
971
+ let r = await pm2resolve(core)()
972
+ if (isErr(r.msg)) {
973
+ r.msg = r.msg.message
974
+ }
975
+ // console.log('getPerm r', r)
976
+
977
+ return r
978
+ },
979
+ },
980
+ {
981
+ method: 'GET',
982
+ path: '/api/getPermUserInfor',
983
+ handler: async function (req, res) {
984
+ // console.log('getPermUserInfor', req)
985
+
986
+ async function core() {
987
+ //提供對象為node端, 須為app使用者, 須先檢測token是否有效, 再由其userId查詢回傳該使用者權限資訊物件
988
+
989
+ //tokenSelf
990
+ let tokenSelf = get(req, 'query.token', '')
991
+ // console.log('token', token)
992
+
993
+ //check
994
+ if (!isestr(tokenSelf)) {
995
+ console.log('req.query', maskQuery(get(req, 'query')))
996
+ console.log('[API]getPermUserInfor/check tokenSelf: invalid tokenSelf')
997
+ srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
998
+ return Promise.reject(`tokenNoPermission`)
999
+ }
1000
+
1001
+ //getAndVerifyAppUser, 發起token的所屬使用者, 驗證須為app使用者
1002
+ let userSelf = await getAndVerifyAppUser(tokenSelf, 'getPermUserInfor')
1003
+ // console.log('userSelf', userSelf)
1004
+
1005
+ //check
1006
+ if (!iseobj(userSelf)) {
1007
+ console.log('req.query', maskQuery(get(req, 'query')))
1008
+ console.log('tokenSelf', maskTok(tokenSelf))
1009
+ console.log('[API]getPermUserInfor/check userSelf: invalid userSelf')
1010
+ srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
1011
+ return Promise.reject(`tokenNoPermission`)
1012
+ }
1013
+
1014
+ //userIdFind, 欲查找使用者的userId
1015
+ let userIdFind = get(req, 'query.userId', '')
1016
+ // console.log('userIdFind', userIdFind)
1017
+
1018
+ //check
1019
+ if (!isestr(userIdFind)) {
1020
+ console.log('req.query', maskQuery(get(req, 'query')))
1021
+ console.log('tokenSelf', maskTok(tokenSelf))
1022
+ console.log('[API]getPermUserInfor/check userIdFind: invalid userIdFind')
1023
+ srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
1024
+ return Promise.reject(`tokenNoPermission`)
1025
+ }
1026
+
1027
+ //getGenUserAndRulesByUserId, 查找到的使用者與權限
1028
+ let userWithRulesFind = await getGenUserAndRulesByUserId(userIdFind)
1029
+ // console.log('getGenUserAndRulesByUserId', userWithRulesFind)
1030
+
1031
+ //check
1032
+ if (!iseobj(userWithRulesFind)) {
1033
+ console.log('userIdFind', userIdFind)
1034
+ console.log('[API]getPermUserInfor/check userWithRulesFind: invalid userWithRulesFind')
1035
+ srLog.error({ event: 'api/getPermUserInfor-error', msg: 'userIdNoPermrules' })
1036
+ return Promise.reject(`userIdNoPermrules`)
1037
+ }
1038
+
1039
+ //info
1040
+ srLog.info({ event: 'api/getPermUserInfor-success', userId: userIdFind })
1041
+
1042
+ return userWithRulesFind
1043
+ }
1044
+
1045
+ //pm2resolve core
1046
+ let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 須另提取state進行判斷
1047
+ if (isErr(r.msg)) {
1048
+ r.msg = r.msg.message
1049
+ }
1050
+ // console.log('getPermUserInfor', r)
1051
+
1052
+ return r
1053
+ },
1054
+ },
1055
+ {
1056
+ method: 'POST',
1057
+ path: '/syncAndReplaceTabs',
1058
+ handler: async function (req, res) {
1059
+ // console.log('syncAndReplaceTabs', req)
1060
+ // console.log('syncAndReplaceTabs payload', req.payload)
1061
+
1062
+ async function core() {
1063
+ //提供對象為node使用者, 須為系統管理者, 須先檢測token是否有效, 再進行數據變更
1064
+
1065
+ //token
1066
+ let token = get(req, 'query.token', '')
1067
+
1068
+ //check
1069
+ if (!isestr(token)) {
1070
+ console.log('req.query', maskQuery(get(req, 'query')))
1071
+ console.log('token', maskTok(token))
1072
+ console.log('[API]syncAndReplaceTabs/check token: invalid token')
1073
+ srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1074
+ return Promise.reject(`tokenNoPermission`)
1075
+ }
1076
+
1077
+ //keyTable
1078
+ let keyTable = get(req, 'query.keyTable', '')
1079
+
1080
+ //check
1081
+ if (!isestr(keyTable)) {
1082
+ console.log('req.query', maskQuery(get(req, 'query')))
1083
+ console.log('keyTable', keyTable)
1084
+ console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
1085
+ srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1086
+ return Promise.reject(`tokenNoPermission`)
1087
+ }
1088
+
1089
+ //check keyTable
1090
+ let allowedTables = ['targets', 'pemis', 'grups', 'users']
1091
+ if (!allowedTables.includes(keyTable)) {
1092
+ console.log('[API]syncAndReplaceTabs/check keyTable: unexpected keyTable', keyTable)
1093
+ srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'invalidKeyTable', keyTable })
1094
+ return Promise.reject(`invalidKeyTable`)
1095
+ }
1096
+
1097
+ //getAndVerifyAppUser
1098
+ let user = await getAndVerifyAppUser(token, 'syncAndReplaceTabs')
1099
+
1100
+ //check user
1101
+ if (!iseobj(user)) {
1102
+ console.log('token', maskTok(token))
1103
+ console.log('[API]syncAndReplaceTabs/check user: invalid user')
1104
+ srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1105
+ return Promise.reject(`tokenNoPermission`)
1106
+ }
1107
+
1108
+ //userId
1109
+ let userId = get(user, 'id', '')
1110
+
1111
+ //check userId
1112
+ if (!isestr(userId)) {
1113
+ console.log('token', maskTok(token))
1114
+ console.log('user', user)
1115
+ console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
1116
+ srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
1117
+ return Promise.reject(`tokenNoPermission`)
1118
+ }
1119
+
1120
+ //parsePayload
1121
+ let inp = await parsePayload(req)
1122
+
1123
+ //syncAndReplaceTabs
1124
+ let r = await syncAndReplaceTabs(userId, inp, keyTable)
1125
+
1126
+ //info
1127
+ srLog.info({ event: 'api/syncAndReplaceTabs-success', userId, keyTable })
1128
+
1129
+ return r
1130
+ }
1131
+
1132
+ //pm2resolve core
1133
+ let r = await pm2resolve(core)()
1134
+ if (isErr(r.msg)) {
1135
+ r.msg = r.msg.message
1136
+ }
1137
+ // console.log('syncAndReplaceTabs', r)
1138
+
1139
+ return r
1140
+ },
1141
+ },
1142
+ ]
1143
+
1144
+
1145
+ //tableNamesExec, tableNamesSync
1146
+ let tableNamesExec = keys(ds)
1147
+ // let tableNamesSync = filter(tableNamesExec, (v) => {
1148
+ // return strright(v, 5) !== 'Items'//不同步數據
1149
+ // })
1150
+ let tableNamesSync = tableNamesExec
1151
+
1152
+
1153
+ //WServHapiServer, w-serv-hapi 1.1.6 起設定錯誤於建構時同步拋出: 記 srLog(key)後轉拋(規則帳本 R-INIT-01, ADR-026)
1154
+ let optWServHapiServer = {
1155
+ port: serverPort,
1156
+ pathStaticFiles,
1157
+ apiName: 'api',
1158
+ apis,
1159
+ tokenType: 'Bearer',
1160
+ verifyConn: async ({ apiType, authorization, query, headers, req }) => {
1161
+ // console.log('verifyConn', `apiType[${apiType}]`, `authorization[${authorization}]`)
1162
+
1163
+ //token
1164
+ let token = strdelleft(authorization, 7) //刪除Bearer
1165
+ // console.log('token', token)
1166
+
1167
+ //check
1168
+ if (!isestr(token)) {
1169
+ console.log('apiType', apiType)
1170
+ console.log('authorization', maskTok(authorization))
1171
+ console.log('invalid token')
1172
+ srLog.error({ event: 'verifyConn-error', ok: false, apiType, msg: 'tokenNoPermission' }) //認證失敗須入 log 供稽核/統計(CLAUDE.md「log 記錄要點」; 同 w-web-api、w-web-task); 只記 key, 不記 authorization 原值
1173
+ return false
1174
+ }
1175
+
1176
+ //checkToken, 失敗一律回 false(傳輸層回 permission denied); reject 之 err key 記於 verifyConn log 之 msg
1177
+ let b = false
1178
+ let msg = ''
1179
+ await p.checkToken(token, apiType)
1180
+ .then(() => {
1181
+ b = true
1182
+ })
1183
+ .catch((err) => {
1184
+ msg = (typeof err === 'string') ? err : ((err &amp;&amp; err.message) || 'checkTokenFail')
1185
+ })
1186
+ // console.log('b', b)
1187
+
1188
+ //ip, 對齊sso慣例x-forwarded-for取第1個, fallback到req remoteAddress, 取不到則略過
1189
+ let ip = get(headers, 'x-forwarded-for', '').split(',')[0].trim()
1190
+ if (!isestr(ip)) {
1191
+ ip = get(req, 'info.remoteAddress', '') || get(req, 'socket.remoteAddress', '') || get(req, 'connection.remoteAddress', '')
1192
+ }
1193
+
1194
+ //origin
1195
+ let origin = get(headers, 'origin', '')
1196
+
1197
+ //referer, 只記 origin + pathname(ADR-023): 前端以 ?token= 開站(w-ui-loginout 讀完不清網址), 同源請求之 Referer 預設帶完整網址, 原樣記錄即把使用者權杖寫入 srLog
1198
+ let referer = maskUrl(get(headers, 'referer', ''))
1199
+
1200
+ //info, 失敗時 msg 為 err key(cannotFindUserFromToken / cannotGetUserFromPerm / userNoPermission 等), 供資安稽核與統計
1201
+ if (b) {
1202
+ srLog.info({ event: 'verifyConn', ok: b, apiType, ip, origin, referer })
1203
+ }
1204
+ else {
1205
+ srLog.error({ event: 'verifyConn-error', ok: b, apiType, ip, origin, referer, msg })
1206
+ }
1207
+
1208
+ return b
1209
+ },
1210
+ getUserIdByToken: async (token) => { //可使用async或sync函數
1211
+ // console.log('getUserIdByToken', token)
1212
+ let user = await getUserByToken(token)
1213
+ let userId = get(user, 'id', '')
1214
+ if (!isestr(userId)) {
1215
+ console.log('token', maskTok(token))
1216
+ console.log('userId', userId)
1217
+ srLog.error({ event: 'getUserIdByToken-error', msg: 'cannotFindUserId' })
1218
+ return Promise.reject(`cannotFindUserId`)
1219
+ }
1220
+ return userId
1221
+ },
1222
+ corsOrigins: ['*'],
1223
+ useDbOrm: true,
1224
+ kpOrm: woItems,
1225
+ //operOrm: procOrm 之包裝(輸入為 userId, tableName, methodName, input), ORM 拒絕之原文只入 srLog、對前端回 key(規則帳本 R-ORM-02)
1226
+ operOrm: wrapOperOrm(procOrm, { srLog }),
1227
+ tableNamesExec,
1228
+ //資料通道只開 select(ADR-026、ADR-015 Update; 規則帳本 R-WSD-01): 前端同步只用 select, 寫入一律經 kpFunExt 之 updateXxx(lockSave、kmx、寫入前驗證、整表比對);
1229
+ //裸 insert/save/del/delAll 繞過上述不變式, 2026-10-14 起不開放(直呼回 invalid func[表名.方法]); 對自寫客戶端直呼裸表操作之部署方為行為變更(README Upgrade Notes)
1230
+ methodsExec: ['select'],
1231
+ //authorizeExec: w-serv-webdata 1.1.7 起 useDbOrm:true 必填; 只對已取得 userId 之 select 放行(methodsExec 之外之第二道), 拒絕記 srLog event 'authorizeExec'.
1232
+ //連線者身分之授權在 verifyConn(getAndVerifyClientUser), 此處不重做
1233
+ authorizeExec: createAuthorizeExec({ srLog }),
1234
+ tableNamesSync,
1235
+ kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數
1236
+
1237
+ getWebInfor,
1238
+
1239
+ updateTargets: pc.updateTargets,
1240
+ updatePemis: pc.updatePemis,
1241
+ updateGrups: pc.updateGrups,
1242
+ //updateUsers, modeEditUsers='for:grups' 時 API 與 UI 一致只採納既有使用者之 cgrups 變更, 身分欄/新增/刪除/順序以資料庫為準
1243
+ updateUsers: (userId, rows) => {
1244
+ let opt = (modeEditUsers === 'for:grups') ? { pickKeysOnly: ['cgrups'] } : {}
1245
+ return pc.updateUsers(userId, rows, opt)
1246
+ },
1247
+
1248
+ getStaEvent: async (userId, timeLength, timeInterval) => {
1249
+ try {
1250
+ let rs = await psi.getStaEvent(userId, timeLength, timeInterval)
1251
+ srLog.info({ event: 'getStaEvent-success', userId })
1252
+ return rs
1253
+ }
1254
+ catch (err) {
1255
+ srLog.error({ event: 'getStaEvent-error', userId, msg: (typeof err === 'string') ? err : ((err &amp;&amp; err.message) || 'cannotGetStaEvent') })
1256
+ throw err
1257
+ }
1258
+ },
1259
+
1260
+ getStaEventTable: async (userId) => {
1261
+ try {
1262
+ let rs = await psi.getStaEventTable(userId)
1263
+ srLog.info({ event: 'getStaEventTable-success', userId })
1264
+ return rs
1265
+ }
1266
+ catch (err) {
1267
+ srLog.error({ event: 'getStaEventTable-error', userId, msg: (typeof err === 'string') ? err : ((err &amp;&amp; err.message) || 'cannotGetStaEventTable') })
1268
+ throw err
1269
+ }
1270
+ },
1271
+
1272
+ },
1273
+ //fpTableTags: 各資料表時間戳檔案(供前端判定是否需重新同步)。
1274
+ //w-serv-hapi 1.0.87 起之正式名稱; 舊名 fnTableTags 於該版之前傳下去等同未給(w-serv-webdata 只讀 fpTableTags),
1275
+ //故 1.0.86 及之前實際寫的是預設之 ./tableTags.json, 升版後改用本檔名。
1276
+ fpTableTags: 'tableTags-web-perm.json',
1277
+ }
1278
+ try {
1279
+ instWServHapiServer = new WServHapiServer(optWServHapiServer)
1280
+ }
1281
+ catch (err) {
1282
+ console.log(err)
1283
+ srLog.error({ event: 'WServHapiServer-init-error', msg: (err &amp;&amp; err.message) || 'WServHapiServerInitFailed' })
1284
+ throw err
1285
+ }
1286
+
1287
+
1288
+ return instWServHapiServer
1289
+ }
1290
+
1291
+
1292
+ export default WWebPerm
1242
1293
  </code></pre>
1243
1294
  </article>
1244
1295
  </section>
@@ -1253,7 +1304,7 @@ export default WWebPerm
1253
1304
  <br class="clear">
1254
1305
 
1255
1306
  <footer>
1256
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sun Oct 11 2026 01:45:43 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1307
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 16 2026 13:07:24 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1257
1308
  </footer>
1258
1309
 
1259
1310
  <script>prettyPrint();</script>