w-web-perm 1.0.48 → 1.0.50
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/.github/workflows/ci-test.yml +2 -2
- package/README.md +5 -5
- package/dist/css/app.8781355a.css +1 -0
- package/dist/css/{chunk-vendors.f149e124.css → chunk-vendors.58917286.css} +1 -1
- package/dist/index.tmp +2 -2
- package/dist/js/app-legacy.b2754bb1.js +1 -0
- package/dist/js/app.b2754bb1.js +1 -0
- package/dist/js/chunk-vendors-legacy.c14a1ed1.js +21 -0
- package/dist/js/chunk-vendors.c14a1ed1.js +21 -0
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +3 -3
- package/docs/WWebPerm.mjs.html +162 -210
- package/docs/index.html +1 -1
- package/logs/2026-06-19T16.log +191 -0
- package/logs/2026-06-19T23.log +0 -0
- package/logs/2026-06-20T00.log +207 -0
- package/logs/2026-06-20T11.log +221 -0
- package/logs/2026-06-20T12.log +146 -0
- package/logs/2026-06-20T13.log +134 -0
- package/logs/2026-06-20T14.log +296 -0
- package/logs/2026-06-20T15.log +225 -0
- package/logs/2026-06-20T16.log +174 -0
- package/logs/2026-06-21T16.log +110 -0
- package/logs/2026-06-21T17.log +301 -0
- package/logs/2026-06-21T18.log +238 -0
- package/logs/2026-06-21T19.log +141 -0
- package/logs/2026-06-21T20.log +99 -0
- package/logs/2026-06-21T21.log +143 -0
- package/logs/2026-06-21T23.log +26 -0
- package/logs/2026-06-22T00.log +33 -0
- package/logs/2026-06-22T08.log +235 -0
- package/logs/2026-06-22T09.log +246 -0
- package/logs/2026-06-22T10.log +301 -0
- package/logs/2026-06-22T11.log +140 -0
- package/logs/2026-06-22T12.log +22 -0
- package/logs/2026-06-22T14.log +44 -0
- package/logs/2026-06-22T23.log +951 -0
- package/logs/2026-06-23T00.log +263 -0
- package/logs/2026-06-23T08.log +611 -0
- package/logs/2026-06-30T21.log +3609 -0
- package/logs/2026-06-30T22.log +3682 -0
- package/logs/2026-06-30T23.log +592 -0
- package/logs/2026-07-01T12.log +0 -0
- package/logs/2026-07-09T22.log +2220 -0
- package/logs/2026-07-09T23.log +1140 -0
- package/logs/2026-07-10T12.log +6 -0
- package/logs/2026-07-10T13.log +351 -0
- package/logs/2026-07-10T16.log +1272 -0
- package/logs/2026-07-10T19.log +6 -0
- package/logs/2026-07-10T20.log +1883 -0
- package/logs/2026-07-10T21.log +3505 -0
- package/logs/2026-07-10T22.log +3395 -0
- package/logs/2026-07-11T00.log +1338 -0
- package/logs/2026-07-11T01.log +2492 -0
- package/logs/2026-07-11T17.log +19 -0
- package/package.json +23 -16
- package/public/index.html +2 -2
- package/server/WWebPerm.mjs +161 -209
- package/server/procCore.mjs +187 -0
- package/server/procLang.mjs +276 -60
- package/server/procStaInfor.mjs +128 -0
- package/server/provideTabs.mjs +1 -1
- package/server/srLog.mjs +30 -0
- package/server/staLogs/filterVpfsByWindow.mjs +27 -0
- package/server/staLogs/staEvent.callWorker.mjs +46 -0
- package/server/staLogs/staEvent.mjs +118 -0
- package/server/staLogs/staEvent.shellWorker.mjs +19 -0
- package/server/staLogs/staEventTable.mjs +102 -0
- package/settings.json +43 -0
- package/src/components/Common/CheckYes.vue +18 -1
- package/src/components/Layout.vue +1 -1
- package/src/components/LayoutContent.vue +13 -3
- package/src/components/LayoutContentGrups.vue +32 -25
- package/src/components/LayoutContentPemis.vue +32 -25
- package/src/components/LayoutContentStaInfor.vue +534 -0
- package/src/components/LayoutContentTargets.vue +35 -28
- package/src/components/LayoutContentUsers.vue +32 -25
- package/src/components/LayoutState.vue +5 -0
- package/src/components/VeCgrups.vue +19 -9
- package/src/components/VeCpemis.vue +19 -9
- package/src/components/VeCrules.vue +19 -9
- package/src/components/VeGrupBlngUsers.vue +35 -29
- package/src/components/VePemiBlngGrups.vue +35 -29
- package/src/getPerm.mjs +7 -17
- package/src/getPermUserInfor.mjs +8 -18
- package/src/main.js +4 -9
- package/src/perm.mjs +3 -4
- package/src/plugins/mShare.mjs +8 -13
- package/src/plugins/mUI.mjs +7 -44
- package/src/store/mutations.mjs +0 -10
- package/src/store/types.mjs +0 -2
- package/srv.mjs +76 -101
- package/tableTags.json +1 -1
- package/z/344/277/256/346/255/243/350/246/217/345/212/203/346/233/270.md +450 -0
- package/db/wperm/grups/data.mdb +0 -0
- package/db/wperm/grups/lock.mdb +0 -0
- package/db/wperm/pemis/data.mdb +0 -0
- package/db/wperm/pemis/lock.mdb +0 -0
- package/db/wperm/targets/data.mdb +0 -0
- package/db/wperm/targets/lock.mdb +0 -0
- package/db/wperm/users/data.mdb +0 -0
- package/db/wperm/users/lock.mdb +0 -0
- package/dist/css/app.30e81bfe.css +0 -1
- package/dist/js/app-legacy.3324392e.js +0 -1
- package/dist/js/app.3324392e.js +0 -1
- package/dist/js/chunk-vendors-legacy.e8703728.js +0 -21
- package/dist/js/chunk-vendors.e8703728.js +0 -21
- package/src/plugins/mDataSelectorSchema.mjs +0 -30
- package/tableTags-web-perm.json +0 -1
- package/test/getUserRules.test.mjs +0 -899
package/docs/WWebPerm.mjs.html
CHANGED
|
@@ -48,10 +48,8 @@
|
|
|
48
48
|
<pre class="prettyprint source linenums"><code>import path from 'path'
|
|
49
49
|
import fs from 'fs'
|
|
50
50
|
import get from 'lodash-es/get.js'
|
|
51
|
-
import each from 'lodash-es/each.js'
|
|
52
51
|
import map from 'lodash-es/map.js'
|
|
53
52
|
import keys from 'lodash-es/keys.js'
|
|
54
|
-
import size from 'lodash-es/size.js'
|
|
55
53
|
import join from 'lodash-es/join.js'
|
|
56
54
|
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
57
55
|
import isestr from 'wsemi/src/isestr.mjs'
|
|
@@ -70,15 +68,16 @@ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
|
|
|
70
68
|
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
|
|
71
69
|
import replace from 'wsemi/src/replace.mjs'
|
|
72
70
|
import strdelleft from 'wsemi/src/strdelleft.mjs'
|
|
73
|
-
import haskey from 'wsemi/src/haskey.mjs'
|
|
74
|
-
import arrHas from 'wsemi/src/arrHas.mjs'
|
|
75
|
-
import ltdtDiffByKey from 'wsemi/src/ltdtDiffByKey.mjs'
|
|
76
71
|
import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
|
|
72
|
+
import pmKeyMutex from 'wsemi/src/pmKeyMutex.mjs'
|
|
77
73
|
import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
|
|
78
74
|
import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
|
|
79
75
|
import ds from '../src/schema/index.mjs'
|
|
80
76
|
import { getUserRules } from '../src/plugins/mShare.mjs'
|
|
77
|
+
import procCore from './procCore.mjs'
|
|
81
78
|
import procLang from './procLang.mjs'
|
|
79
|
+
import srLogInit from './srLog.mjs'
|
|
80
|
+
import procStaInfor from './procStaInfor.mjs'
|
|
82
81
|
|
|
83
82
|
|
|
84
83
|
/**
|
|
@@ -88,7 +87,7 @@ import procLang from './procLang.mjs'
|
|
|
88
87
|
* @param {Function} WOrm 輸入資料庫ORM函數
|
|
89
88
|
* @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
|
|
90
89
|
* @param {String} db 輸入資料庫名稱字串
|
|
91
|
-
* @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'
|
|
90
|
+
* @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n'。注意: token 回傳之'isAdmin'僅供識別,權限系統一律以自身users表該使用者之'isAdmin'欄位值為權威,不會被 token 宣稱之值複寫
|
|
92
91
|
* @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
|
|
93
92
|
* @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
|
|
94
93
|
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
@@ -315,6 +314,10 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
315
314
|
let kpLang = procLang({ kpLangExt, webName, webDescription })
|
|
316
315
|
|
|
317
316
|
|
|
317
|
+
//srLog
|
|
318
|
+
let srLog = srLogInit(opt)
|
|
319
|
+
|
|
320
|
+
|
|
318
321
|
//WServOrm
|
|
319
322
|
let optWServOrm = {
|
|
320
323
|
useCheckUser,
|
|
@@ -327,12 +330,35 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
327
330
|
}
|
|
328
331
|
catch (err) {
|
|
329
332
|
console.log(err)
|
|
333
|
+
srLog.error({ event: 'WServOrm-init-error', msg: (err && err.message) || 'WServOrmInitFailed' })
|
|
334
|
+
throw err //fail-fast: ORM 初始化失敗直接終止啟動, 不以 wp={} 繼續組裝出「port 起得來但 woItems/procOrm 為 undefined、CRUD 全 crash」的半死伺服器
|
|
330
335
|
}
|
|
331
336
|
let { woItems, procOrm } = wp
|
|
332
337
|
|
|
333
338
|
|
|
339
|
+
//pmKeyMutex: per-key in-memory mutex, 同 key 序列化、不同 key 並行.
|
|
340
|
+
//perm 為「整表批次 diff」寫入, key 粒度為 {op}:{keyTable}, 防同表並行 race (lost update).
|
|
341
|
+
let kmx = pmKeyMutex()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
//procCore: 整表批次 diff 寫入邏輯 (updateTabItems + updateTargets/Pemis/Grups/Users)
|
|
345
|
+
let pc = procCore(woItems, procOrm, { srLog, kmx })
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
//procStaInfor: 統計資訊, fdLog 對齊 srLog 使用同一目錄(讀 settings.logFd, fallback 邏輯同 srLog.mjs); mock 由 settings.staEventMock 注入(e2e 統計圖穩定用)
|
|
349
|
+
let logFd = get(opt, 'logFd', '')
|
|
350
|
+
if (!isestr(logFd)) {
|
|
351
|
+
logFd = './_logs'
|
|
352
|
+
}
|
|
353
|
+
let psi = procStaInfor({ fdLog: logFd, mock: get(opt, 'staEventMock', false) })
|
|
354
|
+
|
|
355
|
+
|
|
334
356
|
//getWebInfor
|
|
335
357
|
let getWebInfor = (userId) => {
|
|
358
|
+
|
|
359
|
+
//info
|
|
360
|
+
srLog.info({ event: 'getWebInfor-success', userId })
|
|
361
|
+
|
|
336
362
|
return {
|
|
337
363
|
|
|
338
364
|
// webName, //已併入kpLang
|
|
@@ -395,162 +421,32 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
395
421
|
// }
|
|
396
422
|
|
|
397
423
|
|
|
398
|
-
//updateTabItems
|
|
399
|
-
let updateTabItems = async (keyTable, userId, rows, keyDetect) => {
|
|
400
|
-
// console.log('updateTabItems', keyTable, userId, rows.length, keyDetect)
|
|
401
|
-
|
|
402
|
-
//ltdtmapping
|
|
403
|
-
rows = ltdtmapping(rows, ds[keyTable].keys)
|
|
404
|
-
// console.log('ltdtmapping rows', rows)
|
|
405
|
-
|
|
406
|
-
//重給order
|
|
407
|
-
rows = map(rows, (r, k) => {
|
|
408
|
-
r.order = k + 1
|
|
409
|
-
return r
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
//ckKey
|
|
413
|
-
let ckKey = (rows, key) => {
|
|
414
|
-
let err = null
|
|
415
|
-
|
|
416
|
-
//check
|
|
417
|
-
let kp = {}
|
|
418
|
-
each(rows, (v, k) => {
|
|
419
|
-
|
|
420
|
-
//value
|
|
421
|
-
let value = get(v, key, '')
|
|
422
|
-
|
|
423
|
-
//check
|
|
424
|
-
if (!isestr(value)) {
|
|
425
|
-
err = `rows[${k}].${key} is not an effective string`
|
|
426
|
-
return false //跳出
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
//check
|
|
430
|
-
if (haskey(kp, value)) {
|
|
431
|
-
err = `rows[${k}].${key}[${value}] is duplicate`
|
|
432
|
-
return false //跳出
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
//kp
|
|
436
|
-
kp[value] = true
|
|
437
|
-
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
return err
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
//偵測未給予或重複
|
|
444
|
-
let err = null
|
|
445
|
-
if (true) {
|
|
446
|
-
if (arrHas(keyTable, ['targets'])) {
|
|
447
|
-
err = ckKey(rows, 'id')
|
|
448
|
-
if (err !== null) {
|
|
449
|
-
return Promise.reject(err)
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
if (arrHas(keyTable, ['pemis', 'grups'])) { //users可重複name故不列入
|
|
453
|
-
err = ckKey(rows, 'name')
|
|
454
|
-
if (err !== null) {
|
|
455
|
-
return Promise.reject(err)
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
if (arrHas(keyTable, ['users'])) {
|
|
459
|
-
err = ckKey(rows, 'email')
|
|
460
|
-
if (err !== null) {
|
|
461
|
-
return Promise.reject(err)
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
//ltdtDiffByKey
|
|
467
|
-
let ltdtOld = await woItems[keyTable].select()
|
|
468
|
-
let ltdtNew = rows
|
|
469
|
-
let r = ltdtDiffByKey(ltdtOld, ltdtNew, keyDetect)
|
|
470
|
-
// console.log('ltdtDiffByKey r', r)
|
|
471
|
-
|
|
472
|
-
//del
|
|
473
|
-
if (size(r.del) > 0) {
|
|
474
|
-
await procOrm(userId, keyTable, 'del', r.del) //須使用procOrm才有辦法自動給予相關欄位
|
|
475
|
-
// .catch((err) => {
|
|
476
|
-
// console.log('woItems[keyTable].del err', err)
|
|
477
|
-
// })
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
//add
|
|
481
|
-
if (size(r.add) > 0) {
|
|
482
|
-
await procOrm(userId, keyTable, 'insert', r.add) //須使用procOrm才有辦法自動給予相關欄位
|
|
483
|
-
// .catch((err) => {
|
|
484
|
-
// console.log('woItems[keyTable].insert err', err)
|
|
485
|
-
// })
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
//diff
|
|
489
|
-
if (size(r.diff) > 0) {
|
|
490
|
-
await procOrm(userId, keyTable, 'save', r.diff) //須使用procOrm才有辦法自動給予相關欄位
|
|
491
|
-
// .catch((err) => {
|
|
492
|
-
// console.log('woItems[keyTable].save err', err)
|
|
493
|
-
// })
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return ltdtNew
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
//updateTargets
|
|
501
|
-
let updateTargets = async (userId, rows) => {
|
|
502
|
-
rows = await updateTabItems('targets', userId, rows, 'id')
|
|
503
|
-
return rows
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
//updatePemis
|
|
508
|
-
let updatePemis = async (userId, rows) => {
|
|
509
|
-
rows = await updateTabItems('pemis', userId, rows, 'name')
|
|
510
|
-
return rows
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
//updateGrups
|
|
515
|
-
let updateGrups = async (userId, rows) => {
|
|
516
|
-
rows = await updateTabItems('grups', userId, rows, 'name')
|
|
517
|
-
return rows
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
//updateUsers
|
|
522
|
-
let updateUsers = async (userId, rows) => {
|
|
523
|
-
rows = await updateTabItems('users', userId, rows, 'id')
|
|
524
|
-
return rows
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
424
|
//checkUser
|
|
529
425
|
let checkUser = async(user) => {
|
|
530
426
|
let id = get(user, 'id', '')
|
|
531
427
|
if (!isestr(id)) {
|
|
532
428
|
console.log('user', user)
|
|
533
|
-
|
|
534
|
-
return Promise.reject(`
|
|
429
|
+
srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserId' })
|
|
430
|
+
return Promise.reject(`cannotGetUserId`)
|
|
535
431
|
}
|
|
536
432
|
let email = get(user, 'email', '')
|
|
537
433
|
if (!isestr(email)) {
|
|
538
434
|
console.log('user', user)
|
|
539
|
-
|
|
540
|
-
return Promise.reject(`
|
|
435
|
+
srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserEmail' })
|
|
436
|
+
return Promise.reject(`cannotGetUserEmail`)
|
|
541
437
|
}
|
|
542
438
|
let name = get(user, 'name', '')
|
|
543
439
|
if (!isestr(name)) {
|
|
544
440
|
console.log('user', user)
|
|
545
|
-
|
|
546
|
-
return Promise.reject(`
|
|
441
|
+
srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserName' })
|
|
442
|
+
return Promise.reject(`cannotGetUserName`)
|
|
547
443
|
}
|
|
548
444
|
let isAdmin = get(user, 'isAdmin', '')
|
|
549
445
|
if (isAdmin !== 'y' && isAdmin !== 'n') {
|
|
550
446
|
console.log('user', user)
|
|
551
447
|
console.log('user.isAdmin is not y or n', user.isAdmin)
|
|
552
|
-
|
|
553
|
-
return Promise.reject(`
|
|
448
|
+
srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserRole' })
|
|
449
|
+
return Promise.reject(`cannotGetUserRole`)
|
|
554
450
|
}
|
|
555
451
|
return true
|
|
556
452
|
}
|
|
@@ -573,8 +469,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
573
469
|
//check
|
|
574
470
|
if (!iseobj(userSelf)) {
|
|
575
471
|
console.log(`token`, token)
|
|
576
|
-
|
|
577
|
-
return Promise.reject(`
|
|
472
|
+
srLog.error({ event: 'getTokenUser-error', msg: 'cannotFindUserFromToken' })
|
|
473
|
+
return Promise.reject(`cannotFindUserFromToken`)
|
|
578
474
|
}
|
|
579
475
|
|
|
580
476
|
//check userSelf
|
|
@@ -588,8 +484,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
588
484
|
if (!isestr(vSelf)) {
|
|
589
485
|
console.log('userSelf', userSelf)
|
|
590
486
|
console.log('mappingBy', mappingBy)
|
|
591
|
-
|
|
592
|
-
return Promise.reject(`
|
|
487
|
+
srLog.error({ event: 'getTokenUser-error', msg: 'cannotGetUserProp', mappingBy })
|
|
488
|
+
return Promise.reject(`cannotGetUserProp`)
|
|
593
489
|
}
|
|
594
490
|
|
|
595
491
|
//userFind
|
|
@@ -601,17 +497,14 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
601
497
|
if (!iseobj(userFind)) {
|
|
602
498
|
console.log('userSelf', userSelf)
|
|
603
499
|
console.log('mappingBy', mappingBy)
|
|
604
|
-
|
|
605
|
-
return Promise.reject(`
|
|
500
|
+
srLog.error({ event: 'getTokenUser-error', msg: 'cannotGetUserFromPerm', mappingBy })
|
|
501
|
+
return Promise.reject(`cannotGetUserFromPerm`)
|
|
606
502
|
}
|
|
607
503
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
userFind.isAdmin = isAdminSelf
|
|
613
|
-
}
|
|
614
|
-
// console.log('userFind(isAdmin)', userFind)
|
|
504
|
+
//isAdmin 權威為 perm users 表之欄位值, 不以 token(getUserByToken)宣稱之 isAdmin 複寫。
|
|
505
|
+
//(2026-07-11 稽核 A-C1 裁決(b): 原此處有一段恆為 no-op 之「複寫isAdmin」死碼
|
|
506
|
+
// (userFind.isAdmin = isAdminSelf, 把自己指派給自己), 與 JSDoc 舊文「輸入'y'時會複寫」互斥;
|
|
507
|
+
// 依業主裁決採保守解: 刪除死碼、文件對齊實際行為, 不啟用 token→admin 提權路徑。)
|
|
615
508
|
|
|
616
509
|
return userFind
|
|
617
510
|
}
|
|
@@ -649,8 +542,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
649
542
|
//check
|
|
650
543
|
if (!b) {
|
|
651
544
|
console.log('user', user)
|
|
652
|
-
|
|
653
|
-
return Promise.reject(`
|
|
545
|
+
srLog.error({ event: 'getAndVerifyClientUser-error', msg: 'userNoPermission', from })
|
|
546
|
+
return Promise.reject(`userNoPermission`)
|
|
654
547
|
}
|
|
655
548
|
|
|
656
549
|
return user
|
|
@@ -674,8 +567,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
674
567
|
//check
|
|
675
568
|
if (!iseobj(user)) {
|
|
676
569
|
console.log(`token`, token)
|
|
677
|
-
|
|
678
|
-
return Promise.reject(`
|
|
570
|
+
srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'cannotFindUserFromToken' })
|
|
571
|
+
return Promise.reject(`cannotFindUserFromToken`)
|
|
679
572
|
}
|
|
680
573
|
|
|
681
574
|
//verifyAppUser
|
|
@@ -687,8 +580,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
687
580
|
//check
|
|
688
581
|
if (!b) {
|
|
689
582
|
console.log('user', user)
|
|
690
|
-
|
|
691
|
-
return Promise.reject(`
|
|
583
|
+
srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'userNoPermission', from })
|
|
584
|
+
return Promise.reject(`userNoPermission`)
|
|
692
585
|
}
|
|
693
586
|
|
|
694
587
|
return user
|
|
@@ -709,8 +602,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
709
602
|
//check
|
|
710
603
|
if (!iseobj(payload)) {
|
|
711
604
|
console.log('payload', payload)
|
|
712
|
-
|
|
713
|
-
return Promise.reject(`
|
|
605
|
+
srLog.error({ event: 'parsePayload-error', msg: 'invalidPayload' })
|
|
606
|
+
return Promise.reject(`invalidPayload`)
|
|
714
607
|
}
|
|
715
608
|
|
|
716
609
|
//from
|
|
@@ -719,8 +612,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
719
612
|
//check
|
|
720
613
|
if (!isestr(from)) {
|
|
721
614
|
console.log('payload', payload)
|
|
722
|
-
|
|
723
|
-
return Promise.reject(`
|
|
615
|
+
srLog.error({ event: 'parsePayload-error', msg: 'invalidFromInPayload' })
|
|
616
|
+
return Promise.reject(`invalidFromInPayload`)
|
|
724
617
|
}
|
|
725
618
|
|
|
726
619
|
//rows
|
|
@@ -729,8 +622,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
729
622
|
//check
|
|
730
623
|
if (!isearr(rows)) {
|
|
731
624
|
console.log('payload', payload)
|
|
732
|
-
|
|
733
|
-
return Promise.reject(`
|
|
625
|
+
srLog.error({ event: 'parsePayload-error', msg: 'invalidRowsInPayload' })
|
|
626
|
+
return Promise.reject(`invalidRowsInPayload`)
|
|
734
627
|
}
|
|
735
628
|
|
|
736
629
|
//inp
|
|
@@ -746,6 +639,9 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
746
639
|
//syncAndReplaceTabs
|
|
747
640
|
let syncAndReplaceTabs = async (userId, inp, keyTable) => {
|
|
748
641
|
|
|
642
|
+
//序列化同表之並行整表 delAll+insert: 防 lost update (同 keyTable 序列化, 不同表並行)
|
|
643
|
+
return await kmx('syncAndReplaceTabs:' + keyTable, async () => {
|
|
644
|
+
|
|
749
645
|
//from
|
|
750
646
|
let from = get(inp, 'from', '')
|
|
751
647
|
// console.log('from', from)
|
|
@@ -753,8 +649,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
753
649
|
//check
|
|
754
650
|
if (!isestr(from)) {
|
|
755
651
|
console.log('inp', inp)
|
|
756
|
-
|
|
757
|
-
return Promise.reject(`
|
|
652
|
+
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidFrom', keyTable })
|
|
653
|
+
return Promise.reject(`invalidFrom`)
|
|
758
654
|
}
|
|
759
655
|
|
|
760
656
|
//rows
|
|
@@ -765,7 +661,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
765
661
|
if (!isearr(rows)) {
|
|
766
662
|
console.log('inp', inp)
|
|
767
663
|
console.log('keyTable', keyTable)
|
|
768
|
-
|
|
664
|
+
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', keyTable })
|
|
665
|
+
return Promise.reject(`invalidRows`)
|
|
769
666
|
}
|
|
770
667
|
|
|
771
668
|
//save from
|
|
@@ -788,6 +685,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
788
685
|
// console.log('r', r)
|
|
789
686
|
|
|
790
687
|
return r
|
|
688
|
+
|
|
689
|
+
})
|
|
791
690
|
}
|
|
792
691
|
|
|
793
692
|
|
|
@@ -848,8 +747,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
848
747
|
//check
|
|
849
748
|
if (!iseobj(kur)) {
|
|
850
749
|
console.log('userId', userId)
|
|
851
|
-
|
|
852
|
-
return Promise.reject(`
|
|
750
|
+
srLog.error({ event: 'getGenUserAndRulesByUserId-error', msg: 'cannotGetUserRules', userId })
|
|
751
|
+
return Promise.reject(`cannotGetUserRules`)
|
|
853
752
|
}
|
|
854
753
|
|
|
855
754
|
//grupsNames
|
|
@@ -923,8 +822,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
923
822
|
console.log('req.query', get(req, 'query'))
|
|
924
823
|
console.log('token', token)
|
|
925
824
|
console.log('[API]getUserByToken/check token: invalid token')
|
|
926
|
-
|
|
927
|
-
return Promise.reject(`
|
|
825
|
+
srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
|
|
826
|
+
return Promise.reject(`tokenNoPermission`)
|
|
928
827
|
}
|
|
929
828
|
|
|
930
829
|
//getAndVerifyClientUser
|
|
@@ -935,10 +834,13 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
935
834
|
if (!iseobj(user)) {
|
|
936
835
|
console.log('token', token)
|
|
937
836
|
console.log('[API]getUserByToken/check user: invalid user')
|
|
938
|
-
|
|
939
|
-
return Promise.reject(`
|
|
837
|
+
srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
|
|
838
|
+
return Promise.reject(`tokenNoPermission`)
|
|
940
839
|
}
|
|
941
840
|
|
|
841
|
+
//info
|
|
842
|
+
srLog.info({ event: 'api/getUserByToken-success', userId: get(user, 'id', '') })
|
|
843
|
+
|
|
942
844
|
return user
|
|
943
845
|
}
|
|
944
846
|
|
|
@@ -970,8 +872,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
970
872
|
console.log('req.query', get(req, 'query'))
|
|
971
873
|
console.log('token', token)
|
|
972
874
|
console.log('[API]getPerm/check token: invalid token')
|
|
973
|
-
|
|
974
|
-
return Promise.reject(`
|
|
875
|
+
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
876
|
+
return Promise.reject(`tokenNoPermission`)
|
|
975
877
|
}
|
|
976
878
|
|
|
977
879
|
//getAndVerifyClientUser, 發起token的所屬使用者, 驗證僅對client使用者, 不得使用app使用者
|
|
@@ -982,8 +884,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
982
884
|
if (!iseobj(userSelf)) {
|
|
983
885
|
console.log('token', token)
|
|
984
886
|
console.log('[API]getPerm/check userSelf: invalid userSelf')
|
|
985
|
-
|
|
986
|
-
return Promise.reject(`
|
|
887
|
+
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
888
|
+
return Promise.reject(`tokenNoPermission`)
|
|
987
889
|
}
|
|
988
890
|
|
|
989
891
|
//userIdSelf, client使用者的userId
|
|
@@ -995,8 +897,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
995
897
|
console.log('token', token)
|
|
996
898
|
console.log('userSelf', userSelf)
|
|
997
899
|
console.log('[API]getPerm/check userIdSelf: invalid userIdSelf')
|
|
998
|
-
|
|
999
|
-
return Promise.reject(`
|
|
900
|
+
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
901
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1000
902
|
}
|
|
1001
903
|
|
|
1002
904
|
//getGenUserAndRulesByUserId, 查找到的client使用者與權限
|
|
@@ -1009,10 +911,13 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1009
911
|
console.log('userSelf', userSelf)
|
|
1010
912
|
console.log('userIdSelf', userIdSelf)
|
|
1011
913
|
console.log('[API]getPerm/check userWithRulesSelf: invalid userWithRulesSelf')
|
|
1012
|
-
|
|
1013
|
-
return Promise.reject(`
|
|
914
|
+
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
915
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1014
916
|
}
|
|
1015
917
|
|
|
918
|
+
//info
|
|
919
|
+
srLog.info({ event: 'api/getPerm-success', userId: userIdSelf })
|
|
920
|
+
|
|
1016
921
|
return userWithRulesSelf
|
|
1017
922
|
}
|
|
1018
923
|
|
|
@@ -1043,8 +948,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1043
948
|
if (!isestr(tokenSelf)) {
|
|
1044
949
|
console.log('req.query', get(req, 'query'))
|
|
1045
950
|
console.log('[API]getPermUserInfor/check tokenSelf: invalid tokenSelf')
|
|
1046
|
-
|
|
1047
|
-
return Promise.reject(`
|
|
951
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
952
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1048
953
|
}
|
|
1049
954
|
|
|
1050
955
|
//getAndVerifyAppUser, 發起token的所屬使用者, 驗證須為app使用者
|
|
@@ -1056,8 +961,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1056
961
|
console.log('req.query', get(req, 'query'))
|
|
1057
962
|
console.log('tokenSelf', tokenSelf)
|
|
1058
963
|
console.log('[API]getPermUserInfor/check userSelf: invalid userSelf')
|
|
1059
|
-
|
|
1060
|
-
return Promise.reject(`
|
|
964
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
965
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1061
966
|
}
|
|
1062
967
|
|
|
1063
968
|
//userIdFind, 欲查找使用者的userId
|
|
@@ -1069,8 +974,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1069
974
|
console.log('req.query', get(req, 'query'))
|
|
1070
975
|
console.log('tokenSelf', tokenSelf)
|
|
1071
976
|
console.log('[API]getPermUserInfor/check userIdFind: invalid userIdFind')
|
|
1072
|
-
|
|
1073
|
-
return Promise.reject(`
|
|
977
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
978
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1074
979
|
}
|
|
1075
980
|
|
|
1076
981
|
//getGenUserAndRulesByUserId, 查找到的使用者與權限
|
|
@@ -1081,10 +986,13 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1081
986
|
if (!iseobj(userWithRulesFind)) {
|
|
1082
987
|
console.log('userIdFind', userIdFind)
|
|
1083
988
|
console.log('[API]getPermUserInfor/check userWithRulesFind: invalid userWithRulesFind')
|
|
1084
|
-
|
|
1085
|
-
return Promise.reject(`
|
|
989
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'userIdNoPermrules' })
|
|
990
|
+
return Promise.reject(`userIdNoPermrules`)
|
|
1086
991
|
}
|
|
1087
992
|
|
|
993
|
+
//info
|
|
994
|
+
srLog.info({ event: 'api/getPermUserInfor-success', userId: userIdFind })
|
|
995
|
+
|
|
1088
996
|
return userWithRulesFind
|
|
1089
997
|
}
|
|
1090
998
|
|
|
@@ -1116,8 +1024,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1116
1024
|
console.log('req.query', get(req, 'query'))
|
|
1117
1025
|
console.log('token', token)
|
|
1118
1026
|
console.log('[API]syncAndReplaceTabs/check token: invalid token')
|
|
1119
|
-
|
|
1120
|
-
return Promise.reject(`
|
|
1027
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
1028
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1121
1029
|
}
|
|
1122
1030
|
|
|
1123
1031
|
//keyTable
|
|
@@ -1128,15 +1036,16 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1128
1036
|
console.log('req.query', get(req, 'query'))
|
|
1129
1037
|
console.log('keyTable', keyTable)
|
|
1130
1038
|
console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
|
|
1131
|
-
|
|
1132
|
-
return Promise.reject(`
|
|
1039
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
1040
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1133
1041
|
}
|
|
1134
1042
|
|
|
1135
1043
|
//check keyTable
|
|
1136
1044
|
let allowedTables = ['targets', 'pemis', 'grups', 'users']
|
|
1137
1045
|
if (!allowedTables.includes(keyTable)) {
|
|
1138
1046
|
console.log('[API]syncAndReplaceTabs/check keyTable: unexpected keyTable', keyTable)
|
|
1139
|
-
|
|
1047
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'invalidKeyTable', keyTable })
|
|
1048
|
+
return Promise.reject(`invalidKeyTable`)
|
|
1140
1049
|
}
|
|
1141
1050
|
|
|
1142
1051
|
//getAndVerifyAppUser
|
|
@@ -1146,8 +1055,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1146
1055
|
if (!iseobj(user)) {
|
|
1147
1056
|
console.log('token', token)
|
|
1148
1057
|
console.log('[API]syncAndReplaceTabs/check user: invalid user')
|
|
1149
|
-
|
|
1150
|
-
return Promise.reject(`
|
|
1058
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
1059
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1151
1060
|
}
|
|
1152
1061
|
|
|
1153
1062
|
//userId
|
|
@@ -1158,8 +1067,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1158
1067
|
console.log('token', token)
|
|
1159
1068
|
console.log('user', user)
|
|
1160
1069
|
console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
|
|
1161
|
-
|
|
1162
|
-
return Promise.reject(`
|
|
1070
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
1071
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1163
1072
|
}
|
|
1164
1073
|
|
|
1165
1074
|
//parsePayload
|
|
@@ -1168,6 +1077,9 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1168
1077
|
//syncAndReplaceTabs
|
|
1169
1078
|
let r = await syncAndReplaceTabs(userId, inp, keyTable)
|
|
1170
1079
|
|
|
1080
|
+
//info
|
|
1081
|
+
srLog.info({ event: 'api/syncAndReplaceTabs-success', userId, keyTable })
|
|
1082
|
+
|
|
1171
1083
|
return r
|
|
1172
1084
|
}
|
|
1173
1085
|
|
|
@@ -1194,7 +1106,7 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1194
1106
|
|
|
1195
1107
|
//WServHapiServer
|
|
1196
1108
|
instWServHapiServer = new WServHapiServer({
|
|
1197
|
-
port:
|
|
1109
|
+
port: serverPort,
|
|
1198
1110
|
pathStaticFiles,
|
|
1199
1111
|
apiName: 'api',
|
|
1200
1112
|
apis,
|
|
@@ -1218,6 +1130,21 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1218
1130
|
let b = await p.checkToken(token)
|
|
1219
1131
|
// console.log('b', b)
|
|
1220
1132
|
|
|
1133
|
+
//ip, 對齊sso慣例x-forwarded-for取第1個, fallback到req remoteAddress, 取不到則略過
|
|
1134
|
+
let ip = get(headers, 'x-forwarded-for', '').split(',')[0].trim()
|
|
1135
|
+
if (!isestr(ip)) {
|
|
1136
|
+
ip = get(req, 'info.remoteAddress', '') || get(req, 'socket.remoteAddress', '') || get(req, 'connection.remoteAddress', '')
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
//origin
|
|
1140
|
+
let origin = get(headers, 'origin', '')
|
|
1141
|
+
|
|
1142
|
+
//referer
|
|
1143
|
+
let referer = get(headers, 'referer', '')
|
|
1144
|
+
|
|
1145
|
+
//info
|
|
1146
|
+
srLog.info({ event: 'verifyConn', ok: b, apiType, ip, origin, referer })
|
|
1147
|
+
|
|
1221
1148
|
return b
|
|
1222
1149
|
},
|
|
1223
1150
|
getUserIdByToken: async (token) => { //可使用async或sync函數
|
|
@@ -1227,7 +1154,8 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1227
1154
|
if (!isestr(userId)) {
|
|
1228
1155
|
console.log('token', token)
|
|
1229
1156
|
console.log('userId', userId)
|
|
1230
|
-
|
|
1157
|
+
srLog.error({ event: 'getUserIdByToken-error', msg: 'cannotFindUserId' })
|
|
1158
|
+
return Promise.reject(`cannotFindUserId`)
|
|
1231
1159
|
}
|
|
1232
1160
|
return userId
|
|
1233
1161
|
},
|
|
@@ -1247,10 +1175,34 @@ function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser
|
|
|
1247
1175
|
// getGrupsList,
|
|
1248
1176
|
// getUsersList,
|
|
1249
1177
|
|
|
1250
|
-
updateTargets,
|
|
1251
|
-
updatePemis,
|
|
1252
|
-
updateGrups,
|
|
1253
|
-
updateUsers,
|
|
1178
|
+
updateTargets: pc.updateTargets,
|
|
1179
|
+
updatePemis: pc.updatePemis,
|
|
1180
|
+
updateGrups: pc.updateGrups,
|
|
1181
|
+
updateUsers: pc.updateUsers,
|
|
1182
|
+
|
|
1183
|
+
getStaEvent: async (userId, timeLength, timeInterval) => {
|
|
1184
|
+
try {
|
|
1185
|
+
let rs = await psi.getStaEvent(userId, timeLength, timeInterval)
|
|
1186
|
+
srLog.info({ event: 'getStaEvent-success', userId })
|
|
1187
|
+
return rs
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
srLog.error({ event: 'getStaEvent-error', userId, msg: (typeof err === 'string') ? err : ((err && err.message) || 'cannotGetStaEvent') })
|
|
1191
|
+
throw err
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
|
|
1195
|
+
getStaEventTable: async (userId) => {
|
|
1196
|
+
try {
|
|
1197
|
+
let rs = await psi.getStaEventTable(userId)
|
|
1198
|
+
srLog.info({ event: 'getStaEventTable-success', userId })
|
|
1199
|
+
return rs
|
|
1200
|
+
}
|
|
1201
|
+
catch (err) {
|
|
1202
|
+
srLog.error({ event: 'getStaEventTable-error', userId, msg: (typeof err === 'string') ? err : ((err && err.message) || 'cannotGetStaEventTable') })
|
|
1203
|
+
throw err
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1254
1206
|
|
|
1255
1207
|
},
|
|
1256
1208
|
fnTableTags: 'tableTags-web-perm.json',
|
|
@@ -1276,7 +1228,7 @@ export default WWebPerm
|
|
|
1276
1228
|
<br class="clear">
|
|
1277
1229
|
|
|
1278
1230
|
<footer>
|
|
1279
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.
|
|
1231
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Thu Jul 23 2026 11:17:11 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
1280
1232
|
</footer>
|
|
1281
1233
|
|
|
1282
1234
|
<script>prettyPrint();</script>
|