w-web-api 1.0.75 → 1.0.76

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.
@@ -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
- <div class="ctrl">
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
- <select id="timeGroupSel" v-model="timeGroup" class="ctrl-select">
17
- <option value="1hr">{{$t('selectItem1hr')}}</option>
18
- <option value="4hr">{{$t('selectItem4hr')}}</option>
19
- <option value="8hr">{{$t('selectItem8hr')}}</option>
20
- <option value="1day">{{$t('selectItem1day')}}</option>
21
- </select>
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" style="margin-top:12px;">
29
+ <div class="ctrl-row">
26
30
  <span class="ctrl-label">{{$t('selectEvents')}}</span>
27
- <button class="evt-sel-btn stats-sel-all" @click="selectAllEvents">{{$t('selectAll')}}</button>
28
- <button class="evt-sel-btn stats-sel-none" @click="selectNoneEvents">{{$t('selectNone')}}</button>
29
- <span class="evt-count">{{selectedEvents.length}}/{{allEvents.length}}</span>
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(CSS grid 等寬欄對齊,不 ragged wrap) -->
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
- let h = vo.height - 360 //扣標題 + 控制列(含事件選擇) + 表格區估算
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: flex;
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
- width: 64px;
370
- flex-shrink: 0;
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:等寬欄 grid 對齊,避免 ragged wrap 之縮排錯落 */
468
+ /* 事件 chip:flex-wrap 內容自然寬(用滿容器可用寬、排不下才換行),
469
+ 置於 .ctrl grid 第二欄與上方控制項對齊;不設 max-width 上限(由容器自然約束) */
410
470
  .evt-grid {
411
- margin-top: 10px;
412
- margin-left: 72px;
413
- display: grid;
414
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
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 './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
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-19T22:52:31+08:00|3tBf8a"}
1
+ {"apis":"2026-08-25T21:36:29+08:00|2j5C7e"}