adminforth 1.0.35 → 1.0.37
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/auth.ts +5 -1
- package/dataConnectors/mongo.ts +4 -0
- package/dist/auth.js +6 -1
- package/dist/dataConnectors/mongo.js +3 -0
- package/dist/modules/codeInjector.js +15 -24
- package/dist/modules/utils.js +6 -6
- package/dist/spa/spa/src/stores/core.ts +1 -1
- package/modules/codeInjector.ts +15 -24
- package/modules/utils.ts +6 -7
- package/package.json +1 -1
- package/spa/src/stores/core.ts +1 -1
package/auth.ts
CHANGED
|
@@ -43,7 +43,11 @@ class AdminForthAuth {
|
|
|
43
43
|
const decoded = jwt.verify(jwtToken, secret);
|
|
44
44
|
return decoded;
|
|
45
45
|
} catch (err) {
|
|
46
|
-
|
|
46
|
+
if (err.name === 'JsonWebTokenError') {
|
|
47
|
+
console.error('Token expired', err.message);
|
|
48
|
+
} else {
|
|
49
|
+
console.error('Failed to verify JWT token', err);
|
|
50
|
+
}
|
|
47
51
|
return null;
|
|
48
52
|
}
|
|
49
53
|
}
|
package/dataConnectors/mongo.ts
CHANGED
|
@@ -41,6 +41,10 @@ class MongoConnector {
|
|
|
41
41
|
|
|
42
42
|
async discoverFields(resource) {
|
|
43
43
|
return resource.columns.reduce((acc, col) => {
|
|
44
|
+
if (!col.type) {
|
|
45
|
+
throw new Error(`Type is not defined for column ${col.name}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
acc[col.name] = {
|
|
45
49
|
name: col.name,
|
|
46
50
|
type: col.type,
|
package/dist/auth.js
CHANGED
|
@@ -46,7 +46,12 @@ class AdminForthAuth {
|
|
|
46
46
|
return decoded;
|
|
47
47
|
}
|
|
48
48
|
catch (err) {
|
|
49
|
-
|
|
49
|
+
if (err.name === 'JsonWebTokenError') {
|
|
50
|
+
console.error('Token expired', err.message);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.error('Failed to verify JWT token', err);
|
|
54
|
+
}
|
|
50
55
|
return null;
|
|
51
56
|
}
|
|
52
57
|
}
|
|
@@ -45,6 +45,9 @@ class MongoConnector {
|
|
|
45
45
|
discoverFields(resource) {
|
|
46
46
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
47
|
return resource.columns.reduce((acc, col) => {
|
|
48
|
+
if (!col.type) {
|
|
49
|
+
throw new Error(`Type is not defined for column ${col.name}`);
|
|
50
|
+
}
|
|
48
51
|
acc[col.name] = {
|
|
49
52
|
name: col.name,
|
|
50
53
|
type: col.type,
|
|
@@ -13,11 +13,9 @@ import filewatcher from 'filewatcher';
|
|
|
13
13
|
import { exec, spawn } from 'child_process';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import path from 'path';
|
|
16
|
-
import { fileURLToPath } from 'url';
|
|
17
16
|
import crypto from 'crypto';
|
|
18
17
|
import os from 'os';
|
|
19
|
-
|
|
20
|
-
const __dirname = path.join(path.dirname(__filename), '..');
|
|
18
|
+
import { ADMIN_FORTH_ABSOLUTE_PATH } from './utils.js';
|
|
21
19
|
let TMP_DIR;
|
|
22
20
|
try {
|
|
23
21
|
TMP_DIR = os.tmpdir();
|
|
@@ -74,13 +72,12 @@ class CodeInjector {
|
|
|
74
72
|
prepareSources(_a) {
|
|
75
73
|
return __awaiter(this, arguments, void 0, function* ({ filesUpdated, verbose = false }) {
|
|
76
74
|
var _b, _c, _d, _e, _f, _g;
|
|
77
|
-
const spaTmpPath = CodeInjector.SPA_TMP_PATH;
|
|
78
75
|
// check SPA_TMP_PATH exists and create if not
|
|
79
76
|
try {
|
|
80
|
-
yield fs.promises.access(
|
|
77
|
+
yield fs.promises.access(CodeInjector.SPA_TMP_PATH, fs.constants.F_OK);
|
|
81
78
|
}
|
|
82
79
|
catch (e) {
|
|
83
|
-
yield fs.promises.mkdir(
|
|
80
|
+
yield fs.promises.mkdir(CodeInjector.SPA_TMP_PATH, { recursive: true });
|
|
84
81
|
}
|
|
85
82
|
const icons = [];
|
|
86
83
|
let routes = '';
|
|
@@ -102,18 +99,11 @@ class CodeInjector {
|
|
|
102
99
|
});
|
|
103
100
|
};
|
|
104
101
|
collectAssetsFromMenu(this.adminforth.config.menu);
|
|
105
|
-
// create spa_tmp folder, or ignore if it exists
|
|
106
|
-
try {
|
|
107
|
-
yield fs.promises.mkdir(spaTmpPath);
|
|
108
|
-
}
|
|
109
|
-
catch (e) {
|
|
110
|
-
// ignore
|
|
111
|
-
}
|
|
112
102
|
if (filesUpdated) {
|
|
113
103
|
// copy only updated files
|
|
114
104
|
yield Promise.all(filesUpdated.map((file) => __awaiter(this, void 0, void 0, function* () {
|
|
115
|
-
const src = path.join(
|
|
116
|
-
const dest = path.join(
|
|
105
|
+
const src = path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa', file);
|
|
106
|
+
const dest = path.join(CodeInjector.SPA_TMP_PATH, file);
|
|
117
107
|
yield fsExtra.copy(src, dest);
|
|
118
108
|
if (process.env.HEAVY_DEBUG) {
|
|
119
109
|
console.log('🪲 await fsExtra.copy filtering', src, dest);
|
|
@@ -122,15 +112,16 @@ class CodeInjector {
|
|
|
122
112
|
}
|
|
123
113
|
else {
|
|
124
114
|
if (process.env.HEAVY_DEBUG) {
|
|
125
|
-
console.log(`🪲 await fsExtra.copy from ${path.join(
|
|
115
|
+
console.log(`🪲 await fsExtra.copy from ${path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa')}, ${CodeInjector.SPA_TMP_PATH}`);
|
|
126
116
|
}
|
|
127
|
-
yield fsExtra.copy(path.join(
|
|
117
|
+
yield fsExtra.copy(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa'), CodeInjector.SPA_TMP_PATH, {
|
|
128
118
|
filter: (src) => {
|
|
129
119
|
if (process.env.HEAVY_DEBUG) {
|
|
130
120
|
console.log('🪲 await fsExtra.copy filtering', src);
|
|
131
121
|
}
|
|
132
122
|
return !src.includes('/adminforth/spa/node_modules') && !src.includes('/adminforth/spa/dist');
|
|
133
123
|
},
|
|
124
|
+
overwrite: true,
|
|
134
125
|
});
|
|
135
126
|
// copy whole custom directory
|
|
136
127
|
if ((_b = this.adminforth.config.customization) === null || _b === void 0 ? void 0 : _b.customComponentsDir) {
|
|
@@ -176,7 +167,7 @@ class CodeInjector {
|
|
|
176
167
|
imports += `import addCustomUses from '${this.adminforth.config.customization.vueUsesFile}';\n`;
|
|
177
168
|
}
|
|
178
169
|
// inject that code into spa_tmp/src/App.vue
|
|
179
|
-
const appVuePath = path.join(
|
|
170
|
+
const appVuePath = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'main.ts');
|
|
180
171
|
let appVueContent = yield fs.promises.readFile(appVuePath, 'utf-8');
|
|
181
172
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH IMPORTS */', imports);
|
|
182
173
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n');
|
|
@@ -198,12 +189,12 @@ class CodeInjector {
|
|
|
198
189
|
//redirect to login
|
|
199
190
|
redirect: '/${homePagePath}'
|
|
200
191
|
},\n`;
|
|
201
|
-
const routerVuePath = path.join(
|
|
192
|
+
const routerVuePath = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'router', 'index.ts');
|
|
202
193
|
let routerVueContent = yield fs.promises.readFile(routerVuePath, 'utf-8');
|
|
203
194
|
routerVueContent = routerVueContent.replace('/* IMPORTANT:ADMINFORTH ROUTES */', routes);
|
|
204
195
|
yield fs.promises.writeFile(routerVuePath, routerVueContent);
|
|
205
196
|
/* hash checking */
|
|
206
|
-
const spaPackageLockPath = path.join(
|
|
197
|
+
const spaPackageLockPath = path.join(CodeInjector.SPA_TMP_PATH, 'package-lock.json');
|
|
207
198
|
const spaPackageLock = JSON.parse(yield fs.promises.readFile(spaPackageLockPath, 'utf-8'));
|
|
208
199
|
const spaLockHash = hashify(spaPackageLock);
|
|
209
200
|
/* customPackageLock */
|
|
@@ -214,7 +205,7 @@ class CodeInjector {
|
|
|
214
205
|
const usersLockHash = hashify(usersLock);
|
|
215
206
|
const packagesNamesHash = hashify(packageNames);
|
|
216
207
|
const fullHash = `${spaLockHash}::${packagesNamesHash}::${usersLockHash}`;
|
|
217
|
-
const hashPath = path.join(
|
|
208
|
+
const hashPath = path.join(CodeInjector.SPA_TMP_PATH, 'node_modules', '.adminforth_hash');
|
|
218
209
|
try {
|
|
219
210
|
const existingHash = yield fs.promises.readFile(hashPath, 'utf-8');
|
|
220
211
|
if (existingHash === fullHash) {
|
|
@@ -233,7 +224,7 @@ class CodeInjector {
|
|
|
233
224
|
console.log('Hash file does not exist, proceeding with npm ci/install');
|
|
234
225
|
}
|
|
235
226
|
}
|
|
236
|
-
yield this.runNpmShell({ command: 'ci', verbose, cwd:
|
|
227
|
+
yield this.runNpmShell({ command: 'ci', verbose, cwd: CodeInjector.SPA_TMP_PATH });
|
|
237
228
|
// get packages with version from customPackage
|
|
238
229
|
const IGNORE_PACKAGES = ['tsx', 'typescript', 'express', 'nodemon'];
|
|
239
230
|
const customPackgeNames = [...Object.keys(usersPackage.dependencies), ...Object.keys(usersPackage.devDependencies || [])]
|
|
@@ -245,14 +236,14 @@ class CodeInjector {
|
|
|
245
236
|
}, []);
|
|
246
237
|
if (packageNames.length) {
|
|
247
238
|
const npmInstallCommand = `install ${[...packageNames, ...customPackgeNames].join(' ')}`;
|
|
248
|
-
yield this.runNpmShell({ command: npmInstallCommand, cwd:
|
|
239
|
+
yield this.runNpmShell({ command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH });
|
|
249
240
|
}
|
|
250
241
|
yield fs.promises.writeFile(hashPath, fullHash);
|
|
251
242
|
});
|
|
252
243
|
}
|
|
253
244
|
watchForReprepare(_a) {
|
|
254
245
|
return __awaiter(this, arguments, void 0, function* ({ verbose }) {
|
|
255
|
-
const spaPath = path.join(
|
|
246
|
+
const spaPath = path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa');
|
|
256
247
|
// get list of all subdirectories in spa recursively
|
|
257
248
|
const directories = [];
|
|
258
249
|
const collectDirectories = (dir) => __awaiter(this, void 0, void 0, function* () {
|
package/dist/modules/utils.js
CHANGED
|
@@ -13,12 +13,12 @@ export function guessLabelFromName(name) {
|
|
|
13
13
|
return name.split(/(?=[A-Z])/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
-
let package_json;
|
|
17
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
let __dirname = path.join(path.dirname(__filename), '..');
|
|
18
|
+
if (__dirname.endsWith('dist')) {
|
|
19
|
+
// in prod build we are in dist also
|
|
20
|
+
__dirname = path.join(__dirname, '..');
|
|
21
|
+
}
|
|
22
22
|
export const ADMIN_FORTH_ABSOLUTE_PATH = __dirname;
|
|
23
|
-
package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
|
|
23
|
+
const package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
|
|
24
24
|
export const ADMINFORTH_VERSION = package_json.version;
|
|
@@ -24,7 +24,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
24
24
|
}, {});
|
|
25
25
|
config.value = resp.config;
|
|
26
26
|
user.value = resp.user;
|
|
27
|
-
console.log('🌍 AdminForth
|
|
27
|
+
console.log('🌍 AdminForth v', resp.version);
|
|
28
28
|
|
|
29
29
|
// find homepage:true in menu recuresively
|
|
30
30
|
|
package/modules/codeInjector.ts
CHANGED
|
@@ -8,11 +8,9 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import AdminForth from '../index.js';
|
|
11
|
+
import { ADMIN_FORTH_ABSOLUTE_PATH } from './utils.js';
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = path.join(path.dirname(__filename), '..');
|
|
15
|
-
|
|
16
14
|
let TMP_DIR;
|
|
17
15
|
|
|
18
16
|
try {
|
|
@@ -83,12 +81,11 @@ class CodeInjector {
|
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
async prepareSources({ filesUpdated, verbose = false }: { filesUpdated?: string[], verbose?: boolean }) {
|
|
86
|
-
const spaTmpPath = CodeInjector.SPA_TMP_PATH;
|
|
87
84
|
// check SPA_TMP_PATH exists and create if not
|
|
88
85
|
try {
|
|
89
|
-
await fs.promises.access(
|
|
86
|
+
await fs.promises.access(CodeInjector.SPA_TMP_PATH, fs.constants.F_OK);
|
|
90
87
|
} catch (e) {
|
|
91
|
-
await fs.promises.mkdir(
|
|
88
|
+
await fs.promises.mkdir(CodeInjector.SPA_TMP_PATH, { recursive: true });
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
const icons = [];
|
|
@@ -113,18 +110,11 @@ class CodeInjector {
|
|
|
113
110
|
};
|
|
114
111
|
collectAssetsFromMenu(this.adminforth.config.menu);
|
|
115
112
|
|
|
116
|
-
// create spa_tmp folder, or ignore if it exists
|
|
117
|
-
try {
|
|
118
|
-
await fs.promises.mkdir(spaTmpPath);
|
|
119
|
-
} catch (e) {
|
|
120
|
-
// ignore
|
|
121
|
-
}
|
|
122
|
-
|
|
123
113
|
if (filesUpdated) {
|
|
124
114
|
// copy only updated files
|
|
125
115
|
await Promise.all(filesUpdated.map(async (file) => {
|
|
126
|
-
const src = path.join(
|
|
127
|
-
const dest = path.join(
|
|
116
|
+
const src = path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa', file);
|
|
117
|
+
const dest = path.join(CodeInjector.SPA_TMP_PATH, file);
|
|
128
118
|
await fsExtra.copy(src, dest);
|
|
129
119
|
if (process.env.HEAVY_DEBUG) {
|
|
130
120
|
console.log('🪲 await fsExtra.copy filtering', src, dest);
|
|
@@ -133,10 +123,10 @@ class CodeInjector {
|
|
|
133
123
|
}));
|
|
134
124
|
} else {
|
|
135
125
|
if (process.env.HEAVY_DEBUG) {
|
|
136
|
-
console.log(`🪲 await fsExtra.copy from ${path.join(
|
|
126
|
+
console.log(`🪲 await fsExtra.copy from ${path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa')}, ${CodeInjector.SPA_TMP_PATH}`);
|
|
137
127
|
}
|
|
138
128
|
|
|
139
|
-
await fsExtra.copy(path.join(
|
|
129
|
+
await fsExtra.copy(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa'), CodeInjector.SPA_TMP_PATH, {
|
|
140
130
|
filter: (src) => {
|
|
141
131
|
if (process.env.HEAVY_DEBUG) {
|
|
142
132
|
console.log('🪲 await fsExtra.copy filtering', src);
|
|
@@ -144,6 +134,7 @@ class CodeInjector {
|
|
|
144
134
|
|
|
145
135
|
return !src.includes('/adminforth/spa/node_modules') && !src.includes('/adminforth/spa/dist');
|
|
146
136
|
},
|
|
137
|
+
overwrite: true,
|
|
147
138
|
});
|
|
148
139
|
|
|
149
140
|
// copy whole custom directory
|
|
@@ -198,7 +189,7 @@ class CodeInjector {
|
|
|
198
189
|
}
|
|
199
190
|
|
|
200
191
|
// inject that code into spa_tmp/src/App.vue
|
|
201
|
-
const appVuePath = path.join(
|
|
192
|
+
const appVuePath = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'main.ts');
|
|
202
193
|
let appVueContent = await fs.promises.readFile(appVuePath, 'utf-8');
|
|
203
194
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH IMPORTS */', imports);
|
|
204
195
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n' );
|
|
@@ -227,14 +218,14 @@ class CodeInjector {
|
|
|
227
218
|
//redirect to login
|
|
228
219
|
redirect: '/${homePagePath}'
|
|
229
220
|
},\n`;
|
|
230
|
-
const routerVuePath = path.join(
|
|
221
|
+
const routerVuePath = path.join(CodeInjector.SPA_TMP_PATH, 'src', 'router', 'index.ts');
|
|
231
222
|
let routerVueContent = await fs.promises.readFile(routerVuePath, 'utf-8');
|
|
232
223
|
routerVueContent = routerVueContent.replace('/* IMPORTANT:ADMINFORTH ROUTES */', routes);
|
|
233
224
|
await fs.promises.writeFile(routerVuePath, routerVueContent);
|
|
234
225
|
|
|
235
226
|
|
|
236
227
|
/* hash checking */
|
|
237
|
-
const spaPackageLockPath = path.join(
|
|
228
|
+
const spaPackageLockPath = path.join(CodeInjector.SPA_TMP_PATH, 'package-lock.json');
|
|
238
229
|
const spaPackageLock = JSON.parse(await fs.promises.readFile(spaPackageLockPath, 'utf-8'));
|
|
239
230
|
const spaLockHash = hashify(spaPackageLock);
|
|
240
231
|
|
|
@@ -249,7 +240,7 @@ class CodeInjector {
|
|
|
249
240
|
const packagesNamesHash = hashify(packageNames);
|
|
250
241
|
|
|
251
242
|
const fullHash = `${spaLockHash}::${packagesNamesHash}::${usersLockHash}`;
|
|
252
|
-
const hashPath = path.join(
|
|
243
|
+
const hashPath = path.join(CodeInjector.SPA_TMP_PATH, 'node_modules', '.adminforth_hash');
|
|
253
244
|
|
|
254
245
|
try {
|
|
255
246
|
const existingHash = await fs.promises.readFile(hashPath, 'utf-8');
|
|
@@ -268,7 +259,7 @@ class CodeInjector {
|
|
|
268
259
|
}
|
|
269
260
|
}
|
|
270
261
|
|
|
271
|
-
await this.runNpmShell({command: 'ci', verbose, cwd:
|
|
262
|
+
await this.runNpmShell({command: 'ci', verbose, cwd: CodeInjector.SPA_TMP_PATH});
|
|
272
263
|
|
|
273
264
|
// get packages with version from customPackage
|
|
274
265
|
const IGNORE_PACKAGES = ['tsx', 'typescript', 'express', 'nodemon'];
|
|
@@ -283,7 +274,7 @@ class CodeInjector {
|
|
|
283
274
|
|
|
284
275
|
if (packageNames.length) {
|
|
285
276
|
const npmInstallCommand = `install ${[...packageNames, ...customPackgeNames].join(' ')}`;
|
|
286
|
-
await this.runNpmShell({command: npmInstallCommand, cwd:
|
|
277
|
+
await this.runNpmShell({command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH});
|
|
287
278
|
}
|
|
288
279
|
|
|
289
280
|
await fs.promises.writeFile(hashPath, fullHash);
|
|
@@ -291,7 +282,7 @@ class CodeInjector {
|
|
|
291
282
|
}
|
|
292
283
|
|
|
293
284
|
async watchForReprepare({ verbose }) {
|
|
294
|
-
const spaPath = path.join(
|
|
285
|
+
const spaPath = path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa');
|
|
295
286
|
// get list of all subdirectories in spa recursively
|
|
296
287
|
const directories = [];
|
|
297
288
|
const collectDirectories = async (dir) => {
|
package/modules/utils.ts
CHANGED
|
@@ -15,16 +15,15 @@ export function guessLabelFromName(name) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
let package_json;
|
|
19
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
let __dirname = path.join(path.dirname(__filename), '..');
|
|
20
|
+
if (__dirname.endsWith('dist')) {
|
|
21
|
+
// in prod build we are in dist also
|
|
22
|
+
__dirname = path.join(__dirname, '..');
|
|
23
|
+
}
|
|
25
24
|
|
|
26
25
|
export const ADMIN_FORTH_ABSOLUTE_PATH = __dirname;
|
|
27
26
|
|
|
28
|
-
package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
|
|
27
|
+
const package_json = JSON.parse(fs.readFileSync(path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'package.json'), 'utf8'));
|
|
29
28
|
|
|
30
29
|
export const ADMINFORTH_VERSION = package_json.version;
|
package/package.json
CHANGED
package/spa/src/stores/core.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
24
24
|
}, {});
|
|
25
25
|
config.value = resp.config;
|
|
26
26
|
user.value = resp.user;
|
|
27
|
-
console.log('🌍 AdminForth
|
|
27
|
+
console.log('🌍 AdminForth v', resp.version);
|
|
28
28
|
|
|
29
29
|
// find homepage:true in menu recuresively
|
|
30
30
|
|