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