lupine.api 1.1.45 → 1.1.47
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/admin/admin-frame-helper.tsx +364 -0
- package/admin/admin-frame.tsx +25 -327
- package/admin/admin-index.tsx +6 -7
- package/admin/admin-login.tsx +39 -27
- package/admin/admin-menu-list.tsx +4 -4
- package/admin/admin-page-list.tsx +4 -4
- package/admin/admin-release.tsx +40 -31
- package/admin/admin-table-list.tsx +3 -4
- package/admin/index.ts +6 -3
- package/package.json +1 -1
- package/src/admin-api/admin-api-helper.ts +205 -0
- package/src/admin-api/admin-api.ts +5 -2
- package/src/admin-api/admin-auth.ts +61 -8
- package/src/admin-api/admin-config.ts +1 -12
- package/src/admin-api/admin-performance.ts +2 -2
- package/src/admin-api/admin-release.ts +66 -20
- package/src/admin-api/admin-resources.ts +3 -3
- package/src/admin-api/admin-token-helper.ts +2 -2
- package/src/admin-api/index.ts +1 -1
- package/src/lang/api-lang-en.ts +0 -1
- package/src/lang/api-lang-zh-cn.ts +0 -1
- package/src/lib/utils/fs-utils.ts +15 -1
- package/admin/admin-frame-props.tsx +0 -9
- package/src/admin-api/admin-helper.ts +0 -111
|
@@ -8,13 +8,12 @@ import {
|
|
|
8
8
|
ApiHelper,
|
|
9
9
|
langHelper,
|
|
10
10
|
FsUtils,
|
|
11
|
-
|
|
11
|
+
adminApiHelper,
|
|
12
12
|
processRefreshCache,
|
|
13
13
|
} from 'lupine.api';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { needDevAdminSession } from './admin-auth';
|
|
16
16
|
import { adminTokenHelper } from './admin-token-helper';
|
|
17
|
-
import { Readable } from 'stream';
|
|
18
17
|
|
|
19
18
|
export class AdminRelease implements IApiBase {
|
|
20
19
|
private logger = new Logger('release-api');
|
|
@@ -86,7 +85,7 @@ export class AdminRelease implements IApiBase {
|
|
|
86
85
|
|
|
87
86
|
async refreshCache(req: ServerRequest, res: ServerResponse) {
|
|
88
87
|
// check whether it's from online admin
|
|
89
|
-
const json = await
|
|
88
|
+
const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
|
|
90
89
|
const jsonData = req.locals.json();
|
|
91
90
|
if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
|
|
92
91
|
await processRefreshCache(req);
|
|
@@ -184,13 +183,25 @@ export class AdminRelease implements IApiBase {
|
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
// local dirs under _web
|
|
187
|
-
const
|
|
186
|
+
const webSub: string[] = [];
|
|
187
|
+
for (let j = 0; j < apps.length; j++) {
|
|
188
|
+
const e = apps[j];
|
|
189
|
+
const p0 = path.join(appData.apiPath, '..');
|
|
190
|
+
const subFolders = await FsUtils.getDirsFullpath(path.join(p0, e + '_web'), 5);
|
|
191
|
+
webSub.push(
|
|
192
|
+
...subFolders
|
|
193
|
+
.filter((i) => i.isDirectory())
|
|
194
|
+
.map((i) => path.join(i.parentPath.substring(p0.length + 1), i.name).replace(/\\/g, '/'))
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
// const webSub = webSubFolders.filter(i => i.isDirectory()).map(i => path.join(i.parentPath.substring(appData.webPath.length + 1), i.name).replace(/\\/g, '/')).sort();
|
|
198
|
+
|
|
188
199
|
const response = {
|
|
189
200
|
status: 'ok',
|
|
190
201
|
message: 'check.',
|
|
191
202
|
appsFrom: apps,
|
|
192
203
|
...remoteResult,
|
|
193
|
-
webSub: webSubFolders.filter((folder) => folder.isDirectory()).map((folder) => folder.name),
|
|
204
|
+
webSub: webSub, // webSubFolders.filter((folder) => folder.isDirectory()).map((folder) => folder.name),
|
|
194
205
|
};
|
|
195
206
|
ApiHelper.sendJson(req, res, response);
|
|
196
207
|
return true;
|
|
@@ -264,6 +275,7 @@ export class AdminRelease implements IApiBase {
|
|
|
264
275
|
return true;
|
|
265
276
|
}
|
|
266
277
|
|
|
278
|
+
const appData = apiCache.getAppData();
|
|
267
279
|
let targetUrl = data.targetUrl as string;
|
|
268
280
|
if (targetUrl.endsWith('/')) {
|
|
269
281
|
targetUrl = targetUrl.slice(0, -1);
|
|
@@ -291,22 +303,42 @@ export class AdminRelease implements IApiBase {
|
|
|
291
303
|
ApiHelper.sendJson(req, res, result);
|
|
292
304
|
return true;
|
|
293
305
|
}
|
|
294
|
-
if (data.webSub) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
306
|
+
// if (data.webSub) {
|
|
307
|
+
// const result2 = await this.updateSendFile(data, 'web-sub');
|
|
308
|
+
// if (!result2 || result2.status !== 'ok') {
|
|
309
|
+
// ApiHelper.sendJson(req, res, result2);
|
|
310
|
+
// return true;
|
|
311
|
+
// }
|
|
312
|
+
// }
|
|
301
313
|
|
|
302
314
|
if (data.webSubs && data.webSubs.length > 0) {
|
|
315
|
+
const subTop = path.join(appData.apiPath, '..', data.fromList + '_web/');
|
|
303
316
|
for (let i = 0; i < data.webSubs.length; i++) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
317
|
+
if (!data.webSubs[i].startsWith(data.fromList + '_web/')) {
|
|
318
|
+
const response = {
|
|
319
|
+
status: 'error',
|
|
320
|
+
message: `Error: ${data.webSubs[i]} is not under ${data.fromList}`,
|
|
321
|
+
};
|
|
322
|
+
ApiHelper.sendJson(req, res, response);
|
|
308
323
|
return true;
|
|
309
324
|
}
|
|
325
|
+
const subFolders = await FsUtils.getDirsFullpath(path.join(appData.apiPath, '..', data.webSubs[i]));
|
|
326
|
+
const subFiles = subFolders
|
|
327
|
+
.filter((e) => e.isFile())
|
|
328
|
+
.map((e) => path.join(e.parentPath.substring(subTop.length), e.name).replace(/\\/g, '/'))
|
|
329
|
+
.sort();
|
|
330
|
+
for (let j = 0; j < subFiles.length; j++) {
|
|
331
|
+
if (subFiles[j].endsWith('.js.map')) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
data.webSub = subFiles[j];
|
|
335
|
+
this.logger.info(`update, webSubs: ${data.webSubs[i]}, subFiles: ${subFiles[j]})`);
|
|
336
|
+
const result2 = await this.updateSendFile(data, 'web-sub');
|
|
337
|
+
if (!result2 || result2.status !== 'ok') {
|
|
338
|
+
ApiHelper.sendJson(req, res, result2);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
310
342
|
}
|
|
311
343
|
}
|
|
312
344
|
}
|
|
@@ -331,6 +363,7 @@ export class AdminRelease implements IApiBase {
|
|
|
331
363
|
message: 'updated',
|
|
332
364
|
};
|
|
333
365
|
ApiHelper.sendJson(req, res, response);
|
|
366
|
+
this.logger.info(`updated, successful`);
|
|
334
367
|
return true;
|
|
335
368
|
}
|
|
336
369
|
|
|
@@ -349,11 +382,13 @@ export class AdminRelease implements IApiBase {
|
|
|
349
382
|
} else if (chkOption === 'web') {
|
|
350
383
|
sendFile = path.join(appData.apiPath, '..', fromList + '_web', 'index.js');
|
|
351
384
|
} else if (chkOption === 'web-sub' && data.webSub) {
|
|
352
|
-
sendFile = path.join(appData.apiPath, '..', fromList + '_web', data.webSub, 'index.js');
|
|
385
|
+
// sendFile = path.join(appData.apiPath, '..', fromList + '_web', data.webSub, 'index.js');
|
|
386
|
+
sendFile = path.join(appData.apiPath, '..', fromList + '_web', data.webSub);
|
|
353
387
|
} else if (chkOption.startsWith('.env')) {
|
|
354
388
|
sendFile = path.join(appData.apiPath, '../../..', chkOption);
|
|
355
389
|
}
|
|
356
390
|
if (!(await FsUtils.pathExist(sendFile))) {
|
|
391
|
+
this.logger.error(`updateSendFile, not found: ${sendFile}`);
|
|
357
392
|
return { status: 'error', message: 'Client file not found: ' + sendFile };
|
|
358
393
|
}
|
|
359
394
|
const fileContent = (await FsUtils.readFile(sendFile))!;
|
|
@@ -368,6 +403,7 @@ export class AdminRelease implements IApiBase {
|
|
|
368
403
|
// })
|
|
369
404
|
const chunkSize = 1024 * 500;
|
|
370
405
|
let cnt = 0;
|
|
406
|
+
this.logger.info(`updateSendFile, sendFile: ${sendFile}, len: ${fileContent.length}`);
|
|
371
407
|
for (let i = 0; i < fileContent.length; i += chunkSize) {
|
|
372
408
|
const chunk = fileContent.slice(i, i + chunkSize);
|
|
373
409
|
if (!chunk) break;
|
|
@@ -376,9 +412,15 @@ export class AdminRelease implements IApiBase {
|
|
|
376
412
|
method: 'POST',
|
|
377
413
|
body: JSON.stringify({ ...data, chkOption, index: cnt, size: fileContent.length }) + '\n\n' + chunk,
|
|
378
414
|
};
|
|
379
|
-
this.logger.
|
|
415
|
+
this.logger.info(
|
|
416
|
+
`updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${
|
|
417
|
+
fileContent.length
|
|
418
|
+
}), f: ${sendFile}`
|
|
419
|
+
);
|
|
420
|
+
i > 0 && (await new Promise((resolve) => setTimeout(resolve, 1000)));
|
|
380
421
|
const remoteData = await fetch(targetUrl + '/api/admin/release/byClientUpdate', postData);
|
|
381
422
|
const resultText = await remoteData.text();
|
|
423
|
+
this.logger.info(`updateSendFile, index: ${cnt}, resultText: ${resultText}`);
|
|
382
424
|
let remoteResult: any;
|
|
383
425
|
try {
|
|
384
426
|
remoteResult = JSON.parse(resultText);
|
|
@@ -437,11 +479,11 @@ export class AdminRelease implements IApiBase {
|
|
|
437
479
|
} else if (chkOption === 'web') {
|
|
438
480
|
saveFile = path.join(appData.apiPath, '..', toList + '_web', 'index.js');
|
|
439
481
|
} else if (chkOption === 'web-sub' && data.webSub) {
|
|
440
|
-
const folder = path.join(appData.apiPath, '..', toList + '_web', data.webSub);
|
|
482
|
+
const folder = path.join(appData.apiPath, '..', toList + '_web', path.basename(data.webSub));
|
|
441
483
|
if (!(await FsUtils.pathExist(folder))) {
|
|
442
484
|
await FsUtils.mkdir(folder);
|
|
443
485
|
}
|
|
444
|
-
saveFile = path.join(appData.apiPath, '..', toList + '_web', data.webSub
|
|
486
|
+
saveFile = path.join(appData.apiPath, '..', toList + '_web', data.webSub);
|
|
445
487
|
} else if ((chkOption as string).startsWith('.env')) {
|
|
446
488
|
saveFile = path.join(appData.apiPath, '../../..', chkOption);
|
|
447
489
|
}
|
|
@@ -460,6 +502,10 @@ export class AdminRelease implements IApiBase {
|
|
|
460
502
|
await FsUtils.writeFile(bakFile, bakContent);
|
|
461
503
|
}
|
|
462
504
|
}
|
|
505
|
+
|
|
506
|
+
this.logger.info(
|
|
507
|
+
`byClientUpdate, index: ${data.index}, saveFile: ${saveFile}, received len: ${(fileContent || '').length}`
|
|
508
|
+
);
|
|
463
509
|
if (data.index === 0) {
|
|
464
510
|
await FsUtils.writeFile(saveFile, fileContent || '');
|
|
465
511
|
} else {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
ApiHelper,
|
|
10
10
|
langHelper,
|
|
11
11
|
FsUtils,
|
|
12
|
-
|
|
12
|
+
adminApiHelper,
|
|
13
13
|
} from 'lupine.api';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
|
|
@@ -64,7 +64,7 @@ export class AdminResources implements IApiBase {
|
|
|
64
64
|
const chunkNumberStr = req.locals.query.get('chunkNumber') as string;
|
|
65
65
|
const chunkNumber = parseInt(chunkNumberStr);
|
|
66
66
|
const totalChunks = parseInt(req.locals.query.get('totalChunks') as string);
|
|
67
|
-
const decryptedKey = key &&
|
|
67
|
+
const decryptedKey = key && adminApiHelper.decryptJson(key.replace(/ /g, '+'));
|
|
68
68
|
const keyNG =
|
|
69
69
|
!chunkNumberStr ||
|
|
70
70
|
!totalChunks ||
|
|
@@ -100,7 +100,7 @@ export class AdminResources implements IApiBase {
|
|
|
100
100
|
chunkNumber,
|
|
101
101
|
totalChunks,
|
|
102
102
|
message: langHelper.getLang('shared:file_part_updated'),
|
|
103
|
-
key:
|
|
103
|
+
key: adminApiHelper.encryptJson({ ind: chunkNumber + 1, cnt: totalChunks, t: new Date().getTime() }),
|
|
104
104
|
};
|
|
105
105
|
ApiHelper.sendJson(req, res, response);
|
|
106
106
|
return true;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { apiStorage } from '../api';
|
|
2
2
|
import { CryptoUtils, Logger } from '../lib';
|
|
3
|
-
import {
|
|
3
|
+
import { adminApiHelper } from './admin-api-helper';
|
|
4
4
|
|
|
5
5
|
export type TokenProps = {
|
|
6
6
|
token: string;
|
|
@@ -64,7 +64,7 @@ export class AdminTokenHelper {
|
|
|
64
64
|
|
|
65
65
|
generate() {
|
|
66
66
|
const salt = 'Lupine:' + CryptoUtils.uuid() + ':' + new Date().getTime().toString();
|
|
67
|
-
return
|
|
67
|
+
return adminApiHelper.encryptJson(salt) as string;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
async validateToken(token: string) {
|
package/src/admin-api/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './admin-api';
|
|
2
|
-
export * from './admin-helper';
|
|
2
|
+
export * from './admin-api-helper';
|
package/src/lang/api-lang-en.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Dirent } from 'fs';
|
|
2
2
|
import * as fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
3
4
|
|
|
4
5
|
export type FileInfoProps = {
|
|
5
6
|
size: number;
|
|
@@ -118,12 +119,25 @@ export class FsUtils {
|
|
|
118
119
|
};
|
|
119
120
|
|
|
120
121
|
// return with fullpath list of Dirent
|
|
121
|
-
static getDirsFullpath = async (dirPath: string): Promise<Dirent[]> => {
|
|
122
|
+
static getDirsFullpath = async (dirPath: string, maxDepth = 1): Promise<Dirent[]> => {
|
|
123
|
+
return this.getDirsFullpathDepthSub(dirPath, 0, maxDepth);
|
|
124
|
+
};
|
|
125
|
+
private static getDirsFullpathDepthSub = async (dirPath: string, depth = 0, maxDepth = 1): Promise<Dirent[]> => {
|
|
122
126
|
try {
|
|
123
127
|
const files = await fs.readdir(dirPath, {
|
|
124
128
|
recursive: false,
|
|
125
129
|
withFileTypes: true,
|
|
126
130
|
});
|
|
131
|
+
if (depth + 1 < maxDepth) {
|
|
132
|
+
for (const entry of files) {
|
|
133
|
+
if (entry.isDirectory()) {
|
|
134
|
+
if (depth < maxDepth) {
|
|
135
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
136
|
+
(entry as any).sub = await this.getDirsFullpathDepthSub(fullPath, depth + 1, maxDepth);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
127
141
|
return files;
|
|
128
142
|
} catch {
|
|
129
143
|
return [];
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { ServerResponse } from 'http';
|
|
2
|
-
import { ApiHelper } from '../api';
|
|
3
|
-
import { CryptoUtils, Logger } from '../lib';
|
|
4
|
-
import { ServerRequest } from '../models';
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
dev-admin uses different authentication method from frontend.
|
|
8
|
-
dev-admin only provides fixed username and password authentication, no user maintenance.
|
|
9
|
-
saved cookie name: _token_dev
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// DEFAULT_ADMIN_PASS is DEFAULT_ADMIN_NAME + ':' + login password hash.
|
|
13
|
-
// Use below command to generate hash:
|
|
14
|
-
// node -e "console.log(require('crypto').createHash('md5').update('admin:F4AZ5O@2fPUjw%f$LmhZpJTQ^DoXnWPkH#hqE', 'utf8').digest('hex'))"
|
|
15
|
-
export type DevAdminSessionProps = {
|
|
16
|
-
u: string; // username
|
|
17
|
-
t: string; // type: admin, user
|
|
18
|
-
ip: string;
|
|
19
|
-
h: string; // md5 of name+pass
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const DEV_ADMIN_TYPE = 'dev-admin';
|
|
23
|
-
export const DEV_ADMIN_CRYPTO_KEY_NAME = 'DEV_CRYPTO_KEY';
|
|
24
|
-
export const DEV_ADMIN_SESSION_NAME = '_token_dev';
|
|
25
|
-
export class AdminHelper {
|
|
26
|
-
private static instance: AdminHelper;
|
|
27
|
-
private logger = new Logger('admin-api');
|
|
28
|
-
|
|
29
|
-
private constructor() {}
|
|
30
|
-
|
|
31
|
-
public static getInstance(): AdminHelper {
|
|
32
|
-
if (!AdminHelper.instance) {
|
|
33
|
-
AdminHelper.instance = new AdminHelper();
|
|
34
|
-
}
|
|
35
|
-
return AdminHelper.instance;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
decryptJson(text: string) {
|
|
39
|
-
const cryptoKey = process.env[DEV_ADMIN_CRYPTO_KEY_NAME];
|
|
40
|
-
if (cryptoKey && text) {
|
|
41
|
-
try {
|
|
42
|
-
const deCrypto = CryptoUtils.decrypt(text, cryptoKey);
|
|
43
|
-
const json = JSON.parse(deCrypto);
|
|
44
|
-
return json;
|
|
45
|
-
} catch (error: any) {
|
|
46
|
-
this.logger.error(error.message);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
encryptJson(jsonOrText: string | object) {
|
|
53
|
-
const cryptoKey = process.env[DEV_ADMIN_CRYPTO_KEY_NAME];
|
|
54
|
-
if (cryptoKey && jsonOrText) {
|
|
55
|
-
try {
|
|
56
|
-
const text = typeof jsonOrText === 'string' ? jsonOrText : JSON.stringify(jsonOrText);
|
|
57
|
-
const encryptText = CryptoUtils.encrypt(text, cryptoKey);
|
|
58
|
-
return encryptText;
|
|
59
|
-
} catch (error: any) {
|
|
60
|
-
this.logger.error(error.message);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async getDevAdminFromCookie(
|
|
67
|
-
req: ServerRequest,
|
|
68
|
-
res: ServerResponse,
|
|
69
|
-
sendResponseWhenError = true
|
|
70
|
-
): Promise<DevAdminSessionProps | false> {
|
|
71
|
-
try {
|
|
72
|
-
const cookies = req.locals.cookies();
|
|
73
|
-
const token = cookies.get(DEV_ADMIN_SESSION_NAME, '');
|
|
74
|
-
if (token) {
|
|
75
|
-
const json = this.decryptJson(token) as DevAdminSessionProps;
|
|
76
|
-
if (!json || json.t !== DEV_ADMIN_TYPE) {
|
|
77
|
-
if (sendResponseWhenError) {
|
|
78
|
-
const response = {
|
|
79
|
-
status: 'error',
|
|
80
|
-
message: 'Wrong session data, contact site admin please.',
|
|
81
|
-
};
|
|
82
|
-
ApiHelper.sendJson(req, res, response);
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// if it's special admin
|
|
88
|
-
if (json.h && json.u === process.env['DEV_ADMIN_USER']) {
|
|
89
|
-
const hash = CryptoUtils.hash(process.env['DEV_ADMIN_USER'] + ':' + process.env['DEV_ADMIN_PASS']);
|
|
90
|
-
if (json.h === hash) {
|
|
91
|
-
return json;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
} catch (error: any) {
|
|
97
|
-
this.logger.error(error.message);
|
|
98
|
-
}
|
|
99
|
-
if (sendResponseWhenError) {
|
|
100
|
-
const response = {
|
|
101
|
-
status: 'error',
|
|
102
|
-
message: 'Please login to use this system.',
|
|
103
|
-
};
|
|
104
|
-
ApiHelper.sendJson(req, res, response);
|
|
105
|
-
}
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// add comment for tree shaking
|
|
111
|
-
export const adminHelper = /* @__PURE__ */ AdminHelper.getInstance();
|