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.
@@ -8,13 +8,12 @@ import {
8
8
  ApiHelper,
9
9
  langHelper,
10
10
  FsUtils,
11
- adminHelper,
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 adminHelper.getDevAdminFromCookie(req, res, false);
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 webSubFolders = await FsUtils.getDirsFullpath(appData.webPath);
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
- const result2 = await this.updateSendFile(data, 'web-sub');
296
- if (!result2 || result2.status !== 'ok') {
297
- ApiHelper.sendJson(req, res, result2);
298
- return true;
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
- data.webSub = data.webSubs[i];
305
- const result2 = await this.updateSendFile(data, 'web-sub');
306
- if (!result2 || result2.status !== 'ok') {
307
- ApiHelper.sendJson(req, res, result2);
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.debug(`updateSendFile, index: ${cnt}, sending (max): ${i + chunkSize} / ${fileContent.length}`);
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, 'index.js');
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
- adminHelper,
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 && adminHelper.decryptJson(key.replace(/ /g, '+'));
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: adminHelper.encryptJson({ ind: chunkNumber + 1, cnt: totalChunks, t: new Date().getTime() }),
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 { adminHelper } from './admin-helper';
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 adminHelper.encryptJson(salt) as string;
67
+ return adminApiHelper.encryptJson(salt) as string;
68
68
  }
69
69
 
70
70
  async validateToken(token: string) {
@@ -1,2 +1,2 @@
1
1
  export * from './admin-api';
2
- export * from './admin-helper';
2
+ export * from './admin-api-helper';
@@ -22,6 +22,5 @@ export const apiLangEn: OneLangProps = {
22
22
  'shared:not_found_file': 'File {fileName} is not found.',
23
23
 
24
24
  'shared:wrong_hash': 'Wrong hash.',
25
- 'shared:crypto_key_not_set': 'Crypto key [{cryptoKey}] not set',
26
25
  },
27
26
  };
@@ -23,6 +23,5 @@ export const apiLangZhCn: OneLangProps = {
23
23
  'shared:not_found_file': '文件 {fileName} 未找到',
24
24
 
25
25
  'shared:wrong_hash': '错误的hash',
26
- 'shared:crypto_key_not_set': 'Crypto key [{cryptoKey}] 未设置',
27
26
  },
28
27
  };
@@ -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,9 +0,0 @@
1
- import { TabsHookProps } from 'lupine.components';
2
-
3
- const adminFrameProps = {
4
- maxWidthMobileMenu: '800px',
5
- maxTabsCount: 20,
6
- tabsHook: {} as TabsHookProps,
7
- };
8
-
9
- export { adminFrameProps };
@@ -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();