w-web-sso 1.1.13 → 1.1.14

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/docs/WWebSso.html CHANGED
@@ -2492,7 +2492,7 @@ token發放/驗證/更新, 後台使用者/金鑰/IP管理與統計資訊, 以
2492
2492
  <br class="clear">
2493
2493
 
2494
2494
  <footer>
2495
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
2495
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
2496
2496
  </footer>
2497
2497
 
2498
2498
  <script>prettyPrint();</script>
package/docs/global.html CHANGED
@@ -3312,7 +3312,7 @@ why: 計時器之 async 回呼 reject 時無人接手, Node 預設即結束行
3312
3312
  <br class="clear">
3313
3313
 
3314
3314
  <footer>
3315
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
3315
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
3316
3316
  </footer>
3317
3317
 
3318
3318
  <script>prettyPrint();</script>
package/docs/index.html CHANGED
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
@@ -1457,7 +1457,7 @@ export default WWebSso
1457
1457
  <br class="clear">
1458
1458
 
1459
1459
  <footer>
1460
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1460
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1461
1461
  </footer>
1462
1462
 
1463
1463
  <script>prettyPrint();</script>
@@ -93,7 +93,7 @@ export default getIgnoredOrmKeys
93
93
  <br class="clear">
94
94
 
95
95
  <footer>
96
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
96
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
97
97
  </footer>
98
98
 
99
99
  <script>prettyPrint();</script>
@@ -102,7 +102,7 @@ export default createLockSave
102
102
  <br class="clear">
103
103
 
104
104
  <footer>
105
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
105
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
106
106
  </footer>
107
107
 
108
108
  <script>prettyPrint();</script>
@@ -108,7 +108,7 @@ export default ormWrite
108
108
  <br class="clear">
109
109
 
110
110
  <footer>
111
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
111
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
112
112
  </footer>
113
113
 
114
114
  <script>prettyPrint();</script>
@@ -167,7 +167,7 @@ export default resolveShellWorker
167
167
  <br class="clear">
168
168
 
169
169
  <footer>
170
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
170
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
171
171
  </footer>
172
172
 
173
173
  <script>prettyPrint();</script>
@@ -117,7 +117,7 @@ export default wrapTimer
117
117
  <br class="clear">
118
118
 
119
119
  <footer>
120
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
120
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
121
121
  </footer>
122
122
 
123
123
  <script>prettyPrint();</script>
@@ -188,7 +188,7 @@ export default {
188
188
  <br class="clear">
189
189
 
190
190
  <footer>
191
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
191
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
192
192
  </footer>
193
193
 
194
194
  <script>prettyPrint();</script>
@@ -211,7 +211,7 @@ export { fetchSsoMsg, convertUser, hasUrlParam }
211
211
  <br class="clear">
212
212
 
213
213
  <footer>
214
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
214
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
215
215
  </footer>
216
216
 
217
217
  <script>prettyPrint();</script>
@@ -100,7 +100,7 @@ export default httpGetJson
100
100
  <br class="clear">
101
101
 
102
102
  <footer>
103
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
103
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
104
104
  </footer>
105
105
 
106
106
  <script>prettyPrint();</script>
@@ -99,7 +99,7 @@ export default maskUrl
99
99
  <br class="clear">
100
100
 
101
101
  <footer>
102
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
102
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
103
103
  </footer>
104
104
 
105
105
  <script>prettyPrint();</script>
@@ -237,7 +237,7 @@ export { getFreqVal, sumFreqVal, groupFreq, calcUsageSummary }
237
237
  <br class="clear">
238
238
 
239
239
  <footer>
240
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
240
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
241
241
  </footer>
242
242
 
243
243
  <script>prettyPrint();</script>
@@ -150,7 +150,7 @@ export default submitGuard
150
150
  <br class="clear">
151
151
 
152
152
  <footer>
153
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 11:30:30 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
153
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 17:28:25 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
154
154
  </footer>
155
155
 
156
156
  <script>prettyPrint();</script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "w-web-sso",
3
- "version": "1.1.13",
3
+ "version": "1.1.14",
4
4
  "main": "dist/w-web-sso.umd.js",
5
5
  "dependencies": {
6
6
  "json5": "^2.2.3",
@@ -1312,9 +1312,14 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
1312
1312
  //'incorrect old password' reject 路徑, 自然不會多寄信.
1313
1313
  return await kmx(`changeUserPassword:${userId}`, async () => {
1314
1314
 
1315
- //讀取、驗舊密碼與寫入於 users 表鎖內(S07; kmx 在外層、表鎖在內層): password 為伺服器專有欄位、isForceChangePw 為雙方欄位(系統直寫須取表鎖),
1316
- //驗證所依之 DB 雜湊與寫入間不被其他寫入者(重設密碼、後台存檔)插隊; 只改不增, 列已被刪則拒絕而不新增殘缺列 (X5)
1317
- let { u, email } = await withTabLocks(['users'], async () => {
1315
+ //讀取、驗舊密碼、撤銷其他 session 與寫入於 [tokens, users] 表鎖內(S07; kmx 在外層、表鎖在內層): password 為伺服器專有欄位、isForceChangePw 為雙方欄位(系統直寫須取表鎖),
1316
+ //驗證所依之 DB 雜湊與寫入間不被其他寫入者(重設密碼、後台存檔、登入建立 session)插隊; 只改不增, 列已被刪則拒絕而不新增殘缺列 (X5)
1317
+ //改密碼後之 session 處置 (R-SESS-01, ADR-093): 保留發出此請求之目前 session(token), 撤銷該使用者其他所有 session 權杖; app 權杖不是 session, 不撤銷
1318
+ let { u, email } = await withTabLocks(['tokens', 'users'], async () => {
1319
+
1320
+ //鎖內再驗呼叫所用之權杖仍有效 (外審 sol#1): 鎖外之 checkToken 通過後、等鎖期間該 session 可能已被同使用者另一個改密碼請求或管理員重設撤銷,
1321
+ //被撤銷之權杖不得再改密碼(否則「保留發出請求之目前 session」不成立, 該請求還會撤銷對方保留之 session); checkToken 只讀不取鎖, 可在鎖內呼叫
1322
+ await checkToken(token)
1318
1323
 
1319
1324
  //getGenUserByUserId (with password)
1320
1325
  let u = await _getGenUserByKV('id', userId, { deletePassword: false })
@@ -1348,6 +1353,21 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
1348
1353
  //hash newPassword
1349
1354
  let passwordNew = hashPassword(newPassword, salt)
1350
1355
 
1356
+ //撤銷其他 session(先於密碼寫入, 驗證皆已通過): 撤銷失敗即 reject 且密碼不變, 不會出現「密碼已變更但舊 session 仍有效」(R-SESS-01)
1357
+ //(以布林旗標判失敗而非 errRevoke 是否為 null: reject 值可能為 null / undefined, 外審 sol#2)
1358
+ let revokeFailed = false
1359
+ let errRevoke = null
1360
+ let nRevoked = await revokeSessionTokensLocked(userId, token)
1361
+ .catch((err) => {
1362
+ revokeFailed = true
1363
+ errRevoke = err
1364
+ })
1365
+ if (revokeFailed) {
1366
+ srLog.error({ event: 'fun-changePassword-revokeSessions', key: 'userChangePasswordFail', userId, err: getErrorMessage(errRevoke) })
1367
+ return Promise.reject('userChangePasswordFail')
1368
+ }
1369
+ srLog.info({ event: 'fun-changePassword-revokeSessions', userId, nRevoked })
1370
+
1351
1371
  //save (清 isForceChangePw='n', 變更密碼成功後即解除強制變更), 更新者記該使用者本人 (R-CONC-04)
1352
1372
  //逐筆檢查(ADR-080): 寫入失敗即 reject, 不寄「密碼已變更」信
1353
1373
  let errTemp = null
@@ -1468,9 +1488,10 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
1468
1488
  return Promise.reject('adminResetPasswordAlreadyTriggered')
1469
1489
  }
1470
1490
 
1471
- //目標 user 查找與寫入於 users 表鎖內(S08; kmx 在外層、表鎖在內層): password 為伺服器專有欄位、isForceChangePw 為雙方欄位(系統直寫須取表鎖);
1491
+ //目標 user 查找、撤銷其全部 session 與寫入於 [tokens, users] 表鎖內(S08; kmx 在外層、表鎖在內層): password 為伺服器專有欄位、isForceChangePw 為雙方欄位(系統直寫須取表鎖);
1472
1492
  //只改不增, 目標已被刪則拒絕而不新增殘缺列 (X5); 更新者記操作管理員 (R-CONC-04)
1473
- let { targetAccount, targetName, targetEmail, newPassword } = await withTabLocks(['users'], async () => {
1493
+ //改密碼後之 session 處置 (R-SESS-01, ADR-093): 撤銷對方全部 session 權杖(不含 app 權杖); 呼叫者(管理員 / app 權杖)自己之權杖不在撤銷範圍(目標不得為自己)
1494
+ let { targetAccount, targetName, targetEmail, newPassword } = await withTabLocks(['tokens', 'users'], async () => {
1474
1495
 
1475
1496
  //目標 user (含 password 拿到也不用, 只是確認存在)
1476
1497
  let uTarget = null
@@ -1491,9 +1512,26 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
1491
1512
  //產生隨機新密碼 (符合 passwordPolicy + account 限制)
1492
1513
  let newPassword = genRandomPassword(passwordPolicy, targetAccount)
1493
1514
 
1494
- //hash + save (含 isForceChangePw='y')
1495
- //逐筆檢查(ADR-080): 寫入失敗即 reject, 不記節流、不寄出未生效之新密碼
1515
+ //hash
1496
1516
  let hashed = hashPassword(newPassword, salt)
1517
+
1518
+ //撤銷對方全部 session(先於密碼寫入): 撤銷失敗即 reject 且密碼不變, 不會出現「密碼已重設但舊 session 仍有效」(R-SESS-01)
1519
+ //(以布林旗標判失敗而非 errRevoke 是否為 null: reject 值可能為 null / undefined, 外審 sol#2)
1520
+ let revokeFailed = false
1521
+ let errRevoke = null
1522
+ let nRevoked = await revokeSessionTokensLocked(targetUserId)
1523
+ .catch((err) => {
1524
+ revokeFailed = true
1525
+ errRevoke = err
1526
+ })
1527
+ if (revokeFailed) {
1528
+ srLog.error({ event: 'fun-adminResetUserPassword-revokeSessions', key: 'adminResetPasswordFailed', token: maskToken(token), targetUserId, err: getErrorMessage(errRevoke) })
1529
+ return Promise.reject('adminResetPasswordFailed')
1530
+ }
1531
+ srLog.info({ event: 'fun-adminResetUserPassword-revokeSessions', targetUserId, nRevoked })
1532
+
1533
+ //save (含 isForceChangePw='y')
1534
+ //逐筆檢查(ADR-080): 寫入失敗即 reject, 不記節流、不寄出未生效之新密碼
1497
1535
  let errTemp = null
1498
1536
  await procOrm(operatorId, 'users', 'save', [{
1499
1537
  id: targetUserId,
@@ -1595,6 +1633,40 @@ function proc(woItems, procOrm, { srLog, srEmail, salt, minExpired, kpLang, pass
1595
1633
  }
1596
1634
 
1597
1635
 
1636
+ //revokeSessionTokensLocked: 改密碼後之 session 處置 (R-SESS-01, ADR-093), 須於 [tokens, users] 表鎖內呼叫(與密碼寫入同一組鎖, 不可重入).
1637
+ //只撤銷 userId 名下之 session 權杖(isApp !== 'y', 即使用者登入所發); app 權杖(isApp === 'y')不是 session, 其 userId 為擁有者, 不撤銷
1638
+ //(與 revokeTokensByUserIdLocked 之刪除 / 停用 / 封鎖情境不同: 該情境是使用者本身失效, 名下一切權杖皆撤).
1639
+ //exceptToken: 發出請求之目前 session 權杖字串, 該筆保留(使用者自行改密碼); 管理員重設不給, 全撤.
1640
+ //逐筆檢查(ormWrite), 單筆失敗不中斷其餘筆, 有失敗即 reject(含失敗筆主鍵), resolve 回傳撤銷筆數; 冪等(無可撤者回 0)
1641
+ let revokeSessionTokensLocked = async (userId, exceptToken = '') => {
1642
+
1643
+ //check
1644
+ if (!isestr(userId)) {
1645
+ return Promise.reject(`invalid userId`)
1646
+ }
1647
+
1648
+ //ts
1649
+ let ts = await woItems.tokens.select({ userId })
1650
+
1651
+ //tsDel, session 權杖且非目前 session
1652
+ let tsDel = filter(ts, (t) => {
1653
+ return get(t, 'isApp', '') !== 'y' && !(isestr(exceptToken) && get(t, 'token', '') === exceptToken)
1654
+ })
1655
+
1656
+ //check
1657
+ if (size(tsDel) === 0) {
1658
+ return 0
1659
+ }
1660
+
1661
+ //del
1662
+ await ormWrite(woItems.tokens, 'del', map(tsDel, (t) => {
1663
+ return { id: t.id }
1664
+ }))
1665
+
1666
+ return size(tsDel)
1667
+ }
1668
+
1669
+
1598
1670
  //updateTabItems
1599
1671
  //後台清單整表存檔(users / tokens / ips), 多人同時編輯之寫入契約 (原則 10; 規格 dspec/多人同時編輯之寫入契約.md; 規則帳本 rulebook_concurrency R-CONC-01~05):
1600
1672
  //- 讀取、比對、驗證依賴庫內之項、diff、寫入、撤銷 token、清快取全程在該表表鎖內(users 為 [tokens, users], 因寫入後同鎖撤銷 token); 讀取為鎖內新鮮讀取(無 ORM 快取).
@@ -251,7 +251,7 @@
251
251
  </template>
252
252
  <!-- redir -->
253
253
  <template v-else-if="props.key === 'redir'">
254
- <span v-if="cellFieldErr('redir', props.value)" :title="cellFieldErr('redir', props.value)">
254
+ <span v-if="cellRedirErr($ui.gv(props.row, 'id'))" :title="cellRedirErr($ui.gv(props.row, 'id'))">
255
255
  <img style="padding-right:4px; vertical-align:sub; width:16px; height:16px;" :src="$ui.getIcon('warning')" />
256
256
  <span style="color:#F57C00;">{{ $ui.cstr(props.value) }}</span>
257
257
  </span>
@@ -424,6 +424,7 @@ 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
426
  import { baselineOfUsers } from '../baselineOf.mjs'
427
+ import { redirOrigOfUsers, redirErrIdsOfUsers } from '../usersRedirCheck.mjs'
427
428
 
428
429
 
429
430
  export default {
@@ -460,6 +461,7 @@ export default {
460
461
 
461
462
  firstLoading: true,
462
463
  baseline: null, //載入時(及存檔成功後重新讀取時)畫面所見之列的有序基準 [[id, 列摘要]], 存檔時送後端比對(多人同時編輯, 先動先贏); null 為未載入或算不出, 送出時被後端拒收
464
+ redirOrig: null, //與 baseline 同一快照各列之 redir 原值 Map(id → redir; 用 Map 以免被 Vue 響應式處理覆蓋 `__ob__` 等鍵), 供判定「載入時即為空之既有列(如自助註冊者)」不擋存檔 (R-VAL-02); null 為算不出(此時 baseline 亦為 null, 後端無基準一律拒收), 只檢查新增列
463
465
  errMsg: '', //清單載入失敗之訊息(getDataError), 有值時以訊息取代表格
464
466
  firstSetting: true,
465
467
  systemProcing: false, //程式端載入/重載清單資料期間為true, 用於排除非使用者操作之rowsChange
@@ -576,6 +578,7 @@ export default {
576
578
  .then((res) => {
577
579
  // console.log(res)
578
580
  vo.baseline = baselineOfUsers(res) //對畫面所見之同一快照自算基準(須在排序、變動之前; buildBaseline 不改輸入)
581
+ vo.redirOrig = redirOrigOfUsers(res)
579
582
  res = sortBy(res, 'order')
580
583
  vo.users = res
581
584
  vo.markDataReload() //程式端寫入資料, 其後續 rowsChange 非使用者變更
@@ -747,29 +750,22 @@ export default {
747
750
  return kpErr
748
751
  },
749
752
 
753
+ //redir(登入後轉址)檢查, 鍵為列主鍵 id (R-VAL-02, ADR-094): 只攔下管理員本次新增或由非空改為空之列;
754
+ //載入時即為空之既有列(如自助註冊者)不擋存檔、不標警示——一列之既有資料問題不得擋住整份清單之存檔
750
755
  errItemsByRedir: function() {
751
756
  let vo = this
752
757
 
753
758
  //rows
754
759
  let rows = get(vo, 'opt.rows', [])
755
760
 
761
+ //ids
762
+ let ids = redirErrIdsOfUsers(rows, vo.redirOrig)
763
+
756
764
  //kpErr
757
765
  let kpErr = Object.create(null)
758
- each(rows, (v, k) => {
759
-
760
- //redir
761
- let redir = get(v, 'redir', '')
762
-
763
- //check
764
- if (!isestr(redir)) {
765
-
766
- //kpErr
767
- kpErr[redir] = vo.$t(`userRedirEmpty`)
768
-
769
- return true //跳出換下一個
770
- }
771
-
772
- })
766
+ for (let id in ids) {
767
+ kpErr[id] = vo.$t(`userRedirEmpty`)
768
+ }
773
769
 
774
770
  return kpErr
775
771
  },
@@ -929,7 +925,6 @@ export default {
929
925
  let errMaps = {
930
926
  'account': vo.errItemsByAccount,
931
927
  'email': vo.errItemsByEmail,
932
- 'redir': vo.errItemsByRedir,
933
928
  }
934
929
  //鍵為使用者輸入之 account / email / redir 值: 自有鍵查值, 不把輸入值當 lodash 路徑 (含 . 或 [ 者會被解析為路徑而取到他列之錯誤文字; R-KEY-01)
935
930
  let kp = errMaps[field]
@@ -937,6 +932,14 @@ export default {
937
932
  return isestr(err) ? err : ''
938
933
  },
939
934
 
935
+ //redir 欄位錯誤查詢, 鍵為 row.id (空值不能當鍵: 否則所有 redir 為空之儲存格會同時被標警示, 含載入時即為空而不擋存檔之既有列)
936
+ cellRedirErr: function(id) {
937
+ let vo = this
938
+ let kp = vo.errItemsByRedir
939
+ let err = (kp !== null && typeof kp === 'object' && Object.prototype.hasOwnProperty.call(kp, id)) ? kp[id] : ''
940
+ return isestr(err) ? err : ''
941
+ },
942
+
940
943
  //新增列密碼欄位錯誤查詢, 鍵為 row.id (其他欄位以 value 當鍵, 但 password 不適合做鍵)
941
944
  cellPasswordErr: function(id) {
942
945
  let vo = this
@@ -1577,6 +1580,7 @@ export default {
1577
1580
  await vo.$fapi.getUsersList(token)
1578
1581
  .then((res) => {
1579
1582
  vo.baseline = baselineOfUsers(res)
1583
+ vo.redirOrig = redirOrigOfUsers(res)
1580
1584
  res = sortBy(res, 'order')
1581
1585
  vo.users = res
1582
1586
  vo.markDataReload() //程式端寫入資料, 其後續 rowsChange 非使用者變更
@@ -1584,6 +1588,7 @@ export default {
1584
1588
  .catch((err) => {
1585
1589
  console.log('refresh getUsersList catch', err)
1586
1590
  vo.baseline = null //重新讀取失敗: 舊基準已過期, 不沿用; 其後再存檔被後端拒收並提示重新載入(不降級為不帶基準)
1591
+ vo.redirOrig = null //同上, redir 比對基準亦不沿用(null 時只檢查新增列; 後端無基準之存檔一律拒收並提示重新載入)
1587
1592
  })
1588
1593
 
1589
1594
  //isModified
@@ -0,0 +1,68 @@
1
+ import get from 'lodash-es/get.js'
2
+ import isestr from 'wsemi/src/isestr.mjs'
3
+
4
+
5
+ //後台使用者清單之 redir(登入後轉址)檢查 (R-VAL-02, ADR-094; 第 116 項):
6
+ //- redir 不可由管理員在本次編輯中設為空: 新增列、或既有列之 redir 由非空改為空者攔下(提示「尚未給予有效登入後轉址」, 該列 redir 格顯示警示)
7
+ //- 載入時即為空的既有列不擋存檔、不標警示: 自助註冊者即如此(註冊流程不收 redir, 預設空字串; 使用者於 view=user / view=backstage 登入不需 redir,
8
+ // view=login 登入時以「無有效轉址」提示, 見 spec/流程_使用者一般登入.md E2E-016、spec/流程_使用者自動登入.md E2E-005); 一列之既有資料問題不得擋住整份清單之存檔
9
+ //- 比對基準 redirOrig 為 Map(列主鍵 → 載入(及存檔成功後重新讀取)時畫面所見之 redir 原值): 用 Map 而非物件, 因基準存於 Vue data,
10
+ // Vue 2 會把純物件轉為響應式並以 `__ob__` 鍵掛 Observer(列主鍵恰為 `__ob__` 時會覆蓋其原值), Map 不被響應式處理, 且任何主鍵字串(`__proto__` 等)皆無原型鍵衝突(R-KEY-01)
11
+ //- 基準算不出(null, 如存檔成功後重新讀取失敗)時: 只檢查新增列; 既有列不檢查——此時 baseline 亦為 null, 後端對無基準之存檔一律拒收(saveBaselineFormat, 提示重新載入),
12
+ // 不會有任何列被寫入, 不必在前端對既有列先擋(先擋反而使使用者先看到不相干之 redir 訊息而到不了重新載入提示); 列不在基準內且非新增者(基準有效時)視為管理員所設, 嚴格檢查
13
+
14
+
15
+ //redirOrigOfUsers: rows 為 getUsersList 之回傳(未排序、未變動); 回傳 Map(id → redir 原值; 非字串者記為空字串)
16
+ function redirOrigOfUsers(rows) {
17
+ let mp = new Map()
18
+ for (let r of Array.isArray(rows) ? rows : []) {
19
+ let id = get(r, 'id', '')
20
+ if (typeof id !== 'string') {
21
+ continue
22
+ }
23
+ let redir = get(r, 'redir', '')
24
+ mp.set(id, typeof redir === 'string' ? redir : '')
25
+ }
26
+ return mp
27
+ }
28
+
29
+
30
+ //redirErrIdsOfUsers: rows 為畫面當前之列(含 _isNew 標記), redirOrig 為 redirOrigOfUsers 之結果或 null; 回傳 { [id]: true }(無原型物件)為不合規則之列
31
+ function redirErrIdsOfUsers(rows, redirOrig) {
32
+ let kp = Object.create(null)
33
+ let known = redirOrig instanceof Map
34
+ for (let r of Array.isArray(rows) ? rows : []) {
35
+
36
+ //redir 非空即合規
37
+ if (isestr(get(r, 'redir', ''))) {
38
+ continue
39
+ }
40
+
41
+ //id
42
+ let id = get(r, 'id', '')
43
+
44
+ //新增列: 一律嚴格(標記優先於基準, 即使主鍵碰巧與基準內某列相同)
45
+ let isNew = get(r, '_isNew') === true
46
+
47
+ if (!isNew) {
48
+
49
+ //基準算不出: 既有列不檢查(後端無基準一律拒收, 見檔頭)
50
+ if (!known) {
51
+ continue
52
+ }
53
+
54
+ //載入時即為空之既有列: 既有資料之狀態, 不屬本次編輯所致
55
+ if (typeof id === 'string' && redirOrig.has(id) && !isestr(redirOrig.get(id))) {
56
+ continue
57
+ }
58
+
59
+ }
60
+
61
+ kp[typeof id === 'string' ? id : String(id)] = true
62
+ }
63
+ return kp
64
+ }
65
+
66
+
67
+ export { redirOrigOfUsers, redirErrIdsOfUsers }
68
+ export default { redirOrigOfUsers, redirErrIdsOfUsers }