w-web-sso 1.1.12 → 1.1.13
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 +16 -0
- package/dist/css/{app.b17b0ba0.css → app.f000459f.css} +1 -1
- package/dist/index.tmp +1 -1
- package/dist/js/{app-legacy.bee6840f.js → app-legacy.7c006893.js} +1 -1
- package/dist/js/{app.bee6840f.js → app.7c006893.js} +1 -1
- package/dist/js/{chunk-vendors.0492b6f5.js → chunk-vendors-legacy.97013ddc.js} +5 -5
- package/dist/js/{chunk-vendors-legacy.0492b6f5.js → chunk-vendors.97013ddc.js} +5 -5
- package/dist/w-web-sso.umd.js +4 -4
- package/dist/w-web-sso.umd.js.map +1 -1
- package/docs/WWebSso.html +1 -1
- package/docs/global.html +1 -1
- package/docs/index.html +1 -1
- package/docs/server_WWebSso.mjs.html +16 -10
- package/docs/server_getIgnoredOrmKeys.mjs.html +1 -1
- package/docs/server_lockSave.mjs.html +1 -1
- package/docs/server_ormWrite.mjs.html +1 -1
- package/docs/server_staLogs_resolveShellWorker.mjs.html +1 -1
- package/docs/server_wrapTimer.mjs.html +1 -1
- package/docs/src_components_Common_CheckYesNo.vue.html +1 -1
- package/docs/src_fetchSsoMsg.mjs.html +1 -1
- package/docs/src_httpGetJson.mjs.html +1 -1
- package/docs/src_maskUrl.mjs.html +1 -1
- package/docs/src_plugins_staUsage.mjs.html +1 -1
- package/docs/src_plugins_submitGuard.mjs.html +1 -1
- package/package.json +2 -2
- package/server/WWebSso.mjs +15 -9
- package/server/procCore.mjs +791 -379
- package/server/procLang.mjs +18 -0
- package/server/procProtect.mjs +150 -88
- package/src/baselineOf.mjs +76 -0
- package/src/components/LayoutContentIps.vue +24 -2
- package/src/components/LayoutContentTokens.vue +24 -2
- package/src/components/LayoutContentUsers.vue +9 -3
- package/src/schema/tables/ips.mjs +4 -0
- package/src/schema/tables/tokens.mjs +4 -0
- package/src/writeContracts/index.mjs +9 -0
- package/src/writeContracts/ips.mjs +9 -0
- package/src/writeContracts/tokens.mjs +12 -0
- package/src/writeContracts/tokensSession.mjs +10 -0
- package/src/writeContracts/users.mjs +15 -0
package/server/procCore.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import size from 'lodash-es/size.js'
|
|
|
7
7
|
import keys from 'lodash-es/keys.js'
|
|
8
8
|
import filter from 'lodash-es/filter.js'
|
|
9
9
|
import isEqual from 'lodash-es/isEqual.js'
|
|
10
|
+
import isPlainObject from 'lodash-es/isPlainObject.js'
|
|
11
|
+
import { BASELINE_REJECT, digestRow, compareBaseline, isBaselineValid, pickWriteRow, getRejectKey } from 'w-serv-orm/src/baseline.mjs'
|
|
10
12
|
import genIDSeq from 'wsemi/src/genIDSeq.mjs'
|
|
11
13
|
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
12
14
|
import isestr from 'wsemi/src/isestr.mjs'
|
|
@@ -24,7 +26,6 @@ import j2o from 'wsemi/src/j2o.mjs'
|
|
|
24
26
|
import strleft from 'wsemi/src/strleft.mjs'
|
|
25
27
|
import strright from 'wsemi/src/strright.mjs'
|
|
26
28
|
import strdelright from 'wsemi/src/strdelright.mjs'
|
|
27
|
-
import ltdtDiffByKey from 'wsemi/src/ltdtDiffByKey.mjs'
|
|
28
29
|
import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
|
|
29
30
|
import haskey from 'wsemi/src/haskey.mjs'
|
|
30
31
|
import arrHas from 'wsemi/src/arrHas.mjs'
|
|
@@ -46,12 +47,91 @@ import createLockSave from './lockSave.mjs'
|
|
|
46
47
|
import ormWrite from './ormWrite.mjs'
|
|
47
48
|
import { maskToken, maskKv, keysUserQuery, capText } from './srLog.mjs'
|
|
48
49
|
import { hasAppPerm, isAppPermsValue, normAppPerms } from '../src/appPerms.mjs'
|
|
50
|
+
import contracts from '../src/writeContracts/index.mjs'
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
//keysUserCredential: users 表之憑證欄(密碼雜湊 / 註冊驗證用 token), 對外介接輸出一律剝除 (ADR-068); 新增憑證欄時於此登錄
|
|
52
54
|
let keysUserCredential = ['password', 'tokenVerify']
|
|
53
55
|
|
|
54
56
|
|
|
57
|
+
//kpSaveRejectKey: 寫入契約之拒收鍵(w-serv-orm baseline.mjs 之 BASELINE_REJECT, 固定英文) → 本套件語系鍵 (procLang), 一律提示重新載入 (R-CONC-03 規則 5)
|
|
58
|
+
let kpSaveRejectKey = Object.create(null)
|
|
59
|
+
kpSaveRejectKey[BASELINE_REJECT.mismatch] = 'saveStale'
|
|
60
|
+
kpSaveRejectKey[BASELINE_REJECT.rowNotFound] = 'saveRowNotFound'
|
|
61
|
+
kpSaveRejectKey[BASELINE_REJECT.format] = 'saveBaselineFormat'
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
//getSaveRejectKey: 由被 reject 之值取語系鍵; 非契約拒收者回 null. 取鍵一律用核心之 getRejectKey (procOrm 為字串, guardWrite 為帶 key 之 Error)
|
|
65
|
+
function getSaveRejectKey(err) {
|
|
66
|
+
let k = getRejectKey(err)
|
|
67
|
+
if (typeof k === 'string' && kpSaveRejectKey[k] !== undefined) {
|
|
68
|
+
return kpSaveRejectKey[k]
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
//copyBaseline: 基準之防禦性複製, 只取資料屬性(RPC 之 JSON 不會帶存取器, 行程內直接呼叫才可能): 外層與各二元項須為陣列且二元項長度恰為 2,
|
|
75
|
+
//存取器 / 洞 / 取值例外一律回 null; 複製後之驗證與其後使用皆不再觸及客戶端物件, 存取器不被執行
|
|
76
|
+
function copyBaseline(b) {
|
|
77
|
+
try {
|
|
78
|
+
if (!Array.isArray(b)) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
let out = []
|
|
82
|
+
for (let i = 0; i < b.length; i++) {
|
|
83
|
+
let dI = Object.getOwnPropertyDescriptor(b, i)
|
|
84
|
+
if (dI === undefined || !('value' in dI) || !Array.isArray(dI.value) || dI.value.length !== 2) {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
let d0 = Object.getOwnPropertyDescriptor(dI.value, 0)
|
|
88
|
+
let d1 = Object.getOwnPropertyDescriptor(dI.value, 1)
|
|
89
|
+
if (d0 === undefined || d1 === undefined || !('value' in d0) || !('value' in d1)) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
out.push([d0.value, d1.value])
|
|
93
|
+
}
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
//parseSaveOpt: 驗證前端經 RPC 送入之第 4 參數(clientOpt)並自組伺服端使用之基準; opt 只由伺服端自組, 不展開客戶端物件 (規格 §6).
|
|
103
|
+
//- undefined: 不帶基準(app 權杖直呼、已開啟之舊版頁面, 本 major 允許, 後者蓋前者), 回 {}
|
|
104
|
+
//- 其餘一律須為純物件且只含白名單鍵(users / ips: baseline; tokens: baseline 與 baselineSession 兩者皆須有), 基準格式或版本不識 → baselineFormat; 不得降級為不帶基準
|
|
105
|
+
//回傳 { baseline, baselineSession } 或 { err: 'baselineFormat' }
|
|
106
|
+
function parseSaveOpt(woName, clientOpt) {
|
|
107
|
+
let bad = { err: BASELINE_REJECT.format }
|
|
108
|
+
if (clientOpt === undefined) {
|
|
109
|
+
return {}
|
|
110
|
+
}
|
|
111
|
+
if (!isPlainObject(clientOpt)) {
|
|
112
|
+
return bad
|
|
113
|
+
}
|
|
114
|
+
let allow = woName === 'tokens' ? ['baseline', 'baselineSession'] : ['baseline']
|
|
115
|
+
let ks = Reflect.ownKeys(clientOpt)
|
|
116
|
+
if (ks.some((k) => allow.indexOf(k) < 0) || allow.some((k) => ks.indexOf(k) < 0)) {
|
|
117
|
+
return bad
|
|
118
|
+
}
|
|
119
|
+
let o = {}
|
|
120
|
+
for (let k of allow) {
|
|
121
|
+
let d = Object.getOwnPropertyDescriptor(clientOpt, k)
|
|
122
|
+
if (d === undefined || !('value' in d)) {
|
|
123
|
+
return bad
|
|
124
|
+
}
|
|
125
|
+
let v = copyBaseline(d.value)
|
|
126
|
+
if (v === null || !isBaselineValid(v)) {
|
|
127
|
+
return bad
|
|
128
|
+
}
|
|
129
|
+
o[k] = v
|
|
130
|
+
}
|
|
131
|
+
return o
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
55
135
|
//maxKeyBytes: 主鍵 UTF-8 位元組數上限, 同 LMDB 預設鍵長上限(lmdb/open.js 之 DEFAULT_MAX_KEY_SIZE; 實測 1978 位元組可寫、1979 即拒), 供清單儲存於寫入前驗證 (ADR-081)
|
|
56
136
|
let maxKeyBytes = 1978
|
|
57
137
|
|
|
@@ -82,7 +162,13 @@ function timingSafePasswordEqual(a, b) {
|
|
|
82
162
|
}
|
|
83
163
|
|
|
84
164
|
|
|
85
|
-
function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, passwordPolicy, allowUserRegistration, siteUrl, verifyBaseUrl }) {
|
|
165
|
+
function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, passwordPolicy, allowUserRegistration, siteUrl, verifyBaseUrl, withTabLocks, guardWrite }) {
|
|
166
|
+
|
|
167
|
+
//withTabLocks / guardWrite: 取自 w-serv-orm WServOrm 實例(每實例一個鎖管理器、每表一鍵, 規格 §7); 所有寫入 users / tokens / ips 之站點皆須在該表表鎖內執行 (R-CONC-01),
|
|
168
|
+
//且與 procProtect 共用同一組(同表只由同一鎖管理器寫入, 規格 §12). 缺少即無法保證互斥, 故不提供預設替身
|
|
169
|
+
if (!isfun(withTabLocks) || !isfun(guardWrite)) {
|
|
170
|
+
throw new Error('procCore needs withTabLocks and guardWrite from the WServOrm instance')
|
|
171
|
+
}
|
|
86
172
|
|
|
87
173
|
|
|
88
174
|
//pmKeyMutex: per-key in-memory mutex, 同 key 序列化、不同 key 並行.
|
|
@@ -360,22 +446,24 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
360
446
|
let userId = get(u, 'id', '')
|
|
361
447
|
// console.log('userId', userId)
|
|
362
448
|
|
|
363
|
-
//createToken
|
|
364
|
-
|
|
449
|
+
//createToken, recheckUser: 驗密碼(CPU 密集)於鎖外, 鑄 token 時於 tokens 鎖內重讀使用者狀態 (R-CONC-01 W2-2):
|
|
450
|
+
//登入與停用 / 刪除 / 自動封鎖 / 重設密碼(皆於 [tokens, users] 鎖內撤銷 token)交錯時, 不致於撤銷之後才鑄出屬於已停用、已刪除、封鎖中或密碼已變更使用者之 token;
|
|
451
|
+
//verifiedUser 為鎖外通過驗證所依據之使用者, 鎖內以新鮮資料再判密碼未變、已驗證、未到期; withUser 令回傳 { token, user } 取鎖內新鮮之使用者, 登入回傳值由同一次有效狀態產生
|
|
452
|
+
let { token, user: uNow } = await createToken(userId, { recheckUser: true, verifiedUser: u, withUser: true })
|
|
365
453
|
// console.log('token', token)
|
|
366
454
|
|
|
367
455
|
//r
|
|
368
456
|
let r = {
|
|
369
|
-
id:
|
|
370
|
-
account:
|
|
371
|
-
name:
|
|
372
|
-
email:
|
|
373
|
-
description:
|
|
374
|
-
from:
|
|
375
|
-
redir:
|
|
376
|
-
isAdmin:
|
|
377
|
-
isActive:
|
|
378
|
-
isForceChangePw:
|
|
457
|
+
id: uNow.id,
|
|
458
|
+
account: uNow.account,
|
|
459
|
+
name: uNow.name,
|
|
460
|
+
email: uNow.email,
|
|
461
|
+
description: uNow.description,
|
|
462
|
+
from: uNow.from,
|
|
463
|
+
redir: uNow.redir,
|
|
464
|
+
isAdmin: uNow.isAdmin,
|
|
465
|
+
isActive: uNow.isActive,
|
|
466
|
+
isForceChangePw: uNow.isForceChangePw, //強制變更密碼旗標, 前端登入後判斷是否拉去 user view 強制變更
|
|
379
467
|
token,
|
|
380
468
|
}
|
|
381
469
|
|
|
@@ -384,7 +472,11 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
384
472
|
|
|
385
473
|
|
|
386
474
|
//createToken
|
|
387
|
-
|
|
475
|
+
//寫入於 tokens 表鎖內(S01); opt.recheckUser 為 true 時於鎖內重讀使用者, 已不存在、已停用或封鎖中即 reject 'failedLoginForCatch' (對外與密碼錯誤同回應, 不洩漏帳號狀態);
|
|
476
|
+
//另給 opt.verifiedUser(鎖外通過驗證所依據之使用者)時, 鎖內以新鮮資料再判登入條件: 密碼已變更 → 'failedLoginForCatch'、已不是已驗證 → 'userRegistrationNotVerified'、已到期 → 'loginAccountExpired',
|
|
477
|
+
//與鎖外同序同鍵; opt.withUser 為 true 時回傳 { token, user }(user 為鎖內新鮮之使用者, 僅 recheckUser 時有值), 否則回傳 token 字串
|
|
478
|
+
//userIdUpdate 記 token 擁有者(procOrm 以 userId 參數戳記, R-CONC-04)
|
|
479
|
+
let createToken = async (userId, opt = {}) => {
|
|
388
480
|
let errTemp = null
|
|
389
481
|
|
|
390
482
|
//check
|
|
@@ -407,13 +499,57 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
407
499
|
//token
|
|
408
500
|
let token = get(t, 'token', '')
|
|
409
501
|
|
|
410
|
-
//insert
|
|
411
|
-
|
|
502
|
+
//insert, tokens 表鎖內
|
|
503
|
+
let rejectKey = null
|
|
504
|
+
let uFresh = null
|
|
505
|
+
await withTabLocks(['tokens'], async () => {
|
|
506
|
+
|
|
507
|
+
//recheckUser
|
|
508
|
+
if (get(opt, 'recheckUser') === true) {
|
|
509
|
+
let us = await woItems.users.select({ id: userId })
|
|
510
|
+
let u = get(us, 0, null)
|
|
511
|
+
let blocked = false
|
|
512
|
+
let timeBlocked = get(u, 'timeBlocked', '')
|
|
513
|
+
if (istimemsTZ(timeBlocked) && ot().format('YYYY-MM-DDTHH:mm:ss.SSSZ') <= timeBlocked) {
|
|
514
|
+
blocked = true
|
|
515
|
+
}
|
|
516
|
+
if (size(us) !== 1 || get(u, 'isActive', '') === 'n' || blocked) {
|
|
517
|
+
rejectKey = 'failedLoginForCatch'
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
//verifiedUser: 鎖外通過驗證所依據之使用者; 登入條件以鎖內新鮮資料再判一次(與 loginByAccountAndPassword 同序同鍵)
|
|
522
|
+
let vu = get(opt, 'verifiedUser', null)
|
|
523
|
+
if (vu !== null) {
|
|
524
|
+
if (get(u, 'password', '') !== get(vu, 'password', '')) {
|
|
525
|
+
rejectKey = 'failedLoginForCatch' //等待期間密碼已被變更(如管理員重設): 先前以舊密碼通過之驗證不再有效
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
if (!isestr(get(u, 'timeVerified', ''))) {
|
|
529
|
+
rejectKey = 'userRegistrationNotVerified'
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
let timeExpired = get(u, 'timeExpired', '')
|
|
533
|
+
if (isestr(timeExpired) && istimemsTZ(timeExpired) && ot().format('YYYY-MM-DDTHH:mm:ss.SSSZ') > timeExpired) {
|
|
534
|
+
rejectKey = 'loginAccountExpired'
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
uFresh = u
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
await procOrm(userId, 'tokens', 'insert', [t])
|
|
543
|
+
|
|
544
|
+
})
|
|
412
545
|
.catch((err) => {
|
|
413
546
|
errTemp = err
|
|
414
547
|
})
|
|
415
548
|
|
|
416
549
|
//check
|
|
550
|
+
if (rejectKey !== null) {
|
|
551
|
+
return Promise.reject(rejectKey)
|
|
552
|
+
}
|
|
417
553
|
if (errTemp) {
|
|
418
554
|
console.log(errTemp)
|
|
419
555
|
// console.log(`token`, token)
|
|
@@ -421,6 +557,9 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
421
557
|
return Promise.reject(`can not create a token from userId`)
|
|
422
558
|
}
|
|
423
559
|
|
|
560
|
+
if (get(opt, 'withUser') === true) {
|
|
561
|
+
return { token, user: uFresh }
|
|
562
|
+
}
|
|
424
563
|
return token
|
|
425
564
|
}
|
|
426
565
|
|
|
@@ -616,6 +755,7 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
616
755
|
|
|
617
756
|
|
|
618
757
|
//refreshToken
|
|
758
|
+
//讀取、驗證與續期寫入皆在 tokens 表鎖內(S02): 讀到之 token 於寫入前不會被登出 / 撤銷插隊刪除, 且寫入為只改不增, 不致把已刪之 token 以殘缺列新增回庫 (X5)
|
|
619
759
|
let refreshToken = async (token) => {
|
|
620
760
|
|
|
621
761
|
//defense-in-depth NoSQL operator injection guard (reject key 名, 對齊 token 鏈統一 tokenExpired)
|
|
@@ -623,6 +763,13 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
623
763
|
return Promise.reject('tokenExpired')
|
|
624
764
|
}
|
|
625
765
|
|
|
766
|
+
return await withTabLocks(['tokens'], () => _refreshToken(token))
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
//_refreshToken, 須於 tokens 表鎖內呼叫
|
|
771
|
+
let _refreshToken = async (token) => {
|
|
772
|
+
|
|
626
773
|
let errTemp = null
|
|
627
774
|
|
|
628
775
|
//tks
|
|
@@ -677,15 +824,11 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
677
824
|
let timeEndNew = ot().add(minExpired, 'minute').format('YYYY-MM-DDTHH:mm:ss.SSSZ')
|
|
678
825
|
// console.log('timeEndNew', timeEndNew)
|
|
679
826
|
|
|
680
|
-
//timeUpdate
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
//save, 逐筆檢查(ADR-080): 單筆失敗時 ORM 仍 resolve, 不檢查即回報未生效之新到期時間
|
|
684
|
-
await ormWrite(woItems.tokens, 'save', {
|
|
827
|
+
//save, 經 procOrm 且 mustExist(只改不增, 列已不存在 → reject 'rowNotFound' 零寫入); procOrm 逐筆檢查(ADR-080)並戳記 timeUpdate / userIdUpdate(記 token 擁有者, R-CONC-04)
|
|
828
|
+
await procOrm(get(tk, 'userId', ''), 'tokens', 'save', [{
|
|
685
829
|
id: tk.id,
|
|
686
830
|
timeEnd: timeEndNew,
|
|
687
|
-
|
|
688
|
-
})
|
|
831
|
+
}], { mustExist: true })
|
|
689
832
|
.catch((err) => {
|
|
690
833
|
errTemp = err
|
|
691
834
|
})
|
|
@@ -704,6 +847,7 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
704
847
|
|
|
705
848
|
|
|
706
849
|
//logoutByToken
|
|
850
|
+
//讀取與刪除在 tokens 表鎖內(S03); 刪除未命中回報既有鍵 'can not delete the token'
|
|
707
851
|
let logoutByToken = async (token) => {
|
|
708
852
|
|
|
709
853
|
//defense-in-depth NoSQL operator injection guard (reject key 名, 對齊 token 鏈統一 tokenExpired)
|
|
@@ -711,6 +855,13 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
711
855
|
return Promise.reject('tokenExpired')
|
|
712
856
|
}
|
|
713
857
|
|
|
858
|
+
return await withTabLocks(['tokens'], () => _logoutByToken(token))
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
//_logoutByToken, 須於 tokens 表鎖內呼叫
|
|
863
|
+
let _logoutByToken = async (token) => {
|
|
864
|
+
|
|
714
865
|
let errTemp = null
|
|
715
866
|
|
|
716
867
|
//tks
|
|
@@ -849,7 +1000,9 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
849
1000
|
let keyAccount = `createUser:account:${account}`
|
|
850
1001
|
let keyEmail = `createUser:email:${email}`
|
|
851
1002
|
let tokenVerify
|
|
852
|
-
|
|
1003
|
+
//唯一性檢查與 insert 於 users 表鎖內(S04, 修 X8): cst 之鍵空間與後台清單儲存(updateTabItems 取 users 表鎖)不同, 兩者原不互斥,
|
|
1004
|
+
//自助註冊與管理員新增同帳號可同時成功; 取同一把表鎖後, 後到者必能讀到先到者所寫之列而被唯一性檢查擋下
|
|
1005
|
+
await cst.setWithFree([keyAccount, keyEmail], () => withTabLocks(['users'], async () => {
|
|
853
1006
|
|
|
854
1007
|
//check account unique (全域唯一,不限 isActive)
|
|
855
1008
|
let allUsers = await woItems.users.select()
|
|
@@ -890,7 +1043,7 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
890
1043
|
srLog.error({ event: 'fun-createUser-insert', key: 'userRegistrationFailed', account, err: getErrorMessage(err) })
|
|
891
1044
|
return Promise.reject('userRegistrationFailed')
|
|
892
1045
|
})
|
|
893
|
-
})
|
|
1046
|
+
}))
|
|
894
1047
|
.catch((err) => {
|
|
895
1048
|
//remap setWithFree 之 'key in use' reject → 業務語意之 i18n 訊息.
|
|
896
1049
|
//其他 reject (select unique check 之 'account/email already exists', insert 失敗之 userRegistrationFailed 等)
|
|
@@ -932,41 +1085,46 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
932
1085
|
return Promise.reject('verifyEmailInvalidToken')
|
|
933
1086
|
}
|
|
934
1087
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
if (size(us) === 0) {
|
|
938
|
-
return Promise.reject('verifyEmailInvalidToken')
|
|
939
|
-
}
|
|
940
|
-
let user = us[0]
|
|
1088
|
+
//查找、檢查與寫入於 users 表鎖內(S05): timeVerified 為雙方欄位(後台可改、驗證信連結亦寫), 系統直寫須取表鎖, 後台持舊畫面存檔才會被偵測而拒收 (V4)
|
|
1089
|
+
return await withTabLocks(['users'], async () => {
|
|
941
1090
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1091
|
+
//以 tokenVerify 查找使用者
|
|
1092
|
+
let us = await woItems.users.select({ tokenVerify: token })
|
|
1093
|
+
if (size(us) === 0) {
|
|
1094
|
+
return Promise.reject('verifyEmailInvalidToken')
|
|
1095
|
+
}
|
|
1096
|
+
let user = us[0]
|
|
947
1097
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
id: user.id,
|
|
954
|
-
timeVerified: nowms2str(),
|
|
955
|
-
//F-053 fix (lazy clear): 不主動清 tokenVerify, 避免並發 resendVerifyEmail 寫的新 token
|
|
956
|
-
//被此處 race window 覆蓋. token 重用風險由 line ~818 之 isestr(timeVerified) → reject
|
|
957
|
-
//'verifyEmailAlreadyVerified' 擋住, 已驗證 user 點舊連結會得友善訊息.
|
|
958
|
-
})
|
|
959
|
-
.catch((err) => {
|
|
960
|
-
errTemp = err
|
|
961
|
-
})
|
|
1098
|
+
//check timeVerified (是否已驗證)
|
|
1099
|
+
let timeVerified = get(user, 'timeVerified', '')
|
|
1100
|
+
if (isestr(timeVerified)) {
|
|
1101
|
+
return Promise.reject('verifyEmailAlreadyVerified')
|
|
1102
|
+
}
|
|
962
1103
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1104
|
+
//update timeVerified: timemsTZ 格式(同其他時間欄與後台編輯之寫入). 原 now2str 為秒級, 共用判斷 getIsVerified(istimemsTZ)
|
|
1105
|
+
//判為未驗證 → 後台清單顯示「未驗證」, 且該帳號設為管理者後過不了管理權限閘 funCheckAdmin (2026-09-29 修正)
|
|
1106
|
+
//經 procOrm 且 mustExist(只改不增, 列已不存在則拒絕而不新增殘缺列, X5), 更新者記該使用者本人 (R-CONC-04)
|
|
1107
|
+
//逐筆檢查(ADR-080): 寫入失敗回 verifyEmailFailed, 不顯示驗證成功; tokenVerify 未清, 使用者可再點同一連結重試
|
|
1108
|
+
let errTemp = null
|
|
1109
|
+
await procOrm(user.id, 'users', 'save', [{
|
|
1110
|
+
id: user.id,
|
|
1111
|
+
timeVerified: nowms2str(),
|
|
1112
|
+
//F-053 fix (lazy clear): 不主動清 tokenVerify, 避免並發 resendVerifyEmail 寫的新 token
|
|
1113
|
+
//被此處 race window 覆蓋. token 重用風險由 line ~818 之 isestr(timeVerified) → reject
|
|
1114
|
+
//'verifyEmailAlreadyVerified' 擋住, 已驗證 user 點舊連結會得友善訊息.
|
|
1115
|
+
}], { mustExist: true })
|
|
1116
|
+
.catch((err) => {
|
|
1117
|
+
errTemp = err
|
|
1118
|
+
})
|
|
968
1119
|
|
|
969
|
-
|
|
1120
|
+
//check
|
|
1121
|
+
if (errTemp !== null) {
|
|
1122
|
+
srLog.error({ event: 'fun-verifyEmail-save', key: 'verifyEmailFailed', userId: user.id, err: getErrorMessage(errTemp) })
|
|
1123
|
+
return Promise.reject('verifyEmailFailed')
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return { state: 'success', msg: 'ok' }
|
|
1127
|
+
})
|
|
970
1128
|
}
|
|
971
1129
|
|
|
972
1130
|
|
|
@@ -994,43 +1152,50 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
994
1152
|
return Promise.reject('userRegistrationResendThrottled')
|
|
995
1153
|
}
|
|
996
1154
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
u = await _getGenUserByKV('account', account)
|
|
1001
|
-
}
|
|
1002
|
-
catch (err) {}
|
|
1003
|
-
if (!u) {
|
|
1004
|
-
return Promise.reject('userRegistrationResendInvalidEmail') //不洩露帳號是否存在
|
|
1005
|
-
}
|
|
1155
|
+
//查找、檢查與寫入於 users 表鎖內(S06; kmx 在外層、表鎖在內層): tokenVerify 雖屬伺服器專有欄位(規格 §4 不要求表鎖), 本套件統一取鎖並只改不增 (決策 D2),
|
|
1156
|
+
//使讀到之列於寫入前不被刪除插隊而以殘缺列新增回庫 (X5)
|
|
1157
|
+
let { userId, name, tokenVerify, errTemp } = await withTabLocks(['users'], async () => {
|
|
1006
1158
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1159
|
+
//getGenUserByAccount
|
|
1160
|
+
let u = null
|
|
1161
|
+
try {
|
|
1162
|
+
u = await _getGenUserByKV('account', account)
|
|
1163
|
+
}
|
|
1164
|
+
catch (err) {}
|
|
1165
|
+
if (!u) {
|
|
1166
|
+
return Promise.reject('userRegistrationResendInvalidEmail') //不洩露帳號是否存在
|
|
1167
|
+
}
|
|
1012
1168
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1169
|
+
//check email match
|
|
1170
|
+
let uEmail = get(u, 'email', '')
|
|
1171
|
+
if (uEmail !== email) {
|
|
1172
|
+
return Promise.reject('userRegistrationResendInvalidEmail')
|
|
1173
|
+
}
|
|
1018
1174
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1175
|
+
//check timeVerified
|
|
1176
|
+
let timeVerified = get(u, 'timeVerified', '')
|
|
1177
|
+
if (isestr(timeVerified)) {
|
|
1178
|
+
return Promise.reject('userRegistrationAlreadyVerified')
|
|
1179
|
+
}
|
|
1022
1180
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
tokenVerify
|
|
1181
|
+
//userId
|
|
1182
|
+
let userId = get(u, 'id', '')
|
|
1183
|
+
let name = get(u, 'name', '')
|
|
1184
|
+
|
|
1185
|
+
//產生新 tokenVerify 並更新至 users, 更新者記該使用者 (R-CONC-04)
|
|
1186
|
+
//逐筆檢查(ADR-080): 寫入失敗即 reject, 不記節流、不寄出連結無效之信
|
|
1187
|
+
let tokenVerify = `${genIDSeq()}`
|
|
1188
|
+
let errTemp = null
|
|
1189
|
+
await procOrm(userId, 'users', 'save', [{
|
|
1190
|
+
id: userId,
|
|
1191
|
+
tokenVerify,
|
|
1192
|
+
}], { mustExist: true })
|
|
1193
|
+
.catch((err) => {
|
|
1194
|
+
errTemp = err
|
|
1195
|
+
})
|
|
1196
|
+
|
|
1197
|
+
return { userId, name, tokenVerify, errTemp }
|
|
1030
1198
|
})
|
|
1031
|
-
.catch((err) => {
|
|
1032
|
-
errTemp = err
|
|
1033
|
-
})
|
|
1034
1199
|
|
|
1035
1200
|
//check
|
|
1036
1201
|
if (errTemp !== null) {
|
|
@@ -1147,55 +1312,62 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1147
1312
|
//'incorrect old password' reject 路徑, 自然不會多寄信.
|
|
1148
1313
|
return await kmx(`changeUserPassword:${userId}`, async () => {
|
|
1149
1314
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1315
|
+
//讀取、驗舊密碼與寫入於 users 表鎖內(S07; kmx 在外層、表鎖在內層): password 為伺服器專有欄位、isForceChangePw 為雙方欄位(系統直寫須取表鎖),
|
|
1316
|
+
//驗證所依之 DB 雜湊與寫入間不被其他寫入者(重設密碼、後台存檔)插隊; 只改不增, 列已被刪則拒絕而不新增殘缺列 (X5)
|
|
1317
|
+
let { u, email } = await withTabLocks(['users'], async () => {
|
|
1152
1318
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1319
|
+
//getGenUserByUserId (with password)
|
|
1320
|
+
let u = await _getGenUserByKV('id', userId, { deletePassword: false })
|
|
1155
1321
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if (r.state === 'error') {
|
|
1159
|
-
return Promise.reject(r.key)
|
|
1160
|
-
}
|
|
1322
|
+
//account
|
|
1323
|
+
let account = get(u, 'account', '')
|
|
1161
1324
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1325
|
+
//check newPassword
|
|
1326
|
+
let r = checkUserPassword(lang, newPassword, { account })
|
|
1327
|
+
if (r.state === 'error') {
|
|
1328
|
+
return Promise.reject(r.key)
|
|
1329
|
+
}
|
|
1164
1330
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
return Promise.reject('userChangePasswordIncorrectOld')
|
|
1168
|
-
}
|
|
1331
|
+
//passwordTrue
|
|
1332
|
+
let passwordTrue = get(u, 'password', '')
|
|
1169
1333
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
// console.log('u', u)
|
|
1175
|
-
return Promise.reject('anUnexpectedErrorOccurred')
|
|
1176
|
-
}
|
|
1177
|
-
// console.log('email', email)
|
|
1334
|
+
//check (verifyPassword: 以明文舊密碼 + DB 自描述雜湊字串 + pepper(salt) 驗證)
|
|
1335
|
+
if (!verifyPassword(oldPassword, passwordTrue, salt)) {
|
|
1336
|
+
return Promise.reject('userChangePasswordIncorrectOld')
|
|
1337
|
+
}
|
|
1178
1338
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1339
|
+
//email
|
|
1340
|
+
let email = get(u, 'email', '')
|
|
1341
|
+
if (!isestr(email)) {
|
|
1342
|
+
// console.log('token', token)
|
|
1343
|
+
// console.log('u', u)
|
|
1344
|
+
return Promise.reject('anUnexpectedErrorOccurred')
|
|
1345
|
+
}
|
|
1346
|
+
// console.log('email', email)
|
|
1347
|
+
|
|
1348
|
+
//hash newPassword
|
|
1349
|
+
let passwordNew = hashPassword(newPassword, salt)
|
|
1350
|
+
|
|
1351
|
+
//save (清 isForceChangePw='n', 變更密碼成功後即解除強制變更), 更新者記該使用者本人 (R-CONC-04)
|
|
1352
|
+
//逐筆檢查(ADR-080): 寫入失敗即 reject, 不寄「密碼已變更」信
|
|
1353
|
+
let errTemp = null
|
|
1354
|
+
await procOrm(userId, 'users', 'save', [{
|
|
1355
|
+
id: userId,
|
|
1356
|
+
password: passwordNew,
|
|
1357
|
+
isForceChangePw: 'n',
|
|
1358
|
+
}], { mustExist: true })
|
|
1359
|
+
.catch((err) => {
|
|
1360
|
+
errTemp = err
|
|
1361
|
+
})
|
|
1181
1362
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
password: passwordNew,
|
|
1188
|
-
isForceChangePw: 'n',
|
|
1189
|
-
})
|
|
1190
|
-
.catch((err) => {
|
|
1191
|
-
errTemp = err
|
|
1192
|
-
})
|
|
1363
|
+
//check
|
|
1364
|
+
if (errTemp !== null) {
|
|
1365
|
+
srLog.error({ event: 'fun-changePassword-save', key: 'userChangePasswordFail', userId, err: getErrorMessage(errTemp) })
|
|
1366
|
+
return Promise.reject('userChangePasswordFail')
|
|
1367
|
+
}
|
|
1193
1368
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
srLog.error({ event: 'fun-changePassword-save', key: 'userChangePasswordFail', userId, err: getErrorMessage(errTemp) })
|
|
1197
|
-
return Promise.reject('userChangePasswordFail')
|
|
1198
|
-
}
|
|
1369
|
+
return { u, email }
|
|
1370
|
+
})
|
|
1199
1371
|
|
|
1200
1372
|
//若已變更密碼, 但寄送email失敗時, 不能報錯中斷流程
|
|
1201
1373
|
try {
|
|
@@ -1296,43 +1468,50 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1296
1468
|
return Promise.reject('adminResetPasswordAlreadyTriggered')
|
|
1297
1469
|
}
|
|
1298
1470
|
|
|
1299
|
-
//目標 user (
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
uTarget = await _getGenUserByKV('id', targetUserId, { deletePassword: true })
|
|
1303
|
-
}
|
|
1304
|
-
catch (err) {
|
|
1305
|
-
return Promise.reject('adminResetPasswordUserNotFound')
|
|
1306
|
-
}
|
|
1307
|
-
if (!iseobj(uTarget)) {
|
|
1308
|
-
return Promise.reject('adminResetPasswordUserNotFound')
|
|
1309
|
-
}
|
|
1471
|
+
//目標 user 查找與寫入於 users 表鎖內(S08; kmx 在外層、表鎖在內層): password 為伺服器專有欄位、isForceChangePw 為雙方欄位(系統直寫須取表鎖);
|
|
1472
|
+
//只改不增, 目標已被刪則拒絕而不新增殘缺列 (X5); 更新者記操作管理員 (R-CONC-04)
|
|
1473
|
+
let { targetAccount, targetName, targetEmail, newPassword } = await withTabLocks(['users'], async () => {
|
|
1310
1474
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1475
|
+
//目標 user (含 password 拿到也不用, 只是確認存在)
|
|
1476
|
+
let uTarget = null
|
|
1477
|
+
try {
|
|
1478
|
+
uTarget = await _getGenUserByKV('id', targetUserId, { deletePassword: true })
|
|
1479
|
+
}
|
|
1480
|
+
catch (err) {
|
|
1481
|
+
return Promise.reject('adminResetPasswordUserNotFound')
|
|
1482
|
+
}
|
|
1483
|
+
if (!iseobj(uTarget)) {
|
|
1484
|
+
return Promise.reject('adminResetPasswordUserNotFound')
|
|
1485
|
+
}
|
|
1314
1486
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1487
|
+
let targetAccount = get(uTarget, 'account', '')
|
|
1488
|
+
let targetName = get(uTarget, 'name', 'unknow')
|
|
1489
|
+
let targetEmail = get(uTarget, 'email', '')
|
|
1490
|
+
|
|
1491
|
+
//產生隨機新密碼 (符合 passwordPolicy + account 限制)
|
|
1492
|
+
let newPassword = genRandomPassword(passwordPolicy, targetAccount)
|
|
1493
|
+
|
|
1494
|
+
//hash + save (含 isForceChangePw='y')
|
|
1495
|
+
//逐筆檢查(ADR-080): 寫入失敗即 reject, 不記節流、不寄出未生效之新密碼
|
|
1496
|
+
let hashed = hashPassword(newPassword, salt)
|
|
1497
|
+
let errTemp = null
|
|
1498
|
+
await procOrm(operatorId, 'users', 'save', [{
|
|
1499
|
+
id: targetUserId,
|
|
1500
|
+
password: hashed,
|
|
1501
|
+
isForceChangePw: 'y',
|
|
1502
|
+
}], { mustExist: true })
|
|
1503
|
+
.catch((err) => {
|
|
1504
|
+
errTemp = err
|
|
1505
|
+
})
|
|
1317
1506
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
id: targetUserId,
|
|
1324
|
-
password: hashed,
|
|
1325
|
-
isForceChangePw: 'y',
|
|
1326
|
-
})
|
|
1327
|
-
.catch((err) => {
|
|
1328
|
-
errTemp = err
|
|
1329
|
-
})
|
|
1507
|
+
//check
|
|
1508
|
+
if (errTemp !== null) {
|
|
1509
|
+
srLog.error({ event: 'fun-adminResetUserPassword-save', key: 'adminResetPasswordFailed', token: maskToken(token), targetUserId, err: getErrorMessage(errTemp) })
|
|
1510
|
+
return Promise.reject('adminResetPasswordFailed')
|
|
1511
|
+
}
|
|
1330
1512
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
srLog.error({ event: 'fun-adminResetUserPassword-save', key: 'adminResetPasswordFailed', token: maskToken(token), targetUserId, err: getErrorMessage(errTemp) })
|
|
1334
|
-
return Promise.reject('adminResetPasswordFailed')
|
|
1335
|
-
}
|
|
1513
|
+
return { targetAccount, targetName, targetEmail, newPassword }
|
|
1514
|
+
})
|
|
1336
1515
|
|
|
1337
1516
|
//寫 throttle 時間戳 (放在 save 後, 確認此次 reset 已生效才記)
|
|
1338
1517
|
lastResetTime.set(targetUserId, now)
|
|
@@ -1384,7 +1563,15 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1384
1563
|
//revokeTokensByUserId: 撤銷(刪除)使用者名下全部 token, 供刪除 / 停用使用者(updateTabItems)與登入失敗封鎖(procProtect.blockAccount)共用.
|
|
1385
1564
|
//一次刪除該使用者全部 token 並逐筆檢查(ormWrite), 單筆失敗不中斷其餘筆, 有失敗即 reject 錯誤訊息(含失敗筆主鍵)供呼叫端記錄.
|
|
1386
1565
|
//冪等(已無 token 時刪 0 筆), resolve 回傳刪除之 token 數. 使用者設計上不持有 isApp='y' token, 故一併撤 (ADR-037).
|
|
1566
|
+
//S09: 對外之 revokeTokensByUserId 自取 tokens 表鎖; 上游已持 tokens 鎖者(updateTabItems users 路徑、procProtect 封鎖, 皆在 [tokens, users] 鎖內)
|
|
1567
|
+
//須呼叫 revokeTokensByUserIdLocked(持鎖版), 不可重入 (規格 §7)
|
|
1387
1568
|
let revokeTokensByUserId = async (userId) => {
|
|
1569
|
+
return await withTabLocks(['tokens'], () => revokeTokensByUserIdLocked(userId))
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
|
|
1573
|
+
//revokeTokensByUserIdLocked, 須於 tokens 表鎖內呼叫
|
|
1574
|
+
let revokeTokensByUserIdLocked = async (userId) => {
|
|
1388
1575
|
|
|
1389
1576
|
//check
|
|
1390
1577
|
if (!isestr(userId)) {
|
|
@@ -1409,6 +1596,14 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1409
1596
|
|
|
1410
1597
|
|
|
1411
1598
|
//updateTabItems
|
|
1599
|
+
//後台清單整表存檔(users / tokens / ips), 多人同時編輯之寫入契約 (原則 10; 規格 dspec/多人同時編輯之寫入契約.md; 規則帳本 rulebook_concurrency R-CONC-01~05):
|
|
1600
|
+
//- 讀取、比對、驗證依賴庫內之項、diff、寫入、撤銷 token、清快取全程在該表表鎖內(users 為 [tokens, users], 因寫入後同鎖撤銷 token); 讀取為鎖內新鮮讀取(無 ORM 快取).
|
|
1601
|
+
//- 帶基準(opt.optClient, 前端以 buildBaseline 對畫面所見之列自算): 與鎖內新鮮讀取比對, 不符整次拒收(零寫入, 先動先贏), 回語系鍵 saveStale / saveRowNotFound / saveBaselineFormat 提示重新載入.
|
|
1602
|
+
// 刪除集合 = 基準內(且在範圍內) − 送出, 不刪載入後他人新增者; 修改 = 送出列之摘要 ≠ 基準, 只寫契約 fields(+ order), 伺服器專有欄位取庫內值, 摘要相同之列不寫(不戳記更新者).
|
|
1603
|
+
//- 不帶基準(app 權杖直呼、已開啟之舊版頁面; 本 major 允許, 後者蓋前者, 下一 major 後台必帶): 仍取鎖、只改不增、伺服器專有欄位不採用客戶端值.
|
|
1604
|
+
// users / ips 刪除集合 = 庫內全部 − 送出(以外部為主); tokens 只處理應用系統金鑰列(isApp === 'y'), session 列不刪、不覆蓋、不復活.
|
|
1605
|
+
//- tokens 採選項 P (R-CONC-03): app 列(契約 tokens)嚴格整表比對; session 列(契約 tokensSession)逐列寬鬆比對:
|
|
1606
|
+
// 只比對管理員有修改或刪除之列; 載入後新增者不刪; 已不在庫內者不復活; 系統續期而管理員未動者不寫; 管理員有動且庫內已變者拒收.
|
|
1412
1607
|
//分驗證與寫入兩階段 (ADR-080):
|
|
1413
1608
|
//- 驗證階段: 全部驗證於任何寫入之前完成, 任一失敗即 reject 該驗證之 key, 資料庫無任何變更 (ADR-021).
|
|
1414
1609
|
// 凡可於寫入前判定、寫入時才會被拒或被靜默改寫之輸入一律於此拒絕 (ADR-081): rows 非陣列、主鍵非字串 / 重複 / 超過鍵長上限 → 'invalidRows';
|
|
@@ -1419,6 +1614,13 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1419
1614
|
let updateTabItems = async (woName, rows, keyDetect, opt = {}) => {
|
|
1420
1615
|
// console.log('updateTabItems', woName, rows.length, keyDetect)
|
|
1421
1616
|
|
|
1617
|
+
//contract: 寫入契約(src/writeContracts, 前後端同一來源); tokens 另有 session 列契約
|
|
1618
|
+
let contract = contracts[woName]
|
|
1619
|
+
let contractSession = contracts.tokensSession
|
|
1620
|
+
if (!iseobj(contract) || keyDetect !== contract.pk) {
|
|
1621
|
+
throw new Error(`invalid woName[${woName}] or keyDetect[${keyDetect}] of updateTabItems`)
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1422
1624
|
//resetOrder
|
|
1423
1625
|
let resetOrder = get(opt, 'resetOrder')
|
|
1424
1626
|
if (!isbol(resetOrder)) {
|
|
@@ -1437,6 +1639,9 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1437
1639
|
//ocList: 該清單之讀取快取 (wsemi cache), 於寫入階段後清除 (ADR-008, ADR-080); 未給則不清
|
|
1438
1640
|
let ocList = get(opt, 'ocList', null)
|
|
1439
1641
|
|
|
1642
|
+
//optClient: 前端經 RPC 送入之第 4 參數(未驗證), 於下方 parseSaveOpt 驗證後自組基準; 不展開給 procOrm (規格 §6)
|
|
1643
|
+
let optClient = get(opt, 'optClient')
|
|
1644
|
+
|
|
1440
1645
|
//check, rows 須為陣列 (ADR-081): 非陣列經 ltdtmapping 成空陣列, 比對時舊表全數歸為刪除
|
|
1441
1646
|
if (!isarr(rows)) {
|
|
1442
1647
|
return Promise.reject('invalidRows')
|
|
@@ -1444,11 +1649,16 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1444
1649
|
|
|
1445
1650
|
//idsNew: 前端標為新增(transient 欄位 _isNew === true)之列 id, 供下方比對既有列 (ADR-074);
|
|
1446
1651
|
//_isNew 不在 schema keys 內, 下方 ltdtmapping 取欄時即剝除不入庫
|
|
1652
|
+
//permsRaw: tokens 之 perms 於 ltdtmapping 前取原值(缺欄為 undefined), 供鎖內驗證 (ADR-069; ltdtmapping 會把缺欄補成 '')
|
|
1447
1653
|
let idsNew = []
|
|
1654
|
+
let permsRaw = new Map()
|
|
1448
1655
|
each(rows, (r) => {
|
|
1449
1656
|
if (get(r, '_isNew') === true) {
|
|
1450
1657
|
idsNew.push(get(r, 'id', ''))
|
|
1451
1658
|
}
|
|
1659
|
+
if (woName === 'tokens') {
|
|
1660
|
+
permsRaw.set(get(r, 'id', ''), get(r, 'perms'))
|
|
1661
|
+
}
|
|
1452
1662
|
})
|
|
1453
1663
|
|
|
1454
1664
|
//ltdtmapping
|
|
@@ -1508,223 +1718,428 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1508
1718
|
}
|
|
1509
1719
|
}
|
|
1510
1720
|
|
|
1511
|
-
//
|
|
1512
|
-
let
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
//新增列已存在即拒絕 (ADR-074): 同一包重送且第 2 次於第 1 次完成後才處理時, 第 1 次已建立之列會被當「修改」,
|
|
1516
|
-
//以前端之空值 / 佔位字覆寫伺服器於新增時填入之欄位(實測: 新使用者 timeVerified 被清空、建立者 / 建立時間寫成佔位字而無法登入)
|
|
1517
|
-
if (size(idsNew) > 0) {
|
|
1518
|
-
let kpIdOld = Object.create(null)
|
|
1519
|
-
each(ltdtOld, (r) => {
|
|
1520
|
-
kpIdOld[get(r, 'id', '')] = true
|
|
1521
|
-
})
|
|
1522
|
-
if (idsNew.some((id) => kpIdOld[id] === true)) {
|
|
1523
|
-
return Promise.reject('saveNewRowExists')
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
//users 路徑: account 唯一性 + 非空驗證 (補 id/email 之外的 account; 防直打 API 寫入空/重複帳號).
|
|
1528
|
-
//(a) 空值 → reject 'accountRequired'; (b) 本批內重複 或 與既有其他 user 之 account 衝突 → reject 'accountDuplicate'.
|
|
1529
|
-
//帳號全域唯一, 與既有比對時排除「本批同 id 之自身既有列」(更新自己時 account 可維持不變).
|
|
1530
|
-
if (woName === 'users') {
|
|
1531
|
-
//既有 account → id 對照 (排除自身用)
|
|
1532
|
-
let accountToIdOld = Object.create(null)
|
|
1533
|
-
each(ltdtOld, (r) => {
|
|
1534
|
-
let a = get(r, 'account', '')
|
|
1535
|
-
if (isestr(a)) {
|
|
1536
|
-
accountToIdOld[a] = get(r, 'id', '')
|
|
1537
|
-
}
|
|
1538
|
-
})
|
|
1539
|
-
let kpAccount = Object.create(null)
|
|
1540
|
-
let errAccount = null
|
|
1541
|
-
each(rows, (row) => {
|
|
1542
|
-
let account = get(row, 'account', '')
|
|
1543
|
-
let id = get(row, 'id', '')
|
|
1544
|
-
//空值
|
|
1545
|
-
if (!isestr(account)) {
|
|
1546
|
-
errAccount = 'accountRequired'
|
|
1547
|
-
return false //跳出
|
|
1548
|
-
}
|
|
1549
|
-
//本批內重複
|
|
1550
|
-
if (haskey(kpAccount, account)) {
|
|
1551
|
-
errAccount = 'accountDuplicate'
|
|
1552
|
-
return false //跳出
|
|
1553
|
-
}
|
|
1554
|
-
//與既有其他 user (非自身) 衝突
|
|
1555
|
-
if (haskey(accountToIdOld, account) && accountToIdOld[account] !== id) {
|
|
1556
|
-
errAccount = 'accountDuplicate'
|
|
1557
|
-
return false //跳出
|
|
1558
|
-
}
|
|
1559
|
-
kpAccount[account] = true
|
|
1560
|
-
})
|
|
1561
|
-
if (errAccount !== null) {
|
|
1562
|
-
return Promise.reject(errAccount)
|
|
1563
|
-
}
|
|
1721
|
+
//parseSaveOpt: 驗證前端送入之基準(格式或版本不識 → baselineFormat, 不得降級為不帶基準); 於取鎖、讀庫之前
|
|
1722
|
+
let po = parseSaveOpt(woName, optClient)
|
|
1723
|
+
if (po.err !== undefined) {
|
|
1724
|
+
return Promise.reject(kpSaveRejectKey[po.err])
|
|
1564
1725
|
}
|
|
1726
|
+
let isBased = po.baseline !== undefined
|
|
1565
1727
|
|
|
1566
|
-
//
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1728
|
+
//subRaw / subNorm: 送出之列(依 id); subRaw 為前端所見形式(供與基準比對摘要), subNorm 為寫入形式(tokens 之 perms 去重並依全集排序, ADR-069, 於鎖內驗證合法後才正規化)
|
|
1729
|
+
let subRaw = new Map()
|
|
1730
|
+
each(rows, (row) => {
|
|
1731
|
+
subRaw.set(row.id, row)
|
|
1732
|
+
})
|
|
1733
|
+
let subNorm = subRaw
|
|
1734
|
+
if (woName === 'tokens') {
|
|
1735
|
+
subNorm = new Map()
|
|
1573
1736
|
each(rows, (row) => {
|
|
1574
|
-
|
|
1575
|
-
if (oldRow) {
|
|
1576
|
-
row.password = oldRow.password
|
|
1577
|
-
}
|
|
1737
|
+
subNorm.set(row.id, { ...row, perms: normAppPerms(permsRaw.get(row.id)) })
|
|
1578
1738
|
})
|
|
1579
1739
|
}
|
|
1580
|
-
|
|
1581
1740
|
let ltdtNew = rows
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
//新增或修改列之任一欄位值(含主鍵; password 此時為明文、其後雜湊, 不檢查)會被 clearXSS 改寫即拒絕, 屬驗證階段 (ADR-021, ADR-081):
|
|
1586
|
-
//procOrm 寫入時對各欄位 clearXSS(w-serv-orm getMapOrm 之 adjustData), 主鍵會被改寫者 1.1.8 起整批拒絕(屆時同批之刪除已落地),
|
|
1587
|
-
//其餘欄位則被靜默改寫, 使儲存值與送入值不同(account / email 之唯一性與登入即以送入值比對).
|
|
1588
|
-
//刪除不經 clearXSS、比對後屬 same 之列不寫入, 皆不檢查; 惟 users 以 resetOrder 依位置重編 order, 既有列只要位置變動即屬 diff 而受檢
|
|
1589
|
-
let rowsMarkup = filter([...r.add, ...r.diff], (row) => {
|
|
1590
|
-
return keys(row).some((k) => k !== 'password' && !isEqual(clearXSS(row[k]), row[k]))
|
|
1591
|
-
})
|
|
1592
|
-
if (size(rowsMarkup) > 0) {
|
|
1593
|
-
return Promise.reject('htmlMarkupNotAllowed')
|
|
1741
|
+
if (woName === 'tokens') {
|
|
1742
|
+
ltdtNew = map(rows, (row) => subNorm.get(row.id))
|
|
1594
1743
|
}
|
|
1595
1744
|
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
if (woName === 'users' && size(r.add) > 0) {
|
|
1599
|
-
for (let row of r.add) {
|
|
1600
|
-
let pw = get(row, 'password', '')
|
|
1601
|
-
let account = get(row, 'account', '')
|
|
1602
|
-
let chk = checkUserPassword(lang, pw, { account })
|
|
1603
|
-
if (chk.state === 'error') {
|
|
1604
|
-
return Promise.reject(chk.key)
|
|
1605
|
-
}
|
|
1606
|
-
row.password = hashPassword(pw, salt)
|
|
1607
|
-
}
|
|
1608
|
-
//後台建帳自動填 timeVerified: timemsTZ 格式(見 verifyEmail 同處說明; 原 now2str 秒級使新建帳號顯示未驗證、設為管理者後過不了 funCheckAdmin, 2026-09-29 修正)
|
|
1609
|
-
each(r.add, (row) => {
|
|
1610
|
-
let tv = get(row, 'timeVerified', '')
|
|
1611
|
-
if (!isestr(tv)) {
|
|
1612
|
-
row.timeVerified = nowms2str()
|
|
1613
|
-
}
|
|
1614
|
-
})
|
|
1615
|
-
}
|
|
1745
|
+
//表鎖: users 寫入後須同鎖撤銷其 token, 故為 [tokens, users] (withTabLocks 依表名碼元序取鎖, 不死結)
|
|
1746
|
+
let tables = woName === 'users' ? ['tokens', 'users'] : [woName]
|
|
1616
1747
|
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1748
|
+
//讀取 → 比對基準 → 驗證 → diff → 寫入 → 撤銷 → 清快取, 全在表鎖內(guardWrite: 取鎖 → 新鮮讀取 → compareBaseline → write); tokens 之 app 列嚴格整表比對於此, session 列寬鬆比對於 write 內
|
|
1749
|
+
try {
|
|
1750
|
+
await guardWrite({
|
|
1751
|
+
tables,
|
|
1752
|
+
read: () => woItems[woName].select(),
|
|
1753
|
+
baseline: po.baseline, //undefined 為不帶基準(仍取鎖與讀取)
|
|
1754
|
+
contract,
|
|
1755
|
+
write: async (ltdtOld) => {
|
|
1756
|
+
//console.log(`...woName[${woName}].select`)
|
|
1757
|
+
|
|
1758
|
+
//nowById: 鎖內新鮮讀取之列
|
|
1759
|
+
let nowById = new Map()
|
|
1760
|
+
each(ltdtOld, (r) => {
|
|
1761
|
+
nowById.set(get(r, 'id', ''), r)
|
|
1762
|
+
})
|
|
1763
|
+
|
|
1764
|
+
//新增列已存在即拒絕 (ADR-074): 同一包重送且第 2 次於第 1 次完成後才處理時, 第 1 次已建立之列會被當「修改」,
|
|
1765
|
+
//以前端之空值 / 佔位字覆寫伺服器於新增時填入之欄位(實測: 新使用者 timeVerified 被清空、建立者 / 建立時間寫成佔位字而無法登入)
|
|
1766
|
+
if (size(idsNew) > 0 && idsNew.some((id) => nowById.has(id))) {
|
|
1767
|
+
return Promise.reject('saveNewRowExists')
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
//tokens 路徑: 清單存檔不得新增庫內不存在之 session 列(isApp 非 'y'; ADR-092), 不論帶不帶基準, 整次拒收(零寫入, 回固定語系鍵, 不回顯值):
|
|
1771
|
+
//session 權杖只由登入產生, 後台清單本就無新增 session 列之入口 (ADR-042); 不帶基準時無從分辨「新增」與「載入後已被登出 / 自動封鎖撤銷之舊列」,
|
|
1772
|
+
//送回舊列會使其復活而繞過撤銷. 例外: 帶基準而該列在 baselineSession 內者(載入時尚在, 其後被撤銷)不屬新增——管理員未動者下方略過、已動者拒收 saveStale.
|
|
1773
|
+
//應用系統金鑰列(isApp === 'y')新增、既有 session 列之刪除與升級為應用系統金鑰照舊.
|
|
1774
|
+
if (woName === 'tokens') {
|
|
1775
|
+
let idsBase = new Set(isBased ? [...po.baseline, ...po.baselineSession].map(([id]) => id) : [])
|
|
1776
|
+
let rowSessionNew = rows.find((r) => get(r, 'isApp', '') !== 'y' && !nowById.has(r.id) && !idsBase.has(r.id))
|
|
1777
|
+
if (rowSessionNew !== undefined) {
|
|
1778
|
+
srLog.warn({ event: 'fun-updateTabItems-reject', key: 'tokenSessionAddNotAllowed', woName })
|
|
1779
|
+
return Promise.reject('tokenSessionAddNotAllowed')
|
|
1634
1780
|
}
|
|
1635
|
-
clear[k] = null
|
|
1636
1781
|
}
|
|
1637
|
-
})
|
|
1638
|
-
if (clear !== null) {
|
|
1639
|
-
rowsClear.push(clear)
|
|
1640
|
-
}
|
|
1641
|
-
})
|
|
1642
|
-
}
|
|
1643
1782
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1783
|
+
//users 路徑: account 唯一性 + 非空驗證 (補 id/email 之外的 account; 防直打 API 寫入空/重複帳號).
|
|
1784
|
+
//(a) 空值 → reject 'accountRequired'; (b) 本批內重複 或 與既有其他 user 之 account 衝突 → reject 'accountDuplicate'.
|
|
1785
|
+
//帳號全域唯一, 與既有比對時排除「本批同 id 之自身既有列」(更新自己時 account 可維持不變).
|
|
1786
|
+
if (woName === 'users') {
|
|
1787
|
+
//既有 account → id 對照 (排除自身用)
|
|
1788
|
+
let accountToIdOld = Object.create(null)
|
|
1789
|
+
each(ltdtOld, (r) => {
|
|
1790
|
+
let a = get(r, 'account', '')
|
|
1791
|
+
if (isestr(a)) {
|
|
1792
|
+
accountToIdOld[a] = get(r, 'id', '')
|
|
1793
|
+
}
|
|
1794
|
+
})
|
|
1795
|
+
let kpAccount = Object.create(null)
|
|
1796
|
+
let errAccount = null
|
|
1797
|
+
each(rows, (row) => {
|
|
1798
|
+
let account = get(row, 'account', '')
|
|
1799
|
+
let id = get(row, 'id', '')
|
|
1800
|
+
//空值
|
|
1801
|
+
if (!isestr(account)) {
|
|
1802
|
+
errAccount = 'accountRequired'
|
|
1803
|
+
return false //跳出
|
|
1804
|
+
}
|
|
1805
|
+
//本批內重複
|
|
1806
|
+
if (haskey(kpAccount, account)) {
|
|
1807
|
+
errAccount = 'accountDuplicate'
|
|
1808
|
+
return false //跳出
|
|
1809
|
+
}
|
|
1810
|
+
//與既有其他 user (非自身) 衝突
|
|
1811
|
+
if (haskey(accountToIdOld, account) && accountToIdOld[account] !== id) {
|
|
1812
|
+
errAccount = 'accountDuplicate'
|
|
1813
|
+
return false //跳出
|
|
1814
|
+
}
|
|
1815
|
+
kpAccount[account] = true
|
|
1816
|
+
})
|
|
1817
|
+
if (errAccount !== null) {
|
|
1818
|
+
return Promise.reject(errAccount)
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1647
1821
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1822
|
+
//tokens 路徑: 本批有變動(或新增)之列須為未設定或已登錄權限字串之陣列, 任一列不合法則整批 reject 不寫入 (ADR-069).
|
|
1823
|
+
//與 DB 相同之列不驗: DB 內既有之毀損值(非陣列等)若也擋, 後台整批存回將永遠失敗而無從修正; 其正規化後之有效權限與讀取端 fail closed 結果相同.
|
|
1824
|
+
//帶基準時, 摘要同基準(管理員未動)之列亦不驗 (外審 sol#5): 該列不會被寫入——app 列已由嚴格整表比對確認庫內即基準, session 列未動不寫——
|
|
1825
|
+
//如舊資料毀損值之 session 於載入後已被登出(庫內已無此列), 不得因此阻擋其他列之合法修改; 管理員有動或新增之列仍照驗
|
|
1826
|
+
if (woName === 'tokens') {
|
|
1827
|
+
let dgBase = new Map(isBased ? [...po.baseline, ...po.baselineSession] : [])
|
|
1828
|
+
for (let row of rows) {
|
|
1829
|
+
let perms = permsRaw.get(row.id)
|
|
1830
|
+
let old = nowById.get(row.id)
|
|
1831
|
+
if (old !== undefined && JSON.stringify(get(old, 'perms')) === JSON.stringify(perms)) {
|
|
1832
|
+
continue
|
|
1833
|
+
}
|
|
1834
|
+
if (dgBase.has(row.id) && dgBase.get(row.id) === digestRow(subRaw.get(row.id), contract)) {
|
|
1835
|
+
continue
|
|
1836
|
+
}
|
|
1837
|
+
if (!isAppPermsValue(perms)) {
|
|
1838
|
+
return Promise.reject('tokenPermsInvalid')
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
//dels / adds / mods: 刪除(庫內之列)、新增(送出之列)、修改(送出之列)
|
|
1844
|
+
let dels = []
|
|
1845
|
+
let adds = []
|
|
1846
|
+
let mods = []
|
|
1847
|
+
let isApp = (r) => get(r, 'isApp', '') === 'y'
|
|
1848
|
+
|
|
1849
|
+
if (woName === 'tokens' && isBased) {
|
|
1850
|
+
|
|
1851
|
+
//app 列已由 guardWrite 嚴格整表比對相符(增、刪、改皆無差異); session 列(契約 tokensSession)逐列寬鬆比對,
|
|
1852
|
+
//只取 compareBaseline 之 changed(庫內摘要與基準不同者, 如續期改 timeEnd); added(載入後新增者)忽略; removed(已不在庫內者)以 nowById 判斷
|
|
1853
|
+
let baseA = new Map(po.baseline)
|
|
1854
|
+
let baseS = new Map(po.baselineSession)
|
|
1855
|
+
let cs = compareBaseline(ltdtOld, po.baselineSession, contractSession)
|
|
1856
|
+
let changedS = new Set(cs.ok ? [] : cs.changed)
|
|
1857
|
+
|
|
1858
|
+
//app 列: 基準內未送出 → 刪除; 送出之摘要 ≠ 基準 → 修改
|
|
1859
|
+
for (let [id, dg] of baseA) {
|
|
1860
|
+
let sub = subRaw.get(id)
|
|
1861
|
+
if (sub === undefined) {
|
|
1862
|
+
dels.push(nowById.get(id))
|
|
1863
|
+
}
|
|
1864
|
+
else if (digestRow(sub, contract) !== dg) {
|
|
1865
|
+
mods.push(subNorm.get(id))
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1652
1868
|
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1869
|
+
//session 列
|
|
1870
|
+
for (let [id, dg] of baseS) {
|
|
1871
|
+
let sub = subRaw.get(id)
|
|
1872
|
+
let now = nowById.get(id)
|
|
1873
|
+
let live = now !== undefined && contractSession.scope(now)
|
|
1874
|
+
if (sub === undefined) {
|
|
1875
|
+
//管理員刪除: 已不在庫內(已被登出 / 撤銷)則冪等略過; 在庫內而庫內已變(如已續期)→ 管理員有動且庫內已變, 拒收
|
|
1876
|
+
if (!live) {
|
|
1877
|
+
continue
|
|
1878
|
+
}
|
|
1879
|
+
if (changedS.has(id)) {
|
|
1880
|
+
return Promise.reject('saveStale')
|
|
1881
|
+
}
|
|
1882
|
+
dels.push(now)
|
|
1883
|
+
continue
|
|
1884
|
+
}
|
|
1885
|
+
//管理員未動(摘要同基準): 不寫, 系統續期之 timeEnd 不被舊值蓋回, 已撤銷者不復活 (X1, X7)
|
|
1886
|
+
if (digestRow(sub, contract) === dg) {
|
|
1887
|
+
continue
|
|
1888
|
+
}
|
|
1889
|
+
//管理員有動: 已不在庫內或庫內已變 → 拒收; 否則修改(含升級為應用系統金鑰)
|
|
1890
|
+
if (!live || changedS.has(id)) {
|
|
1891
|
+
return Promise.reject('saveStale')
|
|
1892
|
+
}
|
|
1893
|
+
mods.push(subNorm.get(id))
|
|
1664
1894
|
}
|
|
1665
|
-
|
|
1895
|
+
|
|
1896
|
+
//基準之外之列(管理員新增): 已存在於庫內(他人載入後新增而撞 id)→ 拒收; 否則新增
|
|
1897
|
+
for (let [id] of subRaw) {
|
|
1898
|
+
if (baseA.has(id) || baseS.has(id)) {
|
|
1899
|
+
continue
|
|
1900
|
+
}
|
|
1901
|
+
if (nowById.has(id)) {
|
|
1902
|
+
return Promise.reject('saveStale')
|
|
1903
|
+
}
|
|
1904
|
+
adds.push(subNorm.get(id))
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
}
|
|
1908
|
+
else if (woName === 'tokens') {
|
|
1909
|
+
|
|
1910
|
+
//不帶基準(app 權杖直呼、已開啟之舊版頁面): 刪除與修改只處理應用系統金鑰列(庫內 isApp === 'y',
|
|
1911
|
+
//修改另含送出 isApp === 'y' 者, 即升級為應用系統金鑰); session 列(登入新增、續期改 timeEnd、登出或封鎖刪除, 系統頻繁增刪)
|
|
1912
|
+
//不刪、不以送入值覆蓋, 載入後才登入之 session 因而不被誤刪 (E1, D3). 新增列(送出而庫內無此 id)照常新增,
|
|
1913
|
+
//含直打 API 鑄造使用者 token 與應用系統金鑰(ADR-042, ADR-084); 此路徑無基準可辨是否為已被撤銷之 session, 該風險由帶基準之後台頁(X1)承擔防護
|
|
1914
|
+
each(ltdtOld, (r) => {
|
|
1915
|
+
if (isApp(r) && !subRaw.has(r.id)) {
|
|
1916
|
+
dels.push(r)
|
|
1917
|
+
}
|
|
1918
|
+
})
|
|
1919
|
+
for (let [id, sub] of subNorm) {
|
|
1920
|
+
let old = nowById.get(id)
|
|
1921
|
+
if (old === undefined) {
|
|
1922
|
+
adds.push(sub)
|
|
1923
|
+
continue
|
|
1924
|
+
}
|
|
1925
|
+
if (!isApp(old) && !isApp(sub)) {
|
|
1926
|
+
continue
|
|
1927
|
+
}
|
|
1928
|
+
//以正規化後之列(perms 去重排序,缺欄 / 毀損值變 [])比對庫內: DB 既有之毀損值與無 perms 欄之舊列於存回時正規化為空陣列(ADR-069, PERM-A-005),
|
|
1929
|
+
//此為既有且有意之行為, 不以送入原值比對(外審 sol#8 之『原樣重送零寫入』與之衝突, 不採)
|
|
1930
|
+
if (digestRow(sub, contract) !== digestRow(old, contract)) {
|
|
1931
|
+
mods.push(sub)
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
}
|
|
1936
|
+
else {
|
|
1937
|
+
|
|
1938
|
+
//users / ips: 帶基準時 guardWrite 已嚴格比對相符(庫內 = 基準), 刪除集合 = 基準內 − 送出; 不帶基準時 = 庫內全部 − 送出
|
|
1939
|
+
if (isBased) {
|
|
1940
|
+
for (let [id] of po.baseline) {
|
|
1941
|
+
if (!subRaw.has(id) && nowById.has(id)) {
|
|
1942
|
+
dels.push(nowById.get(id))
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
else {
|
|
1947
|
+
each(ltdtOld, (r) => {
|
|
1948
|
+
if (!subRaw.has(r.id)) {
|
|
1949
|
+
dels.push(r)
|
|
1950
|
+
}
|
|
1951
|
+
})
|
|
1952
|
+
}
|
|
1953
|
+
//修改 = 送出列之摘要(契約 fields)≠ 庫內; 名次欄(users.order)值不同亦寫(存檔依列序重編)
|
|
1954
|
+
each(rows, (sub) => {
|
|
1955
|
+
let old = nowById.get(sub.id)
|
|
1956
|
+
if (old === undefined) {
|
|
1957
|
+
adds.push(sub)
|
|
1958
|
+
return
|
|
1959
|
+
}
|
|
1960
|
+
let changed = digestRow(sub, contract) !== digestRow(old, contract)
|
|
1961
|
+
if (!changed && contract.orderKey) {
|
|
1962
|
+
changed = !isEqual(get(old, contract.orderKey), get(sub, contract.orderKey))
|
|
1963
|
+
}
|
|
1964
|
+
if (changed) {
|
|
1965
|
+
mods.push(sub)
|
|
1966
|
+
}
|
|
1967
|
+
})
|
|
1968
|
+
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
//rowsAdd: 新增列保留全欄形狀, 伺服器專有欄位(含稽核欄)不採用客戶端值, 由 procOrm 戳記或留空; users 之 password 為新增列例外(明文, 其後檢查策略並雜湊)
|
|
1972
|
+
let rowsAdd = map(adds, (sub) => {
|
|
1973
|
+
let row = { ...sub }
|
|
1974
|
+
each(contract.serverOnly, (k) => {
|
|
1975
|
+
row[k] = ''
|
|
1976
|
+
})
|
|
1977
|
+
if (woName === 'users') {
|
|
1978
|
+
row.password = get(sub, 'password', '')
|
|
1979
|
+
}
|
|
1980
|
+
return row
|
|
1981
|
+
})
|
|
1982
|
+
|
|
1983
|
+
//rowsMod: 修改列只寫契約 fields(+ 名次欄); 伺服器專有欄位(password、tokenVerify、稽核欄)取庫內值, 不入寫入列 (X2)
|
|
1984
|
+
let rowsMod = map(mods, (sub) => {
|
|
1985
|
+
let row = pickWriteRow(sub, contract)
|
|
1986
|
+
if (contract.orderKey) {
|
|
1987
|
+
row[contract.orderKey] = sub[contract.orderKey]
|
|
1988
|
+
}
|
|
1989
|
+
return row
|
|
1990
|
+
})
|
|
1991
|
+
|
|
1992
|
+
//新增或修改列之任一欄位值(含主鍵; password 此時為明文、其後雜湊, 不檢查)會被 clearXSS 改寫即拒絕, 屬驗證階段 (ADR-021, ADR-081):
|
|
1993
|
+
//procOrm 寫入時對各欄位 clearXSS(w-serv-orm getMapOrm 之 adjustData), 主鍵會被改寫者 1.1.8 起整批拒絕(屆時同批之刪除已落地),
|
|
1994
|
+
//其餘欄位則被靜默改寫, 使儲存值與送入值不同(account / email 之唯一性與登入即以送入值比對).
|
|
1995
|
+
//刪除不經 clearXSS、摘要相同之列不寫入, 皆不檢查; 惟 users 以 resetOrder 依位置重編 order, 既有列只要位置變動即屬修改而受檢
|
|
1996
|
+
let rowsMarkup = filter([...rowsAdd, ...rowsMod], (row) => {
|
|
1997
|
+
return keys(row).some((k) => k !== 'password' && !isEqual(clearXSS(row[k]), row[k]))
|
|
1666
1998
|
})
|
|
1667
|
-
|
|
1668
|
-
|
|
1999
|
+
if (size(rowsMarkup) > 0) {
|
|
2000
|
+
return Promise.reject('htmlMarkupNotAllowed')
|
|
1669
2001
|
}
|
|
1670
|
-
}
|
|
1671
|
-
else {
|
|
1672
|
-
await procOrm(operatorId, woName, 'insert', r.add)
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
2002
|
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2003
|
+
//users 路徑: 對新使用者的明文密碼進行 policy 檢查與 hash, 屬驗證階段, 須於任何寫入之前 (ADR-021, ADR-080;
|
|
2004
|
+
//原置於刪除寫入之後, 密碼不符策略時刪除與撤銷已落地卻回驗證錯誤)
|
|
2005
|
+
if (woName === 'users' && size(rowsAdd) > 0) {
|
|
2006
|
+
for (let row of rowsAdd) {
|
|
2007
|
+
let pw = get(row, 'password', '')
|
|
2008
|
+
let account = get(row, 'account', '')
|
|
2009
|
+
let chk = checkUserPassword(lang, pw, { account })
|
|
2010
|
+
if (chk.state === 'error') {
|
|
2011
|
+
return Promise.reject(chk.key)
|
|
2012
|
+
}
|
|
2013
|
+
row.password = hashPassword(pw, salt)
|
|
2014
|
+
}
|
|
2015
|
+
//後台建帳自動填 timeVerified: timemsTZ 格式(見 verifyEmail 同處說明; 原 now2str 秒級使新建帳號顯示未驗證、設為管理者後過不了 funCheckAdmin, 2026-09-29 修正)
|
|
2016
|
+
each(rowsAdd, (row) => {
|
|
2017
|
+
let tv = get(row, 'timeVerified', '')
|
|
2018
|
+
if (!isestr(tv)) {
|
|
2019
|
+
row.timeVerified = nowms2str()
|
|
2020
|
+
}
|
|
2021
|
+
})
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
//rowsClear: 陣列欄變短之列先清空該欄: 儲存層 save 以 lodash merge 合併新舊列, 陣列逐索引合併, 新陣列較短時舊陣列尾端元素會殘留
|
|
2025
|
+
//(如 tokens.perms 由 ['readTokens'] 撤銷為 [] 將無效, ADR-069). 先存 null 令該欄不再為陣列, 其後整列存入即為新陣列.
|
|
2026
|
+
//兩次寫入之間該欄短暫為 null, tokens.perms 讀取端視同未設定(僅基本權限), 屬 fail closed 方向.
|
|
2027
|
+
let rowsClear = []
|
|
2028
|
+
each(rowsMod, (row) => {
|
|
2029
|
+
let id = get(row, keyDetect, '')
|
|
2030
|
+
let clear = null
|
|
2031
|
+
each(row, (v, k) => {
|
|
2032
|
+
let vOld = get(nowById.get(id), k)
|
|
2033
|
+
if (isarr(v) && isarr(vOld) && v.length < vOld.length) {
|
|
2034
|
+
if (clear === null) {
|
|
2035
|
+
clear = { [keyDetect]: id }
|
|
2036
|
+
}
|
|
2037
|
+
clear[k] = null
|
|
2038
|
+
}
|
|
2039
|
+
})
|
|
2040
|
+
if (clear !== null) {
|
|
2041
|
+
rowsClear.push(clear)
|
|
2042
|
+
}
|
|
2043
|
+
})
|
|
2044
|
+
|
|
2045
|
+
//寫入階段: 刪除 → 新增 → 修改, 任一步 reject 即停止後續步驟 (不回滾, ADR-044); 同一表鎖內, 刪除與修改皆 mustExist(只改不增, 列已不存在 → rowNotFound 而不新增殘缺列)
|
|
2046
|
+
let errWrite = null
|
|
2047
|
+
try {
|
|
2048
|
+
|
|
2049
|
+
//del, operatorId 用於 audit (userIdUpdate)
|
|
2050
|
+
if (size(dels) > 0) {
|
|
2051
|
+
await procOrm(operatorId, woName, 'del', map(dels, (row) => ({ id: row.id })), { mustExist: true })
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
//add
|
|
2055
|
+
if (size(rowsAdd) > 0) {
|
|
2056
|
+
if (woName === 'tokens') {
|
|
2057
|
+
//tokens 之 userId 為令牌擁有者(非建立者): keepUserId 保留列上非空之擁有者, userIdUpdate 記操作者 (ADR-042, ADR-084; w-serv-orm 1.1.17);
|
|
2058
|
+
//擁有者為空者以操作者補, 不再寫成無擁有者之金鑰
|
|
2059
|
+
await procOrm(operatorId, woName, 'insert', rowsAdd, { keepUserId: true })
|
|
2060
|
+
}
|
|
2061
|
+
else {
|
|
2062
|
+
await procOrm(operatorId, woName, 'insert', rowsAdd)
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
1683
2065
|
|
|
2066
|
+
//mod
|
|
2067
|
+
if (size(rowsClear) > 0) {
|
|
2068
|
+
await procOrm(operatorId, woName, 'save', rowsClear, { mustExist: true })
|
|
2069
|
+
}
|
|
2070
|
+
if (size(rowsMod) > 0) {
|
|
2071
|
+
await procOrm(operatorId, woName, 'save', rowsMod, { mustExist: true })
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
}
|
|
2075
|
+
catch (err) {
|
|
2076
|
+
errWrite = err
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
//users 路徑: 撤銷本批欲刪除與欲停用(isActive==='n')者名下全部 token, 避免 orphan token 仍可 checkToken/refreshToken 續命 (ADR-037).
|
|
2080
|
+
//撤銷範圍依本批意圖, 不依寫入成敗(寫入失敗之列亦撤, fail closed); 先寫入再撤銷: 刪除 / 停用落地後已無法登入, 不會再產生新 token.
|
|
2081
|
+
//停用者沿用整表掃描(本批表內 isActive==='n' 者皆撤, 冪等). 撤銷失敗僅 srLog.error 不阻斷主流程.
|
|
2082
|
+
//與寫入同在 [tokens, users] 鎖內, 故用持鎖版 revokeTokensByUserIdLocked (不可重入, R-CONC-01)
|
|
2083
|
+
if (woName === 'users') {
|
|
2084
|
+
for (let row of dels) {
|
|
2085
|
+
let userId = get(row, 'id', '')
|
|
2086
|
+
await revokeTokensByUserIdLocked(userId)
|
|
2087
|
+
.catch((err) => {
|
|
2088
|
+
srLog.error({ event: 'fun-updateTabItems-revokeTokenOnDelete', userId, err: getErrorMessage(err) })
|
|
2089
|
+
})
|
|
2090
|
+
}
|
|
2091
|
+
for (let row of rows) {
|
|
2092
|
+
if (get(row, 'isActive', '') !== 'n') {
|
|
2093
|
+
continue
|
|
2094
|
+
}
|
|
2095
|
+
let userId = get(row, 'id', '')
|
|
2096
|
+
await revokeTokensByUserIdLocked(userId)
|
|
2097
|
+
.catch((err) => {
|
|
2098
|
+
srLog.error({ event: 'fun-updateTabItems-revokeTokenOnDisable', userId, err: getErrorMessage(err) })
|
|
2099
|
+
})
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
//寫入後立即 invalidate 30s cache, 避免改完之統計頁顯示舊資料 (ADR-008, audit F-050); 寫入失敗時資料庫可能已部分變更, 一併清除 (ADR-080)
|
|
2104
|
+
if (ocList !== null) {
|
|
2105
|
+
ocList.clear('fun')
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
//check, 寫入階段失敗一律回 saveWriteFail, procOrm 原文(含失敗筆主鍵)僅記 log (ADR-034, ADR-080)
|
|
2109
|
+
if (errWrite !== null) {
|
|
2110
|
+
srLog.error({ event: 'fun-updateTabItems-write', key: 'saveWriteFail', woName, err: getErrorMessage(errWrite) })
|
|
2111
|
+
return Promise.reject('saveWriteFail')
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
},
|
|
2115
|
+
})
|
|
1684
2116
|
}
|
|
1685
2117
|
catch (err) {
|
|
1686
|
-
errWrite = err
|
|
1687
|
-
}
|
|
1688
2118
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
let userId = get(row, 'id', '')
|
|
1695
|
-
await revokeTokensByUserId(userId)
|
|
1696
|
-
.catch((err) => {
|
|
1697
|
-
srLog.error({ event: 'fun-updateTabItems-revokeTokenOnDelete', userId, err: getErrorMessage(err) })
|
|
1698
|
-
})
|
|
1699
|
-
}
|
|
1700
|
-
for (let row of ltdtNew) {
|
|
1701
|
-
if (get(row, 'isActive', '') !== 'n') {
|
|
1702
|
-
continue
|
|
2119
|
+
//契約拒收鍵(baselineMismatch / rowNotFound / baselineFormat)轉語系鍵, 提示重新載入; 取鍵一律用核心 getRejectKey
|
|
2120
|
+
let kSave = getSaveRejectKey(err)
|
|
2121
|
+
if (kSave !== null) {
|
|
2122
|
+
if (srLog && isfun(srLog.warn)) {
|
|
2123
|
+
srLog.warn({ event: 'fun-updateTabItems-reject', key: kSave, woName }) //原則 3: 後端錯誤 log error key
|
|
1703
2124
|
}
|
|
1704
|
-
|
|
1705
|
-
await revokeTokensByUserId(userId)
|
|
1706
|
-
.catch((err) => {
|
|
1707
|
-
srLog.error({ event: 'fun-updateTabItems-revokeTokenOnDisable', userId, err: getErrorMessage(err) })
|
|
1708
|
-
})
|
|
2125
|
+
return Promise.reject(kSave)
|
|
1709
2126
|
}
|
|
1710
|
-
}
|
|
1711
2127
|
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
2128
|
+
//rowsInvalid: 庫內讀出之列主鍵重複或缺漏, 屬伺服端資料錯誤(重新載入無法解決), 記 log 後回伺服端錯誤鍵
|
|
2129
|
+
if (getRejectKey(err) === 'rowsInvalid') {
|
|
2130
|
+
srLog.error({ event: 'fun-updateTabItems-rowsInvalid', key: 'anUnexpectedErrorOccurred', woName, err: getErrorMessage(err) })
|
|
2131
|
+
return Promise.reject('anUnexpectedErrorOccurred')
|
|
2132
|
+
}
|
|
1716
2133
|
|
|
1717
|
-
|
|
1718
|
-
if (errWrite !== null) {
|
|
1719
|
-
srLog.error({ event: 'fun-updateTabItems-write', key: 'saveWriteFail', woName, err: getErrorMessage(errWrite) })
|
|
1720
|
-
return Promise.reject('saveWriteFail')
|
|
2134
|
+
return Promise.reject(err)
|
|
1721
2135
|
}
|
|
1722
2136
|
|
|
1723
|
-
//users 路徑: 回傳前剝除 password 欄位, 避免明文/hash 經 API 洩漏
|
|
2137
|
+
//users 路徑: 回傳前剝除 password 與 tokenVerify 欄位, 避免明文/hash/驗證 token 經 API 洩漏
|
|
1724
2138
|
if (woName === 'users') {
|
|
1725
2139
|
ltdtNew = map(ltdtNew, (row) => {
|
|
1726
2140
|
let copy = { ...row }
|
|
1727
2141
|
delete copy.password
|
|
2142
|
+
delete copy.tokenVerify
|
|
1728
2143
|
return copy
|
|
1729
2144
|
})
|
|
1730
2145
|
}
|
|
@@ -1938,9 +2353,11 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
1938
2353
|
//console.log(`...users.select`)
|
|
1939
2354
|
|
|
1940
2355
|
//delete password, 無錯誤取得後即先刪除, 避免調整程式時意外洩漏hash後密碼
|
|
2356
|
+
//tokenVerify (註冊驗證用 token, 憑證類) 同步收回 (T3, 原則 8): 契約生效後後台不再需要整列存回它 (伺服器專有欄位, 存檔不採用客戶端值, R-CONC-02)
|
|
1941
2357
|
if (deletePassword) {
|
|
1942
2358
|
us = map(us, (u) => {
|
|
1943
2359
|
delete u.password
|
|
2360
|
+
delete u.tokenVerify
|
|
1944
2361
|
return u
|
|
1945
2362
|
})
|
|
1946
2363
|
}
|
|
@@ -2001,7 +2418,8 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
2001
2418
|
|
|
2002
2419
|
|
|
2003
2420
|
//checkTokenAndUpdateUsersList
|
|
2004
|
-
|
|
2421
|
+
//optClient: 前端經 RPC 送入之第 4 參數 { baseline }(未驗證, 由 updateTabItems 內 parseSaveOpt 驗證; 不展開給 procOrm); 未給即不帶基準
|
|
2422
|
+
let checkTokenAndUpdateUsersList = async (token, lang, rows, opt = {}, optClient = undefined) => {
|
|
2005
2423
|
|
|
2006
2424
|
//checkToken
|
|
2007
2425
|
await checkToken(token, opt) //resolve僅回傳true, reject代表無效token或檢測token發生錯誤
|
|
@@ -2053,7 +2471,7 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
2053
2471
|
|
|
2054
2472
|
//updateUsersList (帶 lang/operatorId 給下層用於 add 群組密碼策略檢查與 audit)
|
|
2055
2473
|
//停用使用者即時撤銷其全部 token (ADR-037) 與清除使用者清單快取 (ADR-008), 皆於 updateTabItems 寫入階段後執行, 不論寫入成敗 (ADR-080)
|
|
2056
|
-
rows = await updateUsersList(rows, { lang, operatorId })
|
|
2474
|
+
rows = await updateUsersList(rows, { lang, operatorId, optClient })
|
|
2057
2475
|
|
|
2058
2476
|
return rows
|
|
2059
2477
|
})
|
|
@@ -2098,43 +2516,30 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
2098
2516
|
|
|
2099
2517
|
|
|
2100
2518
|
//updateTokensList
|
|
2101
|
-
|
|
2519
|
+
//opt: { operatorId, optClient }, operatorId 為操作者 id(稽核 userIdUpdate, R-CONC-04), optClient 見 checkTokenAndUpdateUsersList
|
|
2520
|
+
let updateTokensList = async (rows, opt = {}) => {
|
|
2102
2521
|
|
|
2103
2522
|
//rows 來自使用端, 須為陣列才可走訪 (同 checkTokenAndUpdateUsersList; 非陣列〔如帶 length 之物件〕原於下方 for…of 拋 TypeError, R-OBJ-ITER-01)
|
|
2104
2523
|
if (!isarr(rows)) {
|
|
2105
2524
|
return Promise.reject('invalidRows')
|
|
2106
2525
|
}
|
|
2107
2526
|
|
|
2108
|
-
//perms
|
|
2109
|
-
|
|
2110
|
-
let kpPermsOld = Object.create(null)
|
|
2111
|
-
each(await woItems.tokens.select(), (t) => {
|
|
2112
|
-
kpPermsOld[t.id] = JSON.stringify(get(t, 'perms'))
|
|
2113
|
-
})
|
|
2114
|
-
for (let r of rows) {
|
|
2115
|
-
let perms = get(r, 'perms')
|
|
2116
|
-
let id = get(r, 'id', '')
|
|
2117
|
-
if (haskey(kpPermsOld, id) && kpPermsOld[id] === JSON.stringify(perms)) {
|
|
2118
|
-
continue
|
|
2119
|
-
}
|
|
2120
|
-
if (!isAppPermsValue(perms)) {
|
|
2121
|
-
return Promise.reject('tokenPermsInvalid')
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
//合法者去重並依全集排序後儲存
|
|
2125
|
-
rows = map(rows, (r) => {
|
|
2126
|
-
return { ...r, perms: normAppPerms(get(r, 'perms')) }
|
|
2127
|
-
})
|
|
2128
|
-
|
|
2527
|
+
//perms 之驗證(本批有變動或新增之列須為未設定或已登錄權限字串之陣列, 任一列不合法則整批 reject 不寫入, ADR-069)與合法者去重並依全集排序,
|
|
2528
|
+
//須對照鎖內新鮮讀取之庫內值, 故移入 updateTabItems 之表鎖內(原於鎖外讀庫比對, 與寫入之間可被插隊)
|
|
2129
2529
|
//updateTabItems, 清除權杖清單快取於其寫入階段後執行 (ADR-080)
|
|
2130
|
-
rows = await updateTabItems('tokens', rows, 'id', {
|
|
2530
|
+
rows = await updateTabItems('tokens', rows, 'id', {
|
|
2531
|
+
resetOrder: false,
|
|
2532
|
+
operatorId: get(opt, 'operatorId', ''),
|
|
2533
|
+
optClient: get(opt, 'optClient'),
|
|
2534
|
+
ocList: ocGetTokensList,
|
|
2535
|
+
})
|
|
2131
2536
|
|
|
2132
2537
|
return rows
|
|
2133
2538
|
}
|
|
2134
2539
|
|
|
2135
2540
|
|
|
2136
2541
|
//checkTokenAndUpdateTokensList
|
|
2137
|
-
let checkTokenAndUpdateTokensList = async (token, rows, opt = {}) => {
|
|
2542
|
+
let checkTokenAndUpdateTokensList = async (token, rows, opt = {}, optClient = undefined) => {
|
|
2138
2543
|
|
|
2139
2544
|
//checkToken
|
|
2140
2545
|
await checkToken(token, opt) //resolve僅回傳true, reject代表無效token或檢測token發生錯誤
|
|
@@ -2147,7 +2552,7 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
2147
2552
|
return await lockSave('updateTokensList', operatorId, async () => {
|
|
2148
2553
|
|
|
2149
2554
|
//updateTokensList, 清除權杖清單快取 (ADR-008) 於 updateTabItems 寫入階段後執行, 不論寫入成敗 (ADR-080)
|
|
2150
|
-
rows = await updateTokensList(rows)
|
|
2555
|
+
rows = await updateTokensList(rows, { operatorId, optClient })
|
|
2151
2556
|
|
|
2152
2557
|
return rows
|
|
2153
2558
|
})
|
|
@@ -2187,17 +2592,23 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
2187
2592
|
|
|
2188
2593
|
|
|
2189
2594
|
//updateIpsList
|
|
2190
|
-
|
|
2595
|
+
//opt: { operatorId, optClient }, 同 updateTokensList
|
|
2596
|
+
let updateIpsList = async (rows, opt = {}) => {
|
|
2191
2597
|
|
|
2192
2598
|
//updateTabItems, 清除 IP 清單快取於其寫入階段後執行 (ADR-080)
|
|
2193
|
-
rows = await updateTabItems('ips', rows, 'id', {
|
|
2599
|
+
rows = await updateTabItems('ips', rows, 'id', {
|
|
2600
|
+
resetOrder: false,
|
|
2601
|
+
operatorId: get(opt, 'operatorId', ''),
|
|
2602
|
+
optClient: get(opt, 'optClient'),
|
|
2603
|
+
ocList: ocGetIpsList,
|
|
2604
|
+
})
|
|
2194
2605
|
|
|
2195
2606
|
return rows
|
|
2196
2607
|
}
|
|
2197
2608
|
|
|
2198
2609
|
|
|
2199
2610
|
//checkTokenAndUpdateIpsList
|
|
2200
|
-
let checkTokenAndUpdateIpsList = async (token, rows, opt = {}) => {
|
|
2611
|
+
let checkTokenAndUpdateIpsList = async (token, rows, opt = {}, optClient = undefined) => {
|
|
2201
2612
|
|
|
2202
2613
|
//checkToken
|
|
2203
2614
|
await checkToken(token, opt) //resolve僅回傳true, reject代表無效token或檢測token發生錯誤
|
|
@@ -2210,7 +2621,7 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
2210
2621
|
return await lockSave('updateIpsList', operatorId, async () => {
|
|
2211
2622
|
|
|
2212
2623
|
//updateIpsList, 清除 IP 清單快取 (ADR-008) 於 updateTabItems 寫入階段後執行, 不論寫入成敗 (ADR-080)
|
|
2213
|
-
rows = await updateIpsList(rows)
|
|
2624
|
+
rows = await updateIpsList(rows, { operatorId, optClient })
|
|
2214
2625
|
|
|
2215
2626
|
return rows
|
|
2216
2627
|
})
|
|
@@ -2256,6 +2667,7 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
|
|
|
2256
2667
|
getIpByKV,
|
|
2257
2668
|
|
|
2258
2669
|
revokeTokensByUserId,
|
|
2670
|
+
revokeTokensByUserIdLocked, //持鎖版: 呼叫端須已持有 tokens 表鎖(procProtect 封鎖於 [tokens, users] 鎖內使用), 不可重入
|
|
2259
2671
|
|
|
2260
2672
|
getGenUserByKV,
|
|
2261
2673
|
getGenUserByUserId,
|