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,930 +1,930 @@
1
- import path from 'path'
2
- import fs from 'fs'
3
- import get from 'lodash-es/get.js'
4
- import each from 'lodash-es/each.js'
5
- import map from 'lodash-es/map.js'
6
- import keys from 'lodash-es/keys.js'
7
- import j2o from 'wsemi/src/j2o.mjs'
8
- import iseobj from 'wsemi/src/iseobj.mjs'
9
- import isestr from 'wsemi/src/isestr.mjs'
10
- import isearr from 'wsemi/src/isearr.mjs'
11
- import ispint from 'wsemi/src/ispint.mjs'
12
- import isErr from 'wsemi/src/isErr.mjs'
13
- import isfun from 'wsemi/src/isfun.mjs'
14
- import ispm from 'wsemi/src/ispm.mjs'
15
- import cint from 'wsemi/src/cint.mjs'
16
- import strleft from 'wsemi/src/strleft.mjs'
17
- import strdelleft from 'wsemi/src/strdelleft.mjs'
18
- import strright from 'wsemi/src/strright.mjs'
19
- import strdelright from 'wsemi/src/strdelright.mjs'
20
- import pm2resolve from 'wsemi/src/pm2resolve.mjs'
21
- import b642str from 'wsemi/src/b642str.mjs'
22
- import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
23
- import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
24
- import fsIsFile from 'wsemi/src/fsIsFile.mjs'
25
- import replace from 'wsemi/src/replace.mjs'
26
- import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
27
- import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
28
- import procLang from './procLang.mjs'
29
- import procProxy from './procProxy.mjs'
30
- import procApis from './procApis.mjs'
31
- import staEvent from './staLogs/staEvent.mjs'
32
- import srLogInit from './srLog.mjs'
33
- import ds from '../src/schema/index.mjs'
34
-
35
-
36
- /**
37
- * API伺服器
38
- *
39
- * @class
40
- * @param {Function} WOrm 輸入資料庫ORM函數
41
- * @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
42
- * @param {String} db 輸入資料庫名稱字串
43
- * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n',且輸入'y'時會複寫權限系統該使用者之'isAdmin'欄位值
44
- * @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
45
- * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
46
- * @param {Object} [opt={}] 輸入設定物件,預設{}
47
- * @param {Integer} [opt.serverPort=11005] 輸入伺服器通訊port,預設11005
48
- * @param {Boolean} [opt.useCheckUser=false] 輸入是否檢查使用者資訊布林值,預設false
49
- * @param {Function} [opt.getUserById=null] 輸入當useCheckUser=true時依照使用者ID取得使用者資訊物件函數,預設null
50
- * @param {Boolean} [opt.useExcludeWhenNotAdmin=false] 輸入使用ORM的select方法時是否自動刪除數據內isActive欄位之布林值,預設false
51
- * @param {Object} [opt.webName={}] 輸入站台名稱物件,至少包含語系eng與cht鍵的名稱,預設{}
52
- * @param {Object} [opt.webDescription={}] 輸入站台描述物件,至少包含語系eng與cht鍵的名稱,預設{}
53
- * @param {String} [opt.webLogo=''] 輸入站台logo字串,採base64格式,預設''
54
- * @param {String} [opt.subfolder=''] 輸入站台所在子目錄字串,提供站台位於內網採反向代理進行服務時,故需支援位於子目錄情形,預設''
55
- * @param {String} [opt.urlRedirect=''] 輸入錯誤時自動轉址字串,提供站台例如無法登入或驗證失敗時須自動轉址,預設''
56
- * @returns {Object} 回傳物件,其內server為hapi伺服器實體,wsrv為w-converhp的伺服器事件物件,wsds為w-serv-webdata的伺服器事件物件,可監聽error事件
57
- * @example
58
- *
59
- * import WOrm from 'w-orm-lmdb/src/WOrmLmdb.mjs'
60
- * import WWebApi from './server/WWebApi.mjs'
61
- * import getSettings from './g.getSettings.mjs'
62
- *
63
- *
64
- * //st
65
- * let st = getSettings()
66
- *
67
- * let url = st.dbUrl
68
- * let db = st.dbName
69
- * let opt = {
70
- *
71
- * useCheckUser: false,
72
- * getUserById: null,
73
- * useExcludeWhenNotAdmin: false,
74
- *
75
- * serverPort: 11005,
76
- * subfolder: '', //mapi
77
- * urlRedirect: 'https://www.google.com/', //本機測試時得先編譯, 再瀏覽: http://localhost:11005/
78
- *
79
- * webName: {
80
- * 'eng': 'API Service',
81
- * 'cht': 'API管理系統',
82
- * },
83
- * webDescription: {
84
- * 'eng': 'A web service package as methods to send requests to and receive responses from an API.',
85
- * 'cht': 'A web service package as methods to send requests to and receive responses from an API.',
86
- * },
87
- * webLogo: 'data:image/svg+xml;base64,...',
88
- *
89
- * }
90
- *
91
- * let getUserByToken = async (token) => {
92
- * // return {} //測試無法登入
93
- * if (token === '{token-for-application}') { //提供外部應用系統作為存取使用者
94
- * return {
95
- * id: 'id-for-application',
96
- * name: 'application',
97
- * email: 'application@example.com',
98
- * isAdmin: 'y',
99
- * }
100
- * }
101
- * if (token === 'sys') { //開發階段w-ui-loginout自動給予browser使用者(且位於localhost)的token為sys
102
- * return {
103
- * id: 'id-for-admin',
104
- * name: '測試者',
105
- * email: 'admin@example.com',
106
- * isAdmin: 'y',
107
- * }
108
- * }
109
- * console.log('invalid token', token)
110
- * console.log('於生產環境時得加入SSO等驗證token機制')
111
- * return {}
112
- * }
113
- *
114
- * let verifyClientUser = (user, from) => {
115
- * console.log('verifyClientUser/user', user)
116
- * // return false //測試無法登入
117
- * console.log('於生產環境時得加入限制瀏覽器使用者身份機制')
118
- * return user.isAdmin === 'y' //測試僅系統管理者使用
119
- * }
120
- *
121
- * let verifyAppUser = (user, from) => {
122
- * console.log('verifyAppUser/user', user)
123
- * // return false //測試無法登入
124
- * console.log('於生產環境時得加入限制應用程式使用者身份機制')
125
- * return user.isAdmin === 'y' //測試僅系統管理者使用
126
- * }
127
- *
128
- * //WWebApi
129
- * let instWWebApi = WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt)
130
- *
131
- * instWWebApi.on('error', (err) => {
132
- * console.log(err)
133
- * })
134
- *
135
- */
136
- function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt = {}) {
137
- let instWServHapiServer = null
138
-
139
-
140
- //check WOrm
141
- if (!isfun(WOrm)) {
142
- console.log('invalid WOrm', WOrm)
143
- throw new Error('invalid WOrm')
144
- }
145
-
146
-
147
- //check url
148
- if (!isestr(url)) {
149
- console.log('invalid url', url)
150
- throw new Error('invalid url')
151
- }
152
-
153
-
154
- //check db
155
- if (!isestr(db)) {
156
- console.log('invalid db', db)
157
- throw new Error('invalid db')
158
- }
159
-
160
-
161
- //check getUserByToken
162
- if (!isfun(getUserByToken)) {
163
- console.log('invalid getUserByToken', getUserByToken)
164
- throw new Error('invalid getUserByToken')
165
- }
166
-
167
-
168
- //check verifyClientUser
169
- if (!isfun(verifyClientUser)) {
170
- console.log('invalid verifyClientUser', verifyClientUser)
171
- throw new Error('invalid verifyClientUser')
172
- }
173
-
174
-
175
- //check verifyAppUser
176
- if (!isfun(verifyAppUser)) {
177
- console.log('invalid verifyAppUser', verifyAppUser)
178
- throw new Error('invalid verifyAppUser')
179
- }
180
-
181
-
182
- //serverPort
183
- let serverPort = get(opt, 'serverPort')
184
- if (!ispint(serverPort)) {
185
- serverPort = 11005
186
- }
187
- serverPort = cint(serverPort)
188
-
189
-
190
- //useCheckUser
191
- let useCheckUser = get(opt, 'useCheckUser', false)
192
-
193
-
194
- //getUserById
195
- let getUserById = get(opt, 'getUserById', null)
196
-
197
-
198
- //useExcludeWhenNotAdmin
199
- let useExcludeWhenNotAdmin = get(opt, 'useExcludeWhenNotAdmin', false)
200
-
201
-
202
- //webName
203
- let webName = get(opt, 'webName', {})
204
-
205
-
206
- //webDescription
207
- let webDescription = get(opt, 'webDescription', {})
208
-
209
-
210
- //webLogo
211
- let webLogo = get(opt, 'webLogo', '')
212
-
213
-
214
- //subfolder
215
- let subfolder = get(opt, 'subfolder', '')
216
- if (isestr(subfolder)) {
217
- if (strright(subfolder, 1) === '/') { //右邊不需要給「/」
218
- subfolder = strdelright(subfolder, 1)
219
- }
220
- if (strleft(subfolder, 1) !== '/') { //左邊需要給「/」
221
- subfolder = `/${subfolder}`
222
- }
223
- }
224
-
225
-
226
- //urlRedirect
227
- let urlRedirect = get(opt, 'urlRedirect', '')
228
-
229
-
230
- //params
231
- let showLanguage = get(opt, 'showLanguage', 'y')
232
- let language = get(opt, 'language', 'eng')
233
- let logFd = get(opt, 'logFd', './logs') //srLog 寫入 + staEvent 統計共用之 log 資料夾
234
-
235
-
236
- //kpLangExt
237
- let kpLangExt = get(opt, 'kpLangExt', {})
238
-
239
-
240
- //kpLang
241
- let kpLang = procLang({ kpLangExt, webName, webDescription })
242
-
243
-
244
- //WServOrm
245
- let optWServOrm = {
246
- useCheckUser,
247
- getUserById,
248
- useExcludeWhenNotAdmin,
249
- }
250
- let wp = {}
251
- try {
252
- wp = WServOrm(ds, WOrm, url, db, optWServOrm)
253
- }
254
- catch (err) {
255
- //ORM 為必要依賴:初始化失敗即 fail-fast,不以 wp={} 帶 undefined woItems/procOrm 殘缺啟動(與建構子其餘必要依賴一致)
256
- console.log('WServOrm init failed', err)
257
- throw err
258
- }
259
- let { woItems, procOrm } = wp
260
-
261
-
262
- //srLog(後端 log;錯誤一律 log err-key 供識別,對齊 w-web-sso)
263
- let srLog = srLogInit(opt)
264
-
265
- //maskTok:debug console.log 不印完整 token/authorization(憑證),僅前 4 碼 + ***,避免憑證落入 stdout
266
- let maskTok = (t) => isestr(t) ? (strleft(t, 4) + '***') : t
267
-
268
- //kpFunExt 邊界統一處理:成功記 info(供 staEvent 統計呼叫頻率);catch 後端錯誤 → srLog 該 err-key →
269
- //原樣 re-reject(前端依 key 查自己 lang 顯示)。event 統一加 'kpfun-' 前綴 → 專用事件、與 api/verifyConn 區分。
270
- let wrapErrLog = (event, fn) => {
271
- let ev = 'kpfun-' + event
272
- return async (...args) => {
273
- try {
274
- let result = await fn(...args)
275
- srLog.info({ event: ev }) //成功也記 info
276
- return result
277
- }
278
- catch (err) {
279
- srLog.error({ event: ev, err })
280
- return Promise.reject(err)
281
- }
282
- }
283
- }
284
-
285
-
286
- //procProxy
287
- let { proxyRequest } = procProxy({ srLog })
288
-
289
-
290
- //procApis
291
- let { getApisList, saveApi, deleteApi } = procApis({ woItems, procOrm, ds })
292
-
293
-
294
- //getWebInfor
295
- let getWebInfor = (userId) => {
296
- return {
297
-
298
- // webName, //已併入kpLang
299
- // webDescription, //已併入kpLang
300
- webLogo,
301
-
302
- showLanguage,
303
- language,
304
- kpLang,
305
-
306
- }
307
- }
308
-
309
-
310
- //checkUser
311
- let checkUser = async(user) => {
312
- let id = get(user, 'id', '')
313
- if (!isestr(id)) {
314
- console.log('user', user)
315
- console.log('can not get the userId')
316
- return Promise.reject('errUserIdMissing')
317
- }
318
- let email = get(user, 'email', '')
319
- if (!isestr(email)) {
320
- console.log('user', user)
321
- console.log('can not get the email of user')
322
- return Promise.reject('errUserEmailMissing')
323
- }
324
- let name = get(user, 'name', '')
325
- if (!isestr(name)) {
326
- console.log('user', user)
327
- console.log('can not get userName')
328
- return Promise.reject('errUserNameMissing')
329
- }
330
- let isAdmin = get(user, 'isAdmin', '')
331
- if (isAdmin !== 'y' && isAdmin !== 'n') {
332
- console.log('user', user)
333
- console.log('user.isAdmin is not y or n', user.isAdmin)
334
- console.log('can not get the role of user')
335
- return Promise.reject('errUserRoleMissing')
336
- }
337
- return true
338
- }
339
-
340
-
341
- //getTokenUser
342
- let getTokenUser = async (token) => {
343
-
344
- //getUserByToken
345
- let userSelf = getUserByToken(token)
346
- if (ispm(userSelf)) {
347
- userSelf = await userSelf
348
- }
349
-
350
- //check
351
- await checkUser(userSelf)
352
-
353
- return userSelf
354
- }
355
-
356
-
357
- //p
358
- let p = {
359
- checkToken: async(token) => {
360
-
361
- //getUserByToken
362
- let u = await getUserByToken(token)
363
- // console.log('u', u)
364
-
365
- //checkUser
366
- await checkUser(u)
367
-
368
- return true
369
- },
370
- }
371
-
372
-
373
- //getAndVerifyClientUser
374
- let getAndVerifyClientUser = async (token, from = '') => {
375
- //基於token查找client使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
376
-
377
- //getTokenUser
378
- let user = await getTokenUser(token)
379
-
380
- //verifyClientUser
381
- let b = verifyClientUser(user, from)
382
- if (ispm(b)) {
383
- b = await b
384
- }
385
-
386
- //check
387
- if (!b) {
388
- console.log('user', user)
389
- console.log(`user does not have permission`)
390
- return Promise.reject('errUserNoPermission')
391
- }
392
-
393
- srLog.info({ event: 'verifyClientUser', from, userId: get(user, 'id', '') }) //瀏覽使用者驗證通過
394
- return user
395
- }
396
-
397
-
398
- //getAndVerifyAppUser
399
- let getAndVerifyAppUser = async (token, from = '') => {
400
- //基於token查找app使用者, 會基於外部getUserByToken進行查找, 故只要getUserByToken提供使用者即可, 可支援getUserByToken給予針對應用程式所產生之虛擬使用者
401
-
402
- //user
403
- let user = null
404
- if (isestr(token)) {
405
- user = getUserByToken(token)
406
- if (ispm(user)) {
407
- user = await user
408
- }
409
- }
410
- // console.log('user', user)
411
-
412
- //check
413
- if (!iseobj(user)) {
414
- console.log(`token`, token)
415
- console.log(`can not find the user from token`)
416
- return Promise.reject('errUserNotFound')
417
- }
418
-
419
- //verifyAppUser
420
- let b = verifyAppUser(user, from)
421
- if (ispm(b)) {
422
- b = await b
423
- }
424
-
425
- //check
426
- if (!b) {
427
- console.log('user', user)
428
- console.log(`user does not have permission`)
429
- return Promise.reject('errUserNoPermission')
430
- }
431
-
432
- srLog.info({ event: 'verifyAppUser', from, userId: get(user, 'id', '') }) //應用程式使用者驗證通過
433
- return user
434
- }
435
-
436
-
437
- // //getAPIsList
438
- // let getAPIsList = async (query = {}) => {
439
- // let rs = await woItems.apis.select(query)
440
- // return rs
441
- // }
442
-
443
-
444
- //parsePayload
445
- let parsePayload = async (req) => {
446
-
447
- //payload
448
- let payload = get(req, 'payload')
449
-
450
- //to obj
451
- if (isestr(payload)) {
452
- payload = j2o(payload)
453
- }
454
-
455
- //check
456
- if (!iseobj(payload)) {
457
- console.log('payload', payload)
458
- console.log(`invalid payload in req`)
459
- return Promise.reject('errPayloadInvalid')
460
- }
461
-
462
- //group
463
- let group = get(payload, 'group', '')
464
-
465
- //check
466
- if (!isestr(group)) {
467
- console.log('payload', payload)
468
- console.log(`invalid group in payload`)
469
- return Promise.reject('errGroupInvalid')
470
- }
471
-
472
- //rows
473
- let rows = get(payload, 'rows', [])
474
-
475
- //check
476
- if (!isearr(rows)) {
477
- console.log('payload', payload)
478
- console.log(`invalid rows in payload`)
479
- return Promise.reject('errRowsInvalid')
480
- }
481
-
482
- //r
483
- let r = {
484
- group,
485
- rows,
486
- }
487
-
488
- return r
489
- }
490
-
491
-
492
- // //updateAPIs
493
- // let updateAPIs = async (inp = {}) => {
494
-
495
- // //spread inp
496
- // let { apis: rsApis } = inp
497
- // // console.log('group', group)
498
- // // console.log('rsApis', rsApis)
499
-
500
- // //save
501
- // let r = await woItems.apis.save(rsApis)
502
-
503
- // return r
504
- // }
505
-
506
-
507
- //syncAndReplaceTabs
508
- let syncAndReplaceTabs = async (userId, inp, keyTable) => {
509
-
510
- //group
511
- let group = get(inp, 'group', '')
512
- // console.log('group', group)
513
-
514
- //check
515
- if (!isestr(group)) {
516
- console.log('inp', inp)
517
- console.log('group', group)
518
- console.log(`invalid group`)
519
- return Promise.reject('errGroupInvalid')
520
- }
521
-
522
- //rows
523
- let rows = get(inp, 'rows', [])
524
- // console.log('rows', rows)
525
-
526
- //check
527
- if (!isearr(rows)) {
528
- console.log('inp', inp)
529
- console.log('keyTable', keyTable)
530
- return Promise.reject('errRowsInvalid')
531
- }
532
-
533
- //save from
534
- rows = map(rows, (v, k) => {
535
- // v.order = k + 1 //order大部分由外部給予, 不能複寫
536
- v.group = group
537
- return v
538
- })
539
-
540
- //偵測b64自動恢復
541
- if (keyTable === 'apis') {
542
- each(rows, (row, krow) => {
543
- // console.log(kapi, row.name)
544
- each(row, (v, k) => {
545
- // console.log(k, v)
546
- if (isestr(v)) {
547
- if (strleft(v, 7) === 'base64:') {
548
- // console.log(k, 'get base64')
549
- v = strdelleft(v, 7)
550
- v = b642str(v)
551
- // console.log(v)
552
- rows[krow][k] = v
553
- }
554
- }
555
- })
556
- })
557
- }
558
-
559
- //ltdtmapping
560
- rows = ltdtmapping(rows, ds[keyTable].keys)
561
- // console.log('ltdtmapping rows', rows)
562
-
563
- //delAll group
564
- await woItems[keyTable].delAll({ group })
565
-
566
- //insert
567
- // let r = await woItems[keyTable].insert(rows)
568
- let r = await procOrm(userId, keyTable, 'insert', rows) //須使用procOrm才有辦法自動給予相關欄位
569
- // console.log('r', r)
570
-
571
- return r
572
- }
573
-
574
-
575
- //pathStaticFiles
576
- let pathStaticFiles = 'dist'
577
- let npmPathStaticFiles = './node_modules/w-web-api/dist'
578
- if (fsIsFolder(npmPathStaticFiles)) {
579
- pathStaticFiles = npmPathStaticFiles
580
- }
581
- // console.log('pathStaticFiles', pathStaticFiles)
582
-
583
-
584
- //subfolder
585
- let fnEntryIn = 'index.tmp'
586
- let fnEntryOut = 'index.html'
587
- try {
588
- let fpEntryIn = path.resolve(pathStaticFiles, fnEntryIn)
589
- if (!fsIsFile(fpEntryIn)) {
590
- fpEntryIn = path.resolve(pathStaticFiles, fnEntryOut) //本機開發另使用html替代tmp
591
- }
592
- if (!fsIsFile(fpEntryIn)) {
593
- console.log('fpEntryIn', fpEntryIn)
594
- throw new Error(`invalid fpEntryIn`)
595
- }
596
- let fpEntryOut = path.resolve(pathStaticFiles, fnEntryOut)
597
- let c = fs.readFileSync(fpEntryIn, 'utf8')
598
- c = replace(c, '/mapi/', '{sfd}/') //方法同genEntry
599
- c = replace(c, '{sfd}', subfolder)
600
- c = replace(c, '{urlRedirect}', urlRedirect)
601
- c = replace(c, '{language}', language) //注入初始語系 → window.___pmwperm___.language(前端 getLang 之來源之一)
602
- fs.writeFileSync(fpEntryOut, c, 'utf8')
603
- }
604
- catch (err) {
605
- console.log(err)
606
- console.log(`can not generate ${fnEntryOut}`)
607
- }
608
-
609
-
610
- //apis
611
- let apis = [
612
-
613
- {
614
- method: 'GET',
615
- path: '/api/getUserByToken', //未登入主頁時需先檢測token, getUserByToken為w-ui-loginout預設值, 若要更改兩邊須同時修改
616
- handler: async function (req, res) {
617
- // console.log('getUserByToken', req)
618
-
619
- async function core() {
620
-
621
- //token
622
- let token = get(req, 'query.token', '')
623
-
624
- //check
625
- if (!isestr(token)) {
626
- console.log('req.query', get(req, 'query'))
627
- console.log('token', maskTok(token))
628
- console.log('[API]getUserByToken/check token: invalid token')
629
- return Promise.reject('errTokenNoPermission')
630
- }
631
-
632
- //getAndVerifyClientUser
633
- let user = await getAndVerifyClientUser(token, 'getUserByToken')
634
-
635
- //check
636
- if (!iseobj(user)) {
637
- console.log('token', maskTok(token))
638
- console.log('[API]getUserByToken/check user: invalid user')
639
- return Promise.reject('errTokenNoPermission')
640
- }
641
-
642
- srLog.info({ event: 'api/getUserByToken', userId: get(user, 'id', '') })
643
- return user
644
- }
645
-
646
- //pm2resolve core
647
- let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 會提取state進行判斷
648
- if (isErr(r.msg)) {
649
- r.msg = r.msg.message
650
- }
651
- // console.log('getUserByToken', r)
652
-
653
- return r
654
- },
655
- },
656
-
657
- // {
658
- // method: 'GET',
659
- // path: '/getAPIsList',
660
- // handler: async function (req, res) {
661
- // // console.log('getAPIsList', req)
662
-
663
- // async function core() {
664
-
665
- // //token
666
- // let token = get(req, 'query.token', '')
667
-
668
- // //check
669
- // if (!isestr(token)) {
670
- // console.log('req.query', get(req, 'query'))
671
- // console.log('token', token)
672
- // console.log('[API]getAPIsList/check token: invalid token')
673
- // return Promise.reject('errTokenNoPermission')
674
- // }
675
-
676
- // //getAndVerifyClientUser
677
- // let user = await getAndVerifyClientUser(token, 'getAPIsList')
678
-
679
- // //check
680
- // if (!iseobj(user)) {
681
- // console.log('token', token)
682
- // console.log('[API]getAPIsList/check user: invalid user')
683
- // return Promise.reject('errTokenNoPermission')
684
- // }
685
-
686
- // //getAPIsList
687
- // let r = await getAPIsList({ isActive: 'y' })
688
-
689
- // return r
690
- // }
691
-
692
- // //pm2resolve core
693
- // let r = await pm2resolve(core)()
694
- // if (isErr(r.msg)) {
695
- // r.msg = r.msg.message
696
- // }
697
- // // console.log('getAPIsList', r)
698
-
699
- // return r
700
- // },
701
- // },
702
-
703
- // {
704
- // method: 'POST',
705
- // path: '/updateAPIs',
706
- // handler: async function (req, res) {
707
- // // console.log('updateAPIs', req)
708
- // // console.log('payload', req.payload)
709
-
710
- // async function core() {
711
-
712
- // //token
713
- // let token = get(req, 'query.token', '')
714
-
715
- // //check
716
- // if (!isestr(token)) {
717
- // console.log('req.query', get(req, 'query'))
718
- // console.log('token', token)
719
- // console.log('[API]updateAPIs/check token: invalid token')
720
- // return Promise.reject('errTokenNoPermission')
721
- // }
722
-
723
- // //getAndVerifyClientUser
724
- // let user = await getAndVerifyClientUser(token, 'updateAPIs')
725
-
726
- // //check
727
- // if (!iseobj(user)) {
728
- // console.log('token', token)
729
- // console.log('[API]updateAPIs/check user: invalid user')
730
- // return Promise.reject('errTokenNoPermission')
731
- // }
732
-
733
- // //parsePayload
734
- // let inp = await parsePayload(req)
735
-
736
- // //updateAPIs
737
- // let r = await updateAPIs(inp)
738
-
739
- // return r
740
- // }
741
-
742
- // //pm2resolve core
743
- // let r = await pm2resolve(core)()
744
- // if (isErr(r.msg)) {
745
- // r.msg = r.msg.message
746
- // }
747
- // // console.log('updateAPIs', r)
748
-
749
- // return r
750
- // },
751
- // },
752
-
753
- {
754
- method: 'POST',
755
- path: '/syncAndReplaceTabs',
756
- handler: async function (req, res) {
757
- // console.log('syncAndReplaceTabs', req)
758
- // console.log('payload', req.payload)
759
-
760
- async function core() {
761
-
762
- //token
763
- let token = get(req, 'query.token', '')
764
-
765
- //check
766
- if (!isestr(token)) {
767
- console.log('req.query', get(req, 'query'))
768
- console.log('token', maskTok(token))
769
- console.log('[API]syncAndReplaceTabs/check token: invalid token')
770
- return Promise.reject('errTokenNoPermission')
771
- }
772
-
773
- //keyTable
774
- let keyTable = get(req, 'query.keyTable', '')
775
-
776
- //check
777
- if (!isestr(keyTable)) {
778
- console.log('req.query', get(req, 'query'))
779
- console.log('keyTable', keyTable)
780
- console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
781
- return Promise.reject('errKeyTableInvalid')
782
- }
783
-
784
- //check keyTable
785
- let allowedTables = ['apis']
786
- if (!allowedTables.includes(keyTable)) {
787
- console.log('[API]syncAndReplaceTabs/check keyTable: unexpected keyTable', keyTable)
788
- return Promise.reject('errKeyTableInvalid')
789
- }
790
-
791
- //getAndVerifyAppUser
792
- let user = await getAndVerifyAppUser(token, 'syncAndReplaceTabs')
793
-
794
- //check
795
- if (!iseobj(user)) {
796
- console.log('token', maskTok(token))
797
- console.log('[API]syncAndReplaceTabs/check user: invalid user')
798
- return Promise.reject('errTokenNoPermission')
799
- }
800
-
801
- //userId
802
- let userId = get(user, 'id', '')
803
-
804
- //check userId
805
- if (!isestr(userId)) {
806
- console.log('token', maskTok(token))
807
- console.log('user', user)
808
- console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
809
- return Promise.reject('errTokenNoPermission')
810
- }
811
-
812
- //parsePayload
813
- let inp = await parsePayload(req)
814
-
815
- //syncAndReplaceTabs
816
- let r = await syncAndReplaceTabs(userId, inp, keyTable)
817
-
818
- srLog.info({ event: 'api/syncAndReplaceTabs', userId, keyTable })
819
- return r
820
- }
821
-
822
- //pm2resolve core
823
- let r = await pm2resolve(core)()
824
- if (isErr(r.msg)) {
825
- r.msg = r.msg.message
826
- }
827
- // console.log('syncAndReplaceTabs', r)
828
-
829
- return r
830
- },
831
- },
832
-
833
- ]
834
-
835
-
836
- //tableNamesExec, tableNamesSync
837
- let tableNamesExec = keys(ds)
838
- // let tableNamesSync = filter(tableNamesExec, (v) => {
839
- // return strright(v, 5) !== 'Items'//不同步數據
840
- // })
841
- let tableNamesSync = tableNamesExec
842
-
843
-
844
- //WServHapiServer
845
- instWServHapiServer = new WServHapiServer({
846
- port: serverPort,
847
- pathStaticFiles,
848
- apiName: 'api',
849
- apis,
850
- tokenType: 'Bearer',
851
- verifyConn: async ({ apiType, authorization, query, headers, req }) => {
852
- // console.log('verifyConn', `apiType[${apiType}]`, `authorization[${authorization}]`)
853
-
854
- //token
855
- let token = strdelleft(authorization, 7) //刪除Bearer
856
- // console.log('token', token)
857
-
858
- //check
859
- if (!isestr(token)) {
860
- console.log('apiType', apiType)
861
- console.log('authorization', maskTok(authorization))
862
- console.log('invalid token')
863
- return false
864
- }
865
-
866
- //checkToken:套件層連線閘僅做「token 認證」(解析 user + 驗欄位齊備)。**授權**(如限管理員)屬
867
- //套件使用者之責——由其提供之 getUserByToken(可對非授權者回 {})/ verifyClientUser / verifyAppUser,
868
- //或 WServOrm 之 useCheckUser/useExcludeWhenNotAdmin 等鉤子決定;套件不對連線層強加特定授權政策(避免越界、
869
- //保留使用者彈性)。詳見 spec/設計要點與取捨.md「套件授權責任」。
870
- let b = await p.checkToken(token)
871
-
872
- srLog.info({ event: 'verifyConn', apiType }) //連線之 token 認證通過(checkToken 失敗會 throw 不到此)
873
- return b
874
- },
875
- getUserIdByToken: async (token) => { //可使用async或sync函數
876
- // console.log('getUserIdByToken', token)
877
- if (!isestr(token)) {
878
- return ''
879
- }
880
- let user = await getUserByToken(token)
881
- let userId = get(user, 'id', '')
882
- return userId
883
- },
884
- useDbOrm: true,
885
- kpOrm: woItems,
886
- operOrm: procOrm, //procOrm的輸入為: userId, tableName, methodName, input
887
- tableNamesExec,
888
- methodsExec: ['select', 'insert', 'save', 'del'], //mix需於procOrm內註冊以提供
889
- tableNamesSync,
890
- kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數(皆經 wrapErrLog:錯誤 srLog err-key 後 re-reject)
891
-
892
- getWebInfor: wrapErrLog('getWebInfor', getWebInfor),
893
-
894
- proxyRequest: wrapErrLog('proxyRequest', (userId, reqSpec) => {
895
- return proxyRequest(reqSpec)
896
- }),
897
-
898
- getApisList: wrapErrLog('getApisList', (userId) => {
899
- return getApisList(userId)
900
- }),
901
- saveApi: wrapErrLog('saveApi', (userId, row) => {
902
- return saveApi(userId, row)
903
- }),
904
- deleteApi: wrapErrLog('deleteApi', (userId, id) => {
905
- return deleteApi(userId, id)
906
- }),
907
-
908
- //統計資訊頁:回傳各 event 之時段發生頻率(讀 logFd 之 log)。連線之 token 認證由 verifyConn 把關;
909
- //授權(限管理員)屬套件使用者責任(見 verifyConn 註解與設計要點與取捨「套件授權責任」)。timeLength 天數 / timeInterval 'hr'|'day'。
910
- getStaEvent: wrapErrLog('getStaEvent', (userId, timeLength = 7, timeInterval = 'hr') => {
911
- return staEvent(timeLength, timeInterval, { fdLog: logFd })
912
- }),
913
-
914
- // getAPIsList: (userId, query = {}) => { //待提供api管理系統界面使用
915
- // return getAPIsList(query)
916
- // },
917
- // updateApis: (userId, rows) => { //待提供api管理系統界面使用
918
- // return updateApis(rows)
919
- // },
920
-
921
- },
922
- fnTableTags: 'tableTags-web-api.json',
923
- })
924
-
925
-
926
- return instWServHapiServer
927
- }
928
-
929
-
930
- export default WWebApi
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import get from 'lodash-es/get.js'
4
+ import each from 'lodash-es/each.js'
5
+ import map from 'lodash-es/map.js'
6
+ import keys from 'lodash-es/keys.js'
7
+ import j2o from 'wsemi/src/j2o.mjs'
8
+ import iseobj from 'wsemi/src/iseobj.mjs'
9
+ import isestr from 'wsemi/src/isestr.mjs'
10
+ import isearr from 'wsemi/src/isearr.mjs'
11
+ import ispint from 'wsemi/src/ispint.mjs'
12
+ import isErr from 'wsemi/src/isErr.mjs'
13
+ import isfun from 'wsemi/src/isfun.mjs'
14
+ import ispm from 'wsemi/src/ispm.mjs'
15
+ import cint from 'wsemi/src/cint.mjs'
16
+ import strleft from 'wsemi/src/strleft.mjs'
17
+ import strdelleft from 'wsemi/src/strdelleft.mjs'
18
+ import strright from 'wsemi/src/strright.mjs'
19
+ import strdelright from 'wsemi/src/strdelright.mjs'
20
+ import pm2resolve from 'wsemi/src/pm2resolve.mjs'
21
+ import b642str from 'wsemi/src/b642str.mjs'
22
+ import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
23
+ import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
24
+ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
25
+ import replace from 'wsemi/src/replace.mjs'
26
+ import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
27
+ import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
28
+ import procLang from './procLang.mjs'
29
+ import procProxy from './procProxy.mjs'
30
+ import procApis from './procApis.mjs'
31
+ import staEvent from './staLogs/staEvent.mjs'
32
+ import srLogInit from './srLog.mjs'
33
+ import ds from '../src/schema/index.mjs'
34
+
35
+
36
+ /**
37
+ * API伺服器
38
+ *
39
+ * @class
40
+ * @param {Function} WOrm 輸入資料庫ORM函數
41
+ * @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
42
+ * @param {String} db 輸入資料庫名稱字串
43
+ * @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n',且輸入'y'時會複寫權限系統該使用者之'isAdmin'欄位值
44
+ * @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
45
+ * @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
46
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
47
+ * @param {Integer} [opt.serverPort=11005] 輸入伺服器通訊port,預設11005
48
+ * @param {Boolean} [opt.useCheckUser=false] 輸入是否檢查使用者資訊布林值,預設false
49
+ * @param {Function} [opt.getUserById=null] 輸入當useCheckUser=true時依照使用者ID取得使用者資訊物件函數,預設null
50
+ * @param {Boolean} [opt.useExcludeWhenNotAdmin=false] 輸入使用ORM的select方法時是否自動刪除數據內isActive欄位之布林值,預設false
51
+ * @param {Object} [opt.webName={}] 輸入站台名稱物件,至少包含語系eng與cht鍵的名稱,預設{}
52
+ * @param {Object} [opt.webDescription={}] 輸入站台描述物件,至少包含語系eng與cht鍵的名稱,預設{}
53
+ * @param {String} [opt.webLogo=''] 輸入站台logo字串,採base64格式,預設''
54
+ * @param {String} [opt.subfolder=''] 輸入站台所在子目錄字串,提供站台位於內網採反向代理進行服務時,故需支援位於子目錄情形,預設''
55
+ * @param {String} [opt.urlRedirect=''] 輸入錯誤時自動轉址字串,提供站台例如無法登入或驗證失敗時須自動轉址,預設''
56
+ * @returns {Object} 回傳物件,其內server為hapi伺服器實體,wsrv為w-converhp的伺服器事件物件,wsds為w-serv-webdata的伺服器事件物件,可監聽error事件
57
+ * @example
58
+ *
59
+ * import WOrm from 'w-orm-lmdb/src/WOrmLmdb.mjs'
60
+ * import WWebApi from './server/WWebApi.mjs'
61
+ * import getSettings from './g_getSettings.mjs'
62
+ *
63
+ *
64
+ * //st
65
+ * let st = getSettings()
66
+ *
67
+ * let url = st.dbUrl
68
+ * let db = st.dbName
69
+ * let opt = {
70
+ *
71
+ * useCheckUser: false,
72
+ * getUserById: null,
73
+ * useExcludeWhenNotAdmin: false,
74
+ *
75
+ * serverPort: 11005,
76
+ * subfolder: '', //mapi
77
+ * urlRedirect: 'https://www.google.com/', //本機測試時得先編譯, 再瀏覽: http://localhost:11005/
78
+ *
79
+ * webName: {
80
+ * 'eng': 'API Service',
81
+ * 'cht': 'API管理系統',
82
+ * },
83
+ * webDescription: {
84
+ * 'eng': 'A web service package as methods to send requests to and receive responses from an API.',
85
+ * 'cht': 'A web service package as methods to send requests to and receive responses from an API.',
86
+ * },
87
+ * webLogo: 'data:image/svg+xml;base64,...',
88
+ *
89
+ * }
90
+ *
91
+ * let getUserByToken = async (token) => {
92
+ * // return {} //測試無法登入
93
+ * if (token === '{token-for-application}') { //提供外部應用系統作為存取使用者
94
+ * return {
95
+ * id: 'id-for-application',
96
+ * name: 'application',
97
+ * email: 'application@example.com',
98
+ * isAdmin: 'y',
99
+ * }
100
+ * }
101
+ * if (token === 'sys') { //開發階段w-ui-loginout自動給予browser使用者(且位於localhost)的token為sys
102
+ * return {
103
+ * id: 'id-for-admin',
104
+ * name: '測試者',
105
+ * email: 'admin@example.com',
106
+ * isAdmin: 'y',
107
+ * }
108
+ * }
109
+ * console.log('invalid token', token)
110
+ * console.log('於生產環境時得加入SSO等驗證token機制')
111
+ * return {}
112
+ * }
113
+ *
114
+ * let verifyClientUser = (user, from) => {
115
+ * console.log('verifyClientUser/user', user)
116
+ * // return false //測試無法登入
117
+ * console.log('於生產環境時得加入限制瀏覽器使用者身份機制')
118
+ * return user.isAdmin === 'y' //測試僅系統管理者使用
119
+ * }
120
+ *
121
+ * let verifyAppUser = (user, from) => {
122
+ * console.log('verifyAppUser/user', user)
123
+ * // return false //測試無法登入
124
+ * console.log('於生產環境時得加入限制應用程式使用者身份機制')
125
+ * return user.isAdmin === 'y' //測試僅系統管理者使用
126
+ * }
127
+ *
128
+ * //WWebApi
129
+ * let instWWebApi = WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt)
130
+ *
131
+ * instWWebApi.on('error', (err) => {
132
+ * console.log(err)
133
+ * })
134
+ *
135
+ */
136
+ function WWebApi(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt = {}) {
137
+ let instWServHapiServer = null
138
+
139
+
140
+ //check WOrm
141
+ if (!isfun(WOrm)) {
142
+ console.log('invalid WOrm', WOrm)
143
+ throw new Error('invalid WOrm')
144
+ }
145
+
146
+
147
+ //check url
148
+ if (!isestr(url)) {
149
+ console.log('invalid url', url)
150
+ throw new Error('invalid url')
151
+ }
152
+
153
+
154
+ //check db
155
+ if (!isestr(db)) {
156
+ console.log('invalid db', db)
157
+ throw new Error('invalid db')
158
+ }
159
+
160
+
161
+ //check getUserByToken
162
+ if (!isfun(getUserByToken)) {
163
+ console.log('invalid getUserByToken', getUserByToken)
164
+ throw new Error('invalid getUserByToken')
165
+ }
166
+
167
+
168
+ //check verifyClientUser
169
+ if (!isfun(verifyClientUser)) {
170
+ console.log('invalid verifyClientUser', verifyClientUser)
171
+ throw new Error('invalid verifyClientUser')
172
+ }
173
+
174
+
175
+ //check verifyAppUser
176
+ if (!isfun(verifyAppUser)) {
177
+ console.log('invalid verifyAppUser', verifyAppUser)
178
+ throw new Error('invalid verifyAppUser')
179
+ }
180
+
181
+
182
+ //serverPort
183
+ let serverPort = get(opt, 'serverPort')
184
+ if (!ispint(serverPort)) {
185
+ serverPort = 11005
186
+ }
187
+ serverPort = cint(serverPort)
188
+
189
+
190
+ //useCheckUser
191
+ let useCheckUser = get(opt, 'useCheckUser', false)
192
+
193
+
194
+ //getUserById
195
+ let getUserById = get(opt, 'getUserById', null)
196
+
197
+
198
+ //useExcludeWhenNotAdmin
199
+ let useExcludeWhenNotAdmin = get(opt, 'useExcludeWhenNotAdmin', false)
200
+
201
+
202
+ //webName
203
+ let webName = get(opt, 'webName', {})
204
+
205
+
206
+ //webDescription
207
+ let webDescription = get(opt, 'webDescription', {})
208
+
209
+
210
+ //webLogo
211
+ let webLogo = get(opt, 'webLogo', '')
212
+
213
+
214
+ //subfolder
215
+ let subfolder = get(opt, 'subfolder', '')
216
+ if (isestr(subfolder)) {
217
+ if (strright(subfolder, 1) === '/') { //右邊不需要給「/」
218
+ subfolder = strdelright(subfolder, 1)
219
+ }
220
+ if (strleft(subfolder, 1) !== '/') { //左邊需要給「/」
221
+ subfolder = `/${subfolder}`
222
+ }
223
+ }
224
+
225
+
226
+ //urlRedirect
227
+ let urlRedirect = get(opt, 'urlRedirect', '')
228
+
229
+
230
+ //params
231
+ let showLanguage = get(opt, 'showLanguage', 'y')
232
+ let language = get(opt, 'language', 'eng')
233
+ let logFd = get(opt, 'logFd', './logs') //srLog 寫入 + staEvent 統計共用之 log 資料夾
234
+
235
+
236
+ //kpLangExt
237
+ let kpLangExt = get(opt, 'kpLangExt', {})
238
+
239
+
240
+ //kpLang
241
+ let kpLang = procLang({ kpLangExt, webName, webDescription })
242
+
243
+
244
+ //WServOrm
245
+ let optWServOrm = {
246
+ useCheckUser,
247
+ getUserById,
248
+ useExcludeWhenNotAdmin,
249
+ }
250
+ let wp = {}
251
+ try {
252
+ wp = WServOrm(ds, WOrm, url, db, optWServOrm)
253
+ }
254
+ catch (err) {
255
+ //ORM 為必要依賴:初始化失敗即 fail-fast,不以 wp={} 帶 undefined woItems/procOrm 殘缺啟動(與建構子其餘必要依賴一致)
256
+ console.log('WServOrm init failed', err)
257
+ throw err
258
+ }
259
+ let { woItems, procOrm } = wp
260
+
261
+
262
+ //srLog(後端 log;錯誤一律 log err-key 供識別,對齊 w-web-sso)
263
+ let srLog = srLogInit(opt)
264
+
265
+ //maskTok:debug console.log 不印完整 token/authorization(憑證),僅前 4 碼 + ***,避免憑證落入 stdout
266
+ let maskTok = (t) => isestr(t) ? (strleft(t, 4) + '***') : t
267
+
268
+ //kpFunExt 邊界統一處理:成功記 info(供 staEvent 統計呼叫頻率);catch 後端錯誤 → srLog 該 err-key →
269
+ //原樣 re-reject(前端依 key 查自己 lang 顯示)。event 統一加 'kpfun-' 前綴 → 專用事件、與 api/verifyConn 區分。
270
+ let wrapErrLog = (event, fn) => {
271
+ let ev = 'kpfun-' + event
272
+ return async (...args) => {
273
+ try {
274
+ let result = await fn(...args)
275
+ srLog.info({ event: ev }) //成功也記 info
276
+ return result
277
+ }
278
+ catch (err) {
279
+ srLog.error({ event: ev, err })
280
+ return Promise.reject(err)
281
+ }
282
+ }
283
+ }
284
+
285
+
286
+ //procProxy
287
+ let { proxyRequest } = procProxy({ srLog })
288
+
289
+
290
+ //procApis
291
+ let { getApisList, saveApi, deleteApi } = procApis({ woItems, procOrm, ds })
292
+
293
+
294
+ //getWebInfor
295
+ let getWebInfor = (userId) => {
296
+ return {
297
+
298
+ // webName, //已併入kpLang
299
+ // webDescription, //已併入kpLang
300
+ webLogo,
301
+
302
+ showLanguage,
303
+ language,
304
+ kpLang,
305
+
306
+ }
307
+ }
308
+
309
+
310
+ //checkUser
311
+ let checkUser = async(user) => {
312
+ let id = get(user, 'id', '')
313
+ if (!isestr(id)) {
314
+ console.log('user', user)
315
+ console.log('can not get the userId')
316
+ return Promise.reject('errUserIdMissing')
317
+ }
318
+ let email = get(user, 'email', '')
319
+ if (!isestr(email)) {
320
+ console.log('user', user)
321
+ console.log('can not get the email of user')
322
+ return Promise.reject('errUserEmailMissing')
323
+ }
324
+ let name = get(user, 'name', '')
325
+ if (!isestr(name)) {
326
+ console.log('user', user)
327
+ console.log('can not get userName')
328
+ return Promise.reject('errUserNameMissing')
329
+ }
330
+ let isAdmin = get(user, 'isAdmin', '')
331
+ if (isAdmin !== 'y' && isAdmin !== 'n') {
332
+ console.log('user', user)
333
+ console.log('user.isAdmin is not y or n', user.isAdmin)
334
+ console.log('can not get the role of user')
335
+ return Promise.reject('errUserRoleMissing')
336
+ }
337
+ return true
338
+ }
339
+
340
+
341
+ //getTokenUser
342
+ let getTokenUser = async (token) => {
343
+
344
+ //getUserByToken
345
+ let userSelf = getUserByToken(token)
346
+ if (ispm(userSelf)) {
347
+ userSelf = await userSelf
348
+ }
349
+
350
+ //check
351
+ await checkUser(userSelf)
352
+
353
+ return userSelf
354
+ }
355
+
356
+
357
+ //p
358
+ let p = {
359
+ checkToken: async(token) => {
360
+
361
+ //getUserByToken
362
+ let u = await getUserByToken(token)
363
+ // console.log('u', u)
364
+
365
+ //checkUser
366
+ await checkUser(u)
367
+
368
+ return true
369
+ },
370
+ }
371
+
372
+
373
+ //getAndVerifyClientUser
374
+ let getAndVerifyClientUser = async (token, from = '') => {
375
+ //基於token查找client使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
376
+
377
+ //getTokenUser
378
+ let user = await getTokenUser(token)
379
+
380
+ //verifyClientUser
381
+ let b = verifyClientUser(user, from)
382
+ if (ispm(b)) {
383
+ b = await b
384
+ }
385
+
386
+ //check
387
+ if (!b) {
388
+ console.log('user', user)
389
+ console.log(`user does not have permission`)
390
+ return Promise.reject('errUserNoPermission')
391
+ }
392
+
393
+ srLog.info({ event: 'verifyClientUser', from, userId: get(user, 'id', '') }) //瀏覽使用者驗證通過
394
+ return user
395
+ }
396
+
397
+
398
+ //getAndVerifyAppUser
399
+ let getAndVerifyAppUser = async (token, from = '') => {
400
+ //基於token查找app使用者, 會基於外部getUserByToken進行查找, 故只要getUserByToken提供使用者即可, 可支援getUserByToken給予針對應用程式所產生之虛擬使用者
401
+
402
+ //user
403
+ let user = null
404
+ if (isestr(token)) {
405
+ user = getUserByToken(token)
406
+ if (ispm(user)) {
407
+ user = await user
408
+ }
409
+ }
410
+ // console.log('user', user)
411
+
412
+ //check
413
+ if (!iseobj(user)) {
414
+ console.log(`token`, token)
415
+ console.log(`can not find the user from token`)
416
+ return Promise.reject('errUserNotFound')
417
+ }
418
+
419
+ //verifyAppUser
420
+ let b = verifyAppUser(user, from)
421
+ if (ispm(b)) {
422
+ b = await b
423
+ }
424
+
425
+ //check
426
+ if (!b) {
427
+ console.log('user', user)
428
+ console.log(`user does not have permission`)
429
+ return Promise.reject('errUserNoPermission')
430
+ }
431
+
432
+ srLog.info({ event: 'verifyAppUser', from, userId: get(user, 'id', '') }) //應用程式使用者驗證通過
433
+ return user
434
+ }
435
+
436
+
437
+ // //getAPIsList
438
+ // let getAPIsList = async (query = {}) => {
439
+ // let rs = await woItems.apis.select(query)
440
+ // return rs
441
+ // }
442
+
443
+
444
+ //parsePayload
445
+ let parsePayload = async (req) => {
446
+
447
+ //payload
448
+ let payload = get(req, 'payload')
449
+
450
+ //to obj
451
+ if (isestr(payload)) {
452
+ payload = j2o(payload)
453
+ }
454
+
455
+ //check
456
+ if (!iseobj(payload)) {
457
+ console.log('payload', payload)
458
+ console.log(`invalid payload in req`)
459
+ return Promise.reject('errPayloadInvalid')
460
+ }
461
+
462
+ //group
463
+ let group = get(payload, 'group', '')
464
+
465
+ //check
466
+ if (!isestr(group)) {
467
+ console.log('payload', payload)
468
+ console.log(`invalid group in payload`)
469
+ return Promise.reject('errGroupInvalid')
470
+ }
471
+
472
+ //rows
473
+ let rows = get(payload, 'rows', [])
474
+
475
+ //check
476
+ if (!isearr(rows)) {
477
+ console.log('payload', payload)
478
+ console.log(`invalid rows in payload`)
479
+ return Promise.reject('errRowsInvalid')
480
+ }
481
+
482
+ //r
483
+ let r = {
484
+ group,
485
+ rows,
486
+ }
487
+
488
+ return r
489
+ }
490
+
491
+
492
+ // //updateAPIs
493
+ // let updateAPIs = async (inp = {}) => {
494
+
495
+ // //spread inp
496
+ // let { apis: rsApis } = inp
497
+ // // console.log('group', group)
498
+ // // console.log('rsApis', rsApis)
499
+
500
+ // //save
501
+ // let r = await woItems.apis.save(rsApis)
502
+
503
+ // return r
504
+ // }
505
+
506
+
507
+ //syncAndReplaceTabs
508
+ let syncAndReplaceTabs = async (userId, inp, keyTable) => {
509
+
510
+ //group
511
+ let group = get(inp, 'group', '')
512
+ // console.log('group', group)
513
+
514
+ //check
515
+ if (!isestr(group)) {
516
+ console.log('inp', inp)
517
+ console.log('group', group)
518
+ console.log(`invalid group`)
519
+ return Promise.reject('errGroupInvalid')
520
+ }
521
+
522
+ //rows
523
+ let rows = get(inp, 'rows', [])
524
+ // console.log('rows', rows)
525
+
526
+ //check
527
+ if (!isearr(rows)) {
528
+ console.log('inp', inp)
529
+ console.log('keyTable', keyTable)
530
+ return Promise.reject('errRowsInvalid')
531
+ }
532
+
533
+ //save from
534
+ rows = map(rows, (v, k) => {
535
+ // v.order = k + 1 //order大部分由外部給予, 不能複寫
536
+ v.group = group
537
+ return v
538
+ })
539
+
540
+ //偵測b64自動恢復
541
+ if (keyTable === 'apis') {
542
+ each(rows, (row, krow) => {
543
+ // console.log(kapi, row.name)
544
+ each(row, (v, k) => {
545
+ // console.log(k, v)
546
+ if (isestr(v)) {
547
+ if (strleft(v, 7) === 'base64:') {
548
+ // console.log(k, 'get base64')
549
+ v = strdelleft(v, 7)
550
+ v = b642str(v)
551
+ // console.log(v)
552
+ rows[krow][k] = v
553
+ }
554
+ }
555
+ })
556
+ })
557
+ }
558
+
559
+ //ltdtmapping
560
+ rows = ltdtmapping(rows, ds[keyTable].keys)
561
+ // console.log('ltdtmapping rows', rows)
562
+
563
+ //delAll group
564
+ await woItems[keyTable].delAll({ group })
565
+
566
+ //insert
567
+ // let r = await woItems[keyTable].insert(rows)
568
+ let r = await procOrm(userId, keyTable, 'insert', rows) //須使用procOrm才有辦法自動給予相關欄位
569
+ // console.log('r', r)
570
+
571
+ return r
572
+ }
573
+
574
+
575
+ //pathStaticFiles
576
+ let pathStaticFiles = 'dist'
577
+ let npmPathStaticFiles = './node_modules/w-web-api/dist'
578
+ if (fsIsFolder(npmPathStaticFiles)) {
579
+ pathStaticFiles = npmPathStaticFiles
580
+ }
581
+ // console.log('pathStaticFiles', pathStaticFiles)
582
+
583
+
584
+ //subfolder
585
+ let fnEntryIn = 'index.tmp'
586
+ let fnEntryOut = 'index.html'
587
+ try {
588
+ let fpEntryIn = path.resolve(pathStaticFiles, fnEntryIn)
589
+ if (!fsIsFile(fpEntryIn)) {
590
+ fpEntryIn = path.resolve(pathStaticFiles, fnEntryOut) //本機開發另使用html替代tmp
591
+ }
592
+ if (!fsIsFile(fpEntryIn)) {
593
+ console.log('fpEntryIn', fpEntryIn)
594
+ throw new Error(`invalid fpEntryIn`)
595
+ }
596
+ let fpEntryOut = path.resolve(pathStaticFiles, fnEntryOut)
597
+ let c = fs.readFileSync(fpEntryIn, 'utf8')
598
+ c = replace(c, '/mapi/', '{sfd}/') //方法同genEntry
599
+ c = replace(c, '{sfd}', subfolder)
600
+ c = replace(c, '{urlRedirect}', urlRedirect)
601
+ c = replace(c, '{language}', language) //注入初始語系 → window.___pmwperm___.language(前端 getLang 之來源之一)
602
+ fs.writeFileSync(fpEntryOut, c, 'utf8')
603
+ }
604
+ catch (err) {
605
+ console.log(err)
606
+ console.log(`can not generate ${fnEntryOut}`)
607
+ }
608
+
609
+
610
+ //apis
611
+ let apis = [
612
+
613
+ {
614
+ method: 'GET',
615
+ path: '/api/getUserByToken', //未登入主頁時需先檢測token, getUserByToken為w-ui-loginout預設值, 若要更改兩邊須同時修改
616
+ handler: async function (req, res) {
617
+ // console.log('getUserByToken', req)
618
+
619
+ async function core() {
620
+
621
+ //token
622
+ let token = get(req, 'query.token', '')
623
+
624
+ //check
625
+ if (!isestr(token)) {
626
+ console.log('req.query', get(req, 'query'))
627
+ console.log('token', maskTok(token))
628
+ console.log('[API]getUserByToken/check token: invalid token')
629
+ return Promise.reject('errTokenNoPermission')
630
+ }
631
+
632
+ //getAndVerifyClientUser
633
+ let user = await getAndVerifyClientUser(token, 'getUserByToken')
634
+
635
+ //check
636
+ if (!iseobj(user)) {
637
+ console.log('token', maskTok(token))
638
+ console.log('[API]getUserByToken/check user: invalid user')
639
+ return Promise.reject('errTokenNoPermission')
640
+ }
641
+
642
+ srLog.info({ event: 'api/getUserByToken', userId: get(user, 'id', '') })
643
+ return user
644
+ }
645
+
646
+ //pm2resolve core
647
+ let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 會提取state進行判斷
648
+ if (isErr(r.msg)) {
649
+ r.msg = r.msg.message
650
+ }
651
+ // console.log('getUserByToken', r)
652
+
653
+ return r
654
+ },
655
+ },
656
+
657
+ // {
658
+ // method: 'GET',
659
+ // path: '/getAPIsList',
660
+ // handler: async function (req, res) {
661
+ // // console.log('getAPIsList', req)
662
+
663
+ // async function core() {
664
+
665
+ // //token
666
+ // let token = get(req, 'query.token', '')
667
+
668
+ // //check
669
+ // if (!isestr(token)) {
670
+ // console.log('req.query', get(req, 'query'))
671
+ // console.log('token', token)
672
+ // console.log('[API]getAPIsList/check token: invalid token')
673
+ // return Promise.reject('errTokenNoPermission')
674
+ // }
675
+
676
+ // //getAndVerifyClientUser
677
+ // let user = await getAndVerifyClientUser(token, 'getAPIsList')
678
+
679
+ // //check
680
+ // if (!iseobj(user)) {
681
+ // console.log('token', token)
682
+ // console.log('[API]getAPIsList/check user: invalid user')
683
+ // return Promise.reject('errTokenNoPermission')
684
+ // }
685
+
686
+ // //getAPIsList
687
+ // let r = await getAPIsList({ isActive: 'y' })
688
+
689
+ // return r
690
+ // }
691
+
692
+ // //pm2resolve core
693
+ // let r = await pm2resolve(core)()
694
+ // if (isErr(r.msg)) {
695
+ // r.msg = r.msg.message
696
+ // }
697
+ // // console.log('getAPIsList', r)
698
+
699
+ // return r
700
+ // },
701
+ // },
702
+
703
+ // {
704
+ // method: 'POST',
705
+ // path: '/updateAPIs',
706
+ // handler: async function (req, res) {
707
+ // // console.log('updateAPIs', req)
708
+ // // console.log('payload', req.payload)
709
+
710
+ // async function core() {
711
+
712
+ // //token
713
+ // let token = get(req, 'query.token', '')
714
+
715
+ // //check
716
+ // if (!isestr(token)) {
717
+ // console.log('req.query', get(req, 'query'))
718
+ // console.log('token', token)
719
+ // console.log('[API]updateAPIs/check token: invalid token')
720
+ // return Promise.reject('errTokenNoPermission')
721
+ // }
722
+
723
+ // //getAndVerifyClientUser
724
+ // let user = await getAndVerifyClientUser(token, 'updateAPIs')
725
+
726
+ // //check
727
+ // if (!iseobj(user)) {
728
+ // console.log('token', token)
729
+ // console.log('[API]updateAPIs/check user: invalid user')
730
+ // return Promise.reject('errTokenNoPermission')
731
+ // }
732
+
733
+ // //parsePayload
734
+ // let inp = await parsePayload(req)
735
+
736
+ // //updateAPIs
737
+ // let r = await updateAPIs(inp)
738
+
739
+ // return r
740
+ // }
741
+
742
+ // //pm2resolve core
743
+ // let r = await pm2resolve(core)()
744
+ // if (isErr(r.msg)) {
745
+ // r.msg = r.msg.message
746
+ // }
747
+ // // console.log('updateAPIs', r)
748
+
749
+ // return r
750
+ // },
751
+ // },
752
+
753
+ {
754
+ method: 'POST',
755
+ path: '/syncAndReplaceTabs',
756
+ handler: async function (req, res) {
757
+ // console.log('syncAndReplaceTabs', req)
758
+ // console.log('payload', req.payload)
759
+
760
+ async function core() {
761
+
762
+ //token
763
+ let token = get(req, 'query.token', '')
764
+
765
+ //check
766
+ if (!isestr(token)) {
767
+ console.log('req.query', get(req, 'query'))
768
+ console.log('token', maskTok(token))
769
+ console.log('[API]syncAndReplaceTabs/check token: invalid token')
770
+ return Promise.reject('errTokenNoPermission')
771
+ }
772
+
773
+ //keyTable
774
+ let keyTable = get(req, 'query.keyTable', '')
775
+
776
+ //check
777
+ if (!isestr(keyTable)) {
778
+ console.log('req.query', get(req, 'query'))
779
+ console.log('keyTable', keyTable)
780
+ console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
781
+ return Promise.reject('errKeyTableInvalid')
782
+ }
783
+
784
+ //check keyTable
785
+ let allowedTables = ['apis']
786
+ if (!allowedTables.includes(keyTable)) {
787
+ console.log('[API]syncAndReplaceTabs/check keyTable: unexpected keyTable', keyTable)
788
+ return Promise.reject('errKeyTableInvalid')
789
+ }
790
+
791
+ //getAndVerifyAppUser
792
+ let user = await getAndVerifyAppUser(token, 'syncAndReplaceTabs')
793
+
794
+ //check
795
+ if (!iseobj(user)) {
796
+ console.log('token', maskTok(token))
797
+ console.log('[API]syncAndReplaceTabs/check user: invalid user')
798
+ return Promise.reject('errTokenNoPermission')
799
+ }
800
+
801
+ //userId
802
+ let userId = get(user, 'id', '')
803
+
804
+ //check userId
805
+ if (!isestr(userId)) {
806
+ console.log('token', maskTok(token))
807
+ console.log('user', user)
808
+ console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
809
+ return Promise.reject('errTokenNoPermission')
810
+ }
811
+
812
+ //parsePayload
813
+ let inp = await parsePayload(req)
814
+
815
+ //syncAndReplaceTabs
816
+ let r = await syncAndReplaceTabs(userId, inp, keyTable)
817
+
818
+ srLog.info({ event: 'api/syncAndReplaceTabs', userId, keyTable })
819
+ return r
820
+ }
821
+
822
+ //pm2resolve core
823
+ let r = await pm2resolve(core)()
824
+ if (isErr(r.msg)) {
825
+ r.msg = r.msg.message
826
+ }
827
+ // console.log('syncAndReplaceTabs', r)
828
+
829
+ return r
830
+ },
831
+ },
832
+
833
+ ]
834
+
835
+
836
+ //tableNamesExec, tableNamesSync
837
+ let tableNamesExec = keys(ds)
838
+ // let tableNamesSync = filter(tableNamesExec, (v) => {
839
+ // return strright(v, 5) !== 'Items'//不同步數據
840
+ // })
841
+ let tableNamesSync = tableNamesExec
842
+
843
+
844
+ //WServHapiServer
845
+ instWServHapiServer = new WServHapiServer({
846
+ port: serverPort,
847
+ pathStaticFiles,
848
+ apiName: 'api',
849
+ apis,
850
+ tokenType: 'Bearer',
851
+ verifyConn: async ({ apiType, authorization, query, headers, req }) => {
852
+ // console.log('verifyConn', `apiType[${apiType}]`, `authorization[${authorization}]`)
853
+
854
+ //token
855
+ let token = strdelleft(authorization, 7) //刪除Bearer
856
+ // console.log('token', token)
857
+
858
+ //check
859
+ if (!isestr(token)) {
860
+ console.log('apiType', apiType)
861
+ console.log('authorization', maskTok(authorization))
862
+ console.log('invalid token')
863
+ return false
864
+ }
865
+
866
+ //checkToken:套件層連線閘僅做「token 認證」(解析 user + 驗欄位齊備)。**授權**(如限管理員)屬
867
+ //套件使用者之責——由其提供之 getUserByToken(可對非授權者回 {})/ verifyClientUser / verifyAppUser,
868
+ //或 WServOrm 之 useCheckUser/useExcludeWhenNotAdmin 等鉤子決定;套件不對連線層強加特定授權政策(避免越界、
869
+ //保留使用者彈性)。詳見 spec/設計要點與取捨.md「套件授權責任」。
870
+ let b = await p.checkToken(token)
871
+
872
+ srLog.info({ event: 'verifyConn', apiType }) //連線之 token 認證通過(checkToken 失敗會 throw 不到此)
873
+ return b
874
+ },
875
+ getUserIdByToken: async (token) => { //可使用async或sync函數
876
+ // console.log('getUserIdByToken', token)
877
+ if (!isestr(token)) {
878
+ return ''
879
+ }
880
+ let user = await getUserByToken(token)
881
+ let userId = get(user, 'id', '')
882
+ return userId
883
+ },
884
+ useDbOrm: true,
885
+ kpOrm: woItems,
886
+ operOrm: procOrm, //procOrm的輸入為: userId, tableName, methodName, input
887
+ tableNamesExec,
888
+ methodsExec: ['select', 'insert', 'save', 'del'], //mix需於procOrm內註冊以提供
889
+ tableNamesSync,
890
+ kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數(皆經 wrapErrLog:錯誤 srLog err-key 後 re-reject)
891
+
892
+ getWebInfor: wrapErrLog('getWebInfor', getWebInfor),
893
+
894
+ proxyRequest: wrapErrLog('proxyRequest', (userId, reqSpec) => {
895
+ return proxyRequest(reqSpec)
896
+ }),
897
+
898
+ getApisList: wrapErrLog('getApisList', (userId) => {
899
+ return getApisList(userId)
900
+ }),
901
+ saveApi: wrapErrLog('saveApi', (userId, row) => {
902
+ return saveApi(userId, row)
903
+ }),
904
+ deleteApi: wrapErrLog('deleteApi', (userId, id) => {
905
+ return deleteApi(userId, id)
906
+ }),
907
+
908
+ //統計資訊頁:回傳各 event 之時段發生頻率(讀 logFd 之 log)。連線之 token 認證由 verifyConn 把關;
909
+ //授權(限管理員)屬套件使用者責任(見 verifyConn 註解與設計要點與取捨「套件授權責任」)。timeLength 天數 / timeInterval 'hr'|'day'。
910
+ getStaEvent: wrapErrLog('getStaEvent', (userId, timeLength = 7, timeInterval = 'hr') => {
911
+ return staEvent(timeLength, timeInterval, { fdLog: logFd })
912
+ }),
913
+
914
+ // getAPIsList: (userId, query = {}) => { //待提供api管理系統界面使用
915
+ // return getAPIsList(query)
916
+ // },
917
+ // updateApis: (userId, rows) => { //待提供api管理系統界面使用
918
+ // return updateApis(rows)
919
+ // },
920
+
921
+ },
922
+ fnTableTags: 'tableTags-web-api.json',
923
+ })
924
+
925
+
926
+ return instWServHapiServer
927
+ }
928
+
929
+
930
+ export default WWebApi