w-web-perm 1.0.48 → 1.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci-test.yml +2 -2
- package/_gh-pages-deploy/.nojekyll +0 -0
- package/_gh-pages-deploy/WWebPerm.html +938 -0
- package/_gh-pages-deploy/WWebPerm.mjs.html +1228 -0
- package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/_gh-pages-deploy/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/_gh-pages-deploy/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/_gh-pages-deploy/index.html +84 -0
- package/_gh-pages-deploy/scripts/collapse.js +39 -0
- package/_gh-pages-deploy/scripts/commonNav.js +28 -0
- package/_gh-pages-deploy/scripts/linenumber.js +25 -0
- package/_gh-pages-deploy/scripts/nav.js +12 -0
- package/_gh-pages-deploy/scripts/polyfill.js +4 -0
- package/_gh-pages-deploy/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/_gh-pages-deploy/scripts/prettify/lang-css.js +2 -0
- package/_gh-pages-deploy/scripts/prettify/prettify.js +28 -0
- package/_gh-pages-deploy/scripts/search.js +99 -0
- package/_gh-pages-deploy/styles/jsdoc.css +776 -0
- package/_gh-pages-deploy/styles/prettify.css +80 -0
- package/dist/css/app.d8b16216.css +1 -0
- package/dist/css/{chunk-vendors.f149e124.css → chunk-vendors.2d3ef2ba.css} +1 -1
- package/dist/index.tmp +2 -2
- package/dist/js/app-legacy.fb48310a.js +1 -0
- package/dist/js/app.fb48310a.js +1 -0
- package/dist/js/chunk-vendors-legacy.8fdd4435.js +21 -0
- package/dist/js/chunk-vendors.8fdd4435.js +21 -0
- package/dist/w-web-perm.umd.js +2 -2
- package/dist/w-web-perm.umd.js.map +1 -1
- package/docs/WWebPerm.html +2 -2
- package/docs/WWebPerm.mjs.html +138 -199
- package/docs/index.html +1 -1
- package/package.json +15 -11
- package/public/index.html +2 -2
- package/server/WWebPerm.mjs +137 -198
- package/server/procCore.mjs +169 -0
- package/server/procLang.mjs +197 -0
- package/server/procStaInfor.mjs +118 -0
- package/server/provideTabs.mjs +1 -1
- package/server/srLog.mjs +30 -0
- package/server/staLogs/staEvent.callWorker.mjs +46 -0
- package/server/staLogs/staEvent.mjs +116 -0
- package/server/staLogs/staEvent.shellWorker.mjs +19 -0
- package/server/staLogs/staEventTable.mjs +100 -0
- package/server/staLogs/test_staEvent.mjs +22 -0
- package/settings.json +40 -0
- package/src/components/Common/CheckYes.vue +18 -1
- package/src/components/Layout.vue +1 -1
- package/src/components/LayoutContent.vue +13 -1
- package/src/components/LayoutContentGrups.vue +32 -23
- package/src/components/LayoutContentPemis.vue +32 -23
- package/src/components/LayoutContentStaInfor.vue +536 -0
- package/src/components/LayoutContentTargets.vue +32 -23
- package/src/components/LayoutContentUsers.vue +32 -23
- package/src/components/LayoutState.vue +5 -0
- package/src/components/VeCgrups.vue +12 -3
- package/src/components/VeCpemis.vue +12 -3
- package/src/components/VeCrules.vue +12 -3
- package/src/components/VeGrupBlngUsers.vue +18 -6
- package/src/components/VePemiBlngGrups.vue +18 -6
- package/src/getPerm.mjs +7 -17
- package/src/getPermUserInfor.mjs +8 -18
- package/src/main.js +1 -0
- package/src/plugins/mShare.mjs +8 -13
- package/src/plugins/mUI.mjs +11 -0
- package/srv.mjs +76 -101
- package/tableTags.json +1 -1
- package/db/wperm/grups/data.mdb +0 -0
- package/db/wperm/grups/lock.mdb +0 -0
- package/db/wperm/pemis/data.mdb +0 -0
- package/db/wperm/pemis/lock.mdb +0 -0
- package/db/wperm/targets/data.mdb +0 -0
- package/db/wperm/targets/lock.mdb +0 -0
- package/db/wperm/users/data.mdb +0 -0
- package/db/wperm/users/lock.mdb +0 -0
- package/dist/css/app.30e81bfe.css +0 -1
- package/dist/js/app-legacy.3324392e.js +0 -1
- package/dist/js/app.3324392e.js +0 -1
- package/dist/js/chunk-vendors-legacy.e8703728.js +0 -21
- package/dist/js/chunk-vendors.e8703728.js +0 -21
- package/test/getUserRules.test.mjs +0 -899
|
@@ -0,0 +1,1228 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<title>WWebPerm.mjs - Documentation</title>
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
<script src="scripts/prettify/prettify.js"></script>
|
|
10
|
+
<script src="scripts/prettify/lang-css.js"></script>
|
|
11
|
+
<!--[if lt IE 9]>
|
|
12
|
+
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
|
|
13
|
+
<![endif]-->
|
|
14
|
+
<link type="text/css" rel="stylesheet" href="styles/prettify.css">
|
|
15
|
+
<link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
|
|
16
|
+
<script src="scripts/nav.js" defer></script>
|
|
17
|
+
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
|
|
22
|
+
<input type="checkbox" id="nav-trigger" class="nav-trigger" />
|
|
23
|
+
<label for="nav-trigger" class="navicon-button x">
|
|
24
|
+
<div class="navicon"></div>
|
|
25
|
+
</label>
|
|
26
|
+
|
|
27
|
+
<label for="nav-trigger" class="overlay"></label>
|
|
28
|
+
|
|
29
|
+
<nav >
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WWebPerm.html">WWebPerm</a></li></ul>
|
|
33
|
+
|
|
34
|
+
</nav>
|
|
35
|
+
|
|
36
|
+
<div id="main">
|
|
37
|
+
|
|
38
|
+
<h1 class="page-title">WWebPerm.mjs</h1>
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
<section>
|
|
47
|
+
<article>
|
|
48
|
+
<pre class="prettyprint source linenums"><code>import path from 'path'
|
|
49
|
+
import fs from 'fs'
|
|
50
|
+
import get from 'lodash-es/get.js'
|
|
51
|
+
import each from 'lodash-es/each.js'
|
|
52
|
+
import map from 'lodash-es/map.js'
|
|
53
|
+
import keys from 'lodash-es/keys.js'
|
|
54
|
+
import size from 'lodash-es/size.js'
|
|
55
|
+
import join from 'lodash-es/join.js'
|
|
56
|
+
import iseobj from 'wsemi/src/iseobj.mjs'
|
|
57
|
+
import isestr from 'wsemi/src/isestr.mjs'
|
|
58
|
+
import ispint from 'wsemi/src/ispint.mjs'
|
|
59
|
+
import isearr from 'wsemi/src/isearr.mjs'
|
|
60
|
+
import isErr from 'wsemi/src/isErr.mjs'
|
|
61
|
+
import isfun from 'wsemi/src/isfun.mjs'
|
|
62
|
+
import ispm from 'wsemi/src/ispm.mjs'
|
|
63
|
+
import cint from 'wsemi/src/cint.mjs'
|
|
64
|
+
import j2o from 'wsemi/src/j2o.mjs'
|
|
65
|
+
import strleft from 'wsemi/src/strleft.mjs'
|
|
66
|
+
import strright from 'wsemi/src/strright.mjs'
|
|
67
|
+
import strdelright from 'wsemi/src/strdelright.mjs'
|
|
68
|
+
import pm2resolve from 'wsemi/src/pm2resolve.mjs'
|
|
69
|
+
import fsIsFolder from 'wsemi/src/fsIsFolder.mjs'
|
|
70
|
+
import fsIsFile from 'wsemi/src/fsIsFile.mjs'
|
|
71
|
+
import replace from 'wsemi/src/replace.mjs'
|
|
72
|
+
import strdelleft from 'wsemi/src/strdelleft.mjs'
|
|
73
|
+
import ltdtmapping from 'wsemi/src/ltdtmapping.mjs'
|
|
74
|
+
import pmKeyMutex from 'wsemi/src/pmKeyMutex.mjs'
|
|
75
|
+
import WServHapiServer from 'w-serv-hapi/src/WServHapiServer.mjs'
|
|
76
|
+
import WServOrm from 'w-serv-orm/src/WServOrm.mjs'
|
|
77
|
+
import ds from '../src/schema/index.mjs'
|
|
78
|
+
import { getUserRules } from '../src/plugins/mShare.mjs'
|
|
79
|
+
import procCore from './procCore.mjs'
|
|
80
|
+
import procLang from './procLang.mjs'
|
|
81
|
+
import srLogInit from './srLog.mjs'
|
|
82
|
+
import procStaInfor from './procStaInfor.mjs'
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 權限伺服器
|
|
87
|
+
*
|
|
88
|
+
* @class
|
|
89
|
+
* @param {Function} WOrm 輸入資料庫ORM函數
|
|
90
|
+
* @param {String} url 輸入資料庫連線字串,例如w-orm-lmdb為'./db',或w-orm-mongodb為'mongodb://username:password@$127.0.0.1:27017'
|
|
91
|
+
* @param {String} db 輸入資料庫名稱字串
|
|
92
|
+
* @param {Function} getUserByToken 輸入處理函數,函數會傳入使用者token,通過此函數處理後並回傳使用者資訊物件,並至少須提供'id'、'email'、'name'、'isAdmin'欄位,且'isAdmin'限輸入'y'或'n',且輸入'y'時會複寫權限系統該使用者之'isAdmin'欄位值
|
|
93
|
+
* @param {Function} verifyClientUser 輸入驗證瀏覽使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
|
|
94
|
+
* @param {Function} verifyAppUser 輸入驗證應用程序使用者身份之處理函數,函數會傳入使用者資訊物件,通過此函數識別後回傳布林值,允許使用者回傳true,反之回傳false
|
|
95
|
+
* @param {Object} [opt={}] 輸入設定物件,預設{}
|
|
96
|
+
* @param {Integer} [opt.serverPort=11006] 輸入伺服器通訊port,預設11006
|
|
97
|
+
* @param {Boolean} [opt.useCheckUser=false] 輸入是否檢查使用者資訊布林值,預設false
|
|
98
|
+
* @param {Function} [opt.getUserById=null] 輸入當useCheckUser=true時依照使用者ID取得使用者資訊物件函數,預設null
|
|
99
|
+
* @param {Boolean} [opt.useExcludeWhenNotAdmin=false] 輸入使用ORM的select方法時是否自動刪除數據內isActive欄位之布林值,預設false
|
|
100
|
+
* @param {Object} [opt.webName={}] 輸入站台名稱物件,至少包含語系eng與cht鍵的名稱,預設{}
|
|
101
|
+
* @param {Object} [opt.webDescription={}] 輸入站台描述物件,至少包含語系eng與cht鍵的名稱,預設{}
|
|
102
|
+
* @param {String} [opt.webLogo=''] 輸入站台logo字串,採base64格式,預設''
|
|
103
|
+
* @param {String} [opt.subfolder=''] 輸入站台所在子目錄字串,提供站台位於內網採反向代理進行服務時,故需支援位於子目錄情形,預設''
|
|
104
|
+
* @param {String} [opt.urlRedirect=''] 輸入錯誤時自動轉址字串,提供站台例如無法登入或驗證失敗時須自動轉址,預設''
|
|
105
|
+
* @param {String} [opt.mappingBy='email'] 輸入外部系統識別使用者token後所提供之資料物件,與權限系統之使用者資料物件,兩者間查找之對應欄位,可選'id'、'email'、'name',預設'email'
|
|
106
|
+
* @returns {Object} 回傳物件,其內server為hapi伺服器實體,wsrv為w-converhp的伺服器事件物件,wsds為w-serv-webdata的伺服器事件物件,可監聽error事件
|
|
107
|
+
* @example
|
|
108
|
+
*
|
|
109
|
+
* import WOrm from 'w-orm-lmdb/src/WOrmLmdb.mjs'
|
|
110
|
+
* import WWebPerm from './server/WWebPerm.mjs'
|
|
111
|
+
* import getSettings from './g.getSettings.mjs'
|
|
112
|
+
*
|
|
113
|
+
*
|
|
114
|
+
* //st
|
|
115
|
+
* let st = getSettings()
|
|
116
|
+
*
|
|
117
|
+
* let url = st.dbUrl
|
|
118
|
+
* let db = st.dbName
|
|
119
|
+
* let opt = {
|
|
120
|
+
*
|
|
121
|
+
* useCheckUser: false,
|
|
122
|
+
* getUserById: null,
|
|
123
|
+
* useExcludeWhenNotAdmin: false,
|
|
124
|
+
*
|
|
125
|
+
* serverPort: 11006,
|
|
126
|
+
* subfolder: '', //mperm
|
|
127
|
+
* urlRedirect: 'https://www.google.com/', //本機測試時得先編譯, 再瀏覽: http://localhost:11006/
|
|
128
|
+
*
|
|
129
|
+
* webName: {
|
|
130
|
+
* 'eng': 'Permission Service',
|
|
131
|
+
* 'cht': '權限管理系統',
|
|
132
|
+
* },
|
|
133
|
+
* webDescription: {
|
|
134
|
+
* 'eng': 'A web service package for user permissions and management targets.',
|
|
135
|
+
* 'cht': 'A web service package for user permissions and management targets.',
|
|
136
|
+
* },
|
|
137
|
+
* webLogo: 'data:image/svg+xml;base64,...',
|
|
138
|
+
*
|
|
139
|
+
* }
|
|
140
|
+
*
|
|
141
|
+
* let getUserByToken = async (token) => {
|
|
142
|
+
* // return {} //測試無法登入
|
|
143
|
+
* if (token === '{token-for-application}') { //提供外部應用系統作為存取使用者
|
|
144
|
+
* return {
|
|
145
|
+
* id: 'id-for-application',
|
|
146
|
+
* name: 'application',
|
|
147
|
+
* email: 'application@example.com',
|
|
148
|
+
* isAdmin: 'y',
|
|
149
|
+
* }
|
|
150
|
+
* }
|
|
151
|
+
* if (token === 'sys') { //開發階段w-ui-loginout自動給予browser使用者(且位於localhost)的token為sys
|
|
152
|
+
* return {
|
|
153
|
+
* id: 'id-for-admin',
|
|
154
|
+
* name: 'tester',
|
|
155
|
+
* email: 'admin@example.com', //mappingBy為email, 開發階段時會使用email找到所建置之使用者資料
|
|
156
|
+
* isAdmin: 'y',
|
|
157
|
+
* }
|
|
158
|
+
* }
|
|
159
|
+
* console.log('invalid token', token)
|
|
160
|
+
* console.log('於生產環境時得加入SSO等驗證token機制')
|
|
161
|
+
* return {}
|
|
162
|
+
* }
|
|
163
|
+
*
|
|
164
|
+
* let verifyClientUser = (user, from) => {
|
|
165
|
+
* console.log('verifyClientUser/user', user)
|
|
166
|
+
* // return false //測試無法登入
|
|
167
|
+
* console.log('於生產環境時得加入限制瀏覽器使用者身份機制')
|
|
168
|
+
* return user.isAdmin === 'y' //測試僅系統管理者使用
|
|
169
|
+
* }
|
|
170
|
+
*
|
|
171
|
+
* let verifyAppUser = (user, from) => {
|
|
172
|
+
* console.log('verifyAppUser/user', user)
|
|
173
|
+
* // return false //測試無法登入
|
|
174
|
+
* console.log('於生產環境時得加入限制應用程式使用者身份機制')
|
|
175
|
+
* return user.isAdmin === 'y' //測試僅系統管理者使用
|
|
176
|
+
* }
|
|
177
|
+
*
|
|
178
|
+
* //WWebPerm
|
|
179
|
+
* let instWWebPerm = WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt)
|
|
180
|
+
*
|
|
181
|
+
* instWWebPerm.on('error', (err) => {
|
|
182
|
+
* console.log(err)
|
|
183
|
+
* })
|
|
184
|
+
*
|
|
185
|
+
*/
|
|
186
|
+
function WWebPerm(WOrm, url, db, getUserByToken, verifyClientUser, verifyAppUser, opt = {}) {
|
|
187
|
+
let instWServHapiServer = null
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
//check WOrm
|
|
191
|
+
if (!isfun(WOrm)) {
|
|
192
|
+
console.log('invalid WOrm', WOrm)
|
|
193
|
+
throw new Error('invalid WOrm')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
//check url
|
|
198
|
+
if (!isestr(url)) {
|
|
199
|
+
console.log('invalid url', url)
|
|
200
|
+
throw new Error('invalid url')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
//check db
|
|
205
|
+
if (!isestr(db)) {
|
|
206
|
+
console.log('invalid db', db)
|
|
207
|
+
throw new Error('invalid db')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
//check getUserByToken
|
|
212
|
+
if (!isfun(getUserByToken)) {
|
|
213
|
+
console.log('invalid getUserByToken', getUserByToken)
|
|
214
|
+
throw new Error('invalid getUserByToken')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
//check verifyClientUser
|
|
219
|
+
if (!isfun(verifyClientUser)) {
|
|
220
|
+
console.log('invalid verifyClientUser', verifyClientUser)
|
|
221
|
+
throw new Error('invalid verifyClientUser')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
//check verifyAppUser
|
|
226
|
+
if (!isfun(verifyAppUser)) {
|
|
227
|
+
console.log('invalid verifyAppUser', verifyAppUser)
|
|
228
|
+
throw new Error('invalid verifyAppUser')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
//serverPort
|
|
233
|
+
let serverPort = get(opt, 'serverPort')
|
|
234
|
+
if (!ispint(serverPort)) {
|
|
235
|
+
serverPort = 11006
|
|
236
|
+
}
|
|
237
|
+
serverPort = cint(serverPort)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
//useCheckUser
|
|
241
|
+
let useCheckUser = get(opt, 'useCheckUser', false)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
//getUserById
|
|
245
|
+
let getUserById = get(opt, 'getUserById', null)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
//useExcludeWhenNotAdmin
|
|
249
|
+
let useExcludeWhenNotAdmin = get(opt, 'useExcludeWhenNotAdmin', false)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
//webName
|
|
253
|
+
let webName = get(opt, 'webName', {})
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
//webDescription
|
|
257
|
+
let webDescription = get(opt, 'webDescription', {})
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
//webLogo
|
|
261
|
+
let webLogo = get(opt, 'webLogo', '')
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
//subfolder
|
|
265
|
+
let subfolder = get(opt, 'subfolder', '')
|
|
266
|
+
if (isestr(subfolder)) {
|
|
267
|
+
if (strright(subfolder, 1) === '/') { //右邊不需要給「/」
|
|
268
|
+
subfolder = strdelright(subfolder, 1)
|
|
269
|
+
}
|
|
270
|
+
if (strleft(subfolder, 1) !== '/') { //左邊需要給「/」
|
|
271
|
+
subfolder = `/${subfolder}`
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
//urlRedirect
|
|
277
|
+
let urlRedirect = get(opt, 'urlRedirect', '')
|
|
278
|
+
// console.log('urlRedirect', urlRedirect)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
//params
|
|
282
|
+
let showLanguage = get(opt, 'showLanguage', 'n')
|
|
283
|
+
let language = get(opt, 'language', 'eng')
|
|
284
|
+
let showModeEditTargets = get(opt, 'showModeEditTargets', 'n')
|
|
285
|
+
let showModeEditPemis = get(opt, 'showModeEditPemis', 'n')
|
|
286
|
+
let showModeEditGrups = get(opt, 'showModeEditGrups', 'n')
|
|
287
|
+
let showModeEditUsers = get(opt, 'showModeEditUsers', 'n')
|
|
288
|
+
let modeEditTargets = get(opt, 'modeEditTargets', 'n')
|
|
289
|
+
let modeEditPemis = get(opt, 'modeEditPemis', 'n')
|
|
290
|
+
let modeEditGrups = get(opt, 'modeEditGrups', 'n')
|
|
291
|
+
let modeEditUsers = get(opt, 'modeEditUsers', 'n')
|
|
292
|
+
let widthTargetId = get(opt, 'widthTargetId', '')
|
|
293
|
+
let widthTargetDescription = get(opt, 'widthTargetDescription', '')
|
|
294
|
+
let widthPemisName = get(opt, 'widthPemisName', '')
|
|
295
|
+
let widthPemisDescription = get(opt, 'widthPemisDescription', '')
|
|
296
|
+
let widthGrupsName = get(opt, 'widthGrupsName', '')
|
|
297
|
+
let widthGrupsDescription = get(opt, 'widthGrupsDescription', '')
|
|
298
|
+
let widthUsersName = get(opt, 'widthUsersName', '')
|
|
299
|
+
let widthUsersEmail = get(opt, 'widthUsersEmail', '')
|
|
300
|
+
let widthUsersDescription = get(opt, 'widthUsersDescription', '')
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
//mappingBy
|
|
304
|
+
let mappingBy = get(opt, 'mappingBy', '')
|
|
305
|
+
if (mappingBy !== 'id' && mappingBy !== 'email' && mappingBy !== 'name') {
|
|
306
|
+
mappingBy = 'id'
|
|
307
|
+
}
|
|
308
|
+
// console.log('mappingBy', mappingBy)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
//kpLangExt
|
|
312
|
+
let kpLangExt = get(opt, 'kpLangExt', null)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
//kpLang
|
|
316
|
+
let kpLang = procLang({ kpLangExt, webName, webDescription })
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
//srLog
|
|
320
|
+
let srLog = srLogInit(opt)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
//WServOrm
|
|
324
|
+
let optWServOrm = {
|
|
325
|
+
useCheckUser,
|
|
326
|
+
getUserById,
|
|
327
|
+
useExcludeWhenNotAdmin,
|
|
328
|
+
}
|
|
329
|
+
let wp = {}
|
|
330
|
+
try {
|
|
331
|
+
wp = WServOrm(ds, WOrm, url, db, optWServOrm)
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
console.log(err)
|
|
335
|
+
}
|
|
336
|
+
let { woItems, procOrm } = wp
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
//pmKeyMutex: per-key in-memory mutex, 同 key 序列化、不同 key 並行.
|
|
340
|
+
//perm 為「整表批次 diff」寫入, key 粒度為 {op}:{keyTable}, 防同表並行 race (lost update).
|
|
341
|
+
let kmx = pmKeyMutex()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
//procCore: 整表批次 diff 寫入邏輯 (updateTabItems + updateTargets/Pemis/Grups/Users)
|
|
345
|
+
let pc = procCore(woItems, procOrm, { srLog, kmx })
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
//procStaInfor: 統計資訊, fdLog 對齊 srLog 使用同一目錄; mock 由 settings.staEventMock 注入(e2e 統計圖穩定用)
|
|
349
|
+
let psi = procStaInfor({ fdLog: './_logs', mock: get(opt, 'staEventMock', false) })
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
//getWebInfor
|
|
353
|
+
let getWebInfor = (userId) => {
|
|
354
|
+
|
|
355
|
+
//info
|
|
356
|
+
srLog.info({ event: 'getWebInfor-success', userId })
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
|
|
360
|
+
// webName, //已併入kpLang
|
|
361
|
+
// webDescription, //已併入kpLang
|
|
362
|
+
webLogo,
|
|
363
|
+
// urlRedirect, //登入失敗就需要轉址, 故須通過html模板取代提供, 無法用api提供
|
|
364
|
+
|
|
365
|
+
showLanguage,
|
|
366
|
+
language,
|
|
367
|
+
kpLang,
|
|
368
|
+
|
|
369
|
+
showModeEditTargets,
|
|
370
|
+
showModeEditPemis,
|
|
371
|
+
showModeEditGrups,
|
|
372
|
+
showModeEditUsers,
|
|
373
|
+
modeEditTargets,
|
|
374
|
+
modeEditPemis,
|
|
375
|
+
modeEditGrups,
|
|
376
|
+
modeEditUsers,
|
|
377
|
+
|
|
378
|
+
widthTargetId,
|
|
379
|
+
widthTargetDescription,
|
|
380
|
+
widthPemisName,
|
|
381
|
+
widthPemisDescription,
|
|
382
|
+
widthGrupsName,
|
|
383
|
+
widthGrupsDescription,
|
|
384
|
+
widthUsersName,
|
|
385
|
+
widthUsersEmail,
|
|
386
|
+
widthUsersDescription,
|
|
387
|
+
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
// //getTargetsList
|
|
393
|
+
// let getTargetsList = async (userId) => {
|
|
394
|
+
// let rs = await woItems.targets.select()
|
|
395
|
+
// return rs
|
|
396
|
+
// }
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
// //getPemisList
|
|
400
|
+
// let getPemisList = async (userId) => {
|
|
401
|
+
// let rs = await woItems.pemis.select()
|
|
402
|
+
// return rs
|
|
403
|
+
// }
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
// //getGrupsList
|
|
407
|
+
// let getGrupsList = async (userId) => {
|
|
408
|
+
// let rs = await woItems.grups.select()
|
|
409
|
+
// return rs
|
|
410
|
+
// }
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
// //getUsersList
|
|
414
|
+
// let getUsersList = async (userId) => {
|
|
415
|
+
// let rs = await woItems.users.select()
|
|
416
|
+
// return rs
|
|
417
|
+
// }
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
//checkUser
|
|
421
|
+
let checkUser = async(user) => {
|
|
422
|
+
let id = get(user, 'id', '')
|
|
423
|
+
if (!isestr(id)) {
|
|
424
|
+
console.log('user', user)
|
|
425
|
+
srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserId' })
|
|
426
|
+
return Promise.reject(`cannotGetUserId`)
|
|
427
|
+
}
|
|
428
|
+
let email = get(user, 'email', '')
|
|
429
|
+
if (!isestr(email)) {
|
|
430
|
+
console.log('user', user)
|
|
431
|
+
srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserEmail' })
|
|
432
|
+
return Promise.reject(`cannotGetUserEmail`)
|
|
433
|
+
}
|
|
434
|
+
let name = get(user, 'name', '')
|
|
435
|
+
if (!isestr(name)) {
|
|
436
|
+
console.log('user', user)
|
|
437
|
+
srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserName' })
|
|
438
|
+
return Promise.reject(`cannotGetUserName`)
|
|
439
|
+
}
|
|
440
|
+
let isAdmin = get(user, 'isAdmin', '')
|
|
441
|
+
if (isAdmin !== 'y' && isAdmin !== 'n') {
|
|
442
|
+
console.log('user', user)
|
|
443
|
+
console.log('user.isAdmin is not y or n', user.isAdmin)
|
|
444
|
+
srLog.error({ event: 'checkUser-error', msg: 'cannotGetUserRole' })
|
|
445
|
+
return Promise.reject(`cannotGetUserRole`)
|
|
446
|
+
}
|
|
447
|
+
return true
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
//getTokenUser
|
|
452
|
+
let getTokenUser = async(token) => {
|
|
453
|
+
//基於token查找使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
|
|
454
|
+
|
|
455
|
+
//userSelf
|
|
456
|
+
let userSelf = null
|
|
457
|
+
if (isestr(token)) {
|
|
458
|
+
userSelf = getUserByToken(token)
|
|
459
|
+
if (ispm(userSelf)) {
|
|
460
|
+
userSelf = await userSelf
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// console.log('userSelf', userSelf)
|
|
464
|
+
|
|
465
|
+
//check
|
|
466
|
+
if (!iseobj(userSelf)) {
|
|
467
|
+
console.log(`token`, token)
|
|
468
|
+
srLog.error({ event: 'getTokenUser-error', msg: 'cannotFindUserFromToken' })
|
|
469
|
+
return Promise.reject(`cannotFindUserFromToken`)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
//check userSelf
|
|
473
|
+
await checkUser(userSelf)
|
|
474
|
+
|
|
475
|
+
//須反查perm內users, 提供正規化屬性
|
|
476
|
+
let vSelf = get(userSelf, mappingBy, '')
|
|
477
|
+
// console.log('vSelf', vSelf)
|
|
478
|
+
|
|
479
|
+
//check
|
|
480
|
+
if (!isestr(vSelf)) {
|
|
481
|
+
console.log('userSelf', userSelf)
|
|
482
|
+
console.log('mappingBy', mappingBy)
|
|
483
|
+
srLog.error({ event: 'getTokenUser-error', msg: 'cannotGetUserProp', mappingBy })
|
|
484
|
+
return Promise.reject(`cannotGetUserProp`)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
//userFind
|
|
488
|
+
let userFind = await woItems.users.select({ [mappingBy]: vSelf, isActive: 'y' })
|
|
489
|
+
userFind = get(userFind, 0, null)
|
|
490
|
+
// console.log('userFind', userFind)
|
|
491
|
+
|
|
492
|
+
//check
|
|
493
|
+
if (!iseobj(userFind)) {
|
|
494
|
+
console.log('userSelf', userSelf)
|
|
495
|
+
console.log('mappingBy', mappingBy)
|
|
496
|
+
srLog.error({ event: 'getTokenUser-error', msg: 'cannotGetUserFromPerm', mappingBy })
|
|
497
|
+
return Promise.reject(`cannotGetUserFromPerm`)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
//複寫isAdmin
|
|
501
|
+
let isAdminSrc = get(userSelf, 'isAdmin', '')
|
|
502
|
+
let isAdminSelf = get(userFind, 'isAdmin', '')
|
|
503
|
+
if (isAdminSrc !== isAdminSelf) {
|
|
504
|
+
userFind.isAdmin = isAdminSelf
|
|
505
|
+
}
|
|
506
|
+
// console.log('userFind(isAdmin)', userFind)
|
|
507
|
+
|
|
508
|
+
return userFind
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
//p
|
|
513
|
+
let p = {
|
|
514
|
+
checkToken: async(token) => {
|
|
515
|
+
|
|
516
|
+
//getUserByToken
|
|
517
|
+
let u = await getUserByToken(token)
|
|
518
|
+
// console.log('u', u)
|
|
519
|
+
|
|
520
|
+
//checkUser
|
|
521
|
+
await checkUser(u)
|
|
522
|
+
|
|
523
|
+
return true
|
|
524
|
+
},
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
//getAndVerifyClientUser
|
|
529
|
+
let getAndVerifyClientUser = async (token, from = '') => {
|
|
530
|
+
//基於token查找client使用者, 會基於外部getUserByToken以及perm內部users進行查找並比對
|
|
531
|
+
|
|
532
|
+
//getTokenUser
|
|
533
|
+
let user = await getTokenUser(token)
|
|
534
|
+
|
|
535
|
+
//verifyClientUser
|
|
536
|
+
let b = verifyClientUser(user, from)
|
|
537
|
+
if (ispm(b)) {
|
|
538
|
+
b = await b
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
//check
|
|
542
|
+
if (!b) {
|
|
543
|
+
console.log('user', user)
|
|
544
|
+
srLog.error({ event: 'getAndVerifyClientUser-error', msg: 'userNoPermission', from })
|
|
545
|
+
return Promise.reject(`userNoPermission`)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return user
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
//getAndVerifyAppUser
|
|
553
|
+
let getAndVerifyAppUser = async (token, from = '') => {
|
|
554
|
+
//基於token查找app使用者, 會基於外部getUserByToken進行查找, 故只要getUserByToken提供使用者即可, 可支援getUserByToken給予針對應用程式所產生之虛擬使用者
|
|
555
|
+
|
|
556
|
+
//user
|
|
557
|
+
let user = null
|
|
558
|
+
if (isestr(token)) {
|
|
559
|
+
user = getUserByToken(token)
|
|
560
|
+
if (ispm(user)) {
|
|
561
|
+
user = await user
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// console.log('user', user)
|
|
565
|
+
|
|
566
|
+
//check
|
|
567
|
+
if (!iseobj(user)) {
|
|
568
|
+
console.log(`token`, token)
|
|
569
|
+
srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'cannotFindUserFromToken' })
|
|
570
|
+
return Promise.reject(`cannotFindUserFromToken`)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
//verifyAppUser
|
|
574
|
+
let b = verifyAppUser(user, from)
|
|
575
|
+
if (ispm(b)) {
|
|
576
|
+
b = await b
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
//check
|
|
580
|
+
if (!b) {
|
|
581
|
+
console.log('user', user)
|
|
582
|
+
srLog.error({ event: 'getAndVerifyAppUser-error', msg: 'userNoPermission', from })
|
|
583
|
+
return Promise.reject(`userNoPermission`)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return user
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
//parsePayload
|
|
591
|
+
let parsePayload = async (req) => {
|
|
592
|
+
|
|
593
|
+
//payload
|
|
594
|
+
let payload = get(req, 'payload')
|
|
595
|
+
|
|
596
|
+
//to obj
|
|
597
|
+
if (isestr(payload)) {
|
|
598
|
+
payload = j2o(payload)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
//check
|
|
602
|
+
if (!iseobj(payload)) {
|
|
603
|
+
console.log('payload', payload)
|
|
604
|
+
srLog.error({ event: 'parsePayload-error', msg: 'invalidPayload' })
|
|
605
|
+
return Promise.reject(`invalidPayload`)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
//from
|
|
609
|
+
let from = get(payload, 'from', '')
|
|
610
|
+
|
|
611
|
+
//check
|
|
612
|
+
if (!isestr(from)) {
|
|
613
|
+
console.log('payload', payload)
|
|
614
|
+
srLog.error({ event: 'parsePayload-error', msg: 'invalidFromInPayload' })
|
|
615
|
+
return Promise.reject(`invalidFromInPayload`)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
//rows
|
|
619
|
+
let rows = get(payload, 'rows', [])
|
|
620
|
+
|
|
621
|
+
//check
|
|
622
|
+
if (!isearr(rows)) {
|
|
623
|
+
console.log('payload', payload)
|
|
624
|
+
srLog.error({ event: 'parsePayload-error', msg: 'invalidRowsInPayload' })
|
|
625
|
+
return Promise.reject(`invalidRowsInPayload`)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
//inp
|
|
629
|
+
let inp = {
|
|
630
|
+
from,
|
|
631
|
+
rows,
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return inp
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
//syncAndReplaceTabs
|
|
639
|
+
let syncAndReplaceTabs = async (userId, inp, keyTable) => {
|
|
640
|
+
|
|
641
|
+
//序列化同表之並行整表 delAll+insert: 防 lost update (同 keyTable 序列化, 不同表並行)
|
|
642
|
+
return await kmx('syncAndReplaceTabs:' + keyTable, async () => {
|
|
643
|
+
|
|
644
|
+
//from
|
|
645
|
+
let from = get(inp, 'from', '')
|
|
646
|
+
// console.log('from', from)
|
|
647
|
+
|
|
648
|
+
//check
|
|
649
|
+
if (!isestr(from)) {
|
|
650
|
+
console.log('inp', inp)
|
|
651
|
+
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidFrom', keyTable })
|
|
652
|
+
return Promise.reject(`invalidFrom`)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
//rows
|
|
656
|
+
let rows = get(inp, 'rows', [])
|
|
657
|
+
// console.log(keyTable, rows)
|
|
658
|
+
|
|
659
|
+
//check
|
|
660
|
+
if (!isearr(rows)) {
|
|
661
|
+
console.log('inp', inp)
|
|
662
|
+
console.log('keyTable', keyTable)
|
|
663
|
+
srLog.error({ event: 'syncAndReplaceTabs-error', msg: 'invalidRows', keyTable })
|
|
664
|
+
return Promise.reject(`invalidRows`)
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
//save from
|
|
668
|
+
rows = map(rows, (v, k) => {
|
|
669
|
+
// v.order = k + 1 //order大部分由外部給予, 不能複寫
|
|
670
|
+
v.from = from
|
|
671
|
+
return v
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
//ltdtmapping
|
|
675
|
+
rows = ltdtmapping(rows, ds[keyTable].keys)
|
|
676
|
+
// console.log('ltdtmapping rows', rows)
|
|
677
|
+
|
|
678
|
+
//delAll from
|
|
679
|
+
await woItems[keyTable].delAll({ from })
|
|
680
|
+
|
|
681
|
+
//insert
|
|
682
|
+
// let r = await woItems[keyTable].insert(rows)
|
|
683
|
+
let r = await procOrm(userId, keyTable, 'insert', rows) //須使用procOrm才有辦法自動給予相關欄位
|
|
684
|
+
// console.log('r', r)
|
|
685
|
+
|
|
686
|
+
return r
|
|
687
|
+
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
//getGenUserByKV
|
|
693
|
+
let getGenUserByKV = async(keyUser, valueUser) => {
|
|
694
|
+
|
|
695
|
+
//userFind
|
|
696
|
+
let userFind = await woItems.users.select({ [keyUser]: valueUser, isActive: 'y' })
|
|
697
|
+
userFind = get(userFind, 0, null)
|
|
698
|
+
|
|
699
|
+
//check
|
|
700
|
+
if (!iseobj(userFind)) {
|
|
701
|
+
return null
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return userFind
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
//getGenUserByUserId
|
|
709
|
+
let getGenUserByUserId = async(userId) => {
|
|
710
|
+
|
|
711
|
+
//userFind
|
|
712
|
+
let userFind = await getGenUserByKV('id', userId)
|
|
713
|
+
|
|
714
|
+
//check
|
|
715
|
+
if (!iseobj(userFind)) {
|
|
716
|
+
return null
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return userFind
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
//getGenUserAndRulesByUserId
|
|
724
|
+
let getGenUserAndRulesByUserId = async (userId) => {
|
|
725
|
+
|
|
726
|
+
//userFind
|
|
727
|
+
let userFind = await getGenUserByUserId(userId)
|
|
728
|
+
// console.log('userFind', userFind)
|
|
729
|
+
|
|
730
|
+
//grups
|
|
731
|
+
let grups = await woItems.grups.select()
|
|
732
|
+
// console.log('grups', grups)
|
|
733
|
+
|
|
734
|
+
//pemis
|
|
735
|
+
let pemis = await woItems.pemis.select()
|
|
736
|
+
// console.log('pemis', pemis)
|
|
737
|
+
|
|
738
|
+
//targets
|
|
739
|
+
let targets = await woItems.targets.select()
|
|
740
|
+
// console.log('targets', targets)
|
|
741
|
+
|
|
742
|
+
//getUserRules
|
|
743
|
+
let kur = getUserRules(userFind, grups, pemis, targets)
|
|
744
|
+
// console.log('getUserRules', kur)
|
|
745
|
+
|
|
746
|
+
//check
|
|
747
|
+
if (!iseobj(kur)) {
|
|
748
|
+
console.log('userId', userId)
|
|
749
|
+
srLog.error({ event: 'getGenUserAndRulesByUserId-error', msg: 'cannotGetUserRules', userId })
|
|
750
|
+
return Promise.reject(`cannotGetUserRules`)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
//grupsNames
|
|
754
|
+
let grupsNames = map(get(kur, 'grups', []), 'name') //kur.grups僅會提供isActive='y', 故grupsNames為有效權限群組名稱
|
|
755
|
+
grupsNames = join(grupsNames, ';')
|
|
756
|
+
|
|
757
|
+
return {
|
|
758
|
+
user: {
|
|
759
|
+
...userFind,
|
|
760
|
+
grupsNames,
|
|
761
|
+
},
|
|
762
|
+
// grups: get(kur, 'grups', []),
|
|
763
|
+
rules: get(kur, 'rules', []),
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
//pathStaticFiles
|
|
769
|
+
let pathStaticFiles = 'dist'
|
|
770
|
+
let npmPathStaticFiles = './node_modules/w-web-perm/dist'
|
|
771
|
+
if (fsIsFolder(npmPathStaticFiles)) {
|
|
772
|
+
pathStaticFiles = npmPathStaticFiles
|
|
773
|
+
}
|
|
774
|
+
// console.log('pathStaticFiles', pathStaticFiles)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
//subfolder
|
|
778
|
+
let fnEntryIn = 'index.tmp'
|
|
779
|
+
let fnEntryOut = 'index.html'
|
|
780
|
+
try {
|
|
781
|
+
let fpEntryIn = path.resolve(pathStaticFiles, fnEntryIn)
|
|
782
|
+
if (!fsIsFile(fpEntryIn)) {
|
|
783
|
+
fpEntryIn = path.resolve(pathStaticFiles, fnEntryOut) //本機開發另使用html替代tmp
|
|
784
|
+
}
|
|
785
|
+
if (!fsIsFile(fpEntryIn)) {
|
|
786
|
+
console.log('fpEntryIn', fpEntryIn)
|
|
787
|
+
throw new Error(`invalid fpEntryIn`)
|
|
788
|
+
}
|
|
789
|
+
let fpEntryOut = path.resolve(pathStaticFiles, fnEntryOut)
|
|
790
|
+
let c = fs.readFileSync(fpEntryIn, 'utf8')
|
|
791
|
+
c = replace(c, '/mperm/', '{sfd}/') //方法同genEntry
|
|
792
|
+
c = replace(c, '{sfd}', subfolder)
|
|
793
|
+
c = replace(c, '{urlRedirect}', urlRedirect)
|
|
794
|
+
c = replace(c, '{language}', language)
|
|
795
|
+
fs.writeFileSync(fpEntryOut, c, 'utf8')
|
|
796
|
+
}
|
|
797
|
+
catch (err) {
|
|
798
|
+
console.log(err)
|
|
799
|
+
console.log(`can not generate ${fnEntryOut}`)
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
//apis
|
|
804
|
+
let apis = [
|
|
805
|
+
|
|
806
|
+
{
|
|
807
|
+
method: 'GET',
|
|
808
|
+
path: '/api/getUserByToken', //未登入主頁時需先檢測token, getUserByToken為w-ui-loginout預設值, 若要更改兩邊須同時修改
|
|
809
|
+
handler: async function (req, res) {
|
|
810
|
+
// console.log('getUserByToken', req)
|
|
811
|
+
|
|
812
|
+
async function core() {
|
|
813
|
+
//提供對象為browser使用者, 須先檢測token是否有效, 確認後回傳該使用者之資訊物件
|
|
814
|
+
|
|
815
|
+
//token
|
|
816
|
+
let token = get(req, 'query.token', '')
|
|
817
|
+
// console.log('token', token)
|
|
818
|
+
|
|
819
|
+
//check
|
|
820
|
+
if (!isestr(token)) {
|
|
821
|
+
console.log('req.query', get(req, 'query'))
|
|
822
|
+
console.log('token', token)
|
|
823
|
+
console.log('[API]getUserByToken/check token: invalid token')
|
|
824
|
+
srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
|
|
825
|
+
return Promise.reject(`tokenNoPermission`)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
//getAndVerifyClientUser
|
|
829
|
+
let user = await getAndVerifyClientUser(token, 'getUserByToken')
|
|
830
|
+
// console.log('user', user)
|
|
831
|
+
|
|
832
|
+
//check
|
|
833
|
+
if (!iseobj(user)) {
|
|
834
|
+
console.log('token', token)
|
|
835
|
+
console.log('[API]getUserByToken/check user: invalid user')
|
|
836
|
+
srLog.error({ event: 'api/getUserByToken-error', msg: 'tokenNoPermission' })
|
|
837
|
+
return Promise.reject(`tokenNoPermission`)
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
//info
|
|
841
|
+
srLog.info({ event: 'api/getUserByToken-success', userId: get(user, 'id', '') })
|
|
842
|
+
|
|
843
|
+
return user
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
//pm2resolve core
|
|
847
|
+
let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 須另提取state進行判斷
|
|
848
|
+
if (isErr(r.msg)) {
|
|
849
|
+
r.msg = r.msg.message
|
|
850
|
+
}
|
|
851
|
+
// console.log('getUserByToken', r)
|
|
852
|
+
|
|
853
|
+
return r
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
method: 'GET',
|
|
858
|
+
path: '/api/getPerm',
|
|
859
|
+
handler: async function (req, res) {
|
|
860
|
+
// console.log('getPerm', req)
|
|
861
|
+
|
|
862
|
+
async function core() {
|
|
863
|
+
//提供對象為node與browser端, 須為client使用者, 須先檢測token是否有效, 再依照token取得使用者資訊物件, 再由其userId查詢回傳該使用者之權限資訊物件
|
|
864
|
+
|
|
865
|
+
//token
|
|
866
|
+
let token = get(req, 'query.token', '')
|
|
867
|
+
// console.log('token', token)
|
|
868
|
+
|
|
869
|
+
//check
|
|
870
|
+
if (!isestr(token)) {
|
|
871
|
+
console.log('req.query', get(req, 'query'))
|
|
872
|
+
console.log('token', token)
|
|
873
|
+
console.log('[API]getPerm/check token: invalid token')
|
|
874
|
+
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
875
|
+
return Promise.reject(`tokenNoPermission`)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
//getAndVerifyClientUser, 發起token的所屬使用者, 驗證僅對client使用者, 不得使用app使用者
|
|
879
|
+
let userSelf = await getAndVerifyClientUser(token, 'getPerm')
|
|
880
|
+
// console.log('userSelf', userSelf)
|
|
881
|
+
|
|
882
|
+
//check
|
|
883
|
+
if (!iseobj(userSelf)) {
|
|
884
|
+
console.log('token', token)
|
|
885
|
+
console.log('[API]getPerm/check userSelf: invalid userSelf')
|
|
886
|
+
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
887
|
+
return Promise.reject(`tokenNoPermission`)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
//userIdSelf, client使用者的userId
|
|
891
|
+
let userIdSelf = get(userSelf, 'id', '')
|
|
892
|
+
// console.log('userIdSelf', userIdSelf)
|
|
893
|
+
|
|
894
|
+
//check
|
|
895
|
+
if (!isestr(userIdSelf)) {
|
|
896
|
+
console.log('token', token)
|
|
897
|
+
console.log('userSelf', userSelf)
|
|
898
|
+
console.log('[API]getPerm/check userIdSelf: invalid userIdSelf')
|
|
899
|
+
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
900
|
+
return Promise.reject(`tokenNoPermission`)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
//getGenUserAndRulesByUserId, 查找到的client使用者與權限
|
|
904
|
+
let userWithRulesSelf = await getGenUserAndRulesByUserId(userIdSelf)
|
|
905
|
+
// console.log('getGenUserAndRulesByUserId', r)
|
|
906
|
+
|
|
907
|
+
//check
|
|
908
|
+
if (!iseobj(userWithRulesSelf)) {
|
|
909
|
+
console.log('token', token)
|
|
910
|
+
console.log('userSelf', userSelf)
|
|
911
|
+
console.log('userIdSelf', userIdSelf)
|
|
912
|
+
console.log('[API]getPerm/check userWithRulesSelf: invalid userWithRulesSelf')
|
|
913
|
+
srLog.error({ event: 'api/getPerm-error', msg: 'tokenNoPermission' })
|
|
914
|
+
return Promise.reject(`tokenNoPermission`)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
//info
|
|
918
|
+
srLog.info({ event: 'api/getPerm-success', userId: userIdSelf })
|
|
919
|
+
|
|
920
|
+
return userWithRulesSelf
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
//pm2resolve core
|
|
924
|
+
let r = await pm2resolve(core)()
|
|
925
|
+
if (isErr(r.msg)) {
|
|
926
|
+
r.msg = r.msg.message
|
|
927
|
+
}
|
|
928
|
+
// console.log('getPerm r', r)
|
|
929
|
+
|
|
930
|
+
return r
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
method: 'GET',
|
|
935
|
+
path: '/api/getPermUserInfor',
|
|
936
|
+
handler: async function (req, res) {
|
|
937
|
+
// console.log('getPermUserInfor', req)
|
|
938
|
+
|
|
939
|
+
async function core() {
|
|
940
|
+
//提供對象為node端, 須為app使用者, 須先檢測token是否有效, 再由其userId查詢回傳該使用者權限資訊物件
|
|
941
|
+
|
|
942
|
+
//tokenSelf
|
|
943
|
+
let tokenSelf = get(req, 'query.token', '')
|
|
944
|
+
// console.log('token', token)
|
|
945
|
+
|
|
946
|
+
//check
|
|
947
|
+
if (!isestr(tokenSelf)) {
|
|
948
|
+
console.log('req.query', get(req, 'query'))
|
|
949
|
+
console.log('[API]getPermUserInfor/check tokenSelf: invalid tokenSelf')
|
|
950
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
951
|
+
return Promise.reject(`tokenNoPermission`)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
//getAndVerifyAppUser, 發起token的所屬使用者, 驗證須為app使用者
|
|
955
|
+
let userSelf = await getAndVerifyAppUser(tokenSelf, 'getPermUserInfor')
|
|
956
|
+
// console.log('userSelf', userSelf)
|
|
957
|
+
|
|
958
|
+
//check
|
|
959
|
+
if (!iseobj(userSelf)) {
|
|
960
|
+
console.log('req.query', get(req, 'query'))
|
|
961
|
+
console.log('tokenSelf', tokenSelf)
|
|
962
|
+
console.log('[API]getPermUserInfor/check userSelf: invalid userSelf')
|
|
963
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
964
|
+
return Promise.reject(`tokenNoPermission`)
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
//userIdFind, 欲查找使用者的userId
|
|
968
|
+
let userIdFind = get(req, 'query.userId', '')
|
|
969
|
+
// console.log('userIdFind', userIdFind)
|
|
970
|
+
|
|
971
|
+
//check
|
|
972
|
+
if (!isestr(userIdFind)) {
|
|
973
|
+
console.log('req.query', get(req, 'query'))
|
|
974
|
+
console.log('tokenSelf', tokenSelf)
|
|
975
|
+
console.log('[API]getPermUserInfor/check userIdFind: invalid userIdFind')
|
|
976
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'tokenNoPermission' })
|
|
977
|
+
return Promise.reject(`tokenNoPermission`)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
//getGenUserAndRulesByUserId, 查找到的使用者與權限
|
|
981
|
+
let userWithRulesFind = await getGenUserAndRulesByUserId(userIdFind)
|
|
982
|
+
// console.log('getGenUserAndRulesByUserId', userWithRulesFind)
|
|
983
|
+
|
|
984
|
+
//check
|
|
985
|
+
if (!iseobj(userWithRulesFind)) {
|
|
986
|
+
console.log('userIdFind', userIdFind)
|
|
987
|
+
console.log('[API]getPermUserInfor/check userWithRulesFind: invalid userWithRulesFind')
|
|
988
|
+
srLog.error({ event: 'api/getPermUserInfor-error', msg: 'userIdNoPermrules' })
|
|
989
|
+
return Promise.reject(`userIdNoPermrules`)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
//info
|
|
993
|
+
srLog.info({ event: 'api/getPermUserInfor-success', userId: userIdFind })
|
|
994
|
+
|
|
995
|
+
return userWithRulesFind
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
//pm2resolve core
|
|
999
|
+
let r = await pm2resolve(core)() //w-ui-loginout接收已預設格式用pm2resolve轉過, 須另提取state進行判斷
|
|
1000
|
+
if (isErr(r.msg)) {
|
|
1001
|
+
r.msg = r.msg.message
|
|
1002
|
+
}
|
|
1003
|
+
// console.log('getPermUserInfor', r)
|
|
1004
|
+
|
|
1005
|
+
return r
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
method: 'POST',
|
|
1010
|
+
path: '/syncAndReplaceTabs',
|
|
1011
|
+
handler: async function (req, res) {
|
|
1012
|
+
// console.log('syncAndReplaceTabs', req)
|
|
1013
|
+
// console.log('syncAndReplaceTabs payload', req.payload)
|
|
1014
|
+
|
|
1015
|
+
async function core() {
|
|
1016
|
+
//提供對象為node使用者, 須為系統管理者, 須先檢測token是否有效, 再進行數據變更
|
|
1017
|
+
|
|
1018
|
+
//token
|
|
1019
|
+
let token = get(req, 'query.token', '')
|
|
1020
|
+
|
|
1021
|
+
//check
|
|
1022
|
+
if (!isestr(token)) {
|
|
1023
|
+
console.log('req.query', get(req, 'query'))
|
|
1024
|
+
console.log('token', token)
|
|
1025
|
+
console.log('[API]syncAndReplaceTabs/check token: invalid token')
|
|
1026
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
1027
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
//keyTable
|
|
1031
|
+
let keyTable = get(req, 'query.keyTable', '')
|
|
1032
|
+
|
|
1033
|
+
//check
|
|
1034
|
+
if (!isestr(keyTable)) {
|
|
1035
|
+
console.log('req.query', get(req, 'query'))
|
|
1036
|
+
console.log('keyTable', keyTable)
|
|
1037
|
+
console.log('[API]syncAndReplaceTabs/check keyTable: invalid keyTable')
|
|
1038
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
1039
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
//check keyTable
|
|
1043
|
+
let allowedTables = ['targets', 'pemis', 'grups', 'users']
|
|
1044
|
+
if (!allowedTables.includes(keyTable)) {
|
|
1045
|
+
console.log('[API]syncAndReplaceTabs/check keyTable: unexpected keyTable', keyTable)
|
|
1046
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'invalidKeyTable', keyTable })
|
|
1047
|
+
return Promise.reject(`invalidKeyTable`)
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
//getAndVerifyAppUser
|
|
1051
|
+
let user = await getAndVerifyAppUser(token, 'syncAndReplaceTabs')
|
|
1052
|
+
|
|
1053
|
+
//check user
|
|
1054
|
+
if (!iseobj(user)) {
|
|
1055
|
+
console.log('token', token)
|
|
1056
|
+
console.log('[API]syncAndReplaceTabs/check user: invalid user')
|
|
1057
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
1058
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
//userId
|
|
1062
|
+
let userId = get(user, 'id', '')
|
|
1063
|
+
|
|
1064
|
+
//check userId
|
|
1065
|
+
if (!isestr(userId)) {
|
|
1066
|
+
console.log('token', token)
|
|
1067
|
+
console.log('user', user)
|
|
1068
|
+
console.log('[API]syncAndReplaceTabs/check userId: invalid userId')
|
|
1069
|
+
srLog.error({ event: 'api/syncAndReplaceTabs-error', msg: 'tokenNoPermission' })
|
|
1070
|
+
return Promise.reject(`tokenNoPermission`)
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
//parsePayload
|
|
1074
|
+
let inp = await parsePayload(req)
|
|
1075
|
+
|
|
1076
|
+
//syncAndReplaceTabs
|
|
1077
|
+
let r = await syncAndReplaceTabs(userId, inp, keyTable)
|
|
1078
|
+
|
|
1079
|
+
//info
|
|
1080
|
+
srLog.info({ event: 'api/syncAndReplaceTabs-success', userId, keyTable })
|
|
1081
|
+
|
|
1082
|
+
return r
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
//pm2resolve core
|
|
1086
|
+
let r = await pm2resolve(core)()
|
|
1087
|
+
if (isErr(r.msg)) {
|
|
1088
|
+
r.msg = r.msg.message
|
|
1089
|
+
}
|
|
1090
|
+
// console.log('syncAndReplaceTabs', r)
|
|
1091
|
+
|
|
1092
|
+
return r
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
]
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
//tableNamesExec, tableNamesSync
|
|
1099
|
+
let tableNamesExec = keys(ds)
|
|
1100
|
+
// let tableNamesSync = filter(tableNamesExec, (v) => {
|
|
1101
|
+
// return strright(v, 5) !== 'Items'//不同步數據
|
|
1102
|
+
// })
|
|
1103
|
+
let tableNamesSync = tableNamesExec
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
//WServHapiServer
|
|
1107
|
+
instWServHapiServer = new WServHapiServer({
|
|
1108
|
+
port: opt.serverPort,
|
|
1109
|
+
pathStaticFiles,
|
|
1110
|
+
apiName: 'api',
|
|
1111
|
+
apis,
|
|
1112
|
+
tokenType: 'Bearer',
|
|
1113
|
+
verifyConn: async ({ apiType, authorization, query, headers, req }) => {
|
|
1114
|
+
// console.log('verifyConn', `apiType[${apiType}]`, `authorization[${authorization}]`)
|
|
1115
|
+
|
|
1116
|
+
//token
|
|
1117
|
+
let token = strdelleft(authorization, 7) //刪除Bearer
|
|
1118
|
+
// console.log('token', token)
|
|
1119
|
+
|
|
1120
|
+
//check
|
|
1121
|
+
if (!isestr(token)) {
|
|
1122
|
+
console.log('apiType', apiType)
|
|
1123
|
+
console.log('authorization', authorization)
|
|
1124
|
+
console.log('invalid token')
|
|
1125
|
+
return false
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
//checkToken
|
|
1129
|
+
let b = await p.checkToken(token)
|
|
1130
|
+
// console.log('b', b)
|
|
1131
|
+
|
|
1132
|
+
//ip, 對齊sso慣例x-forwarded-for取第1個, fallback到req remoteAddress, 取不到則略過
|
|
1133
|
+
let ip = get(headers, 'x-forwarded-for', '').split(',')[0].trim()
|
|
1134
|
+
if (!isestr(ip)) {
|
|
1135
|
+
ip = get(req, 'info.remoteAddress', '') || get(req, 'socket.remoteAddress', '') || get(req, 'connection.remoteAddress', '')
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
//origin
|
|
1139
|
+
let origin = get(headers, 'origin', '')
|
|
1140
|
+
|
|
1141
|
+
//referer
|
|
1142
|
+
let referer = get(headers, 'referer', '')
|
|
1143
|
+
|
|
1144
|
+
//info
|
|
1145
|
+
srLog.info({ event: 'verifyConn', ok: b, apiType, ip, origin, referer })
|
|
1146
|
+
|
|
1147
|
+
return b
|
|
1148
|
+
},
|
|
1149
|
+
getUserIdByToken: async (token) => { //可使用async或sync函數
|
|
1150
|
+
// console.log('getUserIdByToken', token)
|
|
1151
|
+
let user = await getUserByToken(token)
|
|
1152
|
+
let userId = get(user, 'id', '')
|
|
1153
|
+
if (!isestr(userId)) {
|
|
1154
|
+
console.log('token', token)
|
|
1155
|
+
console.log('userId', userId)
|
|
1156
|
+
srLog.error({ event: 'getUserIdByToken-error', msg: 'cannotFindUserId' })
|
|
1157
|
+
return Promise.reject(`cannotFindUserId`)
|
|
1158
|
+
}
|
|
1159
|
+
return userId
|
|
1160
|
+
},
|
|
1161
|
+
corsOrigins: ['*'],
|
|
1162
|
+
useDbOrm: true,
|
|
1163
|
+
kpOrm: woItems,
|
|
1164
|
+
operOrm: procOrm, //procOrm的輸入為: userId, tableName, methodName, input
|
|
1165
|
+
tableNamesExec,
|
|
1166
|
+
methodsExec: ['select', 'insert', 'save', 'del', 'delAll'], //mix需於procOrm內註冊以提供
|
|
1167
|
+
tableNamesSync,
|
|
1168
|
+
kpFunExt: { //接收參數第1個為userId, 之後才是前端給予參數
|
|
1169
|
+
|
|
1170
|
+
getWebInfor,
|
|
1171
|
+
|
|
1172
|
+
// getTargetsList,
|
|
1173
|
+
// getPemisList,
|
|
1174
|
+
// getGrupsList,
|
|
1175
|
+
// getUsersList,
|
|
1176
|
+
|
|
1177
|
+
updateTargets: pc.updateTargets,
|
|
1178
|
+
updatePemis: pc.updatePemis,
|
|
1179
|
+
updateGrups: pc.updateGrups,
|
|
1180
|
+
updateUsers: pc.updateUsers,
|
|
1181
|
+
|
|
1182
|
+
getStaEvent: async (userId, timeLength, timeInterval) => {
|
|
1183
|
+
let rs = await psi.getStaEvent(userId, timeLength, timeInterval)
|
|
1184
|
+
srLog.info({ event: 'getStaEvent-success', userId })
|
|
1185
|
+
return rs
|
|
1186
|
+
},
|
|
1187
|
+
|
|
1188
|
+
getStaEventTable: async (userId) => {
|
|
1189
|
+
let rs = await psi.getStaEventTable(userId)
|
|
1190
|
+
srLog.info({ event: 'getStaEventTable-success', userId })
|
|
1191
|
+
return rs
|
|
1192
|
+
},
|
|
1193
|
+
|
|
1194
|
+
},
|
|
1195
|
+
fnTableTags: 'tableTags-web-perm.json',
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
return instWServHapiServer
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
export default WWebPerm
|
|
1204
|
+
</code></pre>
|
|
1205
|
+
</article>
|
|
1206
|
+
</section>
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
</div>
|
|
1214
|
+
|
|
1215
|
+
<br class="clear">
|
|
1216
|
+
|
|
1217
|
+
<footer>
|
|
1218
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Mon Jun 22 2026 15:47:48 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
1219
|
+
</footer>
|
|
1220
|
+
|
|
1221
|
+
<script>prettyPrint();</script>
|
|
1222
|
+
<script src="scripts/polyfill.js"></script>
|
|
1223
|
+
<script src="scripts/linenumber.js"></script>
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
</body>
|
|
1228
|
+
</html>
|