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/procLang.mjs
CHANGED
|
@@ -1013,6 +1013,10 @@ let kpLang = {
|
|
|
1013
1013
|
eng: 'Invalid app token permissions.',
|
|
1014
1014
|
cht: '應用系統金鑰權限設定無效。',
|
|
1015
1015
|
},
|
|
1016
|
+
tokenSessionAddNotAllowed: { //金鑰清單存檔不得新增登入權杖(session 列; ADR-092)
|
|
1017
|
+
eng: 'Login tokens cannot be added. Nothing was saved.',
|
|
1018
|
+
cht: '不可新增登入權杖,本次未儲存任何變更',
|
|
1019
|
+
},
|
|
1016
1020
|
//應用系統金鑰之各權限名稱, 鍵為 appPerm_<權限字串> (權限全集見 src/appPerms.mjs)
|
|
1017
1021
|
appPerm_readUsers: {
|
|
1018
1022
|
eng: 'Read users',
|
|
@@ -1324,6 +1328,20 @@ let kpLang = {
|
|
|
1324
1328
|
cht: `資料寫入未全部完成,部分變更可能已生效,請重新整理頁面確認後再操作`,
|
|
1325
1329
|
},
|
|
1326
1330
|
|
|
1331
|
+
//多人同時編輯之寫入契約(原則 10): 拒收鍵 baselineMismatch / rowNotFound / baselineFormat 由 procCore.getSaveRejectKey 轉為下列語系鍵, 一律提示重新載入; 拒收時零寫入
|
|
1332
|
+
saveStale: { //載入後資料已被他人或系統更動(baselineMismatch)
|
|
1333
|
+
eng: `The data has been changed by another user or the system since you loaded this page. Nothing was saved. Please reload the page and make your changes again.`,
|
|
1334
|
+
cht: `資料自您載入後已被其他使用者或系統更動,本次未儲存任何變更,請重新載入頁面後再操作`,
|
|
1335
|
+
},
|
|
1336
|
+
saveRowNotFound: { //要修改或刪除之資料已不存在(rowNotFound)
|
|
1337
|
+
eng: `Some of the data no longer exists (it may have been deleted by another user). Nothing was saved. Please reload the page and make your changes again.`,
|
|
1338
|
+
cht: `部分資料已不存在(可能已被其他使用者刪除),本次未儲存任何變更,請重新載入頁面後再操作`,
|
|
1339
|
+
},
|
|
1340
|
+
saveBaselineFormat: { //前端送出之載入基準格式或版本不識(baselineFormat)
|
|
1341
|
+
eng: `The page data version is not recognized. Nothing was saved. Please reload the page and make your changes again.`,
|
|
1342
|
+
cht: `頁面資料版本無法辨識,本次未儲存任何變更,請重新載入頁面後再操作`,
|
|
1343
|
+
},
|
|
1344
|
+
|
|
1327
1345
|
}
|
|
1328
1346
|
|
|
1329
1347
|
|
package/server/procProtect.mjs
CHANGED
|
@@ -29,7 +29,6 @@ import pm2resolve from 'wsemi/src/pm2resolve.mjs'
|
|
|
29
29
|
import pmSeries from 'wsemi/src/pmSeries.mjs'
|
|
30
30
|
import getErrorMessage from 'wsemi/src/getErrorMessage.mjs'
|
|
31
31
|
import ds from '../src/schema/index.mjs'
|
|
32
|
-
import ormWrite from './ormWrite.mjs'
|
|
33
32
|
import wrapTimer from './wrapTimer.mjs'
|
|
34
33
|
import { maskToken, safeText } from './srLog.mjs'
|
|
35
34
|
|
|
@@ -49,7 +48,11 @@ let FORWARDED_PORT_MAX = 65535
|
|
|
49
48
|
function proc(woItems, p, opt = {}) {
|
|
50
49
|
|
|
51
50
|
//params
|
|
51
|
+
//procOrm / withTabLocks: 取自 w-serv-orm 實例(與 procCore 同一組, 同表只由同一鎖管理器寫入, 規格 §12); 系統直寫 users / tokens / ips 一律在該表表鎖內、
|
|
52
|
+
//經 procOrm 且 mustExist(只改不增), 更新者記常數 'system' (R-CONC-01, R-CONC-04, 決策 D7)
|
|
52
53
|
let {
|
|
54
|
+
procOrm,
|
|
55
|
+
withTabLocks,
|
|
53
56
|
srLog,
|
|
54
57
|
minForAccountLoginFailed,
|
|
55
58
|
numForAccountLoginFailed,
|
|
@@ -62,6 +65,14 @@ function proc(woItems, p, opt = {}) {
|
|
|
62
65
|
minBlockForIpCallApi,
|
|
63
66
|
} = opt
|
|
64
67
|
|
|
68
|
+
//check
|
|
69
|
+
if (!isfun(procOrm) || !isfun(withTabLocks)) {
|
|
70
|
+
throw new Error('procProtect needs procOrm and withTabLocks from the WServOrm instance')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//SYSTEM_ID: 無使用者可歸屬之系統寫入(計時器、自動封鎖)記於 userIdUpdate 之常數
|
|
74
|
+
let SYSTEM_ID = 'system'
|
|
75
|
+
|
|
65
76
|
|
|
66
77
|
//logError: 封鎖寫入失敗之紀錄 (ADR-080); srLog 未給時略過, 同 blockAccountByToken 之 debug 守衛
|
|
67
78
|
let logError = (o) => {
|
|
@@ -174,57 +185,75 @@ function proc(woItems, p, opt = {}) {
|
|
|
174
185
|
//blockAccount
|
|
175
186
|
let blockAccount = async (account) => {
|
|
176
187
|
|
|
177
|
-
//getGenUserByAccount
|
|
188
|
+
//getGenUserByAccount, 取得使用者主鍵; 封鎖狀態於下方 [tokens, users] 鎖內重讀
|
|
178
189
|
let u = await p.getGenUserByAccount(account)
|
|
179
190
|
|
|
180
191
|
// //check, 不用檢測, 若resolve必定有, 若reject則由外部處理
|
|
181
192
|
// if (!iseobj(u)) {
|
|
182
193
|
// }
|
|
183
194
|
|
|
184
|
-
//
|
|
185
|
-
|
|
195
|
+
//[tokens, users] 鎖內: 重讀使用者(新鮮) → 判斷是否已封鎖 → 寫 timeBlocked → 撤銷 token, 同一把鎖完成 (S15):
|
|
196
|
+
//timeBlocked 為雙方欄位(後台可改、自動封鎖亦寫), 系統直寫須取表鎖, 持舊畫面之後台存檔才會被偵測而拒收 (V4); 讀到之使用者於寫入前不被刪除插隊 (X5)
|
|
197
|
+
await withTabLocks(['tokens', 'users'], async () => {
|
|
186
198
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
199
|
+
//帳號對應再驗: 失敗登入所針對之帳號於等鎖期間可能已被後台改名或停用, 該帳號已不屬於這位使用者則不封鎖(以鎖內新鮮狀態為準)
|
|
200
|
+
let usAcc = await woItems.users.select({ account, isActive: 'y' })
|
|
201
|
+
if (size(usAcc) !== 1 || get(usAcc[0], 'id', '') !== u.id) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
191
204
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
//重讀使用者, 已被刪除則無從封鎖(其 token 已於刪除時同鎖撤銷)
|
|
206
|
+
let us = await woItems.users.select({ id: u.id })
|
|
207
|
+
if (size(us) !== 1) {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
195
210
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
await ormWrite(woItems.users, 'save', {
|
|
199
|
-
id: u.id,
|
|
200
|
-
timeBlocked,
|
|
201
|
-
})
|
|
202
|
-
.catch((err) => {
|
|
203
|
-
errBlock = err
|
|
204
|
-
})
|
|
211
|
+
//getBlockedByUser
|
|
212
|
+
let b = await getBlockedByUser(us[0])
|
|
205
213
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
//對齊 spec/流程_自動封鎖機制.md A32「強制登出」契約 — Round-3 audit B-01 fix.)
|
|
211
|
-
//撤銷與刪除 / 停用使用者共用 p.revokeTokensByUserId (一次刪除並逐筆檢查); 封鎖寫入失敗仍撤銷 (依意圖 fail closed, ADR-080)
|
|
212
|
-
let errRevoke = null
|
|
213
|
-
await p.revokeTokensByUserId(u.id)
|
|
214
|
-
.catch((err) => {
|
|
215
|
-
errRevoke = err
|
|
216
|
-
})
|
|
214
|
+
//check
|
|
215
|
+
if (b) {
|
|
216
|
+
return //已封鎖, 禁止重複封鎖
|
|
217
|
+
}
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
219
|
+
//timeBlocked, 依照minBlockForAccountLoginFailed(min)更新封鎖時間
|
|
220
|
+
let timeBlocked = ot().add(minBlockForAccountLoginFailed, 'minute').format('YYYY-MM-DDTHH:mm:ss.SSSZ')
|
|
221
|
+
// console.log('timeBlocked', timeBlocked)
|
|
222
|
+
|
|
223
|
+
//封鎖使用者, 須更新使用者timeBlocked; 經 procOrm 且 mustExist(只改不增), 更新者記 'system'; 逐筆檢查 (ADR-080)
|
|
224
|
+
let errBlock = null
|
|
225
|
+
await procOrm(SYSTEM_ID, 'users', 'save', [{
|
|
226
|
+
id: u.id,
|
|
227
|
+
timeBlocked,
|
|
228
|
+
}], { mustExist: true })
|
|
229
|
+
.catch((err) => {
|
|
230
|
+
errBlock = err
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
//封鎖使用者, 須刪除該 user 名下所有 token (multi-session 強制全部登出)
|
|
234
|
+
//(原本走 p.getTokenByKV('userId', u.id) 取單筆, 但 ADR-001 接受同帳號多 active token,
|
|
235
|
+
//getTokenByKV 之 nts>=2 reject 'duplicate token' 對 multi-session user 必觸發 → token
|
|
236
|
+
//完全沒刪 → 帳號被鎖但既有 session 仍可用. 改用 select 直接取陣列後全部刪除,
|
|
237
|
+
//對齊 spec/流程_自動封鎖機制.md A32「強制登出」契約 — Round-3 audit B-01 fix.)
|
|
238
|
+
//撤銷與刪除 / 停用使用者共用 p.revokeTokensByUserIdLocked (持鎖版, 一次刪除並逐筆檢查); 封鎖寫入失敗仍撤銷 (依意圖 fail closed, ADR-080)
|
|
239
|
+
let errRevoke = null
|
|
240
|
+
await p.revokeTokensByUserIdLocked(u.id)
|
|
241
|
+
.catch((err) => {
|
|
242
|
+
errRevoke = err
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
//check, 失敗記錄後 reject; 封鎖未寫入時, 下次登入失敗仍超過門檻即再嘗試封鎖
|
|
246
|
+
if (errBlock !== null || errRevoke !== null) {
|
|
247
|
+
logError({
|
|
248
|
+
event: 'fun-blockAccount',
|
|
249
|
+
userId: u.id,
|
|
250
|
+
errBlock: errBlock !== null ? getErrorMessage(errBlock) : '',
|
|
251
|
+
errRevoke: errRevoke !== null ? getErrorMessage(errRevoke) : '',
|
|
252
|
+
})
|
|
253
|
+
return Promise.reject(`failed to block account`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
})
|
|
228
257
|
|
|
229
258
|
}
|
|
230
259
|
|
|
@@ -372,55 +401,78 @@ function proc(woItems, p, opt = {}) {
|
|
|
372
401
|
return Promise.reject(`invalid userId`)
|
|
373
402
|
}
|
|
374
403
|
|
|
375
|
-
//getGenUserByUserId
|
|
404
|
+
//getGenUserByUserId, 取得使用者主鍵; 封鎖狀態於下方 [tokens, users] 鎖內重讀
|
|
376
405
|
let u = await p.getGenUserByUserId(userId)
|
|
377
406
|
|
|
378
407
|
// //check, 不用檢測, 若resolve必定有, 若reject則由外部處理
|
|
379
408
|
// if (!iseobj(u)) {
|
|
380
409
|
// }
|
|
381
410
|
|
|
382
|
-
//
|
|
383
|
-
|
|
411
|
+
//[tokens, users] 鎖內: 重讀使用者(新鮮) → 判斷是否已封鎖 → 寫 timeBlocked → 撤銷 token, 同一把鎖完成 (S16; 理由同 blockAccount)
|
|
412
|
+
await withTabLocks(['tokens', 'users'], async () => {
|
|
384
413
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
414
|
+
//重讀 token 並再驗身分: 鎖外判定其為使用者 token 與擁有者後, 等鎖期間後台存檔可能已將其升為應用系統金鑰(受信任, 不套用個人速率封鎖)、換擁有者, 或已被登出 / 撤銷;
|
|
415
|
+
//身分已與鎖外判定不符則不封鎖(以鎖內新鮮狀態為準)
|
|
416
|
+
let tn = null
|
|
417
|
+
await p.getTokenByKV('token', token)
|
|
418
|
+
.then((r) => {
|
|
419
|
+
tn = r
|
|
420
|
+
})
|
|
421
|
+
.catch(() => {})
|
|
422
|
+
if (tn === null || get(tn, 'isApp', '') === 'y' || get(tn, 'userId', '') !== u.id) {
|
|
423
|
+
return
|
|
424
|
+
}
|
|
389
425
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
426
|
+
//重讀使用者, 已被刪除則無從封鎖(其 token 已於刪除時同鎖撤銷)
|
|
427
|
+
let us = await woItems.users.select({ id: u.id })
|
|
428
|
+
if (size(us) !== 1) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
393
431
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
await ormWrite(woItems.users, 'save', {
|
|
397
|
-
id: u.id,
|
|
398
|
-
timeBlocked,
|
|
399
|
-
})
|
|
400
|
-
.catch((err) => {
|
|
401
|
-
errBlock = err
|
|
402
|
-
})
|
|
432
|
+
//getBlockedByUser
|
|
433
|
+
let b = await getBlockedByUser(us[0])
|
|
403
434
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
await p.revokeTokensByUserId(u.id)
|
|
409
|
-
.catch((err) => {
|
|
410
|
-
errRevoke = err
|
|
411
|
-
})
|
|
435
|
+
//check
|
|
436
|
+
if (b) {
|
|
437
|
+
return //已封鎖, 禁止重複封鎖
|
|
438
|
+
}
|
|
412
439
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
440
|
+
//timeBlocked, 依照minBlockForTokenCallApi(min)更新封鎖時間
|
|
441
|
+
let timeBlocked = ot().add(minBlockForTokenCallApi, 'minute').format('YYYY-MM-DDTHH:mm:ss.SSSZ')
|
|
442
|
+
// console.log('timeBlocked', timeBlocked)
|
|
443
|
+
|
|
444
|
+
//封鎖使用者, 須更新使用者timeBlocked; 經 procOrm 且 mustExist(只改不增), 更新者記 'system'; 逐筆檢查 (ADR-080)
|
|
445
|
+
let errBlock = null
|
|
446
|
+
await procOrm(SYSTEM_ID, 'users', 'save', [{
|
|
447
|
+
id: u.id,
|
|
448
|
+
timeBlocked,
|
|
449
|
+
}], { mustExist: true })
|
|
450
|
+
.catch((err) => {
|
|
451
|
+
errBlock = err
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
//封鎖使用者, 須刪除該 user 名下所有 token (含所用 token; 與 blockAccount 同為 multi-session 強制全部登出, 共用 p.revokeTokensByUserIdLocked 持鎖版);
|
|
455
|
+
//原只刪所用 token, 封鎖期間該使用者其他裝置之既有登入照常可用, 與 spec E2E-005 及登入失敗封鎖不一致 (2026-10-11 改, ADR-080)
|
|
456
|
+
//封鎖寫入失敗仍撤銷 (依意圖 fail closed, ADR-080)
|
|
457
|
+
let errRevoke = null
|
|
458
|
+
await p.revokeTokensByUserIdLocked(u.id)
|
|
459
|
+
.catch((err) => {
|
|
460
|
+
errRevoke = err
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
//check, 失敗記錄後 reject
|
|
464
|
+
if (errBlock !== null || errRevoke !== null) {
|
|
465
|
+
logError({
|
|
466
|
+
event: 'fun-blockAccountByToken',
|
|
467
|
+
userId: u.id,
|
|
468
|
+
token: maskToken(token),
|
|
469
|
+
errBlock: errBlock !== null ? getErrorMessage(errBlock) : '',
|
|
470
|
+
errRevoke: errRevoke !== null ? getErrorMessage(errRevoke) : '',
|
|
471
|
+
})
|
|
472
|
+
return Promise.reject(`failed to block account by token`)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
})
|
|
424
476
|
|
|
425
477
|
}
|
|
426
478
|
|
|
@@ -739,7 +791,14 @@ function proc(woItems, p, opt = {}) {
|
|
|
739
791
|
|
|
740
792
|
//blockIpByIp
|
|
741
793
|
// blockIpByIp 由 timer 單一 caller 觸發, pmSeries 保證序列化, 無 race; 詳 audit F-052 verifier 結論
|
|
794
|
+
// (計時器之間互不競態, 但與後台 IP 清單存檔 / 計時器登錄新 IP 並行: 讀取與寫入整段在 ips 表鎖內, 規格 §7; S17 / S18)
|
|
742
795
|
let blockIpByIp = async (ip) => {
|
|
796
|
+
return await withTabLocks(['ips'], () => _blockIpByIp(ip))
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
//_blockIpByIp, 須於 ips 表鎖內呼叫
|
|
801
|
+
let _blockIpByIp = async (ip) => {
|
|
743
802
|
let errTemp = null
|
|
744
803
|
|
|
745
804
|
//genTimeBlocked
|
|
@@ -785,8 +844,8 @@ function proc(woItems, p, opt = {}) {
|
|
|
785
844
|
})
|
|
786
845
|
// console.log('funNew', oip)
|
|
787
846
|
|
|
788
|
-
//insert
|
|
789
|
-
await
|
|
847
|
+
//insert, 經 procOrm 戳記更新者 'system' (R-CONC-04)
|
|
848
|
+
await procOrm(SYSTEM_ID, 'ips', 'insert', [oip])
|
|
790
849
|
.catch((err) => {
|
|
791
850
|
errTemp = err
|
|
792
851
|
})
|
|
@@ -812,12 +871,12 @@ function proc(woItems, p, opt = {}) {
|
|
|
812
871
|
let timeBlocked = genTimeBlocked()
|
|
813
872
|
// console.log('timeBlocked', timeBlocked)
|
|
814
873
|
|
|
815
|
-
//封鎖ip, 須更新ip的timeBlocked; 逐筆檢查 (ADR-080)
|
|
874
|
+
//封鎖ip, 須更新ip的timeBlocked; 經 procOrm 且 mustExist(只改不增), 更新者記 'system'; 逐筆檢查 (ADR-080)
|
|
816
875
|
let errSave = null
|
|
817
|
-
await
|
|
876
|
+
await procOrm(SYSTEM_ID, 'ips', 'save', [{
|
|
818
877
|
id: oip.id,
|
|
819
878
|
timeBlocked,
|
|
820
|
-
})
|
|
879
|
+
}], { mustExist: true })
|
|
821
880
|
.catch((err) => {
|
|
822
881
|
errSave = err
|
|
823
882
|
})
|
|
@@ -865,7 +924,8 @@ function proc(woItems, p, opt = {}) {
|
|
|
865
924
|
|
|
866
925
|
|
|
867
926
|
//timer, 偵測新ip出現儲存至ips; 讀取或新增失敗由 wrapTimer 記錄(timer-error), 下一輪重試(原讀取失敗時重入鎖不釋放而永久停擺, ADR-085)
|
|
868
|
-
|
|
927
|
+
//讀取已登錄 IP 清單、比對與新增整段在 ips 表鎖內(S19): 與後台 IP 清單存檔、自動封鎖寫入互斥; 載入後才登錄之 IP 使持舊畫面存檔者被拒(先動先贏), 不致被整表存檔刪除 (X4)
|
|
928
|
+
setInterval(wrapTimer('ipDetectNew', () => withTabLocks(['ips'], async () => {
|
|
869
929
|
|
|
870
930
|
//getIpsList
|
|
871
931
|
let oips = await p.getIpsList()
|
|
@@ -911,10 +971,12 @@ function proc(woItems, p, opt = {}) {
|
|
|
911
971
|
})
|
|
912
972
|
// console.log('oipsNew', oipsNew)
|
|
913
973
|
|
|
914
|
-
//insert
|
|
915
|
-
|
|
974
|
+
//insert, 經 procOrm 戳記更新者 'system' (R-CONC-04); 無新 IP 時不呼叫
|
|
975
|
+
if (size(oipsNew) > 0) {
|
|
976
|
+
await procOrm(SYSTEM_ID, 'ips', 'insert', oipsNew)
|
|
977
|
+
}
|
|
916
978
|
|
|
917
|
-
}, timerError), 2000)
|
|
979
|
+
}), timerError), 2000)
|
|
918
980
|
|
|
919
981
|
|
|
920
982
|
//timer, 限制ip最大調用API次數
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { buildBaseline, digestRow } from 'w-serv-orm/src/baseline.mjs'
|
|
2
|
+
import contracts from './writeContracts/index.mjs'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
//後台三個清單頁(使用者 / 金鑰 / IP)共用之基準工具 (多人同時編輯之寫入契約, 原則 10; 規則帳本 rulebook_concurrency R-CONC-05):
|
|
6
|
+
//前端於載入(及存檔成功後重新讀取)時, 以 buildBaseline 對「畫面所見之同一快照」自算有序基準 [[id, 列摘要]], 存檔時隨第 4 參數送後端;
|
|
7
|
+
//後端於表鎖內新鮮讀取、重算比對, 不符整次拒收(零寫入, 先動先贏, 回 saveStale 等語系鍵提示重新載入). 不另向後端取雜湊, RPC 回傳形狀不變.
|
|
8
|
+
//算不出基準(列格式異常等)回 null: 送出時 null 被後端視為格式不識而拒收 saveBaselineFormat(提示重新載入), 不降級為不帶基準.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
//safeBaseline
|
|
12
|
+
function safeBaseline(rows, contract) {
|
|
13
|
+
try {
|
|
14
|
+
return buildBaseline(rows, contract)
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
console.log('buildBaseline catch', err)
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
//baselineOfUsers: rows 為 getUsersList 之回傳(未排序、未變動); 回傳基準陣列或 null
|
|
24
|
+
function baselineOfUsers(rows) {
|
|
25
|
+
return safeBaseline(rows, contracts.users)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
//baselineOfIps: rows 為 getIpsList 之回傳
|
|
30
|
+
function baselineOfIps(rows) {
|
|
31
|
+
return safeBaseline(rows, contracts.ips)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
//baselineOfTokens: rows 為 getTokensList 之回傳; 回傳 { baseline, baselineSession } 或 null
|
|
36
|
+
//baseline 為應用系統金鑰列(isApp === 'y', 嚴格整表比對), baselineSession 為 session 列(逐列寬鬆比對, 契約 tokensSession)
|
|
37
|
+
function baselineOfTokens(rows) {
|
|
38
|
+
let baseline = safeBaseline(rows, contracts.tokens)
|
|
39
|
+
let baselineSession = safeBaseline(rows, contracts.tokensSession)
|
|
40
|
+
if (baseline === null || baselineSession === null) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
return { baseline, baselineSession }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
//isSameSnapshot: 兩份列(各依主鍵)之契約 fields 摘要是否全相同(含列數); 存檔成功重新讀取後, 送出之列與伺服端列相同者保留現畫面,
|
|
48
|
+
//不同者(如他人於載入後有更動、伺服端正規化)須改以伺服端列取代, 令畫面所見之列與新基準來自同一快照 (規格 §8)
|
|
49
|
+
//不比對列序: 僅供無名次欄(orderKey)之金鑰 / IP 清單使用(顯示序由前端自排); 使用者清單存檔成功後一律以重新讀取之列取代, 不用本函數
|
|
50
|
+
function isSameSnapshot(rowsA, rowsB, contract) {
|
|
51
|
+
try {
|
|
52
|
+
if (!Array.isArray(rowsA) || !Array.isArray(rowsB) || rowsA.length !== rowsB.length) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
let mp = new Map()
|
|
56
|
+
for (let r of rowsA) {
|
|
57
|
+
mp.set(r.id, digestRow(r, contract))
|
|
58
|
+
}
|
|
59
|
+
if (mp.size !== rowsA.length) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
for (let r of rowsB) {
|
|
63
|
+
if (mp.get(r.id) !== digestRow(r, contract)) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return true
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
export { baselineOfUsers, baselineOfIps, baselineOfTokens, isSameSnapshot }
|
|
76
|
+
export default { baselineOfUsers, baselineOfIps, baselineOfTokens, isSameSnapshot }
|
|
@@ -239,6 +239,8 @@ import WPopup from 'w-component-vue/src/components/WPopup.vue'
|
|
|
239
239
|
import WInputCheckbox from 'w-component-vue/src/components/WInputCheckbox.vue'
|
|
240
240
|
import WAggridVue from 'w-aggrid-vue/src/components/WAggridVue.vue'
|
|
241
241
|
import WTimeminute from 'w-component-vue/src/components/WTimeminute.vue'
|
|
242
|
+
import contracts from '../writeContracts/index.mjs'
|
|
243
|
+
import { baselineOfIps, isSameSnapshot } from '../baselineOf.mjs'
|
|
242
244
|
|
|
243
245
|
|
|
244
246
|
export default {
|
|
@@ -269,6 +271,7 @@ export default {
|
|
|
269
271
|
headHeight: 100,
|
|
270
272
|
|
|
271
273
|
firstLoading: true,
|
|
274
|
+
baseline: null, //載入時(及存檔成功後重新讀取時)畫面所見之列的有序基準 [[id, 列摘要]], 存檔時送後端比對(多人同時編輯, 先動先贏); null 為未載入或算不出, 送出時被後端拒收
|
|
272
275
|
errMsg: '', //清單載入失敗之訊息(getDataError), 有值時以訊息取代表格
|
|
273
276
|
firstSetting: true,
|
|
274
277
|
systemProcing: false, //程式端載入/重載清單資料期間為true, 用於排除非使用者操作之rowsChange
|
|
@@ -329,6 +332,7 @@ export default {
|
|
|
329
332
|
vo.$fapi.getIpsList(token)
|
|
330
333
|
.then((res) => {
|
|
331
334
|
// console.log(res)
|
|
335
|
+
vo.baseline = baselineOfIps(res) //對畫面所見之同一快照自算基準(須在排序、變動之前; buildBaseline 不改輸入)
|
|
332
336
|
res = sortBy(res, 'ip').reverse()
|
|
333
337
|
vo.ips = res
|
|
334
338
|
vo.markDataReload() //程式端寫入資料, 其後續 rowsChange 非使用者變更
|
|
@@ -765,8 +769,9 @@ export default {
|
|
|
765
769
|
let lang = get(vo, '$store.state.lang', 'eng')
|
|
766
770
|
// console.log('token', token)
|
|
767
771
|
|
|
768
|
-
//updateIpsList (帶 lang, 後端 reject { key, msg } 之 msg 依 lang 翻譯)
|
|
769
|
-
|
|
772
|
+
//updateIpsList (帶 lang, 後端 reject { key, msg } 之 msg 依 lang 翻譯), 第 4 參數帶載入時之基準:
|
|
773
|
+
//載入後資料已被他人或系統(計時器自動登錄新 IP、自動封鎖)更動者整次拒收(零寫入, 回 saveStale 等語系鍵提示重新載入), 草稿保留(不清 isModified)
|
|
774
|
+
await vo.$fapi.updateIpsList(token, lang, rows, { baseline: vo.baseline })
|
|
770
775
|
.catch((err) => {
|
|
771
776
|
errTemp = err
|
|
772
777
|
})
|
|
@@ -780,6 +785,23 @@ export default {
|
|
|
780
785
|
return
|
|
781
786
|
}
|
|
782
787
|
|
|
788
|
+
//儲存成功 → 重新讀取 getIpsList 作為新基準(取自伺服端, 不以自己送出之值當基準), 畫面所見之列須與新基準來自同一快照 (規格 §8):
|
|
789
|
+
//送出之列與伺服端列的契約欄位摘要全相同者保留現畫面(伺服器專有欄位維持原呈現, 與既有行為相同), 不同者改以伺服端列取代;
|
|
790
|
+
//重新讀取失敗則基準置 null(其後再存檔被後端拒收並提示重新載入, 不降級為不帶基準)
|
|
791
|
+
await vo.$fapi.getIpsList(token)
|
|
792
|
+
.then((res) => {
|
|
793
|
+
vo.baseline = baselineOfIps(res)
|
|
794
|
+
if (!isSameSnapshot(rows, res, contracts.ips)) {
|
|
795
|
+
res = sortBy(res, 'ip').reverse()
|
|
796
|
+
vo.ips = res
|
|
797
|
+
vo.markDataReload() //程式端寫入資料, 其後續 rowsChange 非使用者變更
|
|
798
|
+
}
|
|
799
|
+
})
|
|
800
|
+
.catch((err) => {
|
|
801
|
+
console.log('refresh getIpsList catch', err)
|
|
802
|
+
vo.baseline = null
|
|
803
|
+
})
|
|
804
|
+
|
|
783
805
|
//isModified
|
|
784
806
|
vo.isModified = false
|
|
785
807
|
|
|
@@ -376,6 +376,8 @@ import WAggridVue from 'w-aggrid-vue/src/components/WAggridVue.vue'
|
|
|
376
376
|
import WTimeminute from 'w-component-vue/src/components/WTimeminute.vue'
|
|
377
377
|
import { keysAppPerm, keysAppPermBase, keysAppPermHigh, getAppPerms, normAppPerms } from '../appPerms.mjs'
|
|
378
378
|
import AppPermsTags from './AppPermsTags.vue'
|
|
379
|
+
import contracts from '../writeContracts/index.mjs'
|
|
380
|
+
import { baselineOfTokens, isSameSnapshot } from '../baselineOf.mjs'
|
|
379
381
|
|
|
380
382
|
|
|
381
383
|
export default {
|
|
@@ -407,6 +409,7 @@ export default {
|
|
|
407
409
|
headHeight: 100,
|
|
408
410
|
|
|
409
411
|
firstLoading: true,
|
|
412
|
+
baselines: null, //載入時(及存檔成功後重新讀取時)畫面所見之列的基準 { baseline: 應用系統金鑰列〔嚴格整表比對〕, baselineSession: session 列〔逐列寬鬆比對〕 }, 存檔時送後端(多人同時編輯, 先動先贏); null 為未載入或算不出, 送出時被後端拒收
|
|
410
413
|
errMsg: '', //清單載入失敗之訊息(getDataError), 有值時以訊息取代表格
|
|
411
414
|
firstSetting: true,
|
|
412
415
|
systemProcing: false, //程式端載入/重載清單資料期間為true, 用於排除非使用者操作之rowsChange
|
|
@@ -490,6 +493,7 @@ export default {
|
|
|
490
493
|
vo.$fapi.getTokensList(token)
|
|
491
494
|
.then((res) => {
|
|
492
495
|
// console.log(res)
|
|
496
|
+
vo.baselines = baselineOfTokens(res) //對畫面所見之同一快照自算基準(須在排序、變動之前; buildBaseline 不改輸入)
|
|
493
497
|
res = sortBy(res, 'timeCreate').reverse()
|
|
494
498
|
vo.tokens = res
|
|
495
499
|
vo.markDataReload() //程式端寫入資料, 其後續 rowsChange 非使用者變更
|
|
@@ -1119,8 +1123,9 @@ export default {
|
|
|
1119
1123
|
let lang = get(vo, '$store.state.lang', 'eng')
|
|
1120
1124
|
// console.log('token', token)
|
|
1121
1125
|
|
|
1122
|
-
//updateTokensList (帶 lang, 後端 reject { key, msg } 之 msg 依 lang 翻譯)
|
|
1123
|
-
|
|
1126
|
+
//updateTokensList (帶 lang, 後端 reject { key, msg } 之 msg 依 lang 翻譯), 第 4 參數帶載入時之基準:
|
|
1127
|
+
//應用系統金鑰列嚴格整表比對, session 列(登入新增、續期、登出等系統頻繁增刪)逐列寬鬆比對; 不符整次拒收(零寫入, 回 saveStale 等語系鍵提示重新載入), 草稿保留(不清 isModified)
|
|
1128
|
+
await vo.$fapi.updateTokensList(token, lang, rows, vo.baselines === null ? { baseline: null, baselineSession: null } : { baseline: vo.baselines.baseline, baselineSession: vo.baselines.baselineSession })
|
|
1124
1129
|
.catch((err) => {
|
|
1125
1130
|
errTemp = err
|
|
1126
1131
|
})
|
|
@@ -1134,6 +1139,23 @@ export default {
|
|
|
1134
1139
|
return
|
|
1135
1140
|
}
|
|
1136
1141
|
|
|
1142
|
+
//儲存成功 → 重新讀取 getTokensList 作為新基準(取自伺服端, 不以自己送出之值當基準: 寫入時 clearXSS、perms 去重排序等會改寫部分值, 且 session 列於此期間可能已續期或新增),
|
|
1143
|
+
//畫面所見之列須與新基準來自同一快照 (規格 §8): 送出之列與伺服端列的契約欄位摘要全相同者保留現畫面(伺服器專有欄位如 timeUpdate 維持原呈現, 與既有行為相同),
|
|
1144
|
+
//不同者(他人或系統於期間有更動)改以伺服端列取代; 重新讀取失敗則基準置 null(其後再存檔被後端拒收並提示重新載入, 不降級為不帶基準)
|
|
1145
|
+
await vo.$fapi.getTokensList(token)
|
|
1146
|
+
.then((res) => {
|
|
1147
|
+
vo.baselines = baselineOfTokens(res)
|
|
1148
|
+
if (!isSameSnapshot(rows, res, contracts.tokens)) {
|
|
1149
|
+
res = sortBy(res, 'timeCreate').reverse()
|
|
1150
|
+
vo.tokens = res
|
|
1151
|
+
vo.markDataReload() //程式端寫入資料, 其後續 rowsChange 非使用者變更
|
|
1152
|
+
}
|
|
1153
|
+
})
|
|
1154
|
+
.catch((err) => {
|
|
1155
|
+
console.log('refresh getTokensList catch', err)
|
|
1156
|
+
vo.baselines = null
|
|
1157
|
+
})
|
|
1158
|
+
|
|
1137
1159
|
//isModified
|
|
1138
1160
|
vo.isModified = false
|
|
1139
1161
|
|
|
@@ -423,6 +423,7 @@ import WInputCheckbox from 'w-component-vue/src/components/WInputCheckbox.vue'
|
|
|
423
423
|
import WAggridVue from 'w-aggrid-vue/src/components/WAggridVue.vue'
|
|
424
424
|
import WTimeminute from 'w-component-vue/src/components/WTimeminute.vue'
|
|
425
425
|
import WButtonChip from 'w-component-vue/src/components/WButtonChip.vue'
|
|
426
|
+
import { baselineOfUsers } from '../baselineOf.mjs'
|
|
426
427
|
|
|
427
428
|
|
|
428
429
|
export default {
|
|
@@ -458,6 +459,7 @@ export default {
|
|
|
458
459
|
headHeight: 100,
|
|
459
460
|
|
|
460
461
|
firstLoading: true,
|
|
462
|
+
baseline: null, //載入時(及存檔成功後重新讀取時)畫面所見之列的有序基準 [[id, 列摘要]], 存檔時送後端比對(多人同時編輯, 先動先贏); null 為未載入或算不出, 送出時被後端拒收
|
|
461
463
|
errMsg: '', //清單載入失敗之訊息(getDataError), 有值時以訊息取代表格
|
|
462
464
|
firstSetting: true,
|
|
463
465
|
systemProcing: false, //程式端載入/重載清單資料期間為true, 用於排除非使用者操作之rowsChange
|
|
@@ -573,6 +575,7 @@ export default {
|
|
|
573
575
|
vo.$fapi.getUsersList(token)
|
|
574
576
|
.then((res) => {
|
|
575
577
|
// console.log(res)
|
|
578
|
+
vo.baseline = baselineOfUsers(res) //對畫面所見之同一快照自算基準(須在排序、變動之前; buildBaseline 不改輸入)
|
|
576
579
|
res = sortBy(res, 'order')
|
|
577
580
|
vo.users = res
|
|
578
581
|
vo.markDataReload() //程式端寫入資料, 其後續 rowsChange 非使用者變更
|
|
@@ -1550,14 +1553,14 @@ export default {
|
|
|
1550
1553
|
let token = vo.userToken
|
|
1551
1554
|
let lang = get(vo, '$store.state.lang', 'eng')
|
|
1552
1555
|
|
|
1553
|
-
//updateUsersList
|
|
1554
|
-
await vo.$fapi.updateUsersList(token, lang, rows)
|
|
1556
|
+
//updateUsersList, 第 4 參數帶載入時之基準: 載入後資料已被他人或系統更動者整次拒收(零寫入, 回 saveStale 等語系鍵提示重新載入), 草稿保留(不清 isModified)
|
|
1557
|
+
await vo.$fapi.updateUsersList(token, lang, rows, { baseline: vo.baseline })
|
|
1555
1558
|
.catch((err) => {
|
|
1556
1559
|
errTemp = err
|
|
1557
1560
|
})
|
|
1558
1561
|
|
|
1559
1562
|
//check
|
|
1560
|
-
//後端 reject (token 過期 / ckKey 衝突 / 密碼策略違反 /
|
|
1563
|
+
//後端 reject (token 過期 / ckKey 衝突 / 密碼策略違反 / 自我鎖死 / 基準不符等)
|
|
1561
1564
|
//改用 showCheckYes 強制 admin 點擊確認讀過訊息, 避免 transient toast 漏看
|
|
1562
1565
|
if (errTemp !== null) {
|
|
1563
1566
|
//hide loading 因 showCheckYes 是 modal, 等待期間 loading 會疊著
|
|
@@ -1570,14 +1573,17 @@ export default {
|
|
|
1570
1573
|
|
|
1571
1574
|
//儲存成功 → 重拉 getUsersList 同步 server-side data (新增使用者的 timeVerified
|
|
1572
1575
|
//在後端被填 now, 既有使用者的 audit fields 也在後端被覆寫, 須刷新前端表格)
|
|
1576
|
+
//新基準取自伺服端重新讀取之列(不以自己送出之值當基準: 寫入時 clearXSS 等會改寫部分字串), 且與畫面所見之列來自同一快照 (規格 §8)
|
|
1573
1577
|
await vo.$fapi.getUsersList(token)
|
|
1574
1578
|
.then((res) => {
|
|
1579
|
+
vo.baseline = baselineOfUsers(res)
|
|
1575
1580
|
res = sortBy(res, 'order')
|
|
1576
1581
|
vo.users = res
|
|
1577
1582
|
vo.markDataReload() //程式端寫入資料, 其後續 rowsChange 非使用者變更
|
|
1578
1583
|
})
|
|
1579
1584
|
.catch((err) => {
|
|
1580
1585
|
console.log('refresh getUsersList catch', err)
|
|
1586
|
+
vo.baseline = null //重新讀取失敗: 舊基準已過期, 不沿用; 其後再存檔被後端拒收並提示重新載入(不降級為不帶基準)
|
|
1581
1587
|
})
|
|
1582
1588
|
|
|
1583
1589
|
//isModified
|
|
@@ -44,6 +44,10 @@ let settings = {
|
|
|
44
44
|
name: '到期時間',
|
|
45
45
|
type: 'STRING',
|
|
46
46
|
},
|
|
47
|
+
userIdUpdate: { //最後修改者(伺服器專有, 由 procOrm 戳記; 系統計時器寫入記常數 'system', 見 rulebook_concurrency R-CONC-04); userId 為擁有者, 不被操作者覆寫
|
|
48
|
+
name: '最新變更使用者主鍵',
|
|
49
|
+
type: 'STRING',
|
|
50
|
+
},
|
|
47
51
|
timeUpdate: {
|
|
48
52
|
name: '更新時間',
|
|
49
53
|
type: 'STRING',
|