w-web-perm 1.1.13 → 1.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -7
- package/dist/css/{app.f09c7433.css → app.f24c5e50.css} +1 -1
- package/dist/index.tmp +1 -1
- package/dist/js/app-legacy.307bef4f.js +1 -0
- package/dist/js/app.307bef4f.js +1 -0
- package/dist/js/{chunk-vendors.ece5a361.js → chunk-vendors-legacy.6cc4d11e.js} +1 -1
- package/dist/js/{chunk-vendors-legacy.ece5a361.js → chunk-vendors.6cc4d11e.js} +1 -1
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +15 -7
- package/docs/global.html +11733 -5078
- package/docs/index.html +2 -2
- package/docs/server_WWebPerm.mjs.html +154 -345
- package/docs/server_adminCheck.mjs.html +92 -0
- package/docs/server_cascadeWrite.mjs.html +5 -26
- package/docs/server_dataChannel.mjs.html +4 -4
- package/docs/server_keyCheck.mjs.html +150 -23
- package/docs/server_lockSave.mjs.html +2 -2
- package/docs/server_maskLog.mjs.html +2 -2
- package/docs/server_procCore.mjs.html +261 -65
- package/docs/server_staLogs_resolveShellWorker.mjs.html +2 -2
- package/docs/server_syncCore.mjs.html +533 -0
- package/docs/server_syncKeep.mjs.html +27 -20
- package/docs/server_tools_authRefsAudit.mjs.html +30 -10
- package/docs/server_tools_authRefsClean.mjs.html +2 -2
- package/docs/server_tools_fromTools.mjs.html +2 -2
- package/docs/server_tools_isMain.mjs.html +2 -2
- package/docs/server_tools_nameDupReport.mjs.html +40 -7
- package/docs/server_validateRows.mjs.html +2 -2
- package/docs/server_wrapInjected.mjs.html +2 -2
- package/docs/src_fetchJson.mjs.html +2 -2
- package/docs/src_getPermUserInfor.mjs.html +175 -0
- package/docs/src_js_applySaved.mjs.html +128 -0
- package/docs/src_js_cellEditing.mjs.html +163 -0
- package/docs/src_js_cvKpToArr.mjs.html +2 -2
- package/docs/src_js_cvKpToObj.mjs.html +2 -2
- package/docs/src_js_draftSync.mjs.html +83 -7
- package/docs/src_js_eachOwn.mjs.html +2 -2
- package/docs/src_js_editorRefRows.mjs.html +2 -2
- package/docs/src_js_externalRows.mjs.html +34 -10
- package/docs/src_js_externalUi.mjs.html +4 -8
- package/docs/src_js_findRefRows.mjs.html +2 -2
- package/docs/src_js_findTouchedDup.mjs.html +2 -2
- package/docs/src_js_fmtLossyMsg.mjs.html +2 -2
- package/docs/src_js_fromAudit.mjs.html +2 -2
- package/docs/src_js_getAddImpact.mjs.html +2 -2
- package/docs/src_js_getAuthWarn.mjs.html +2 -2
- package/docs/src_js_getCascadePreview.mjs.html +2 -2
- package/docs/src_js_getDelRefCount.mjs.html +2 -2
- package/docs/src_js_getEffMode.mjs.html +2 -2
- package/docs/src_js_getKpLossy.mjs.html +2 -2
- package/docs/src_js_getNameErrs.mjs.html +2 -2
- package/docs/src_js_getOwn.mjs.html +2 -2
- package/docs/src_js_getPropKey.mjs.html +2 -2
- package/docs/src_js_getRuleEnable.mjs.html +2 -2
- package/docs/src_js_isAuthMode.mjs.html +2 -2
- package/docs/src_js_joinNames.mjs.html +2 -2
- package/docs/src_js_legendFit.mjs.html +2 -2
- package/docs/src_js_pairRowsToSnapshot.mjs.html +2 -2
- package/docs/src_js_refAudit.mjs.html +2 -2
- package/docs/src_js_refCascade.mjs.html +2 -2
- package/docs/src_js_refIdentity.mjs.html +500 -0
- package/docs/src_js_relDialogSend.mjs.html +2 -2
- package/docs/src_js_rowDigest.mjs.html +75 -145
- package/docs/src_plugins_dataReloadGuard.mjs.html +2 -2
- package/docs/src_plugins_reuseDeletedIds.mjs.html +2 -2
- package/docs/src_plugins_submitGuard.mjs.html +2 -2
- package/g_provideGrups.mjs +1 -1
- package/g_providePemis.mjs +1 -1
- package/g_provideTargets.mjs +1 -1
- package/g_provideUsers.mjs +1 -1
- package/package.json +2 -2
- package/server/WWebPerm.mjs +152 -343
- package/server/adminCheck.mjs +20 -0
- package/server/cascadeWrite.mjs +3 -24
- package/server/dataChannel.mjs +2 -2
- package/server/keyCheck.mjs +148 -21
- package/server/procCore.mjs +259 -63
- package/server/procLang.mjs +78 -4
- package/server/provideTabs.mjs +6 -4
- package/server/syncCore.mjs +461 -0
- package/server/syncKeep.mjs +25 -18
- package/server/tools/authRefsAudit.mjs +28 -8
- package/server/tools/nameDupReport.mjs +38 -5
- package/src/App.vue +6 -1
- package/src/components/LayoutContentGrups.vue +57 -8
- package/src/components/LayoutContentPemis.vue +57 -8
- package/src/components/LayoutContentTargets.vue +38 -7
- package/src/components/LayoutContentUsers.vue +57 -8
- package/src/components/LayoutState.vue +12 -0
- package/src/components/VeGrupBlngUsers.vue +128 -20
- package/src/components/VePemiBlngGrups.vue +128 -20
- package/src/getPermUserInfor.mjs +10 -0
- package/src/js/applySaved.mjs +56 -0
- package/src/js/cellEditing.mjs +91 -0
- package/src/js/draftSync.mjs +81 -5
- package/src/js/externalRows.mjs +32 -8
- package/src/js/externalUi.mjs +2 -6
- package/src/js/refIdentity.mjs +428 -0
- package/src/js/rowDigest.mjs +73 -143
- package/src/plugins/mUI.mjs +5 -0
- package/src/schema/contracts/grups.mjs +8 -0
- package/src/schema/contracts/index.mjs +17 -0
- package/src/schema/contracts/pemis.mjs +8 -0
- package/src/schema/contracts/targets.mjs +8 -0
- package/src/schema/contracts/users.mjs +13 -0
- package/src/schema/tables/grups.mjs +4 -0
- package/src/schema/tables/pemis.mjs +4 -0
- package/src/schema/tables/targets.mjs +4 -0
- package/src/schema/tables/users.mjs +4 -0
- package/src/store/mutations.mjs +1 -1
- package/srv.mjs +43 -0
- package/dist/js/app-legacy.8af87052.js +0 -1
- package/dist/js/app.8af87052.js +0 -1
package/server/WWebPerm.mjs
CHANGED
|
@@ -21,38 +21,41 @@ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
|
|
|
21
21
|
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
|
|
22
22
|
import replace from 'wsemi/src/replace.mjs'
|
|
23
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
24
|
import cacheSt from 'wsemi/src/cacheSt.mjs'
|
|
27
25
|
import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
|
|
28
26
|
import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
|
|
29
27
|
import checkOptRemoved from 'w-serv-orm/src/checkOptRemoved.mjs'
|
|
30
28
|
import ds from '../src/schema/index.mjs'
|
|
31
29
|
import { getUserRules } from '../src/plugins/mShare.mjs'
|
|
32
|
-
import procCore, { toOutKey,
|
|
30
|
+
import procCore, { toOutKey, pickClientOpt } from './procCore.mjs'
|
|
31
|
+
import syncCore from './syncCore.mjs'
|
|
33
32
|
import createLockSave from './lockSave.mjs'
|
|
34
33
|
import procLang from './procLang.mjs'
|
|
35
34
|
import srLogInit from './srLog.mjs'
|
|
36
35
|
import procStaInfor from './procStaInfor.mjs'
|
|
37
36
|
import { maskTok, maskQuery, maskUrl } from './maskLog.mjs'
|
|
38
37
|
import wrapInjected from './wrapInjected.mjs'
|
|
39
|
-
import
|
|
40
|
-
import planRefChanges, { REF_OF, diffRefTargets } from '../src/js/refCascade.mjs'
|
|
41
|
-
import { findDupIndex, findKeyOtherFrom, withTabLocks, mapKeyOf, checkMapKeyBatch, pickUniqueUser } from './keyCheck.mjs'
|
|
42
|
-
import { checkCascadePlan, writeWithCascade, buildSyncRowsNew } from './cascadeWrite.mjs'
|
|
38
|
+
import { pickUniqueUser, findLoginUsers, mapKeyOf } from './keyCheck.mjs'
|
|
43
39
|
import { createAuthorizeExec, wrapOperOrm } from './dataChannel.mjs'
|
|
44
|
-
import
|
|
40
|
+
import isAdminRow from './adminCheck.mjs'
|
|
45
41
|
|
|
46
42
|
|
|
47
43
|
/**
|
|
48
44
|
* 權限伺服器
|
|
49
45
|
*
|
|
46
|
+
* 身分驗證責任(dspec/使用者身分驗證原則.md 第三節, 使用方套件標準註解): 框架(w-converhp、w-serv-hapi、w-serv-webdata、w-serv-orm)不驗證 token, 一律由本套件負責.
|
|
47
|
+
* 連線閘(verifyConn)與執行身分(getUserIdByToken)皆須完整驗證(token 有效、本系統使用者存在且啟用、部署方授權); hapi apis 路由各自驗證; 函數層級另檢查角色或成員; 公開入口只限列名者.
|
|
48
|
+
* 本套件之落實(perm-run01 起「權限後台與資料通道只限系統管理員」, 規則帳本 R-ADMIN-ONLY-01): 資料通道之連線閘(標頭 token, 所有入口: main 之 RPC/表 select/系統函數, 上傳下載各 apiType)與執行身分(請求本體 __sysToken__)、後台登入檢查(/api/getUserByToken)皆經 getAndVerifyAdminUser
|
|
49
|
+
* (外部 getUserByToken → perm users 存在且 isActive='y' → 部署方 verifyClientUser → perm users 表之列 isAdmin='y'; perm 以自身 users 表為權威, 不採 token 宣稱之 isAdmin), 任一關不過函數不執行(一般使用者即使部署方放行亦被拒, 回 userNotAdmin);
|
|
50
|
+
* 一般使用者(isAdmin='n')只能經 /api/getPerm 以 token 查本人權限樹; 改變狀態之 kpFunExt 函數(updateTargets/updatePemis/updateGrups/updateUsers)另保留 perm users 表中有效且 isAdmin='y' 之縱深檢查(requireAdmin), 不符回 userNotAdmin、零寫入;
|
|
51
|
+
* hapi apis 路由: getUserByToken(只限管理員)/getPerm(任何通過 verifyClientUser 之有效使用者, 回本人權限樹)驗 client 使用者, getPermUserInfor/syncAndReplaceTabs 驗 app 使用者(verifyAppUser); 無公開入口(前端靜態檔除外).
|
|
52
|
+
*
|
|
50
53
|
* @class
|
|
51
54
|
* @param {Function} WOrm 輸入資料庫ORM函數
|
|
52
55
|
* @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
|
|
53
56
|
* @param {String} db 輸入資料庫名稱字串
|
|
54
57
|
* @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫。函數拋錯或reject時視同查無使用者(回自家錯誤key,錯誤原文不回前端、不入log,只於srLog記warn)
|
|
55
|
-
* @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件(perm users 表之列)與來源字串from,通過此函數識別後回傳布林值(可為Promise),允許使用者回傳true,反之回傳false。from 值域: 'getUserByToken'(登入)、'getPerm'(api/getPerm)、以及資料通道之 apiType 'main'(RPC 與 ORM
|
|
58
|
+
* @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件(perm users 表之列)與來源字串from,通過此函數識別後回傳布林值(可為Promise),允許使用者回傳true,反之回傳false。from 值域: 'getUserByToken'(登入)、'getPerm'(api/getPerm)、以及資料通道之 apiType 'main'(RPC 與 ORM 執行; 連線閘驗標頭 token、執行身分〔getUserIdByToken〕驗請求本體 __sysToken__, 兩者各經本函數一次, 本體 token 與標頭 token 不同時兩個使用者各自驗證)、'upload-controller'、'upload-slice'、'download-get-filename'、'download-get-file'、'download';資料通道與登入採同一授權強度(反查 perm users 且 isActive='y' 後再經本函數);函數拋錯或reject時視同回傳false。本函數收到的是該列之淺層副本,改寫其欄位不影響本套件後續採用之執行身分與角色判定。本函數通過後,本套件另要求資料通道與後台登入(from 為 'getUserByToken'、'main'、'upload-controller' 等)限 perm users 表中 isAdmin 為 'y' 者(一般使用者即使本函數放行亦被拒,回 userNotAdmin),故本函數不必再以 from 區分管理員與一般使用者,但可再收緊;一般使用者查本人權限樹之 /api/getPerm(from 為 'getPerm')仍會呼叫本函數,勿回傳 user.isAdmin === 'y' 而擋掉他們
|
|
56
59
|
* @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false;函數拋錯或reject時視同回傳false
|
|
57
60
|
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
58
61
|
* @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,刪除該三鍵即可,無須替代設定
|
|
@@ -61,7 +64,7 @@ import { markHasKey, keepAssignOnMissing, keepAssignByEntity, countIdRenumber }
|
|
|
61
64
|
* @param {String} [opt.webLogo=''] 輸入站台logo字串,採base64格式,預設''
|
|
62
65
|
* @param {String} [opt.subfolder=''] 輸入站台所在子目錄字串,提供站台位於內網採反向代理進行服務時,故需支援位於子目錄情形,預設''
|
|
63
66
|
* @param {String} [opt.urlRedirect=''] 輸入錯誤時自動轉址字串,提供站台例如無法登入或驗證失敗時須自動轉址,預設''
|
|
64
|
-
* @param {String} [opt.mappingBy='id'] 輸入外部系統識別使用者token後所提供之資料物件,與權限系統之使用者資料物件,兩者間查找之對應欄位,可選'id'、'email'、'name',未給或非此三者時為'id',預設'id'。對應鍵須能唯一指出使用者:mappingBy為'email'或'name'時,外部同步(syncAndReplaceTabs)users之有效(isActive為'y')列,該欄位須為非空字串且與本批及其他from之有效列皆不重複(違規整批拒收invalidRows);登入查找以對應鍵於權限系統users命中多於1筆有效使用者時一律拒絕(ambiguousUserFromPerm),不取首筆;mappingBy為'id'(預設)
|
|
67
|
+
* @param {String} [opt.mappingBy='id'] 輸入外部系統識別使用者token後所提供之資料物件,與權限系統之使用者資料物件,兩者間查找之對應欄位,可選'id'、'email'、'name',未給或非此三者時為'id',預設'id'。對應鍵須能唯一指出使用者:mappingBy為'email'或'name'時,外部同步(syncAndReplaceTabs)users之有效(isActive為'y')列,該欄位須為非空字串且與本批及其他from之有效列皆不重複(違規整批拒收invalidRows);登入查找以對應鍵於權限系統users命中多於1筆有效使用者時一律拒絕(ambiguousUserFromPerm),不取首筆;mappingBy為'id'(預設)時,後台列(from為空)以其id對應外部token使用者之id,外部同步列(from非空)則以其來源鍵srcKey對應(外部列之id為本系統配發之內部主鍵,不是登入識別;升版前同步寫入、尚無srcKey之舊外部列在首次匯入補上srcKey前暫以id對應),並防止撞值(登入識別值須跨全表唯一,不論isActive、含他來源外部列;不同外部系統之使用者id碰撞常見而非同一人,已停用者之token不得對到他來源之有效列):外部同步(syncAndReplaceTabs)批次內任一列之srcKey等於他來源任一既有列之登入識別值(後台列id、他來源外部列srcKey)時整批拒收(loginIdCollision,零寫入,請部署方改用可唯一對應之鍵,如以email對應並將mappingBy設為'email')、後台存檔(updateUsers)新增或改變之後台列id撞到任一外部列srcKey時整次拒收(loginIdCollision,零變更),登入時若命中多於1筆(不論有效與否)則拒絕(ambiguousUserFromPerm)、恰命中1筆但已停用視同查無(cannotGetUserFromPerm);mappingBy為'id'時getPermUserInfor之userId亦依此對應解析(外部列傳其srcKey、後台列傳id,不是內部id;命中多筆拒絕ambiguousUserFromPerm),'email'/'name'時userId仍為perm內部id;升版前既有之跨來源重複srcKey歷史資料不阻擋存檔,但登入一律拒絕,請依README Upgrade Notes盤點處置
|
|
65
68
|
* @param {Boolean} [opt.cascadeRename=true] 輸入群組、權限改名時是否連帶更新引用它之授權字串(users.cgrups、grups.cpemis),預設true。改名以主鍵id判定(同id異名),後台整表儲存與外部同步(同一from內)皆適用,於同一次寫入內改寫並可能寫入外部來源之users列之cgrups;給false則不連帶(引用成為懸空,視同引用查無對象而收嚴並於頁面提示)。群組、權限、標的被刪除時之引用連帶清理(users.cgrups、grups.cpemis、pemis.crules)不受此設定影響
|
|
66
69
|
* @param {String} [opt.lockExternalRows='y'] 輸入後台是否不得改外部列(from為非空字串,即經syncAndReplaceTabs由外部來源同步之列),可選'y'或'n',預設'y'。'y'時伺服端強制:後台新增列from須為空字串、外部列不可刪除、不可改from、身分/結構欄(指派欄與順序以外之欄位)不可改,違規整次拒收(saveFromNotAllowed/saveExternalRowChanged);授權指派欄(users.cgrups、grups.cpemis、pemis.crules)後台可寫,但須附載入基準(用戶端自動附帶),無基準者拒收(saveBaselineRequired);外部列之order由外部擁有、後台儲存不重編。給'n'回到舊行為(僅保留from須為字串之檢查)。升版後既有庫中from為非空字串之列即視為外部列而唯讀,請先以g_fixFromType.mjs/g_purgeFrom.mjs盤點(見README Upgrade Notes)
|
|
67
70
|
* @param {String} [opt.logFd='./logs'] 輸入log紀錄儲存位置字串,預設'./logs'
|
|
@@ -125,7 +128,9 @@ import { markHasKey, keepAssignOnMissing, keepAssignByEntity, countIdRenumber }
|
|
|
125
128
|
* console.log('verifyClientUser/user', user)
|
|
126
129
|
* // return false //測試無法登入
|
|
127
130
|
* console.log('於生產環境時得加入限制瀏覽器使用者身份機制')
|
|
128
|
-
*
|
|
131
|
+
* //本套件於此函數通過後,另要求資料通道與後台登入限系統管理員(perm users 表之 isAdmin 為 'y'),一般使用者仍可經 /api/getPerm(from 為 'getPerm')以 token 查本人權限樹,
|
|
132
|
+
* //故此處不可回傳 user.isAdmin === 'y'(會擋掉一般使用者之 /api/getPerm); 僅需放行有效之瀏覽使用者,或依 from 與自家政策再收緊(例如依來源網段或系統別拒絕)
|
|
133
|
+
* return true
|
|
129
134
|
* }
|
|
130
135
|
*
|
|
131
136
|
* let verifyAppUser = (user, from) => {
|
|
@@ -318,34 +323,26 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
318
323
|
let { woItems, procOrm } = wp
|
|
319
324
|
|
|
320
325
|
|
|
321
|
-
//
|
|
322
|
-
//perm 為「整表批次 diff」寫入,
|
|
323
|
-
|
|
326
|
+
//lockTabs, guardWrite: w-serv-orm 1.1.17 的每表一鍵表鎖與原子防護(多人同時編輯規格第 6、7 節; 規則帳本 R-LOCK-CORE-01), 取自 WServOrm 回傳物件——**唯一**的鎖管理器.
|
|
327
|
+
//perm 為「整表批次 diff」寫入, 鎖粒度為整表: 後台整表寫入(procCore.updateTabItems 之 guardWrite)、外部同步(syncAndReplaceTabs)與引用連帶之引用端寫入皆取同一把表鎖, 防同表並行 race (lost update) 與唯一性檢查被交錯略過 (R-WRITE-LOCK-01).
|
|
328
|
+
//不再自建 pmKeyMutex/鎖鍵(原 tabWriteKey 已移除); 鎖不可重入, 多表依階層巢狀取鎖見 keyCheck.withTabLocks(R-WRITE-LOCK-02)
|
|
329
|
+
let lockTabs = wp.withTabLocks
|
|
330
|
+
let guardWrite = wp.guardWrite
|
|
324
331
|
|
|
325
332
|
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
let
|
|
329
|
-
let setSyncWarn = (keyTable, from, n) => {
|
|
330
|
-
let k = `${keyTable}\u0000${from}`
|
|
331
|
-
if (n > 0) {
|
|
332
|
-
kpSyncWarn.set(k, { keyTable, from, n, time: new Date().toISOString() })
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
kpSyncWarn.delete(k)
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
let getSyncWarns = () => [...kpSyncWarn.values()]
|
|
333
|
+
//sc: 外部匯入(POST /syncAndReplaceTabs)核心(server/syncCore.mjs; perm-run03 起依來源鍵 srcKey 差異同步, R-IMPORT-01): 與後台整表寫入共用同一個鎖管理器(lockTabs).
|
|
334
|
+
//syncWarns(同一 from 同名異 srcKey 之疑似來源鍵重編提示)之登錄亦在其內(記憶體, 重啟即清; 不含個資, 僅表名、來源名稱、筆數、時間)
|
|
335
|
+
let sc = syncCore({ woItems, procOrm, lockTabs, srLog, mappingBy, cascadeRename })
|
|
339
336
|
|
|
340
337
|
|
|
341
338
|
//lockSave: 後台整表寫入(updateTabItems)之雙擊防護(後端, 見 lockSave.mjs, ADR-025): 以「updateTabItems:{表名}:{操作者 id}」於 cacheSt 原子占位,
|
|
342
|
-
//同一操作者之同表寫入處理中再送出即 reject 'saveInProgress'(不排隊);
|
|
339
|
+
//同一操作者之同表寫入處理中再送出即 reject 'saveInProgress'(不排隊); 包在表鎖之外層, 不同操作者之並行仍由表鎖序列化(ADR-012)
|
|
343
340
|
let cst = cacheSt()
|
|
344
341
|
let lockSave = createLockSave(cst)
|
|
345
342
|
|
|
346
343
|
|
|
347
344
|
//procCore: 整表批次 diff 寫入邏輯 (updateTabItems + updateTargets/Pemis/Grups/Users)
|
|
348
|
-
let pc = procCore(woItems, procOrm, { srLog,
|
|
345
|
+
let pc = procCore(woItems, procOrm, { srLog, withTabLocks: lockTabs, guardWrite, lockSave, mappingBy, cascadeRename, lockExternalRows }) //mappingBy 供 users 整表寫入的對應鍵檢查(R-PRINCIPLE-00 適用表 h); cascadeRename 供群組/權限改名之引用級聯(適用表 m); lockExternalRows 供後台對外部列之守門(F-23)
|
|
349
346
|
|
|
350
347
|
|
|
351
348
|
//procStaInfor: 統計資訊, fdLog 對齊 srLog 使用同一目錄(讀 settings.logFd, fallback 邏輯同 srLog.mjs); mock 由 settings.staEventMock 注入(e2e 統計圖穩定用)
|
|
@@ -384,7 +381,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
384
381
|
|
|
385
382
|
cascadeRename, //改名是否連帶更新引用(opt.cascadeRename), 供清單頁儲存前預覽與伺服端一致(R-REF-CASCADE-01)
|
|
386
383
|
lockExternalRows: lockExternalRows ? 'y' : 'n', //後台是否不得改外部列(opt.lockExternalRows), 供清單頁逐列唯讀、複製重置 from 與伺服端一致(R-EXT-OWN-01)
|
|
387
|
-
syncWarns: getSyncWarns(),
|
|
384
|
+
syncWarns: sc.getSyncWarns(), //外部同步疑似來源鍵重編之提示(同一 from 同名異 srcKey), 供清單頁橫幅(原則 3: 提示); 不含個資, 僅表名、來源名稱、筆數、時間
|
|
388
385
|
|
|
389
386
|
widthTargetId,
|
|
390
387
|
widthTargetDescription,
|
|
@@ -468,8 +465,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
468
465
|
}
|
|
469
466
|
|
|
470
467
|
//userFind, 對應鍵須唯一(F-14, 規則帳本 R-MAPKEY-02): 命中多於 1 筆即拒絕, 不取首筆(取首筆等於由資料庫列序決定登入者為誰)
|
|
471
|
-
|
|
472
|
-
let { user: userFind, count: countFind } =
|
|
468
|
+
//mappingBy 為 'id' 時(perm-run04, R-MAPKEY-04, O1): 後台列(from 空)對 id、外部列(from 非空)對 srcKey(外部列之 id 是本系統配發之內部主鍵, 不是登入識別); 判準單一擁有者 keyCheck.findLoginUsers
|
|
469
|
+
let { user: userFind, count: countFind } = await findLoginUsers(woItems.users, mappingBy, vSelf)
|
|
473
470
|
// console.log('userFind', userFind)
|
|
474
471
|
|
|
475
472
|
//check, 多筆: 只記 mappingBy 與筆數, 不記使用者之對應值(email/name 為個資); 0 筆行為不變
|
|
@@ -497,14 +494,19 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
497
494
|
|
|
498
495
|
|
|
499
496
|
//getAndVerifyClientUser
|
|
500
|
-
let getAndVerifyClientUser = async (token, from = '') => {
|
|
497
|
+
let getAndVerifyClientUser = async (token, from = '', { adminOnly = false } = {}) => {
|
|
501
498
|
//基於token查找client使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
|
|
499
|
+
//adminOnly 為 true 時(經 getAndVerifyAdminUser)於完整驗證通過後另要求 perm 列為有效之系統管理員(R-ADMIN-ONLY-01); 預設 false 者(/api/getPerm)一般使用者照常
|
|
502
500
|
|
|
503
501
|
//getTokenUser
|
|
504
502
|
let user = await getTokenUser(token)
|
|
505
503
|
|
|
506
|
-
//
|
|
507
|
-
let
|
|
504
|
+
//isAdmin, 於交給部署方前判定(判準 adminCheck.isAdminRow, perm 列為權威)
|
|
505
|
+
let isAdmin = isAdminRow(user)
|
|
506
|
+
|
|
507
|
+
//verifyClientUser, 部署方只收到淺層副本(perm 列之欄位皆為純量): 其函數即使改寫收到之 user(id、isAdmin…)亦不影響本函數回傳之 user——
|
|
508
|
+
//後續以 user.id 為執行身分/查權限樹, 檢查之值即使用之值(perm-run01 外審 sol#2: 否則角色取自 A、執行 id 被改成 M)
|
|
509
|
+
let b = verifyClientUser({ ...user }, from)
|
|
508
510
|
if (ispm(b)) {
|
|
509
511
|
b = await b
|
|
510
512
|
}
|
|
@@ -516,10 +518,27 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
516
518
|
return Promise.reject(`userNoPermission`)
|
|
517
519
|
}
|
|
518
520
|
|
|
521
|
+
//check admin, 一般使用者即使部署方放行亦拒絕(固定語系鍵 userNotAdmin; 只記 from, 不記 token 與使用者資料)
|
|
522
|
+
if (adminOnly && !isAdmin) {
|
|
523
|
+
srLog.error({ event: 'getAndVerifyAdminUser-error', msg: 'userNotAdmin', from })
|
|
524
|
+
return Promise.reject(`userNotAdmin`)
|
|
525
|
+
}
|
|
526
|
+
|
|
519
527
|
return user
|
|
520
528
|
}
|
|
521
529
|
|
|
522
530
|
|
|
531
|
+
//getAndVerifyAdminUser: 權限後台與資料通道只限系統管理員(perm-run01; 使用者 2026-10-24 裁示「系統管理員才能讀寫 4 表, 一般使用者是沒法進權限後台的, 故只能用 token 查自己權限樹」; 規則帳本 R-ADMIN-ONLY-01)
|
|
532
|
+
//在 getAndVerifyClientUser 之完整驗證(外部 getUserByToken → perm users 存在且 isActive='y' → 部署方 verifyClientUser)之後, 另要求該使用者為 perm users 表中 isAdmin='y' 且 isActive='y':
|
|
533
|
+
//perm 以自身 users 表為權威, 不採 token(getUserByToken)宣稱之 isAdmin(user 為 getTokenUser 當次新鮮讀取之 perm 列, 不快取); 不符即 reject 固定語系鍵 userNotAdmin(fail-closed), 記 srLog(只記 from, 不記 token 與使用者資料).
|
|
534
|
+
//部署方 verifyClientUser 仍照常套用(只能再收緊), 不必再以 from 區分管理員與一般使用者. 唯一不經本函數之使用者入口為 /api/getPerm(一般使用者以 token 查本人權限樹), 以及應用系統路由(verifyAppUser).
|
|
535
|
+
//判準單一擁有者 adminCheck.isAdminRow(與 requireAdmin 共用)
|
|
536
|
+
//角色於 getTokenUser 回傳當下判定(早於交給部署方 verifyClientUser), 使「檢查之值即使用之值」: 部署方函數即使修改其收到之 user 物件亦不影響角色判定; 但拒絕鍵次序為先 userNoPermission(部署方否決)後 userNotAdmin
|
|
537
|
+
let getAndVerifyAdminUser = async (token, from = '') => {
|
|
538
|
+
return await getAndVerifyClientUser(token, from, { adminOnly: true })
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
523
542
|
//getAndVerifyAppUser
|
|
524
543
|
let getAndVerifyAppUser = async (token, from = '') => {
|
|
525
544
|
//基於token查找app使用者, 會基於外部getUserByToken進行查找, 故只要getUserByToken提供使用者即可, 可支援getUserByToken給予針對應用程式所產生之虛擬使用者
|
|
@@ -567,10 +586,11 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
567
586
|
//改為與登入入口對稱, 使 UI 之「是否有效」與部署層 verifyClientUser 政策對資料通道亦生效.
|
|
568
587
|
//from 為 w-converhp 之 apiType('main' / 'upload-controller' / 'upload-slice' / 'download-get-filename' / 'download-get-file' / 'download'),
|
|
569
588
|
//供部署層 verifyClientUser(user, from) 區分入口.
|
|
589
|
+
//perm-run01(R-ADMIN-ONLY-01): 資料通道只限系統管理員——完整驗證通過後另要求 perm 列 isAdmin='y' 且 isActive='y', 所有入口(main 之 RPC/表 select/系統函數, 上傳下載各 apiType)同一道閘; 一般使用者即使部署方放行亦拒絕(只能用 /api/getPerm 查本人權限樹)
|
|
570
590
|
checkToken: async(token, from = '') => {
|
|
571
591
|
|
|
572
|
-
//
|
|
573
|
-
await
|
|
592
|
+
//getAndVerifyAdminUser, reject 時 key: cannotFindUserFromToken / cannotGetUserFromPerm / userNoPermission / userNotAdmin 等
|
|
593
|
+
await getAndVerifyAdminUser(token, from)
|
|
574
594
|
|
|
575
595
|
return true
|
|
576
596
|
},
|
|
@@ -625,290 +645,6 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
625
645
|
}
|
|
626
646
|
|
|
627
647
|
|
|
628
|
-
//syncAndReplaceTabs
|
|
629
|
-
let syncAndReplaceTabs = async (userId, inp, keyTable) => {
|
|
630
|
-
|
|
631
|
-
//序列化同表之並行整表 delAll+insert: 防 lost update (同 keyTable 序列化, 不同表並行)
|
|
632
|
-
//與後台整表寫入(procCore.updateTabItems)共用同表互斥鍵 tabWriteKey: 兩者各自先檢查名稱/對應鍵唯一後寫入, 鍵不同則交錯可略過 F-02/F-14(外審 sol R10);
|
|
633
|
-
//有改名或刪除而須改寫引用端時, 於內層依階層再取引用端表之鎖(同步群組/權限/標的時, 被刪除或改名者之引用須連帶清理/更新, R-REF-CASCADE-01), 皆經單一取鎖函數(R-WRITE-LOCK-02)
|
|
634
|
-
return await withTabLocks(kmx, [keyTable], async () => {
|
|
635
|
-
|
|
636
|
-
//from
|
|
637
|
-
let from = get(inp, 'from', '')
|
|
638
|
-
// console.log('from', from)
|
|
639
|
-
|
|
640
|
-
//check
|
|
641
|
-
if (!isestr(from)) {
|
|
642
|
-
console.log('inp', inp)
|
|
643
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidFrom', keyTable })
|
|
644
|
-
return Promise.reject(`invalidFrom`)
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
//rows
|
|
648
|
-
let rows = get(inp, 'rows', [])
|
|
649
|
-
// console.log(keyTable, rows)
|
|
650
|
-
|
|
651
|
-
//check
|
|
652
|
-
if (!isearr(rows)) {
|
|
653
|
-
console.log('inp', inp)
|
|
654
|
-
console.log('keyTable', keyTable)
|
|
655
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', keyTable })
|
|
656
|
-
return Promise.reject(`invalidRows`)
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
//各列須為非空物件(規則帳本 R-ORM-05, 與後台整表寫入共用 isRowObj), 否則整批拒絕且不進 delAll:
|
|
660
|
-
//修正前非物件元素(字串、數字、null)使下方 v.from = from 拋 TypeError, 經 pm2resolve 把引擎原文外送給外部應用(R-ORM-02), 元素為 {} 時則寫入一筆自動主鍵之空白列
|
|
661
|
-
let kBadRow = rows.findIndex((v) => !isRowObj(v))
|
|
662
|
-
if (kBadRow >= 0) {
|
|
663
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', reason: 'rowInvalid', keyTable, from, row: kBadRow })
|
|
664
|
-
return Promise.reject(`invalidRows`)
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
//keyAssign/hasAssign: 後台可寫之指派欄(users.cgrups/grups.cpemis/pemis.crules; targets 無)及各列是否自帶該鍵(須於 ltdtmapping 缺鍵補 '' 之前記錄);
|
|
668
|
-
//缺該鍵者於下方保留庫內指派(F-23, 指派欄屬後台, 見 syncKeep.mjs)
|
|
669
|
-
let keyAssign = (keyTable === 'users') ? 'cgrups' : ((keyTable === 'grups') ? 'cpemis' : ((keyTable === 'pemis') ? 'crules' : null))
|
|
670
|
-
let hasAssign = (keyAssign !== null) ? markHasKey(rows, keyAssign) : null
|
|
671
|
-
|
|
672
|
-
//save from
|
|
673
|
-
rows = map(rows, (v, k) => {
|
|
674
|
-
// v.order = k + 1 //order大部分由外部給予, 不能複寫
|
|
675
|
-
v.from = from
|
|
676
|
-
return v
|
|
677
|
-
})
|
|
678
|
-
|
|
679
|
-
//ltdtmapping
|
|
680
|
-
rows = ltdtmapping(rows, ds[keyTable].keys)
|
|
681
|
-
// console.log('ltdtmapping rows', rows)
|
|
682
|
-
|
|
683
|
-
//映射到 schema 欄位後, 除伺服器管理欄(from 與建立者/建立時間/更新者/更新時間, 寫入時由伺服器覆寫)外, 沒有任何欄位帶有內容者為空白列, 整批拒絕(規則帳本 R-ORM-05; 外審 minimax#3、sol#3/#4):
|
|
684
|
-
//上方 isRowObj 只擋 {} 與非物件, {"foo":1}、{"length":5} 等「只含非 schema 欄位」之列映射後與 {} 相同, 仍會被 insert 成自動主鍵之空白列;
|
|
685
|
-
//只帶伺服器管理欄(如 {"userId":"x"})之列, 該值於寫入時被覆寫, 結果同為空白列; 欄位值為 []/{} 等空容器亦非內容.
|
|
686
|
-
//內容 = 非空字串、有限數字(0 亦是)、布林
|
|
687
|
-
let kpSrvKey = { from: true, userId: true, timeCreate: true, userIdUpdate: true, timeUpdate: true }
|
|
688
|
-
let isContent = (x) => (typeof x === 'string' && x !== '') || (typeof x === 'number' && Number.isFinite(x)) || typeof x === 'boolean'
|
|
689
|
-
let kBlank = rows.findIndex((v) => !Object.keys(v).some((k) => kpSrvKey[k] !== true && isContent(v[k])))
|
|
690
|
-
if (kBlank >= 0) {
|
|
691
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', reason: 'rowBlank', keyTable, from, row: kBlank })
|
|
692
|
-
return Promise.reject(`invalidRows`)
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
//名稱/標的 id 須為非空字串(規則帳本 R-SYNC-NAME-01, 主控複審 R2): 比照後台整表寫入之 ckKey(procCore.mjs: isestr(value)),
|
|
696
|
-
//grups/pemis 之 name、targets 之 id 缺漏、空字串、非字串(數字、布林、null)一律整批拒收, 於 delAll 之前, 該 from 與他 from 之既有列不變.
|
|
697
|
-
//why: 修正前 name:'' 之群組/權限可入庫, 伺服端 arrFinder 以 === 採納之(授權有效), 編輯器(cvKpToObj/doSave)視空名稱為無效, 管理者存檔即移除該授權
|
|
698
|
-
//(移除 AND 限制時等於增權); 與後台路徑(ckKey 拒收空名稱)不對稱. users 不在範圍(name 可重複); 不比照 ckKey 之重複偵測(複審範圍僅「非空」)
|
|
699
|
-
let keyName = (keyTable === 'targets') ? 'id' : ((keyTable === 'grups' || keyTable === 'pemis') ? 'name' : null)
|
|
700
|
-
if (keyName !== null) {
|
|
701
|
-
let kName = rows.findIndex((v) => !isestr(get(v, keyName)))
|
|
702
|
-
if (kName >= 0) {
|
|
703
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', reason: (keyName === 'id') ? 'idInvalid' : 'nameInvalid', keyTable, from, row: kName, field: keyName })
|
|
704
|
-
return Promise.reject(`invalidRows`)
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
//群組/權限名稱唯一(F-02, 規則帳本 R-NAME-UNIQ-01, ADR-034), 本批 rows 內 name 精確 === 重複整批拒收(他 from 已使用者見下方 nameOtherFrom), 於 delAll 之前, 該 from 與他 from 之既有列不變.
|
|
709
|
-
//why: 授權以名稱查找(arrFinder 取第一個同名列), 同名並存即授權取決於資料庫列序; 後台 ckKey/updateTabItems 早已拒同名, 外部同步原不拒(ADR-031 決定 4 曾明載不比照), 兩入口不對稱.
|
|
710
|
-
//外部應用只收固定語系鍵 invalidRows(規則帳本 R-ORM-02); targets 之 id 為主鍵(validateRowsBeforeWrite 已查批內重複), users 之 name 允許重複, 皆不在此
|
|
711
|
-
if (keyName === 'name') {
|
|
712
|
-
let kDup = findDupIndex(rows, 'name')
|
|
713
|
-
if (kDup >= 0) {
|
|
714
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', reason: 'nameDuplicate', keyTable, from, row: kDup, field: 'name' })
|
|
715
|
-
return Promise.reject(`invalidRows`)
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
//users 對應鍵批內檢查(F-14, 規則帳本 R-MAPKEY-01, ADR-034): mappingBy 為 'email'/'name' 時, 每個 isActive==='y' 之列該欄位須非空字串且批內唯一(精確 ===); 'id'(預設)不另檢查;
|
|
720
|
-
//只有「對應鍵」才是登入查找(getTokenUser)之鍵, 其唯一性才是安全前提——預設部署之 email 可空/可重複, 不得因新規則使其外部同步全數失敗. log 不記對應值(個資)
|
|
721
|
-
if (keyTable === 'users') {
|
|
722
|
-
let mk = checkMapKeyBatch(rows, mappingBy)
|
|
723
|
-
if (mk !== null) {
|
|
724
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', reason: mk.reason, keyTable, from, row: mk.k, field: mappingBy })
|
|
725
|
-
return Promise.reject(`invalidRows`)
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
//寫入前驗證(規則帳本 R-ORM-01, ADR-026): 於 delAll 之前擋下可預判之寫入失敗, 該 from 之既有列不變(ADR-008 防誤清源).
|
|
730
|
-
//why: 先 delAll 再 insert, 而 w-serv-orm 1.1.8 起對「會被 clearXSS 改寫之主鍵」整批拒絕、LMDB 對 > 1978 B 之主鍵整批拒絕,
|
|
731
|
-
//修正前遇之即該 from 之既有列全數清空(2026-10-14 V4/V7 實測). 主鍵問題 → invalidRows; 任一字串欄位含標記 → htmlMarkupNotAllowed
|
|
732
|
-
let ve = validateRowsBeforeWrite(rows)
|
|
733
|
-
if (ve !== null) {
|
|
734
|
-
let key = (ve.reason === 'htmlMarkup') ? 'htmlMarkupNotAllowed' : 'invalidRows'
|
|
735
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, reason: ve.reason, keyTable, from, row: ve.k, field: ve.field })
|
|
736
|
-
return Promise.reject(key)
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
//跨 from 同鍵(規則帳本 R-SYNC-01): 本批有給之主鍵若已屬其他 from, 整批拒絕且不進 delAll(本 from 與他 from 之既有列皆不變).
|
|
740
|
-
//why: insert 對已存在之主鍵略過而整批仍回 success, 該列不會寫入本 from(靜默遺失); w-serv-orm 1.1.11 起數字主鍵存為其字串形式(原被換成隨機主鍵),
|
|
741
|
-
//以流水號為主鍵之各應用即互撞(2026-10-14 w-serv-orm 實測). 此處直呼 woItems(不經 procOrm 之轉型), 查詢條件一律用 String(id)(R-ORM-04);
|
|
742
|
-
//同 from 之既有列不算衝突(隨後由 delAll 重寫); 本段與 delAll/insert 同在 kmx 內(同表之並行同步已序列化)
|
|
743
|
-
let keyOfId = (v) => {
|
|
744
|
-
let id = get(v, 'id')
|
|
745
|
-
return (isestr(id) || (typeof id === 'number' && Number.isFinite(id))) ? String(id) : null
|
|
746
|
-
}
|
|
747
|
-
let sids = rows.map(keyOfId).filter((s) => s !== null)
|
|
748
|
-
if (sids.length > 0) {
|
|
749
|
-
let rsExist = null
|
|
750
|
-
try {
|
|
751
|
-
rsExist = await woItems[keyTable].select({ id: { $in: sids } })
|
|
752
|
-
}
|
|
753
|
-
catch (err) {
|
|
754
|
-
let raw = (typeof err === 'string') ? err : ((err && err.message) || String(err))
|
|
755
|
-
let key = toOutKey(err, 'anUnexpectedErrorOccurred') //尚未寫入任何資料, 不回 saveWriteFail
|
|
756
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, raw, keyTable, from, step: 'select' })
|
|
757
|
-
return Promise.reject(key)
|
|
758
|
-
}
|
|
759
|
-
let fromById = new Map()
|
|
760
|
-
for (let v of rsExist) {
|
|
761
|
-
fromById.set(String(v.id), v.from)
|
|
762
|
-
}
|
|
763
|
-
let k = rows.findIndex((v) => {
|
|
764
|
-
let s = keyOfId(v)
|
|
765
|
-
return s !== null && fromById.has(s) && fromById.get(s) !== from
|
|
766
|
-
})
|
|
767
|
-
if (k >= 0) {
|
|
768
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', reason: 'idOtherFrom', keyTable, from, row: k, field: 'id' })
|
|
769
|
-
return Promise.reject('invalidRows')
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
//跨 from 同名/同對應鍵(F-02/F-14, 規則帳本 R-NAME-UNIQ-01/R-MAPKEY-01): grups/pemis 之 name、users 於 mappingBy 為 email/name 時之該欄位(僅 active 列),
|
|
774
|
-
//已為其他 from 之既有列使用 → 整批拒絕且不進 delAll; 同 from 之既有列不算衝突(隨後由 delAll 重寫). 先 select 後比對, select 失敗之處置同上段(toOutKey, 未寫入任何資料)
|
|
775
|
-
let keyOther = (keyName === 'name') ? 'name' : ((keyTable === 'users') ? mapKeyOf(mappingBy) : null)
|
|
776
|
-
if (keyOther !== null) {
|
|
777
|
-
let rsAll = null
|
|
778
|
-
try {
|
|
779
|
-
rsAll = await woItems[keyTable].select()
|
|
780
|
-
}
|
|
781
|
-
catch (err) {
|
|
782
|
-
let raw = (typeof err === 'string') ? err : ((err && err.message) || String(err))
|
|
783
|
-
let key = toOutKey(err, 'anUnexpectedErrorOccurred') //尚未寫入任何資料, 不回 saveWriteFail
|
|
784
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, raw, keyTable, from, step: 'select' })
|
|
785
|
-
return Promise.reject(key)
|
|
786
|
-
}
|
|
787
|
-
let kOther = findKeyOtherFrom(rows, rsAll, from, keyOther, { onlyActive: keyTable === 'users' })
|
|
788
|
-
if (kOther >= 0) {
|
|
789
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', reason: (keyTable === 'users') ? 'mapKeyOtherFrom' : 'nameOtherFrom', keyTable, from, row: kOther, field: keyOther })
|
|
790
|
-
return Promise.reject('invalidRows')
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
//外部同步 payload 缺指派欄鍵之列保留庫內值(F-23; 主控定案: 指派欄屬後台, 同步不得清掉後台指派; 含該鍵則以外部為準): 修正前 ltdtmapping 缺鍵補 ''、delAll+insert 即清空後台已指派者.
|
|
795
|
-
//三個指派欄(users.cgrups/grups.cpemis/pemis.crules; run08 起 cpemis/crules 比照 cgrups)皆是; 只在確有缺鍵之列時才讀庫內(同一 from).
|
|
796
|
-
//users: 對應先依 id、無 id 者依 mappingBy(email/name)恰命中 1 筆, 對不到者視為新使用者(維持 ''), 對應鍵矛盾者不保留(keepAssignOnMissing);
|
|
797
|
-
//grups/pemis: 無登入對應鍵, 實體身分依 id(無 id 者依名稱恰 1 筆; 改名而 cascadeRename 為假者不保留; keepAssignByEntity). select 失敗之處置同上段(toOutKey, 尚未寫入任何資料)
|
|
798
|
-
if (hasAssign !== null && hasAssign.some((b) => b === false)) {
|
|
799
|
-
let rsSame = null
|
|
800
|
-
try {
|
|
801
|
-
rsSame = await woItems[keyTable].select({ from }) //此處 keyTable 為 users/grups/pemis(hasAssign 非 null); users 非登入查找, 不經 pickUniqueUser(SITE-KC-004 只管登入反查)
|
|
802
|
-
}
|
|
803
|
-
catch (err) {
|
|
804
|
-
let raw = (typeof err === 'string') ? err : ((err && err.message) || String(err))
|
|
805
|
-
let key = toOutKey(err, 'anUnexpectedErrorOccurred')
|
|
806
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, raw, keyTable, from, step: 'select' })
|
|
807
|
-
return Promise.reject(key)
|
|
808
|
-
}
|
|
809
|
-
//isKeepable: 保留值於 delAll 之前須再通過一次寫入前驗證(上方 validateRowsBeforeWrite 看到的是補出之空字串, 外審 sol R4); 不過者不保留(視為空, 收嚴), 只記筆數
|
|
810
|
-
let isKeepable = (val) => validateRowsBeforeWrite([{ id: 'keep', [keyAssign]: val }]) === null
|
|
811
|
-
let kr = (keyTable === 'users') ? keepAssignOnMissing({ rows, hasKey: hasAssign, rowsOldSameFrom: rsSame, mappingBy, isKeepable }) : keepAssignByEntity({ rows, hasKey: hasAssign, rowsOldSameFrom: rsSame, key: keyAssign, cascadeRename, isKeepable })
|
|
812
|
-
rows = kr.rows
|
|
813
|
-
if (kr.nKeep > 0) {
|
|
814
|
-
srLog.info({ event: 'syncAndReplaceTabs-keepAssign', keyTable, from, field: keyAssign, n: kr.nKeep })
|
|
815
|
-
}
|
|
816
|
-
if (kr.nSkip > 0) {
|
|
817
|
-
srLog.warn({ event: 'syncAndReplaceTabs-keepAssign-skip', keyTable, from, field: keyAssign, n: kr.nSkip, reason: 'notKeepable' })
|
|
818
|
-
}
|
|
819
|
-
if (kr.nConflict > 0) {
|
|
820
|
-
srLog.warn({ event: 'syncAndReplaceTabs-keepAssign-skip', keyTable, from, field: keyAssign, n: kr.nConflict, reason: (keyTable === 'users') ? 'mapKeyConflict' : 'identityConflict' }) //users: 對應鍵與 id 指向不同使用者; grups/pemis: 同名多筆無從對應或改名而引用不跟著改; 指派不移轉而清空(收嚴須附提示; 不含個資值)
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
//引用連帶(適用表 m/n, R-REF-CASCADE-01): 同步 grups/pemis/targets 時, 同一 from 內依穩定 id 判定——同 id 異名=改名級聯、舊 id 不在本批=刪除而連帶清理引用(寫入後仍存續之同名者不清理);
|
|
825
|
-
//未帶 id 之列(ORM 每次補隨機主鍵)改依名稱對應(見 buildSyncRowsNew). 預檢(新名撞懸空名、歷史同名組改名、容器內相撞、改寫後欄位驗證)於 delAll 之前, 被拒整批零變更;
|
|
826
|
-
//外部應用只收固定語系鍵 invalidRows/htmlMarkupNotAllowed(R-ORM-02), 動態原因入 srLog. select 失敗之處置同上段(toOutKey, 未寫入任何資料).
|
|
827
|
-
//是否須改寫引用端只依本表新舊列判定(diffRefTargets), 持有本表鎖之下即可決定是否再取引用端表之鎖(R-WRITE-LOCK-02); 取齊鎖之後才讀取引用端、規劃與預檢
|
|
828
|
-
let rowsPlan = null
|
|
829
|
-
let kind = null
|
|
830
|
-
if (Object.prototype.hasOwnProperty.call(REF_OF, keyTable)) {
|
|
831
|
-
try {
|
|
832
|
-
let rowsOld = await woItems[keyTable].select()
|
|
833
|
-
rowsPlan = { rowsOld, rowsNew: buildSyncRowsNew(keyTable, rowsOld, rows, from) }
|
|
834
|
-
}
|
|
835
|
-
catch (err) {
|
|
836
|
-
let raw = (typeof err === 'string') ? err : ((err && err.message) || String(err))
|
|
837
|
-
let key = toOutKey(err, 'anUnexpectedErrorOccurred')
|
|
838
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, raw, keyTable, from, step: 'select' })
|
|
839
|
-
return Promise.reject(key)
|
|
840
|
-
}
|
|
841
|
-
kind = diffRefTargets({ keyTable, rowsOld: rowsPlan.rowsOld, rowsNew: rowsPlan.rowsNew, opt: { cascadeRename } })
|
|
842
|
-
|
|
843
|
-
//外部 id 不穩定之偵測(主控定案: 不加開關; 同一 from 同一批內出現「同名異 id」=疑似 id 重編, id 消失視為刪除會每次清理引用而可能抹掉後台指派): 記 warn log(不記個資值, 只記筆數)
|
|
844
|
-
//並登錄供 getWebInfor.syncWarns 於頁面提示; 之後同一(表, from)之同步已無此情形即清除提示. 只適用有 name 之 grups/pemis(users 之 name 可重複, targets 之 id 即名稱)
|
|
845
|
-
if (keyTable === 'grups' || keyTable === 'pemis') {
|
|
846
|
-
let nRenum = countIdRenumber(rowsPlan.rowsOld.filter((o) => o !== null && typeof o === 'object' && o.from === from), rows)
|
|
847
|
-
setSyncWarn(keyTable, from, nRenum)
|
|
848
|
-
if (nRenum > 0) {
|
|
849
|
-
srLog.warn({ event: 'syncAndReplaceTabs-idRenumber', keyTable, from, n: nRenum })
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
//doWrite: delAll from, insert; ORM/I/O 失敗之原文只入 srLog, 外部應用收 saveWriteFail(規則帳本 R-ORM-02); 引用端先寫、失敗補償還原(cascadeWrite.mjs)
|
|
855
|
-
let doWrite = async (plan, holdersOld) => {
|
|
856
|
-
let r = null
|
|
857
|
-
try {
|
|
858
|
-
r = await writeWithCascade({
|
|
859
|
-
plan,
|
|
860
|
-
holdersOld,
|
|
861
|
-
woItems,
|
|
862
|
-
procOrm,
|
|
863
|
-
userId,
|
|
864
|
-
srLog,
|
|
865
|
-
writeMain: async () => {
|
|
866
|
-
await woItems[keyTable].delAll({ from })
|
|
867
|
-
return await procOrm(userId, keyTable, 'insert', rows) //須使用procOrm才有辦法自動給予相關欄位
|
|
868
|
-
},
|
|
869
|
-
})
|
|
870
|
-
}
|
|
871
|
-
catch (err) {
|
|
872
|
-
let raw = (typeof err === 'string') ? err : ((err && err.message) || String(err))
|
|
873
|
-
let key = toOutKey(err)
|
|
874
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, raw, keyTable, from })
|
|
875
|
-
return Promise.reject(key)
|
|
876
|
-
}
|
|
877
|
-
// console.log('r', r)
|
|
878
|
-
return r
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
//有改名或刪除: 再取引用端表之鎖, 讀取引用端、規劃、預檢後寫入
|
|
882
|
-
if (kind !== null && (kind.renames.length > 0 || kind.delNames.length > 0)) {
|
|
883
|
-
let holder = REF_OF[keyTable].holder
|
|
884
|
-
return await withTabLocks(kmx, [holder], async () => {
|
|
885
|
-
let holdersOld = null
|
|
886
|
-
try {
|
|
887
|
-
holdersOld = await woItems[holder].select()
|
|
888
|
-
}
|
|
889
|
-
catch (err) {
|
|
890
|
-
let raw = (typeof err === 'string') ? err : ((err && err.message) || String(err))
|
|
891
|
-
let key = toOutKey(err, 'anUnexpectedErrorOccurred')
|
|
892
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, raw, keyTable, from, step: 'select' })
|
|
893
|
-
return Promise.reject(key)
|
|
894
|
-
}
|
|
895
|
-
let plan = planRefChanges({ keyTable, rowsOld: rowsPlan.rowsOld, rowsNew: rowsPlan.rowsNew, holdersOld, opt: { cascadeRename } })
|
|
896
|
-
let errPlan = plan.err !== null ? plan.err : checkCascadePlan(plan)
|
|
897
|
-
if (errPlan !== null) {
|
|
898
|
-
let key = (errPlan === 'htmlMarkupNotAllowed') ? 'htmlMarkupNotAllowed' : 'invalidRows'
|
|
899
|
-
srLog.error({ event: 'syncAndReplaceTabs-error', msg: key, reason: errPlan, keyTable, from })
|
|
900
|
-
return Promise.reject(key)
|
|
901
|
-
}
|
|
902
|
-
return await doWrite(plan, holdersOld)
|
|
903
|
-
}, [keyTable])
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
return await doWrite(null, [])
|
|
907
|
-
|
|
908
|
-
})
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
|
|
912
648
|
//getGenUserByKV
|
|
913
649
|
let getGenUserByKV = async(keyUser, valueUser) => {
|
|
914
650
|
|
|
@@ -925,6 +661,46 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
925
661
|
}
|
|
926
662
|
|
|
927
663
|
|
|
664
|
+
//requireAdmin: 函數層級之角色檢查(perm-run03 範圍四; dspec/使用者身分驗證原則.md 二.4「函數層級須另檢查角色, 不得只靠部署方之授權設定把關」; w-web呼叫者身分驗證.md C2, 使用者 2026-10-23 裁示採②):
|
|
665
|
+
//改變狀態之 kpFunExt 函數(updateTargets/updatePemis/updateGrups/updateUsers; getWebInfor、getStaEvent、getStaEventTable 為唯讀不在此列)限 perm users 表中有效(isActive='y')且 isAdmin='y' 之使用者.
|
|
666
|
+
//perm 以自身 users 表為權威, 不採 token(getUserByToken)宣稱之 isAdmin(同 getTokenUser); userId 為 getUserIdByToken 回傳之 perm 內 id, 此處再新鮮讀取一次(角色可能於連線後被改, 不信任快取).
|
|
667
|
+
//不符即 reject 固定錯誤鍵 userNotAdmin、零寫入(先於 lockSave/表鎖/任何讀庫), 記 srLog(只記函數名與 userId, 不記 token 與使用者資料); 讀取失敗(ORM 例外)一律視同不符(fail-closed)
|
|
668
|
+
let requireAdmin = async (userId, funcName) => {
|
|
669
|
+
let u = null
|
|
670
|
+
try {
|
|
671
|
+
u = await getGenUserByKV('id', userId)
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
u = null
|
|
675
|
+
}
|
|
676
|
+
if (!isAdminRow(u)) { //縱深防禦: 資料通道已於連線閘與執行身分要求管理員(R-ADMIN-ONLY-01), 此處對新鮮讀取之列再判一次(連線後角色被降級之競態)
|
|
677
|
+
srLog.error({ event: 'requireAdmin-error', msg: 'userNotAdmin', funcName, userId })
|
|
678
|
+
return Promise.reject('userNotAdmin')
|
|
679
|
+
}
|
|
680
|
+
return u
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
//resolvePermUserId: /api/getPermUserInfor 之 userId(外部應用傳入)依登入對應解析(perm-run05, 規則帳本 R-MAPKEY-05; 主控 2026-10-24 收件裁示②):
|
|
685
|
+
//mappingBy 為 'id' 時, 外部應用手上只有外部使用者 id(即登入識別: 外部列對 srcKey、後台列對 id), 不知本系統配發之內部 id, 故與登入入口同一判準(keyCheck.findLoginUsers)解析:
|
|
686
|
+
//命中多筆(不論有效與否)→ 拒絕 ambiguousUserFromPerm(不取首筆); 恰 1 筆有效 → 回 { userId: 其內部 id, userFind: 該列 };
|
|
687
|
+
//其餘(查無或恰 1 筆停用)→ 回 { userId: null, userFind: null }(視同查無使用者, 回全停用之空權限, 與修正前查無使用者行為相同);
|
|
688
|
+
//以解析出之列直接算權限, 不再以內部 id 重讀(外審 sol 第一輪#2: 解析與重讀之間該列可被刪除、同 id 被另一人佔用, 重讀會取得另一人之權限);
|
|
689
|
+
//'email'/'name' 時 userId 維持內部 id(行為不變, userFind 為 undefined 表示照舊以 id 讀取). 僅此路由使用: /api/getPerm 之 userIdSelf 已是 getTokenUser 回傳之內部 id, 不得再解析.
|
|
690
|
+
let resolvePermUserId = async (userIdFind) => {
|
|
691
|
+
if (mapKeyOf(mappingBy) !== null) {
|
|
692
|
+
return { userId: userIdFind, userFind: undefined }
|
|
693
|
+
}
|
|
694
|
+
let { user, count } = await findLoginUsers(woItems.users, mappingBy, userIdFind)
|
|
695
|
+
if (count > 1) {
|
|
696
|
+
console.log('mappingBy', mappingBy, 'count', count)
|
|
697
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'ambiguousUserFromPerm', mappingBy, count })
|
|
698
|
+
return Promise.reject('ambiguousUserFromPerm')
|
|
699
|
+
}
|
|
700
|
+
return iseobj(user) ? { userId: user.id, userFind: user } : { userId: null, userFind: null }
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
|
|
928
704
|
//getGenUserByUserId
|
|
929
705
|
let getGenUserByUserId = async(userId) => {
|
|
930
706
|
|
|
@@ -941,10 +717,10 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
941
717
|
|
|
942
718
|
|
|
943
719
|
//getGenUserAndRulesByUserId
|
|
944
|
-
let getGenUserAndRulesByUserId = async (userId) => {
|
|
720
|
+
let getGenUserAndRulesByUserId = async (userId, userPre = undefined) => {
|
|
945
721
|
|
|
946
|
-
//userFind
|
|
947
|
-
let userFind = await getGenUserByUserId(userId)
|
|
722
|
+
//userFind, userPre 非 undefined 時(含 null=查無)為呼叫端已解析之列, 直接採用不重讀(resolvePermUserId); 否則依 userId 讀取
|
|
723
|
+
let userFind = (userPre !== undefined) ? userPre : await getGenUserByUserId(userId)
|
|
948
724
|
// console.log('userFind', userFind)
|
|
949
725
|
|
|
950
726
|
//grups
|
|
@@ -1054,8 +830,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1054
830
|
return Promise.reject(`tokenNoPermission`)
|
|
1055
831
|
}
|
|
1056
832
|
|
|
1057
|
-
//
|
|
1058
|
-
let user = await
|
|
833
|
+
//getAndVerifyAdminUser, 後台登入檢查只限系統管理員(perm-run01, R-ADMIN-ONLY-01): 一般使用者即使部署方放行亦拒絕(userNotAdmin, 前端顯示「無權進入權限後台」); 一般使用者查本人權限樹走 /api/getPerm
|
|
834
|
+
let user = await getAndVerifyAdminUser(token, 'getUserByToken')
|
|
1059
835
|
// console.log('user', user)
|
|
1060
836
|
|
|
1061
837
|
//check
|
|
@@ -1129,8 +905,9 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1129
905
|
return Promise.reject(`tokenNoPermission`)
|
|
1130
906
|
}
|
|
1131
907
|
|
|
1132
|
-
//getGenUserAndRulesByUserId, 查找到的client
|
|
1133
|
-
|
|
908
|
+
//getGenUserAndRulesByUserId, 查找到的client使用者與權限; 第2參傳入驗證時取得之列(userSelf), 直接採用不以 id 重讀:
|
|
909
|
+
//驗證與重讀之間(含 verifyClientUser 為 async 之等待期間)該列可能被改登入鍵或刪除後同 id 換人, 重讀會回另一人之資料與權限樹(同 /api/getPermUserInfor 之 resolvePermUserId, perm-run01 外審 sol#3)
|
|
910
|
+
let userWithRulesSelf = await getGenUserAndRulesByUserId(userIdSelf, userSelf)
|
|
1134
911
|
// console.log('getGenUserAndRulesByUserId', r)
|
|
1135
912
|
|
|
1136
913
|
//check
|
|
@@ -1206,8 +983,11 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1206
983
|
return Promise.reject(`tokenNoPermission`)
|
|
1207
984
|
}
|
|
1208
985
|
|
|
1209
|
-
//
|
|
1210
|
-
let
|
|
986
|
+
//resolvePermUserId, mappingBy 為 id 時依登入對應解析出使用者列(外部列傳 srcKey、後台列傳 id; 多筆拒絕), email/name 時維持內部 id
|
|
987
|
+
let userPerm = await resolvePermUserId(userIdFind)
|
|
988
|
+
|
|
989
|
+
//getGenUserAndRulesByUserId, 查找到的使用者與權限(已解析之列直接採用)
|
|
990
|
+
let userWithRulesFind = await getGenUserAndRulesByUserId(userPerm.userId, userPerm.userFind)
|
|
1211
991
|
// console.log('getGenUserAndRulesByUserId', userWithRulesFind)
|
|
1212
992
|
|
|
1213
993
|
//check
|
|
@@ -1303,7 +1083,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1303
1083
|
let inp = await parsePayload(req)
|
|
1304
1084
|
|
|
1305
1085
|
//syncAndReplaceTabs
|
|
1306
|
-
let r = await syncAndReplaceTabs(userId, inp, keyTable)
|
|
1086
|
+
let r = await sc.syncAndReplaceTabs(userId, inp, keyTable)
|
|
1307
1087
|
|
|
1308
1088
|
//info
|
|
1309
1089
|
srLog.info({ event: 'api/syncAndReplaceTabs-success', userId, keyTable })
|
|
@@ -1342,6 +1122,10 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1342
1122
|
verifyConn: async ({ apiType, authorization, query, headers, req }) => {
|
|
1343
1123
|
// console.log('verifyConn', `apiType[${apiType}]`, `authorization[${authorization}]`)
|
|
1344
1124
|
|
|
1125
|
+
//身分驗證責任(dspec/使用者身分驗證原則.md 第三節, 使用方套件標準註解): 框架(w-converhp、w-serv-hapi、w-serv-webdata、w-serv-orm)不驗證 token, 一律由本套件負責.
|
|
1126
|
+
//連線閘(verifyConn)與執行身分(getUserIdByToken)皆須完整驗證(token 有效、本系統使用者存在且啟用、部署方授權); hapi apis 路由各自驗證; 函數層級另檢查角色或成員(updateXxx 限 perm users 表之 isAdmin='y', requireAdmin); 公開入口只限列名者.
|
|
1127
|
+
//本函數: 驗連線標頭之 token(getAndVerifyAdminUser: 外部 getUserByToken → perm users 存在且 isActive='y' → 部署方 verifyClientUser(user, apiType) → perm 列 isAdmin='y', R-ADMIN-ONLY-01: 權限後台與資料通道只限系統管理員, 一般使用者查本人權限樹走 /api/getPerm).
|
|
1128
|
+
|
|
1345
1129
|
//token
|
|
1346
1130
|
let token = strdelleft(authorization, 7) //刪除Bearer
|
|
1347
1131
|
// console.log('token', token)
|
|
@@ -1391,11 +1175,25 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1391
1175
|
},
|
|
1392
1176
|
getUserIdByToken: async (token) => { //可使用async或sync函數
|
|
1393
1177
|
// console.log('getUserIdByToken', token)
|
|
1394
|
-
|
|
1178
|
+
|
|
1179
|
+
//身分驗證責任(dspec/使用者身分驗證原則.md 第三節, 使用方套件標準註解; 規則 w-web呼叫者身分驗證.md 規則 4): 框架(w-converhp、w-serv-hapi、w-serv-webdata、w-serv-orm)不驗證 token, 一律由本套件負責.
|
|
1180
|
+
//連線閘(verifyConn)與執行身分(getUserIdByToken)皆須完整驗證(token 有效、本系統使用者存在且啟用、部署方授權); hapi apis 路由各自驗證; 函數層級另檢查角色或成員; 公開入口只限列名者.
|
|
1181
|
+
//本函數: 資料通道只傳遞 token, 反查使用者即驗證身分之步驟(w-serv-orm 1.1.10 起 ORM 層不做使用者檢查): 與連線閘同等完整——經 getAndVerifyAdminUser(外部 getUserByToken → perm users 存在且 isActive='y' → 部署方 verifyClientUser → perm 列 isAdmin='y'),
|
|
1182
|
+
//任一關不過即 reject(框架據此不執行函數, fail-closed)並記 srLog, 不回顯 token 與使用者資料. 請求標頭與本體 __sysToken__ 縱為不同 token, 兩個身分亦各經完整驗證(框架不比對兩 token).
|
|
1183
|
+
//回傳 perm users 表內該使用者之 id(非外部 token 宣稱之 id): 稽核欄位(userId/userIdUpdate)與函數層級角色檢查(requireAdmin)皆以 perm 內之列為準; mappingBy 為 email/name 時兩者可不同.
|
|
1184
|
+
//from 取 'main': 本函數只在資料通道 RPC/表操作(apiType 'main')之執行身分反查時被框架呼叫(上傳下載通道無監聽), 與連線閘對同一請求所用之 apiType 一致, 使部署方 verifyClientUser 依 from 分流之政策兩處一致, 不引入新的 from 值.
|
|
1185
|
+
//perm-run01(R-ADMIN-ONLY-01): 與連線閘同為「只限系統管理員」——另要求 perm 列 isAdmin='y' 且 isActive='y'(getAndVerifyAdminUser), 否則 reject userNotAdmin, 框架即不執行函數(含唯讀函數與表 select).
|
|
1186
|
+
let user = null
|
|
1187
|
+
try {
|
|
1188
|
+
user = await getAndVerifyAdminUser(token, 'main')
|
|
1189
|
+
}
|
|
1190
|
+
catch (err) {
|
|
1191
|
+
let key = toOutKey(err, 'cannotFindUserId') //已知語系鍵原樣(cannotFindUserFromToken/cannotGetUserFromPerm/userNoPermission/userNotAdmin 等), 其餘(ORM/I/O 原文、Error)一律 cannotFindUserId, 不外送原文
|
|
1192
|
+
srLog.error({ event: 'getUserIdByToken-error', msg: key })
|
|
1193
|
+
return Promise.reject(key)
|
|
1194
|
+
}
|
|
1395
1195
|
let userId = get(user, 'id', '')
|
|
1396
1196
|
if (!isestr(userId)) {
|
|
1397
|
-
console.log('token', maskTok(token))
|
|
1398
|
-
console.log('userId', userId)
|
|
1399
1197
|
srLog.error({ event: 'getUserIdByToken-error', msg: 'cannotFindUserId' })
|
|
1400
1198
|
return Promise.reject(`cannotFindUserId`)
|
|
1401
1199
|
}
|
|
@@ -1407,28 +1205,39 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1407
1205
|
//operOrm: procOrm 之包裝(輸入為 userId, tableName, methodName, input), ORM 拒絕之原文只入 srLog、對前端回 key(規則帳本 R-ORM-02)
|
|
1408
1206
|
operOrm: wrapOperOrm(procOrm, { srLog }),
|
|
1409
1207
|
tableNamesExec,
|
|
1410
|
-
//資料通道只開 select(ADR-026、ADR-015 Update; 規則帳本 R-WSD-01): 前端同步只用 select, 寫入一律經 kpFunExt 之 updateXxx(lockSave
|
|
1208
|
+
//資料通道只開 select(ADR-026、ADR-015 Update; 規則帳本 R-WSD-01): 前端同步只用 select, 寫入一律經 kpFunExt 之 updateXxx(lockSave、表鎖、寫入前驗證、整表比對);
|
|
1411
1209
|
//裸 insert/save/del/delAll 繞過上述不變式, 2026-10-14 起不開放(直呼回 invalid func[表名.方法]); 對自寫客戶端直呼裸表操作之部署方為行為變更(README Upgrade Notes)
|
|
1412
1210
|
methodsExec: ['select'],
|
|
1413
1211
|
//authorizeExec: w-serv-webdata 1.1.7 起 useDbOrm:true 必填; 只對已取得 userId 之 select 放行(methodsExec 之外之第二道), 拒絕記 srLog event 'authorizeExec'.
|
|
1414
|
-
//連線者身分之授權在 verifyConn(
|
|
1212
|
+
//連線者身分之授權在 verifyConn(getAndVerifyAdminUser: 完整驗證+限系統管理員), 此處不重做
|
|
1415
1213
|
authorizeExec: createAuthorizeExec({ srLog }),
|
|
1416
1214
|
tableNamesSync,
|
|
1417
1215
|
kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數
|
|
1418
1216
|
|
|
1419
1217
|
getWebInfor,
|
|
1420
1218
|
|
|
1421
|
-
//updateXxx 第 3 參 opt 為用戶端給之 { baseline }(草稿載入基準, F-26, R-BASELINE-01), 其餘鍵一律不採納(pickKeysOnly
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1219
|
+
//updateXxx 第 3 參 opt 為用戶端給之 { baseline, returnRows }(草稿載入基準, F-26, R-BASELINE-01; 是否於成功回傳附寫入後之列, 規格第 8 節), 其餘鍵一律不採納(pickKeysOnly 等為伺服端設定, 更不轉給 procOrm); 未給(舊客戶端)見 procCore 之處置
|
|
1220
|
+
//四個寫入函數皆先經 requireAdmin(函數層級角色檢查, perm-run03 範圍四): 非 perm users 表中有效之系統管理員 → userNotAdmin、零寫入
|
|
1221
|
+
updateTargets: async (userId, rows, opt) => {
|
|
1222
|
+
await requireAdmin(userId, 'updateTargets')
|
|
1223
|
+
return await pc.updateTargets(userId, rows, opt)
|
|
1224
|
+
},
|
|
1225
|
+
updatePemis: async (userId, rows, opt) => {
|
|
1226
|
+
await requireAdmin(userId, 'updatePemis')
|
|
1227
|
+
return await pc.updatePemis(userId, rows, opt)
|
|
1228
|
+
},
|
|
1229
|
+
updateGrups: async (userId, rows, opt) => {
|
|
1230
|
+
await requireAdmin(userId, 'updateGrups')
|
|
1231
|
+
return await pc.updateGrups(userId, rows, opt)
|
|
1232
|
+
},
|
|
1425
1233
|
//updateUsers, modeEditUsers='for:grups' 時 API 與 UI 一致只採納既有使用者之 cgrups 變更, 身分欄/新增/刪除/順序以資料庫為準
|
|
1426
|
-
updateUsers: (userId, rows, optClient) => {
|
|
1427
|
-
|
|
1234
|
+
updateUsers: async (userId, rows, optClient) => {
|
|
1235
|
+
await requireAdmin(userId, 'updateUsers')
|
|
1236
|
+
let opt = pickClientOpt(optClient)
|
|
1428
1237
|
if (modeEditUsers === 'for:grups') {
|
|
1429
1238
|
opt.pickKeysOnly = ['cgrups']
|
|
1430
1239
|
}
|
|
1431
|
-
return pc.updateUsers(userId, rows, opt)
|
|
1240
|
+
return await pc.updateUsers(userId, rows, opt)
|
|
1432
1241
|
},
|
|
1433
1242
|
|
|
1434
1243
|
getStaEvent: async (userId, timeLength, timeInterval) => {
|