w-web-api 1.0.75 → 1.0.77
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 +99 -99
- package/dist/css/{app.e140ee15.css → app.ae472c4d.css} +1 -1
- package/dist/index.tmp +1 -1
- package/dist/js/app-legacy.2aa145e5.js +1 -0
- package/dist/js/app.2aa145e5.js +1 -0
- package/dist/js/{chunk-vendors.cc9f5247.js → chunk-vendors-legacy.47cf6375.js} +1 -1
- package/dist/js/{chunk-vendors-legacy.cc9f5247.js → chunk-vendors.47cf6375.js} +1 -1
- package/dist/w-web-api.umd.js +1 -1
- package/docs/WWebApi.html +76 -2
- package/docs/index.html +1 -1
- package/docs/server_WWebApi.mjs.html +931 -931
- package/docs/src_components_Common_CheckYesNo.vue.html +1 -1
- package/{g.backupDb.mjs → g_backupDb.mjs} +10 -10
- package/{g.initialTestData.mjs → g_initialTestData.mjs} +21 -21
- package/{g.mOrm.mjs → g_mOrm.mjs} +24 -24
- package/{g.provideApis.mjs → g_provideApis.mjs} +53 -53
- package/logs/2026-08-25T20.log +0 -0
- package/logs/2026-08-25T21.log +1891 -0
- package/package.json +12 -12
- package/server/WWebApi.mjs +930 -930
- package/settings.json +1 -1
- package/src/components/LayoutContent.vue +637 -637
- package/src/components/LayoutContentStats.vue +99 -27
- package/srv.mjs +108 -108
- package/tableTags.json +1 -1
- package/dist/js/app-legacy.955c81f0.js +0 -1
- package/dist/js/app.955c81f0.js +0 -1
- /package/{g.getSettings.mjs → g_getSettings.mjs} +0 -0
|
@@ -1,35 +1,41 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="stats-panel" :style="`height:${height}px; overflow-y:auto; padding:20px 24px; box-sizing:border-box;`">
|
|
3
3
|
|
|
4
|
-
<!--
|
|
5
|
-
<div style="margin-bottom:16px;">
|
|
4
|
+
<!-- 標題列(ref 供 chartHeight 量測上方實佔高度) -->
|
|
5
|
+
<div ref="$hdr" style="margin-bottom:16px;">
|
|
6
6
|
<div style="font-size:1.4rem; font-weight:600; color:var(--c-1);">{{$t('statisticsInformation')}}</div>
|
|
7
7
|
<div style="font-size:0.875rem; color:var(--c-3); margin-top:4px;">{{$t('statisticsInformationDescription')}}</div>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
<!-- 控制區(事件選擇 + 時間範圍;把各事件分出來、可區分選取,供趨勢辨認 / 危險識別)
|
|
11
|
-
|
|
10
|
+
<!-- 控制區(事件選擇 + 時間範圍;把各事件分出來、可區分選取,供趨勢辨認 / 危險識別)
|
|
11
|
+
結構性對齊:.ctrl 為兩欄 grid(label 欄寬 max-content 自動隨語系文字),.ctrl-row 以
|
|
12
|
+
display:contents 使 label 與 field 直接成為 grid 子項,中英文自動對齊,免寫死 label 寬度 -->
|
|
13
|
+
<div ref="$ctrl" class="ctrl">
|
|
12
14
|
|
|
13
15
|
<!-- 第一列:時間範圍 -->
|
|
14
16
|
<div class="ctrl-row">
|
|
15
17
|
<span class="ctrl-label">{{$t('timeRange')}}</span>
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
<div class="ctrl-field">
|
|
19
|
+
<select id="timeGroupSel" v-model="timeGroup" class="ctrl-select">
|
|
20
|
+
<option value="1hr">{{$t('selectItem1hr')}}</option>
|
|
21
|
+
<option value="4hr">{{$t('selectItem4hr')}}</option>
|
|
22
|
+
<option value="8hr">{{$t('selectItem8hr')}}</option>
|
|
23
|
+
<option value="1day">{{$t('selectItem1day')}}</option>
|
|
24
|
+
</select>
|
|
25
|
+
</div>
|
|
22
26
|
</div>
|
|
23
27
|
|
|
24
28
|
<!-- 第二列:選擇事件 標題 + 全選/清除 -->
|
|
25
|
-
<div class="ctrl-row"
|
|
29
|
+
<div class="ctrl-row">
|
|
26
30
|
<span class="ctrl-label">{{$t('selectEvents')}}</span>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
<div class="ctrl-field">
|
|
32
|
+
<button class="evt-sel-btn stats-sel-all" @click="selectAllEvents">{{$t('selectAll')}}</button>
|
|
33
|
+
<button class="evt-sel-btn stats-sel-none" @click="selectNoneEvents">{{$t('selectNone')}}</button>
|
|
34
|
+
<span class="evt-count">{{selectedEvents.length}}/{{allEvents.length}}</span>
|
|
35
|
+
</div>
|
|
30
36
|
</div>
|
|
31
37
|
|
|
32
|
-
<!-- 第三列:事件 chip(
|
|
38
|
+
<!-- 第三列:事件 chip(flex-wrap 內容自然寬;chip 依內容取寬、排滿可用寬才換行) -->
|
|
33
39
|
<div class="evt-grid">
|
|
34
40
|
<label
|
|
35
41
|
v-for="ev in allEvents"
|
|
@@ -148,17 +154,43 @@ export default {
|
|
|
148
154
|
timeGroup: '1hr',
|
|
149
155
|
loading: false,
|
|
150
156
|
errMsg: '',
|
|
157
|
+
hUpper: 0, //標題列+控制區實佔高度(ResizeObserver 量測,供 chartHeight 計算;chip 換行數/語系皆會使其變化)
|
|
151
158
|
}
|
|
152
159
|
},
|
|
153
160
|
mounted: function() {
|
|
154
161
|
let vo = this
|
|
162
|
+
|
|
163
|
+
//ResizeObserver 量測標題列+控制區實佔高(含 margin),取代固定扣除值;
|
|
164
|
+
//ro 為瀏覽器原生物件不放 data(避免被深層響應化),掛 this 成非響應式屬性
|
|
165
|
+
let ro = new ResizeObserver(function() {
|
|
166
|
+
vo.updateUpperHeight()
|
|
167
|
+
})
|
|
168
|
+
if (vo.$refs['$hdr']) {
|
|
169
|
+
ro.observe(vo.$refs['$hdr'])
|
|
170
|
+
}
|
|
171
|
+
if (vo.$refs['$ctrl']) {
|
|
172
|
+
ro.observe(vo.$refs['$ctrl'])
|
|
173
|
+
}
|
|
174
|
+
vo.ro = ro
|
|
175
|
+
vo.updateUpperHeight()
|
|
176
|
+
|
|
155
177
|
vo.load()
|
|
156
178
|
},
|
|
179
|
+
beforeDestroy: function() {
|
|
180
|
+
let vo = this
|
|
181
|
+
if (vo.ro) {
|
|
182
|
+
vo.ro.disconnect()
|
|
183
|
+
vo.ro = null
|
|
184
|
+
}
|
|
185
|
+
},
|
|
157
186
|
computed: {
|
|
158
187
|
|
|
159
188
|
chartHeight: function() {
|
|
160
189
|
let vo = this
|
|
161
|
-
|
|
190
|
+
//panel 上下 padding 40 + 上方實佔(hUpper, 量測) + 表格預留 98(既有設計值:舊公式 height−360
|
|
191
|
+
//於 1440 下等價於保留 chart 底至 panel 內容底 98px,沿用該行為,非新設數字)
|
|
192
|
+
let TABLE_RESERVE = 98
|
|
193
|
+
let h = vo.height - 40 - vo.hUpper - TABLE_RESERVE
|
|
162
194
|
return h < 220 ? 220 : h
|
|
163
195
|
},
|
|
164
196
|
|
|
@@ -311,6 +343,24 @@ export default {
|
|
|
311
343
|
},
|
|
312
344
|
methods: {
|
|
313
345
|
|
|
346
|
+
//量測標題列+控制區實佔高(offsetHeight + margin-bottom),存入 hUpper 供 chartHeight 計算
|
|
347
|
+
updateUpperHeight: function() {
|
|
348
|
+
let vo = this
|
|
349
|
+
let mb = function(el) {
|
|
350
|
+
return parseFloat(getComputedStyle(el).marginBottom) || 0
|
|
351
|
+
}
|
|
352
|
+
let h = 0
|
|
353
|
+
let hdr = vo.$refs['$hdr']
|
|
354
|
+
let ctrl = vo.$refs['$ctrl']
|
|
355
|
+
if (hdr) {
|
|
356
|
+
h += hdr.offsetHeight + mb(hdr)
|
|
357
|
+
}
|
|
358
|
+
if (ctrl) {
|
|
359
|
+
h += ctrl.offsetHeight + mb(ctrl)
|
|
360
|
+
}
|
|
361
|
+
vo.hUpper = h
|
|
362
|
+
},
|
|
363
|
+
|
|
314
364
|
colorOf: function(ev) {
|
|
315
365
|
let vo = this
|
|
316
366
|
let idx = vo.allEvents.indexOf(ev)
|
|
@@ -353,21 +403,30 @@ export default {
|
|
|
353
403
|
|
|
354
404
|
<style scoped>
|
|
355
405
|
|
|
406
|
+
/* 控制區:兩欄 grid 結構性對齊(label 欄 max-content 隨語系文字自動定寬;
|
|
407
|
+
第二欄 minmax(0,1fr) 防內容撐大),取代先前 label 寫死 64px + evt-grid margin-left 72px 之魔術數字對齊 */
|
|
356
408
|
.ctrl {
|
|
357
409
|
margin-bottom: 16px;
|
|
410
|
+
display: grid;
|
|
411
|
+
grid-template-columns: max-content minmax(0, 1fr);
|
|
412
|
+
gap: 12px 8px;
|
|
413
|
+
align-items: center;
|
|
358
414
|
}
|
|
359
415
|
|
|
360
416
|
.ctrl-row {
|
|
361
|
-
display:
|
|
362
|
-
align-items: center;
|
|
363
|
-
gap: 8px;
|
|
417
|
+
display: contents; /* label 與 field 直接成為 .ctrl 之 grid 子項 */
|
|
364
418
|
}
|
|
365
419
|
|
|
366
420
|
.ctrl-label {
|
|
367
421
|
font-size: 0.875rem;
|
|
368
422
|
color: var(--c-2);
|
|
369
|
-
|
|
370
|
-
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.ctrl-field {
|
|
426
|
+
display: flex;
|
|
427
|
+
align-items: center;
|
|
428
|
+
gap: 8px;
|
|
429
|
+
min-width: 0;
|
|
371
430
|
}
|
|
372
431
|
|
|
373
432
|
.ctrl-select {
|
|
@@ -406,20 +465,23 @@ export default {
|
|
|
406
465
|
margin-left: 4px;
|
|
407
466
|
}
|
|
408
467
|
|
|
409
|
-
/* 事件 chip
|
|
468
|
+
/* 事件 chip:flex-wrap 內容自然寬(用滿容器可用寬、排不下才換行),
|
|
469
|
+
置於 .ctrl grid 第二欄與上方控制項對齊;不設 max-width 上限(由容器自然約束) */
|
|
410
470
|
.evt-grid {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
display:
|
|
414
|
-
|
|
471
|
+
grid-column: 2;
|
|
472
|
+
min-width: 0;
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-wrap: wrap;
|
|
415
475
|
gap: 8px;
|
|
416
|
-
max-width: 1040px;
|
|
417
476
|
}
|
|
418
477
|
|
|
419
478
|
.evt-chip {
|
|
420
479
|
display: flex;
|
|
421
480
|
align-items: center;
|
|
422
481
|
box-sizing: border-box;
|
|
482
|
+
flex: 0 1 auto;
|
|
483
|
+
min-width: 0;
|
|
484
|
+
max-width: 100%; /* 安全閥:極長事件名時 chip 不超過容器寬(此時 .evt-name 之 ellipsis 才生效) */
|
|
423
485
|
border: 1px solid var(--border);
|
|
424
486
|
border-radius: var(--radius);
|
|
425
487
|
background: var(--bg-1);
|
|
@@ -456,17 +518,27 @@ export default {
|
|
|
456
518
|
}
|
|
457
519
|
|
|
458
520
|
.evt-name {
|
|
521
|
+
min-width: 0; /* flex item 預設 min-width:auto 會阻止收縮,明給 0 使安全閥可生效 */
|
|
459
522
|
overflow: hidden;
|
|
460
523
|
text-overflow: ellipsis;
|
|
461
524
|
white-space: nowrap;
|
|
462
525
|
}
|
|
463
526
|
|
|
527
|
+
.stats-table-area {
|
|
528
|
+
overflow-x: auto; /* 極端長事件名時表格自帶水平捲動,不外溢撐破 panel */
|
|
529
|
+
}
|
|
530
|
+
|
|
464
531
|
.stats-table {
|
|
465
532
|
width: 100%;
|
|
466
533
|
border-collapse: collapse;
|
|
467
534
|
font-size: 0.85rem;
|
|
468
535
|
}
|
|
469
536
|
|
|
537
|
+
.stats-table td:first-child {
|
|
538
|
+
white-space: normal; /* 事件名欄允許換行(數值欄維持 nowrap),與 chip 區同源之防溢出處置 */
|
|
539
|
+
overflow-wrap: anywhere;
|
|
540
|
+
}
|
|
541
|
+
|
|
470
542
|
.stats-table th,
|
|
471
543
|
.stats-table td {
|
|
472
544
|
border-bottom: 1px solid var(--border);
|
package/srv.mjs
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
import WOrm from 'w-orm-lmdb/src/WOrmLmdb.mjs'
|
|
2
|
-
import WWebApi from './server/WWebApi.mjs'
|
|
3
|
-
import getSettings from './
|
|
4
|
-
import get from 'lodash-es/get.js'
|
|
5
|
-
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
6
|
-
import axios from 'axios'
|
|
7
|
-
import JSON5 from 'json5'
|
|
8
|
-
import fs from 'fs'
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//st(db 等系統參數由
|
|
12
|
-
let st = getSettings()
|
|
13
|
-
|
|
14
|
-
//pathSettings:app 設定檔路徑,預設 ./settings.json;可由 `node srv.mjs <path>` 覆寫
|
|
15
|
-
//(供 e2e restartBackend / 部署帶不同設定,日後更細緻功能便於擴充)。settings.json 為 JSON5 格式。
|
|
16
|
-
let pathSettings = process.argv[2] || './settings.json'
|
|
17
|
-
let stApp = JSON5.parse(fs.readFileSync(pathSettings, 'utf8'))
|
|
18
|
-
|
|
19
|
-
let url = st.dbUrl
|
|
20
|
-
let db = st.dbName
|
|
21
|
-
let ssoBaseUrl = st.ssoBaseUrl
|
|
22
|
-
let ssoAppToken = st.ssoAppToken
|
|
23
|
-
let ssoLoginUrl = st.ssoLoginUrl
|
|
24
|
-
let opt = {
|
|
25
|
-
|
|
26
|
-
useCheckUser: false,
|
|
27
|
-
getUserById: null,
|
|
28
|
-
useExcludeWhenNotAdmin: false,
|
|
29
|
-
|
|
30
|
-
serverPort: get(stApp, 'serverPort', 11005),
|
|
31
|
-
subfolder: '', //mapi
|
|
32
|
-
urlRedirect: ssoLoginUrl, //未登入時導向SSO登入頁, 本機測試時得先編譯, 再瀏覽: http://localhost:11005/
|
|
33
|
-
|
|
34
|
-
//語系(來自 settings.json):language 由 WWebApi 注入 index.html ({language}) → 初始畫面即此語系;
|
|
35
|
-
//showLanguage 控右上語系選單顯隱。e2e 可用 restartBackend(genTempSettings({language})) 改 server 初始語系。
|
|
36
|
-
showLanguage: get(stApp, 'showLanguage', 'y'),
|
|
37
|
-
language: get(stApp, 'language', 'eng'),
|
|
38
|
-
|
|
39
|
-
//log 資料夾與輪檔間隔(srLog 寫入 + staEvent 統計皆讀此)。e2e 可用 genTempSettings({logFd}) 指向合成 log 達 hermetic。
|
|
40
|
-
logFd: get(stApp, 'logFd', './logs'),
|
|
41
|
-
logInterval: get(stApp, 'logInterval', 'hr'),
|
|
42
|
-
|
|
43
|
-
webName: {
|
|
44
|
-
'eng': 'API Service',
|
|
45
|
-
'cht': 'API管理系統',
|
|
46
|
-
},
|
|
47
|
-
webDescription: {
|
|
48
|
-
'eng': 'A web service package as methods to send requests to and receive responses from an API.',
|
|
49
|
-
'cht': '一套以方法形式,用於發送請求至API並接收其回應的網頁服務套件。',
|
|
50
|
-
},
|
|
51
|
-
webLogo: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGcgaWQ9IkxheWVyXzIiLz48ZyBpZD0iTGF5ZXJfMyIvPjxnIGlkPSJMYXllcl80Ij48Zz48cGF0aCBkPSJNOS4wMiw2QzkuMDEyNyw2LDkuMDA2OCw1Ljk5OSw5LDZIMkMxLjQ0NzgsNiwxLDUuNTUyNywxLDVWMmMwLTAuNTUyNywwLjQ0NzgtMSwxLTFoNiAgICBjMC40MzA3LDAsMC44MTI1LDAuMjc1NCwwLjk0ODcsMC42ODM2bDAuOTQwNCwyLjgyMTNDOS45NzIyLDQuNjUxNCwxMC4wMiw0LjgyMDMsMTAuMDIsNUMxMC4wMiw1LjU1MjcsOS41NzIzLDYsOS4wMiw2eiBNMyw0ICAgIGg0LjYxMjhMNy4yNzkzLDNIM1Y0eiIgZmlsbD0iI0Y1RDgwMyIvPjwvZz48Zz48cGF0aCBkPSJNMjQuMDE5NSwyMmMtMC4wMDM5LDAtMC4wMTE3LDAuMDAxLTAuMDE5NSwwaC03Yy0wLjU1MjIsMC0xLTAuNDQ3My0xLTF2LTNjMC0wLjU1MjcsMC40NDc4LTEsMS0xaDYgICAgYzAuNDMwNywwLDAuODEyNSwwLjI3NTQsMC45NDgyLDAuNjgzNmwwLjk0MDQsMi44MjEzYzAuMDg0LDAuMTQ2NSwwLjEzMDksMC4zMTU0LDAuMTMwOSwwLjQ5NTEgICAgQzI1LjAxOTUsMjEuNTUyNywyNC41NzIzLDIyLDI0LjAxOTUsMjJ6IE0xOCwyMGg0LjYxMjNsLTAuMzMzLTFIMThWMjB6IiBmaWxsPSIjRjVEODAzIi8+PC9nPjxnPjxwYXRoIGQ9Ik0xNSwxNUgyYy0wLjU1MjIsMC0xLTAuNDQ3My0xLTFWNWMwLTAuNTUyNywwLjQ0NzgtMSwxLTFoMTNjMC41NTIyLDAsMSwwLjQ0NzMsMSwxdjkgICAgQzE2LDE0LjU1MjcsMTUuNTUyMiwxNSwxNSwxNXogTTMsMTNoMTFWNkgzVjEzeiIgZmlsbD0iIzAxODFCMCIvPjwvZz48Zz48cGF0aCBkPSJNMzAsMzFIMTdjLTAuNTUyMiwwLTEtMC40NDczLTEtMXYtOWMwLTAuNTUyNywwLjQ0NzgtMSwxLTFoMTNjMC41NTI3LDAsMSwwLjQ0NzMsMSwxdjkgICAgQzMxLDMwLjU1MjcsMzAuNTUyNywzMSwzMCwzMXogTTE4LDI5aDExdi03SDE4VjI5eiIgZmlsbD0iIzAxODFCMCIvPjwvZz48Zz48cGF0aCBkPSJNMjYsOGgtOGMtMC41NTI3LDAtMSwwLjQ0NzMtMSwxczAuNDQ3MywxLDEsMWg3djZjMCwwLjU1MjcsMC40NDczLDEsMSwxczEtMC40NDczLDEtMVY5ICAgIEMyNyw4LjQ0NzMsMjYuNTUyNyw4LDI2LDh6IiBmaWxsPSIjMDBBQ0JBIi8+PGc+PHBhdGggZD0iTTIwLDEyYy0wLjI1NTksMC0wLjUxMTctMC4wOTc3LTAuNzA3LTAuMjkzbC0yLTJjLTAuMzkwNi0wLjM5MDYtMC4zOTA2LTEuMDIzNCwwLTEuNDE0MWwyLTIgICAgIGMwLjM5MDYtMC4zOTA2LDEuMDIzNC0wLjM5MDYsMS40MTQxLDBzMC4zOTA2LDEuMDIzNCwwLDEuNDE0MUwxOS40MTQxLDlsMS4yOTMsMS4yOTNjMC4zOTA2LDAuMzkwNiwwLjM5MDYsMS4wMjM0LDAsMS40MTQxICAgICBDMjAuNTExNywxMS45MDIzLDIwLjI1NTksMTIsMjAsMTJ6IiBmaWxsPSIjRjVEODAzIi8+PC9nPjwvZz48Zz48cGF0aCBkPSJNMTQsMjVIN3YtNmMwLTAuNTUyNy0wLjQ0NzgtMS0xLTFzLTEsMC40NDczLTEsMXY3YzAsMC41NTI3LDAuNDQ3OCwxLDEsMWg4YzAuNTUyMiwwLDEtMC40NDczLDEtMSAgICBTMTQuNTUyMiwyNSwxNCwyNXoiIGZpbGw9IiMwMEFDQkEiLz48Zz48cGF0aCBkPSJNMTIsMjljLTAuMjU1OSwwLTAuNTExNy0wLjA5NzctMC43MDctMC4yOTNjLTAuMzkwNi0wLjM5MDYtMC4zOTA2LTEuMDIzNCwwLTEuNDE0MUwxMi41ODU5LDI2ICAgICBsLTEuMjkzLTEuMjkzYy0wLjM5MDYtMC4zOTA2LTAuMzkwNi0xLjAyMzQsMC0xLjQxNDFzMS4wMjM0LTAuMzkwNiwxLjQxNDEsMGwyLDJjMC4zOTA2LDAuMzkwNiwwLjM5MDYsMS4wMjM0LDAsMS40MTQxbC0yLDIgICAgIEMxMi41MTE3LDI4LjkwMjMsMTIuMjU1OSwyOSwxMiwyOXoiIGZpbGw9IiNGNUQ4MDMiLz48L2c+PC9nPjwvZz48ZyBpZD0iTGF5ZXJfNSIvPjxnIGlkPSJMYXllcl82Ii8+PGcgaWQ9IkxheWVyXzciLz48ZyBpZD0iTGF5ZXJfOCIvPjxnIGlkPSJMYXllcl85Ii8+PGcgaWQ9IkxheWVyXzEwIi8+PGcgaWQ9IkxheWVyXzExIi8+PGcgaWQ9IkxheWVyXzEyIi8+PGcgaWQ9IkxheWVyXzEzIi8+PGcgaWQ9IkxheWVyXzE0Ii8+PGcgaWQ9IkxheWVyXzE1Ii8+PGcgaWQ9IkxheWVyXzE2Ii8+PGcgaWQ9IkxheWVyXzE3Ii8+PGcgaWQ9IkxheWVyXzE4Ii8+PGcgaWQ9IkxheWVyXzE5Ii8+PGcgaWQ9IkxheWVyXzIwIi8+PGcgaWQ9IkxheWVyXzIxIi8+PGcgaWQ9IkxheWVyXzIyIi8+PGcgaWQ9IkxheWVyXzIzIi8+PGcgaWQ9IkxheWVyXzI0Ii8+PGcgaWQ9IkxheWVyXzI1Ii8+PGcgaWQ9IkxheWVyXzI2Ii8+PC9zdmc+',
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let getUserByToken = async (token) => {
|
|
56
|
-
// return {} //測試無法登入
|
|
57
|
-
//'sys' 為開發捷徑(w-ui-loginout 於 localhost 自動帶入)。加環境守門:NODE_ENV==='production' 時不接受,
|
|
58
|
-
//避免 srv.mjs 若作正式入口時 'sys' 成為遠端管理員後門。e2e/dev 未設 NODE_ENV=production 故仍可用;
|
|
59
|
-
//正式部署設 NODE_ENV=production 即停用此捷徑(並應改提供真實 getUserByToken)。
|
|
60
|
-
if (token === 'sys' && process.env.NODE_ENV !== 'production') {
|
|
61
|
-
return { id: 'id-for-admin', name: '測試者', email: 'admin@example.com', isAdmin: 'y' }
|
|
62
|
-
}
|
|
63
|
-
//未設定 SSO app token 時(本機開發未接SSO), 不打SSO直接拒絕
|
|
64
|
-
if (!ssoAppToken) {
|
|
65
|
-
console.log('ssoAppToken 未設定, 略過 SSO 驗證')
|
|
66
|
-
return {}
|
|
67
|
-
}
|
|
68
|
-
//呼叫 SSO 解析 token -> user
|
|
69
|
-
try {
|
|
70
|
-
let url = `${ssoBaseUrl}/api/getSsoUserInfor?token=${encodeURIComponent(ssoAppToken)}&key=token&value=${encodeURIComponent(token)}`
|
|
71
|
-
let res = await axios.get(url)
|
|
72
|
-
let state = get(res, 'data.state', '')
|
|
73
|
-
let u = get(res, 'data.msg', null)
|
|
74
|
-
if (state !== 'success' || !iseobj(u)) {
|
|
75
|
-
console.log('SSO getSsoUserInfor 失敗', state)
|
|
76
|
-
return {}
|
|
77
|
-
}
|
|
78
|
-
return { id: u.id, name: u.name, email: u.email, isAdmin: u.isAdmin }
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
console.log('SSO getSsoUserInfor error', err.message)
|
|
82
|
-
return {}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let verifyClientUser = (user, from) => {
|
|
87
|
-
console.log('verifyClientUser/user', user)
|
|
88
|
-
console.log('於生產環境時得加入限制瀏覽器使用者身份機制')
|
|
89
|
-
// return false //測試無法登入
|
|
90
|
-
return user.isAdmin === 'y' //測試僅系統管理者使用
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let verifyAppUser = (user, from) => {
|
|
94
|
-
console.log('verifyAppUser/user', user)
|
|
95
|
-
console.log('於生產環境時得加入限制應用程式使用者身份機制')
|
|
96
|
-
// return false //測試無法登入
|
|
97
|
-
return user.isAdmin === 'y' //測試僅系統管理者使用
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
//WWebApi
|
|
101
|
-
let instWWebApi = WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt)
|
|
102
|
-
|
|
103
|
-
instWWebApi.on('error', (err) => {
|
|
104
|
-
console.log(err)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//node srv.mjs
|
|
1
|
+
import WOrm from 'w-orm-lmdb/src/WOrmLmdb.mjs'
|
|
2
|
+
import WWebApi from './server/WWebApi.mjs'
|
|
3
|
+
import getSettings from './g_getSettings.mjs'
|
|
4
|
+
import get from 'lodash-es/get.js'
|
|
5
|
+
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
6
|
+
import axios from 'axios'
|
|
7
|
+
import JSON5 from 'json5'
|
|
8
|
+
import fs from 'fs'
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
//st(db 等系統參數由 g_getSettings 提供,供 g_mOrm 共用)
|
|
12
|
+
let st = getSettings()
|
|
13
|
+
|
|
14
|
+
//pathSettings:app 設定檔路徑,預設 ./settings.json;可由 `node srv.mjs <path>` 覆寫
|
|
15
|
+
//(供 e2e restartBackend / 部署帶不同設定,日後更細緻功能便於擴充)。settings.json 為 JSON5 格式。
|
|
16
|
+
let pathSettings = process.argv[2] || './settings.json'
|
|
17
|
+
let stApp = JSON5.parse(fs.readFileSync(pathSettings, 'utf8'))
|
|
18
|
+
|
|
19
|
+
let url = st.dbUrl
|
|
20
|
+
let db = st.dbName
|
|
21
|
+
let ssoBaseUrl = st.ssoBaseUrl
|
|
22
|
+
let ssoAppToken = st.ssoAppToken
|
|
23
|
+
let ssoLoginUrl = st.ssoLoginUrl
|
|
24
|
+
let opt = {
|
|
25
|
+
|
|
26
|
+
useCheckUser: false,
|
|
27
|
+
getUserById: null,
|
|
28
|
+
useExcludeWhenNotAdmin: false,
|
|
29
|
+
|
|
30
|
+
serverPort: get(stApp, 'serverPort', 11005),
|
|
31
|
+
subfolder: '', //mapi
|
|
32
|
+
urlRedirect: ssoLoginUrl, //未登入時導向SSO登入頁, 本機測試時得先編譯, 再瀏覽: http://localhost:11005/
|
|
33
|
+
|
|
34
|
+
//語系(來自 settings.json):language 由 WWebApi 注入 index.html ({language}) → 初始畫面即此語系;
|
|
35
|
+
//showLanguage 控右上語系選單顯隱。e2e 可用 restartBackend(genTempSettings({language})) 改 server 初始語系。
|
|
36
|
+
showLanguage: get(stApp, 'showLanguage', 'y'),
|
|
37
|
+
language: get(stApp, 'language', 'eng'),
|
|
38
|
+
|
|
39
|
+
//log 資料夾與輪檔間隔(srLog 寫入 + staEvent 統計皆讀此)。e2e 可用 genTempSettings({logFd}) 指向合成 log 達 hermetic。
|
|
40
|
+
logFd: get(stApp, 'logFd', './logs'),
|
|
41
|
+
logInterval: get(stApp, 'logInterval', 'hr'),
|
|
42
|
+
|
|
43
|
+
webName: {
|
|
44
|
+
'eng': 'API Service',
|
|
45
|
+
'cht': 'API管理系統',
|
|
46
|
+
},
|
|
47
|
+
webDescription: {
|
|
48
|
+
'eng': 'A web service package as methods to send requests to and receive responses from an API.',
|
|
49
|
+
'cht': '一套以方法形式,用於發送請求至API並接收其回應的網頁服務套件。',
|
|
50
|
+
},
|
|
51
|
+
webLogo: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGcgaWQ9IkxheWVyXzIiLz48ZyBpZD0iTGF5ZXJfMyIvPjxnIGlkPSJMYXllcl80Ij48Zz48cGF0aCBkPSJNOS4wMiw2QzkuMDEyNyw2LDkuMDA2OCw1Ljk5OSw5LDZIMkMxLjQ0NzgsNiwxLDUuNTUyNywxLDVWMmMwLTAuNTUyNywwLjQ0NzgtMSwxLTFoNiAgICBjMC40MzA3LDAsMC44MTI1LDAuMjc1NCwwLjk0ODcsMC42ODM2bDAuOTQwNCwyLjgyMTNDOS45NzIyLDQuNjUxNCwxMC4wMiw0LjgyMDMsMTAuMDIsNUMxMC4wMiw1LjU1MjcsOS41NzIzLDYsOS4wMiw2eiBNMyw0ICAgIGg0LjYxMjhMNy4yNzkzLDNIM1Y0eiIgZmlsbD0iI0Y1RDgwMyIvPjwvZz48Zz48cGF0aCBkPSJNMjQuMDE5NSwyMmMtMC4wMDM5LDAtMC4wMTE3LDAuMDAxLTAuMDE5NSwwaC03Yy0wLjU1MjIsMC0xLTAuNDQ3My0xLTF2LTNjMC0wLjU1MjcsMC40NDc4LTEsMS0xaDYgICAgYzAuNDMwNywwLDAuODEyNSwwLjI3NTQsMC45NDgyLDAuNjgzNmwwLjk0MDQsMi44MjEzYzAuMDg0LDAuMTQ2NSwwLjEzMDksMC4zMTU0LDAuMTMwOSwwLjQ5NTEgICAgQzI1LjAxOTUsMjEuNTUyNywyNC41NzIzLDIyLDI0LjAxOTUsMjJ6IE0xOCwyMGg0LjYxMjNsLTAuMzMzLTFIMThWMjB6IiBmaWxsPSIjRjVEODAzIi8+PC9nPjxnPjxwYXRoIGQ9Ik0xNSwxNUgyYy0wLjU1MjIsMC0xLTAuNDQ3My0xLTFWNWMwLTAuNTUyNywwLjQ0NzgtMSwxLTFoMTNjMC41NTIyLDAsMSwwLjQ0NzMsMSwxdjkgICAgQzE2LDE0LjU1MjcsMTUuNTUyMiwxNSwxNSwxNXogTTMsMTNoMTFWNkgzVjEzeiIgZmlsbD0iIzAxODFCMCIvPjwvZz48Zz48cGF0aCBkPSJNMzAsMzFIMTdjLTAuNTUyMiwwLTEtMC40NDczLTEtMXYtOWMwLTAuNTUyNywwLjQ0NzgtMSwxLTFoMTNjMC41NTI3LDAsMSwwLjQ0NzMsMSwxdjkgICAgQzMxLDMwLjU1MjcsMzAuNTUyNywzMSwzMCwzMXogTTE4LDI5aDExdi03SDE4VjI5eiIgZmlsbD0iIzAxODFCMCIvPjwvZz48Zz48cGF0aCBkPSJNMjYsOGgtOGMtMC41NTI3LDAtMSwwLjQ0NzMtMSwxczAuNDQ3MywxLDEsMWg3djZjMCwwLjU1MjcsMC40NDczLDEsMSwxczEtMC40NDczLDEtMVY5ICAgIEMyNyw4LjQ0NzMsMjYuNTUyNyw4LDI2LDh6IiBmaWxsPSIjMDBBQ0JBIi8+PGc+PHBhdGggZD0iTTIwLDEyYy0wLjI1NTksMC0wLjUxMTctMC4wOTc3LTAuNzA3LTAuMjkzbC0yLTJjLTAuMzkwNi0wLjM5MDYtMC4zOTA2LTEuMDIzNCwwLTEuNDE0MWwyLTIgICAgIGMwLjM5MDYtMC4zOTA2LDEuMDIzNC0wLjM5MDYsMS40MTQxLDBzMC4zOTA2LDEuMDIzNCwwLDEuNDE0MUwxOS40MTQxLDlsMS4yOTMsMS4yOTNjMC4zOTA2LDAuMzkwNiwwLjM5MDYsMS4wMjM0LDAsMS40MTQxICAgICBDMjAuNTExNywxMS45MDIzLDIwLjI1NTksMTIsMjAsMTJ6IiBmaWxsPSIjRjVEODAzIi8+PC9nPjwvZz48Zz48cGF0aCBkPSJNMTQsMjVIN3YtNmMwLTAuNTUyNy0wLjQ0NzgtMS0xLTFzLTEsMC40NDczLTEsMXY3YzAsMC41NTI3LDAuNDQ3OCwxLDEsMWg4YzAuNTUyMiwwLDEtMC40NDczLDEtMSAgICBTMTQuNTUyMiwyNSwxNCwyNXoiIGZpbGw9IiMwMEFDQkEiLz48Zz48cGF0aCBkPSJNMTIsMjljLTAuMjU1OSwwLTAuNTExNy0wLjA5NzctMC43MDctMC4yOTNjLTAuMzkwNi0wLjM5MDYtMC4zOTA2LTEuMDIzNCwwLTEuNDE0MUwxMi41ODU5LDI2ICAgICBsLTEuMjkzLTEuMjkzYy0wLjM5MDYtMC4zOTA2LTAuMzkwNi0xLjAyMzQsMC0xLjQxNDFzMS4wMjM0LTAuMzkwNiwxLjQxNDEsMGwyLDJjMC4zOTA2LDAuMzkwNiwwLjM5MDYsMS4wMjM0LDAsMS40MTQxbC0yLDIgICAgIEMxMi41MTE3LDI4LjkwMjMsMTIuMjU1OSwyOSwxMiwyOXoiIGZpbGw9IiNGNUQ4MDMiLz48L2c+PC9nPjwvZz48ZyBpZD0iTGF5ZXJfNSIvPjxnIGlkPSJMYXllcl82Ii8+PGcgaWQ9IkxheWVyXzciLz48ZyBpZD0iTGF5ZXJfOCIvPjxnIGlkPSJMYXllcl85Ii8+PGcgaWQ9IkxheWVyXzEwIi8+PGcgaWQ9IkxheWVyXzExIi8+PGcgaWQ9IkxheWVyXzEyIi8+PGcgaWQ9IkxheWVyXzEzIi8+PGcgaWQ9IkxheWVyXzE0Ii8+PGcgaWQ9IkxheWVyXzE1Ii8+PGcgaWQ9IkxheWVyXzE2Ii8+PGcgaWQ9IkxheWVyXzE3Ii8+PGcgaWQ9IkxheWVyXzE4Ii8+PGcgaWQ9IkxheWVyXzE5Ii8+PGcgaWQ9IkxheWVyXzIwIi8+PGcgaWQ9IkxheWVyXzIxIi8+PGcgaWQ9IkxheWVyXzIyIi8+PGcgaWQ9IkxheWVyXzIzIi8+PGcgaWQ9IkxheWVyXzI0Ii8+PGcgaWQ9IkxheWVyXzI1Ii8+PGcgaWQ9IkxheWVyXzI2Ii8+PC9zdmc+',
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let getUserByToken = async (token) => {
|
|
56
|
+
// return {} //測試無法登入
|
|
57
|
+
//'sys' 為開發捷徑(w-ui-loginout 於 localhost 自動帶入)。加環境守門:NODE_ENV==='production' 時不接受,
|
|
58
|
+
//避免 srv.mjs 若作正式入口時 'sys' 成為遠端管理員後門。e2e/dev 未設 NODE_ENV=production 故仍可用;
|
|
59
|
+
//正式部署設 NODE_ENV=production 即停用此捷徑(並應改提供真實 getUserByToken)。
|
|
60
|
+
if (token === 'sys' && process.env.NODE_ENV !== 'production') {
|
|
61
|
+
return { id: 'id-for-admin', name: '測試者', email: 'admin@example.com', isAdmin: 'y' }
|
|
62
|
+
}
|
|
63
|
+
//未設定 SSO app token 時(本機開發未接SSO), 不打SSO直接拒絕
|
|
64
|
+
if (!ssoAppToken) {
|
|
65
|
+
console.log('ssoAppToken 未設定, 略過 SSO 驗證')
|
|
66
|
+
return {}
|
|
67
|
+
}
|
|
68
|
+
//呼叫 SSO 解析 token -> user
|
|
69
|
+
try {
|
|
70
|
+
let url = `${ssoBaseUrl}/api/getSsoUserInfor?token=${encodeURIComponent(ssoAppToken)}&key=token&value=${encodeURIComponent(token)}`
|
|
71
|
+
let res = await axios.get(url)
|
|
72
|
+
let state = get(res, 'data.state', '')
|
|
73
|
+
let u = get(res, 'data.msg', null)
|
|
74
|
+
if (state !== 'success' || !iseobj(u)) {
|
|
75
|
+
console.log('SSO getSsoUserInfor 失敗', state)
|
|
76
|
+
return {}
|
|
77
|
+
}
|
|
78
|
+
return { id: u.id, name: u.name, email: u.email, isAdmin: u.isAdmin }
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.log('SSO getSsoUserInfor error', err.message)
|
|
82
|
+
return {}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let verifyClientUser = (user, from) => {
|
|
87
|
+
console.log('verifyClientUser/user', user)
|
|
88
|
+
console.log('於生產環境時得加入限制瀏覽器使用者身份機制')
|
|
89
|
+
// return false //測試無法登入
|
|
90
|
+
return user.isAdmin === 'y' //測試僅系統管理者使用
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let verifyAppUser = (user, from) => {
|
|
94
|
+
console.log('verifyAppUser/user', user)
|
|
95
|
+
console.log('於生產環境時得加入限制應用程式使用者身份機制')
|
|
96
|
+
// return false //測試無法登入
|
|
97
|
+
return user.isAdmin === 'y' //測試僅系統管理者使用
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//WWebApi
|
|
101
|
+
let instWWebApi = WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt)
|
|
102
|
+
|
|
103
|
+
instWWebApi.on('error', (err) => {
|
|
104
|
+
console.log(err)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
//node srv.mjs
|
package/tableTags.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"apis":"2026-08-
|
|
1
|
+
{"apis":"2026-08-25T21:36:29+08:00|2j5C7e"}
|