lupine.api 1.0.41
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/LICENSE +21 -0
- package/README.md +3 -0
- package/admin/admin-about.tsx +16 -0
- package/admin/admin-config.tsx +44 -0
- package/admin/admin-css.tsx +3 -0
- package/admin/admin-db.tsx +74 -0
- package/admin/admin-frame-props.tsx +9 -0
- package/admin/admin-frame.tsx +466 -0
- package/admin/admin-index.tsx +66 -0
- package/admin/admin-login.tsx +99 -0
- package/admin/admin-menu-edit.tsx +637 -0
- package/admin/admin-menu-list.tsx +87 -0
- package/admin/admin-page-edit.tsx +564 -0
- package/admin/admin-page-list.tsx +83 -0
- package/admin/admin-performance.tsx +28 -0
- package/admin/admin-release.tsx +320 -0
- package/admin/admin-resources.tsx +385 -0
- package/admin/admin-shell.tsx +89 -0
- package/admin/admin-table-data.tsx +146 -0
- package/admin/admin-table-list.tsx +231 -0
- package/admin/admin-test-animations.tsx +379 -0
- package/admin/admin-test-component.tsx +808 -0
- package/admin/admin-test-edit.tsx +319 -0
- package/admin/admin-test-themes.tsx +56 -0
- package/admin/admin-tokens.tsx +338 -0
- package/admin/design/admin-design.tsx +174 -0
- package/admin/design/block-grid.tsx +36 -0
- package/admin/design/block-grid1.tsx +21 -0
- package/admin/design/block-paragraph.tsx +19 -0
- package/admin/design/block-title.tsx +19 -0
- package/admin/design/design-block-box.tsx +140 -0
- package/admin/design/drag-data.tsx +24 -0
- package/admin/index.ts +6 -0
- package/admin/package.json +15 -0
- package/admin/tsconfig.json +127 -0
- package/dev/copy-folder.js +32 -0
- package/dev/cp-index-html.js +69 -0
- package/dev/file-utils.js +12 -0
- package/dev/index.js +19 -0
- package/dev/package.json +12 -0
- package/dev/plugin-gen-versions.js +20 -0
- package/dev/plugin-ifelse.js +155 -0
- package/dev/plugin-ifelse.test.js +37 -0
- package/dev/run-cmd.js +14 -0
- package/dev/send-request.js +12 -0
- package/package.json +55 -0
- package/src/admin-api/admin-api.ts +59 -0
- package/src/admin-api/admin-auth.ts +87 -0
- package/src/admin-api/admin-config.ts +93 -0
- package/src/admin-api/admin-csv.ts +81 -0
- package/src/admin-api/admin-db.ts +269 -0
- package/src/admin-api/admin-helper.ts +111 -0
- package/src/admin-api/admin-menu.ts +135 -0
- package/src/admin-api/admin-page.ts +135 -0
- package/src/admin-api/admin-performance.ts +128 -0
- package/src/admin-api/admin-release.ts +498 -0
- package/src/admin-api/admin-resources.ts +318 -0
- package/src/admin-api/admin-token-helper.ts +79 -0
- package/src/admin-api/admin-tokens.ts +90 -0
- package/src/admin-api/index.ts +2 -0
- package/src/api/api-cache.ts +103 -0
- package/src/api/api-helper.ts +44 -0
- package/src/api/api-module.ts +60 -0
- package/src/api/api-router.ts +177 -0
- package/src/api/api-shared-storage.ts +64 -0
- package/src/api/async-storage.ts +5 -0
- package/src/api/debug-service.ts +56 -0
- package/src/api/encode-html.ts +27 -0
- package/src/api/handle-status.ts +71 -0
- package/src/api/index.ts +16 -0
- package/src/api/mini-web-socket.ts +270 -0
- package/src/api/server-content-type.ts +82 -0
- package/src/api/server-render.ts +216 -0
- package/src/api/shell-service.ts +66 -0
- package/src/api/simple-storage.ts +80 -0
- package/src/api/static-server.ts +125 -0
- package/src/api/to-client-delivery.ts +26 -0
- package/src/app/app-cache.ts +55 -0
- package/src/app/app-loader.ts +62 -0
- package/src/app/app-message.ts +60 -0
- package/src/app/app-shared-storage.ts +317 -0
- package/src/app/app-start.ts +117 -0
- package/src/app/cleanup-exit.ts +12 -0
- package/src/app/host-to-path.ts +38 -0
- package/src/app/index.ts +11 -0
- package/src/app/process-dev-requests.ts +90 -0
- package/src/app/web-listener.ts +230 -0
- package/src/app/web-processor.ts +42 -0
- package/src/app/web-server.ts +86 -0
- package/src/common-js/web-env.js +104 -0
- package/src/index.ts +7 -0
- package/src/lang/api-lang-en.ts +27 -0
- package/src/lang/api-lang-zh-cn.ts +28 -0
- package/src/lang/index.ts +2 -0
- package/src/lang/lang-helper.ts +76 -0
- package/src/lang/lang-props.ts +6 -0
- package/src/lib/db/db-helper.ts +23 -0
- package/src/lib/db/db-mysql.ts +250 -0
- package/src/lib/db/db-sqlite.ts +101 -0
- package/src/lib/db/db.spec.ts +28 -0
- package/src/lib/db/db.ts +304 -0
- package/src/lib/db/index.ts +5 -0
- package/src/lib/index.ts +3 -0
- package/src/lib/logger.spec.ts +214 -0
- package/src/lib/logger.ts +274 -0
- package/src/lib/runtime-require.ts +37 -0
- package/src/lib/utils/cookie-util.ts +34 -0
- package/src/lib/utils/crypto.ts +58 -0
- package/src/lib/utils/date-utils.ts +317 -0
- package/src/lib/utils/deep-merge.ts +37 -0
- package/src/lib/utils/delay.ts +12 -0
- package/src/lib/utils/file-setting.ts +55 -0
- package/src/lib/utils/format-bytes.ts +11 -0
- package/src/lib/utils/fs-utils.ts +144 -0
- package/src/lib/utils/get-env.ts +27 -0
- package/src/lib/utils/index.ts +12 -0
- package/src/lib/utils/is-type.ts +48 -0
- package/src/lib/utils/load-env.ts +14 -0
- package/src/lib/utils/pad.ts +6 -0
- package/src/models/api-base.ts +5 -0
- package/src/models/api-module-props.ts +11 -0
- package/src/models/api-router-props.ts +26 -0
- package/src/models/app-cache-props.ts +33 -0
- package/src/models/app-data-props.ts +10 -0
- package/src/models/app-loader-props.ts +6 -0
- package/src/models/app-shared-storage-props.ts +37 -0
- package/src/models/app-start-props.ts +18 -0
- package/src/models/async-storage-props.ts +13 -0
- package/src/models/db-config.ts +30 -0
- package/src/models/host-to-path-props.ts +12 -0
- package/src/models/index.ts +16 -0
- package/src/models/json-object.ts +8 -0
- package/src/models/locals-props.ts +36 -0
- package/src/models/logger-props.ts +84 -0
- package/src/models/simple-storage-props.ts +14 -0
- package/src/models/to-client-delivery-props.ts +6 -0
- package/tsconfig.json +115 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { ServerResponse } from 'http';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import {
|
|
4
|
+
IApiBase,
|
|
5
|
+
Logger,
|
|
6
|
+
apiCache,
|
|
7
|
+
ServerRequest,
|
|
8
|
+
ApiRouter,
|
|
9
|
+
ApiHelper,
|
|
10
|
+
langHelper,
|
|
11
|
+
FsUtils,
|
|
12
|
+
adminHelper,
|
|
13
|
+
} from 'lupine.api';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
|
|
16
|
+
export class AdminResources implements IApiBase {
|
|
17
|
+
private logger = new Logger('resources-api');
|
|
18
|
+
protected router = new ApiRouter();
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
this.mountDashboard();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public getRouter(): ApiRouter {
|
|
25
|
+
return this.router;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected mountDashboard() {
|
|
29
|
+
// called by FE
|
|
30
|
+
this.router.use('/data', this.data.bind(this));
|
|
31
|
+
this.router.use('/download', this.download.bind(this));
|
|
32
|
+
this.router.use('/upload', this.upload.bind(this));
|
|
33
|
+
this.router.use('/rename', this.rename.bind(this));
|
|
34
|
+
this.router.use('/newFolder', this.newFolder.bind(this));
|
|
35
|
+
this.router.use('/remove', this.remove.bind(this));
|
|
36
|
+
this.router.use('/removeDir', this.removeDir.bind(this));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async upload(req: ServerRequest, res: ServerResponse) {
|
|
40
|
+
const fPath = req.locals.query.get('p');
|
|
41
|
+
const fName = req.locals.query.get('n');
|
|
42
|
+
const data = req.locals.body;
|
|
43
|
+
if (!data || data.length < 1 || !fPath || !fName) {
|
|
44
|
+
const response = {
|
|
45
|
+
status: 'error',
|
|
46
|
+
message: langHelper.getLang('shared:wrong_data'),
|
|
47
|
+
};
|
|
48
|
+
ApiHelper.sendJson(req, res, response);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const appData = apiCache.getAppData();
|
|
53
|
+
const locPath = this.pathJoin(fPath, appData.webPath);
|
|
54
|
+
if (!(await FsUtils.pathExist(locPath))) {
|
|
55
|
+
const response = {
|
|
56
|
+
status: 'error',
|
|
57
|
+
message: langHelper.getLang('shared:wrong_data'),
|
|
58
|
+
};
|
|
59
|
+
ApiHelper.sendJson(req, res, response);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const key = req.locals.query.get('key') as string;
|
|
64
|
+
const chunkNumberStr = req.locals.query.get('chunkNumber') as string;
|
|
65
|
+
const chunkNumber = parseInt(chunkNumberStr);
|
|
66
|
+
const totalChunks = parseInt(req.locals.query.get('totalChunks') as string);
|
|
67
|
+
const decryptedKey = key && adminHelper.decryptJson(key.replace(/ /g, '+'));
|
|
68
|
+
const keyNG =
|
|
69
|
+
!chunkNumberStr ||
|
|
70
|
+
!totalChunks ||
|
|
71
|
+
(chunkNumber !== 0 && (!decryptedKey || decryptedKey.ind !== chunkNumber || decryptedKey.cnt !== totalChunks));
|
|
72
|
+
if (keyNG) {
|
|
73
|
+
const response = {
|
|
74
|
+
status: 'error',
|
|
75
|
+
message: langHelper.getLang('shared:permission_denied'),
|
|
76
|
+
};
|
|
77
|
+
ApiHelper.sendJson(req, res, response);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
const MAX_FILE_SIZE = 1024 * 1024 * 800; // MB
|
|
81
|
+
if (totalChunks * data.length > MAX_FILE_SIZE) {
|
|
82
|
+
const response = {
|
|
83
|
+
status: 'error',
|
|
84
|
+
message: langHelper.getLang('shared:file_too_large'),
|
|
85
|
+
};
|
|
86
|
+
ApiHelper.sendJson(req, res, response);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const filename = path.join(fPath, fName);
|
|
91
|
+
// write data to a file
|
|
92
|
+
if (chunkNumber === 0) {
|
|
93
|
+
await fs.writeFile(filename, data, 'binary');
|
|
94
|
+
} else {
|
|
95
|
+
await fs.appendFile(filename, data, 'binary');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const response = {
|
|
99
|
+
status: 'ok',
|
|
100
|
+
chunkNumber,
|
|
101
|
+
totalChunks,
|
|
102
|
+
message: langHelper.getLang('shared:file_part_updated'),
|
|
103
|
+
key: adminHelper.encryptJson({ ind: chunkNumber + 1, cnt: totalChunks, t: new Date().getTime() }),
|
|
104
|
+
};
|
|
105
|
+
ApiHelper.sendJson(req, res, response);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private chkData(data: any, req: ServerRequest, res: ServerResponse, chkCredential: boolean) {
|
|
110
|
+
if (!data || Array.isArray(data) || typeof data !== 'object') {
|
|
111
|
+
this.logger.error(`chkData, missing parameters`, data);
|
|
112
|
+
const response = {
|
|
113
|
+
status: 'error',
|
|
114
|
+
message: 'Wrong data [missing parameters].', //langHelper.getLang('shared:wrong_data'),
|
|
115
|
+
};
|
|
116
|
+
ApiHelper.sendJson(req, res, response);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async getFolders(locPath: string) {
|
|
123
|
+
const results: any[] = [];
|
|
124
|
+
const folders = await FsUtils.getDirAndFiles(locPath);
|
|
125
|
+
|
|
126
|
+
results.push({
|
|
127
|
+
fullPath: locPath,
|
|
128
|
+
});
|
|
129
|
+
for (let i = 0; i < folders.length; i++) {
|
|
130
|
+
const oneFolder = folders[i];
|
|
131
|
+
const fileInfo = await FsUtils.fileInfo(path.join(locPath, oneFolder));
|
|
132
|
+
if (fileInfo?.isFile) {
|
|
133
|
+
results.push({
|
|
134
|
+
name: oneFolder,
|
|
135
|
+
time: new Date(fileInfo!.mtime).toLocaleString(),
|
|
136
|
+
size: fileInfo?.size,
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
const subFolders = await FsUtils.getDirAndFiles(path.join(locPath, oneFolder));
|
|
140
|
+
const subFoldersWithTime = [];
|
|
141
|
+
for (let j = 0; j < subFolders.length; j++) {
|
|
142
|
+
const subFile = subFolders[j];
|
|
143
|
+
const fileInfo2 = await FsUtils.fileInfo(path.join(locPath, oneFolder, subFile));
|
|
144
|
+
|
|
145
|
+
if (fileInfo2?.isFile) {
|
|
146
|
+
subFoldersWithTime.push({
|
|
147
|
+
name: subFile,
|
|
148
|
+
time: new Date(fileInfo2.mtime).toLocaleString(),
|
|
149
|
+
size: fileInfo2?.size,
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
subFoldersWithTime.push({
|
|
153
|
+
name: subFile,
|
|
154
|
+
time: fileInfo2?.mtime && new Date(fileInfo2.mtime).toLocaleString(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
results.push({
|
|
159
|
+
name: oneFolder,
|
|
160
|
+
time: fileInfo?.mtime && new Date(fileInfo.mtime).toLocaleString(),
|
|
161
|
+
items: subFoldersWithTime,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private pathJoin(folder: string, apiPath: string) {
|
|
169
|
+
return folder !== '/' && folder !== '' && (folder.startsWith('/') || folder.startsWith('\\') || folder[1] === ':')
|
|
170
|
+
? path.join(folder)
|
|
171
|
+
: path.join(apiPath, folder);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async data(req: ServerRequest, res: ServerResponse) {
|
|
175
|
+
const data = this.chkData(req.locals.json(), req, res, false);
|
|
176
|
+
if (!data) return true;
|
|
177
|
+
let results: any[] = [];
|
|
178
|
+
if (typeof data.folder === 'string') {
|
|
179
|
+
const appData = apiCache.getAppData();
|
|
180
|
+
const locPath = this.pathJoin(data.folder, appData.webPath);
|
|
181
|
+
results = await this.getFolders(locPath);
|
|
182
|
+
}
|
|
183
|
+
const response = {
|
|
184
|
+
status: 'ok',
|
|
185
|
+
message: 'OK',
|
|
186
|
+
results,
|
|
187
|
+
};
|
|
188
|
+
ApiHelper.sendJson(req, res, response);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// called by clients
|
|
193
|
+
async download(req: ServerRequest, res: ServerResponse) {
|
|
194
|
+
const data = this.chkData(req.locals.json(), req, res, true);
|
|
195
|
+
if (!data) return true;
|
|
196
|
+
|
|
197
|
+
if (typeof data.resource === 'string') {
|
|
198
|
+
const resPath = path.join(data.resource);
|
|
199
|
+
if (await FsUtils.pathExist(resPath)) {
|
|
200
|
+
ApiHelper.sendFile(req, res, resPath);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const response = {
|
|
205
|
+
status: 'error',
|
|
206
|
+
message: 'Resource not found',
|
|
207
|
+
};
|
|
208
|
+
ApiHelper.sendJson(req, res, response);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async rename(req: ServerRequest, res: ServerResponse) {
|
|
213
|
+
const data = this.chkData(req.locals.json(), req, res, true);
|
|
214
|
+
if (!data) return true;
|
|
215
|
+
|
|
216
|
+
if (typeof data.resource === 'string' && data.oldName && data.newName) {
|
|
217
|
+
const resPath = path.join(data.resource);
|
|
218
|
+
const basePath = path.dirname(resPath);
|
|
219
|
+
const oldPath = path.join(basePath, data.oldName);
|
|
220
|
+
if (await FsUtils.pathExist(oldPath)) {
|
|
221
|
+
const newPath = path.join(basePath, data.newName);
|
|
222
|
+
await FsUtils.rename(oldPath, newPath);
|
|
223
|
+
if (await FsUtils.pathExist(newPath)) {
|
|
224
|
+
const response = {
|
|
225
|
+
status: 'ok',
|
|
226
|
+
message: 'Resource renamed to ' + newPath,
|
|
227
|
+
};
|
|
228
|
+
ApiHelper.sendJson(req, res, response);
|
|
229
|
+
} else {
|
|
230
|
+
const response = {
|
|
231
|
+
status: 'error',
|
|
232
|
+
message: 'Resource not renamed',
|
|
233
|
+
};
|
|
234
|
+
ApiHelper.sendJson(req, res, response);
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const response = {
|
|
240
|
+
status: 'error',
|
|
241
|
+
message: 'Resource not found',
|
|
242
|
+
};
|
|
243
|
+
ApiHelper.sendJson(req, res, response);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async remove(req: ServerRequest, res: ServerResponse) {
|
|
248
|
+
const data = this.chkData(req.locals.json(), req, res, true);
|
|
249
|
+
if (!data) return true;
|
|
250
|
+
|
|
251
|
+
if (typeof data.resource === 'string') {
|
|
252
|
+
const resPath = path.join(data.resource);
|
|
253
|
+
if (await FsUtils.pathExist(resPath)) {
|
|
254
|
+
await FsUtils.unlinkFile(resPath);
|
|
255
|
+
const response = {
|
|
256
|
+
status: 'ok',
|
|
257
|
+
message: 'Resource removed',
|
|
258
|
+
};
|
|
259
|
+
ApiHelper.sendJson(req, res, response);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const response = {
|
|
264
|
+
status: 'error',
|
|
265
|
+
message: 'Resource not found',
|
|
266
|
+
};
|
|
267
|
+
ApiHelper.sendJson(req, res, response);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async removeDir(req: ServerRequest, res: ServerResponse) {
|
|
272
|
+
const data = this.chkData(req.locals.json(), req, res, true);
|
|
273
|
+
if (!data) return true;
|
|
274
|
+
|
|
275
|
+
if (typeof data.resource === 'string') {
|
|
276
|
+
const resPath = path.join(data.resource);
|
|
277
|
+
if (await FsUtils.pathExist(resPath)) {
|
|
278
|
+
await FsUtils.unlinkFolderEmpty(resPath);
|
|
279
|
+
const response = {
|
|
280
|
+
status: 'ok',
|
|
281
|
+
message: 'Directory removed',
|
|
282
|
+
};
|
|
283
|
+
ApiHelper.sendJson(req, res, response);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const response = {
|
|
288
|
+
status: 'error',
|
|
289
|
+
message: 'Directory not found or has files',
|
|
290
|
+
};
|
|
291
|
+
ApiHelper.sendJson(req, res, response);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async newFolder(req: ServerRequest, res: ServerResponse) {
|
|
296
|
+
const data = this.chkData(req.locals.json(), req, res, true);
|
|
297
|
+
if (!data) return true;
|
|
298
|
+
|
|
299
|
+
if (typeof data.resource === 'string' && data.newName) {
|
|
300
|
+
const resPath = path.join(data.resource, data.newName);
|
|
301
|
+
if (!await FsUtils.pathExist(resPath)) {
|
|
302
|
+
await FsUtils.mkdir(resPath);
|
|
303
|
+
const response = {
|
|
304
|
+
status: 'ok',
|
|
305
|
+
message: 'Folder created',
|
|
306
|
+
};
|
|
307
|
+
ApiHelper.sendJson(req, res, response);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const response = {
|
|
312
|
+
status: 'error',
|
|
313
|
+
message: 'Folder not created',
|
|
314
|
+
};
|
|
315
|
+
ApiHelper.sendJson(req, res, response);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { apiStorage } from '../api';
|
|
2
|
+
import { CryptoUtils, Logger } from '../lib';
|
|
3
|
+
import { adminHelper } from './admin-helper';
|
|
4
|
+
|
|
5
|
+
export type TokenProps = {
|
|
6
|
+
token: string;
|
|
7
|
+
description: string;
|
|
8
|
+
timestamp?: number;
|
|
9
|
+
};
|
|
10
|
+
export class AdminTokenHelper {
|
|
11
|
+
private static instance: AdminTokenHelper;
|
|
12
|
+
private logger = new Logger('admin-token-api');
|
|
13
|
+
|
|
14
|
+
private constructor() {}
|
|
15
|
+
|
|
16
|
+
public static getInstance(): AdminTokenHelper {
|
|
17
|
+
if (!AdminTokenHelper.instance) {
|
|
18
|
+
AdminTokenHelper.instance = new AdminTokenHelper();
|
|
19
|
+
}
|
|
20
|
+
return AdminTokenHelper.instance;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async list(search?: string): Promise<TokenProps[]> {
|
|
24
|
+
const tokens = await apiStorage.getApi('access-tokens');
|
|
25
|
+
const tokenJson = JSON.parse(tokens || '[]');
|
|
26
|
+
|
|
27
|
+
const searchTexts = (search || '').trim().split(' ').filter(item => !!item);
|
|
28
|
+
const results = tokenJson.filter((item: TokenProps) =>
|
|
29
|
+
searchTexts.every((text) => item.description.toLowerCase().includes(text.toLowerCase()))
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
async add(tokenData: TokenProps) {
|
|
35
|
+
const tokens = await apiStorage.getApi('access-tokens');
|
|
36
|
+
const tokensJson = JSON.parse(tokens || '[]');
|
|
37
|
+
tokenData.timestamp = new Date().getTime();
|
|
38
|
+
tokensJson.push(tokenData);
|
|
39
|
+
apiStorage.setApi('access-tokens', JSON.stringify(tokensJson));
|
|
40
|
+
await apiStorage.save();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async update(tokenData: TokenProps) {
|
|
44
|
+
const tokens = await apiStorage.getApi('access-tokens');
|
|
45
|
+
const tokensJson = JSON.parse(tokens || '{}');
|
|
46
|
+
const idx = tokensJson.findIndex((item: TokenProps) => item.token === tokenData.token);
|
|
47
|
+
if (idx !== -1) {
|
|
48
|
+
tokensJson[idx].description = tokenData.description;
|
|
49
|
+
}
|
|
50
|
+
apiStorage.setApi('access-tokens', JSON.stringify(tokensJson));
|
|
51
|
+
apiStorage.save();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async remove(token: string) {
|
|
55
|
+
const tokens = await apiStorage.getApi('access-tokens');
|
|
56
|
+
const tokensJson = JSON.parse(tokens || '[]');
|
|
57
|
+
const idx = tokensJson.findIndex((item: TokenProps) => item.token === token);
|
|
58
|
+
if (idx !== -1) {
|
|
59
|
+
tokensJson.splice(idx, 1);
|
|
60
|
+
}
|
|
61
|
+
apiStorage.setApi('access-tokens', JSON.stringify(tokensJson));
|
|
62
|
+
apiStorage.save();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
generate() {
|
|
66
|
+
const salt = 'Lupine:' + CryptoUtils.uuid() + ':' + new Date().getTime().toString();
|
|
67
|
+
return adminHelper.encryptJson(salt) as string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async validateToken(token: string) {
|
|
71
|
+
const tokens = await apiStorage.getApi('access-tokens');
|
|
72
|
+
const tokensJson = JSON.parse(tokens || '[]');
|
|
73
|
+
const idx = tokensJson.findIndex((item: TokenProps) => item.token === token);
|
|
74
|
+
return idx !== -1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// add comment for tree shaking
|
|
79
|
+
export const adminTokenHelper = /* @__PURE__ */ AdminTokenHelper.getInstance();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ServerResponse } from 'http';
|
|
2
|
+
import { IApiBase, Logger, ServerRequest, ApiRouter, ApiHelper, apiStorage } from 'lupine.api';
|
|
3
|
+
import { adminTokenHelper } from './admin-token-helper';
|
|
4
|
+
|
|
5
|
+
export class AdminTokens implements IApiBase {
|
|
6
|
+
logger = new Logger('admin-page');
|
|
7
|
+
protected router = new ApiRouter();
|
|
8
|
+
adminUser: any;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.mountDashboard();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public getRouter(): ApiRouter {
|
|
15
|
+
return this.router;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected mountDashboard() {
|
|
19
|
+
this.router.use('/list', this.list.bind(this));
|
|
20
|
+
this.router.use('/add', this.add.bind(this));
|
|
21
|
+
this.router.use('/generate', this.generateToken.bind(this));
|
|
22
|
+
this.router.use('/update', this.update.bind(this));
|
|
23
|
+
this.router.use('/remove', this.remove.bind(this));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async list(req: ServerRequest, res: ServerResponse) {
|
|
27
|
+
const pageLimit = await apiStorage.getWeb('pageLimit') || '15';
|
|
28
|
+
const data = req.locals.json() as any;
|
|
29
|
+
const search = data['q'];
|
|
30
|
+
const list = await adminTokenHelper.list(search);
|
|
31
|
+
const response = {
|
|
32
|
+
status: 'ok',
|
|
33
|
+
message: 'Token List.',
|
|
34
|
+
result: list,
|
|
35
|
+
pageLimit,
|
|
36
|
+
};
|
|
37
|
+
ApiHelper.sendJson(req, res, response);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async add(req: ServerRequest, res: ServerResponse) {
|
|
42
|
+
const data = req.locals.json() as any;
|
|
43
|
+
const token = data['token'];
|
|
44
|
+
const description = data['description'];
|
|
45
|
+
token && (await adminTokenHelper.add({token, description}));
|
|
46
|
+
const response = {
|
|
47
|
+
status: 'ok',
|
|
48
|
+
message: 'Added token.',
|
|
49
|
+
};
|
|
50
|
+
ApiHelper.sendJson(req, res, response);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async generateToken(req: ServerRequest, res: ServerResponse) {
|
|
55
|
+
const newToken = adminTokenHelper.generate();
|
|
56
|
+
const response = {
|
|
57
|
+
status: 'ok',
|
|
58
|
+
message: 'Generated new token.',
|
|
59
|
+
result: newToken,
|
|
60
|
+
};
|
|
61
|
+
ApiHelper.sendJson(req, res, response);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async update(req: ServerRequest, res: ServerResponse) {
|
|
66
|
+
const data = req.locals.json() as any;
|
|
67
|
+
const token = data['token'];
|
|
68
|
+
const description = data['description'];
|
|
69
|
+
token && (await adminTokenHelper.update({token, description}));
|
|
70
|
+
const response = {
|
|
71
|
+
status: 'ok',
|
|
72
|
+
message: 'Updated token.',
|
|
73
|
+
};
|
|
74
|
+
ApiHelper.sendJson(req, res, response);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async remove(req: ServerRequest, res: ServerResponse) {
|
|
79
|
+
const data = req.locals.json() as any;
|
|
80
|
+
const token = data['token'];
|
|
81
|
+
token && (await adminTokenHelper.remove(token));
|
|
82
|
+
|
|
83
|
+
const response = {
|
|
84
|
+
status: 'ok',
|
|
85
|
+
message: 'Removed token.',
|
|
86
|
+
};
|
|
87
|
+
ApiHelper.sendJson(req, res, response);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple settings/config class for storing key/value pairs in memory
|
|
3
|
+
*/
|
|
4
|
+
import { Db } from '../lib';
|
|
5
|
+
import { asyncLocalStorage } from './async-storage';
|
|
6
|
+
import { AsyncStorageProps, HostToPathProps } from '../models';
|
|
7
|
+
|
|
8
|
+
// This data doesn't need to be shared to other workers
|
|
9
|
+
export const getTemplateCache = () => {
|
|
10
|
+
let cachedHtml = apiCache.get(apiCache.KEYS.TEMPLATE);
|
|
11
|
+
if (!cachedHtml) {
|
|
12
|
+
cachedHtml = {};
|
|
13
|
+
apiCache.set(apiCache.KEYS.TEMPLATE, cachedHtml);
|
|
14
|
+
}
|
|
15
|
+
return cachedHtml;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
enum ApiCacheKeys {
|
|
19
|
+
TEMPLATE = 'TEMPLATE',
|
|
20
|
+
DB = 'DB',
|
|
21
|
+
APP_DATA = 'APP_DATA',
|
|
22
|
+
// APP_CACHE = 'APP_CACHE',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// For cross clusters sharing, use AppSharedStorage
|
|
26
|
+
// ApiCache doesn't share cross clusters
|
|
27
|
+
// Since apis and app are independent, so ApiCache in apis and app are different instances.
|
|
28
|
+
// ApiCache is not shared cross app and apis, so `set` is only supposed to be called when app starts, or
|
|
29
|
+
// `set/get` is only used inside one api only
|
|
30
|
+
export class ApiCache {
|
|
31
|
+
private static instance: ApiCache;
|
|
32
|
+
KEYS = ApiCacheKeys;
|
|
33
|
+
|
|
34
|
+
private cacheMap: { [key: string]: any } = {};
|
|
35
|
+
|
|
36
|
+
private constructor() {}
|
|
37
|
+
|
|
38
|
+
public static getInstance(): ApiCache {
|
|
39
|
+
if (!ApiCache.instance) {
|
|
40
|
+
ApiCache.instance = new ApiCache();
|
|
41
|
+
}
|
|
42
|
+
return ApiCache.instance;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear() {
|
|
46
|
+
Object.keys(this.cacheMap).forEach((key) => {
|
|
47
|
+
delete this.cacheMap[key];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get(key: string) {
|
|
52
|
+
return this.cacheMap[key];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set(key: string, value: any) {
|
|
56
|
+
if (typeof value === 'undefined') {
|
|
57
|
+
delete this.cacheMap[key];
|
|
58
|
+
} else {
|
|
59
|
+
this.cacheMap[key] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// get scope variables inside of asyncLocalStorage.run
|
|
64
|
+
getAsyncStore(): AsyncStorageProps {
|
|
65
|
+
const store = asyncLocalStorage.getStore();
|
|
66
|
+
if (!store) {
|
|
67
|
+
throw new Error('This function should be called inside of asyncLocalStorage.run');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return store;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// get scope variables inside of asyncLocalStorage.run
|
|
74
|
+
getAppData() {
|
|
75
|
+
return this.get(ApiCacheKeys.APP_DATA) as HostToPathProps;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// get scope variables inside of asyncLocalStorage.run
|
|
79
|
+
getAppName() {
|
|
80
|
+
return this.getAsyncStore().appName;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// get scope variables inside of asyncLocalStorage.run
|
|
84
|
+
getDb() {
|
|
85
|
+
return this.get(ApiCacheKeys.DB) as Db;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// get scope variables inside of asyncLocalStorage.run
|
|
89
|
+
getUuid() {
|
|
90
|
+
return this.getAsyncStore().uuid;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// get scope variables inside of asyncLocalStorage.run
|
|
94
|
+
getLang() {
|
|
95
|
+
return this.getAsyncStore().lang;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
clearTemplateCache() {
|
|
99
|
+
this.set(ApiCacheKeys.TEMPLATE, undefined);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const apiCache = /* @__PURE__ */ ApiCache.getInstance();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ServerResponse } from 'http';
|
|
2
|
+
import { ServerRequest } from '../models/locals-props';
|
|
3
|
+
import { JsonObject } from '../models/json-object';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
export class ApiHelper {
|
|
7
|
+
static sendJson(
|
|
8
|
+
req: ServerRequest,
|
|
9
|
+
res: ServerResponse,
|
|
10
|
+
json: JsonObject,
|
|
11
|
+
statusCode = 200,
|
|
12
|
+
headers?: { [key: string]: string }
|
|
13
|
+
) {
|
|
14
|
+
res.writeHead(statusCode, Object.assign({ 'Content-Type': 'application/json' }, headers));
|
|
15
|
+
res.write(JSON.stringify(json));
|
|
16
|
+
res.end();
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static sendHtml(
|
|
21
|
+
req: ServerRequest,
|
|
22
|
+
res: ServerResponse,
|
|
23
|
+
html: string,
|
|
24
|
+
statusCode = 200,
|
|
25
|
+
headers?: { [key: string]: string }
|
|
26
|
+
) {
|
|
27
|
+
res.writeHead(statusCode, Object.assign({ 'Content-Type': 'text/html' }, headers));
|
|
28
|
+
res.write(html);
|
|
29
|
+
res.end();
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static sendFile(
|
|
34
|
+
req: ServerRequest,
|
|
35
|
+
res: ServerResponse,
|
|
36
|
+
filepath: string,
|
|
37
|
+
statusCode = 200,
|
|
38
|
+
headers?: { [key: string]: string }
|
|
39
|
+
) {
|
|
40
|
+
res.writeHead(statusCode, Object.assign({ 'Content-Type': 'application/octet-stream' }, headers));
|
|
41
|
+
fs.createReadStream(filepath).pipe(res);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { DbConfig, DbHelper, HostToPathProps, IApiBase, loadEnv, ServerRequest } from 'lupine.api';
|
|
2
|
+
import { ServerResponse } from 'http';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { apiCache, asyncLocalStorage, bindRenderPageFunctions } from '.';
|
|
5
|
+
import { AppCacheGlobal, AppCacheKeys, AsyncStorageProps, IApiModule, IAppCache, IAppSharedStorage, setAppCache } from '../models';
|
|
6
|
+
import { apiStorage } from './api-shared-storage';
|
|
7
|
+
|
|
8
|
+
export class ApiModule implements IApiModule {
|
|
9
|
+
rootApi: IApiBase;
|
|
10
|
+
constructor(api: IApiBase) {
|
|
11
|
+
this.rootApi = api;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public async processApi(store: AsyncStorageProps, url: string, req: ServerRequest, res: ServerResponse) {
|
|
15
|
+
let result = false;
|
|
16
|
+
await asyncLocalStorage.run(store, async () => {
|
|
17
|
+
if (await this.rootApi.getRouter().findRoute(url, req, res, true)) {
|
|
18
|
+
result = true;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// appCache is from app-loader (parent scope), not the same in current scope
|
|
26
|
+
public async initApi(appConfig: HostToPathProps, appCacheFromApp: IAppCache, appStorageFromApp: IAppSharedStorage) {
|
|
27
|
+
|
|
28
|
+
// const evnFile = appCacheFromApp.get(AppCacheGlobal, AppCacheKeys.APP_ENV_FILE);
|
|
29
|
+
// if (evnFile) {
|
|
30
|
+
// await loadEnv(evnFile);
|
|
31
|
+
// }
|
|
32
|
+
|
|
33
|
+
// set app's instances to api
|
|
34
|
+
setAppCache(appCacheFromApp);
|
|
35
|
+
// setAppStorage(appStorageFromApp);
|
|
36
|
+
apiStorage.setAppSharedStorage(appStorageFromApp);
|
|
37
|
+
// set RENDER_PAGE_FUNCTIONS to API module
|
|
38
|
+
bindRenderPageFunctions(appCacheFromApp.get(AppCacheGlobal, AppCacheKeys.RENDER_PAGE_FUNCTIONS));
|
|
39
|
+
|
|
40
|
+
console.log(`appConfig: `, appConfig);
|
|
41
|
+
apiCache.set(apiCache.KEYS.APP_DATA, appConfig);
|
|
42
|
+
// apiCache.set(apiCache.KEYS.APP_CACHE, appCache);
|
|
43
|
+
|
|
44
|
+
// await this.initConfig(appConfig);
|
|
45
|
+
apiCache.clearTemplateCache();
|
|
46
|
+
|
|
47
|
+
appConfig.dbConfig.filename = path.join(appConfig.dataPath, 'sqlite3.db');
|
|
48
|
+
await this.initDb(appConfig.dbConfig);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async initDb(config: DbConfig) {
|
|
52
|
+
const db = await DbHelper.createInstance(config);
|
|
53
|
+
apiCache.set(apiCache.KEYS.DB, db);
|
|
54
|
+
return db;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// private async initConfig(appConfig: HostToPathProps) {
|
|
58
|
+
// await AppConfig.load(appConfig.dataPath);
|
|
59
|
+
// }
|
|
60
|
+
}
|