adminforth 1.0.0
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.js +66 -0
- package/dataConnectors/mongo.js +187 -0
- package/dataConnectors/postgres.js +283 -0
- package/dataConnectors/sqlite.js +257 -0
- package/index.js +722 -0
- package/modules/codeInjector.js +313 -0
- package/modules/utils.js +12 -0
- package/package.json +23 -0
- package/servers/express.js +217 -0
- package/spa/.eslintrc.cjs +14 -0
- package/spa/.vscode/extensions.json +6 -0
- package/spa/README.md +39 -0
- package/spa/env.d.ts +1 -0
- package/spa/index.html +23 -0
- package/spa/package-lock.json +4152 -0
- package/spa/package.json +40 -0
- package/spa/postcss.config.js +6 -0
- package/spa/public/favicon.ico +0 -0
- package/spa/src/App.vue +172 -0
- package/spa/src/assets/base.css +0 -0
- package/spa/src/assets/logo.svg +1 -0
- package/spa/src/components/AcceptModal.vue +52 -0
- package/spa/src/components/Breadcrumbs.vue +40 -0
- package/spa/src/components/BreadcrumbsWithButtons.vue +26 -0
- package/spa/src/components/CustomDateRangePicker.vue +218 -0
- package/spa/src/components/Dropdown.vue +154 -0
- package/spa/src/components/Filters.vue +141 -0
- package/spa/src/components/HelloWorld.vue +17 -0
- package/spa/src/components/MenuLink.vue +25 -0
- package/spa/src/components/ResourceForm.vue +198 -0
- package/spa/src/components/SingleSkeletLoader.vue +13 -0
- package/spa/src/components/ValueRenderer.vue +44 -0
- package/spa/src/components/icons/IconCalendar.vue +5 -0
- package/spa/src/components/icons/IconCommunity.vue +7 -0
- package/spa/src/components/icons/IconDocumentation.vue +7 -0
- package/spa/src/components/icons/IconEcosystem.vue +7 -0
- package/spa/src/components/icons/IconSupport.vue +7 -0
- package/spa/src/components/icons/IconTime.vue +5 -0
- package/spa/src/components/icons/IconTooling.vue +19 -0
- package/spa/src/index.scss +26 -0
- package/spa/src/main.ts +18 -0
- package/spa/src/router/index.ts +53 -0
- package/spa/src/stores/core.ts +135 -0
- package/spa/src/stores/modal.ts +38 -0
- package/spa/src/utils.ts +44 -0
- package/spa/src/views/CreateView.vue +103 -0
- package/spa/src/views/EditView.vue +95 -0
- package/spa/src/views/HomeView.vue +8 -0
- package/spa/src/views/ListView.vue +466 -0
- package/spa/src/views/LoginView.vue +122 -0
- package/spa/src/views/ResourceParent.vue +18 -0
- package/spa/src/views/ShowView.vue +94 -0
- package/spa/tailwind.config.js +12 -0
- package/spa/tsconfig.app.json +14 -0
- package/spa/tsconfig.json +11 -0
- package/spa/tsconfig.node.json +19 -0
- package/spa/vite.config.ts +42 -0
- package/types.js +34 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import fsExtra from 'fs-extra';
|
|
3
|
+
import filewatcher from 'filewatcher';
|
|
4
|
+
import { exec, spawn } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.join(path.dirname(__filename), '..');
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
|
|
16
|
+
function hashify(obj) {
|
|
17
|
+
return crypto.createHash
|
|
18
|
+
('sha256').update(JSON.stringify(obj)).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class CodeInjector {
|
|
22
|
+
constructor(adminforth) {
|
|
23
|
+
this.adminforth = adminforth;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async runShell({command, verbose = false}) {
|
|
27
|
+
console.time(`Running ${command}...`);
|
|
28
|
+
const { stdout: out, stderr: err } = await execAsync(command);
|
|
29
|
+
console.timeEnd(`Running ${command}...`);
|
|
30
|
+
console.log(`Command ${command} output:`, out, err);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async runNpmShell({command, verbose = false, cwd}) {
|
|
34
|
+
const nodeBinary = process.execPath; // Path to the Node.js binary running this script
|
|
35
|
+
const npmPath = path.join(path.dirname(nodeBinary), 'npm'); // Path to the npm executable
|
|
36
|
+
|
|
37
|
+
const env = {
|
|
38
|
+
VUE_APP_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl,
|
|
39
|
+
FORCE_COLOR: '1',
|
|
40
|
+
...process.env,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
console.time(`Running npm ${command}...`);
|
|
44
|
+
const { stdout: out, stderr: err } = await execAsync(`${nodeBinary} ${npmPath} ${command}`, {
|
|
45
|
+
cwd,
|
|
46
|
+
env,
|
|
47
|
+
});
|
|
48
|
+
console.timeEnd(`Running npm ${command}...`);
|
|
49
|
+
|
|
50
|
+
if (verbose) {
|
|
51
|
+
console.log(`npm ${command} output:`, out);
|
|
52
|
+
}
|
|
53
|
+
if (err) {
|
|
54
|
+
console.error(`npm ${command} errors/warnings:`, err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async rmTmpDir() {
|
|
59
|
+
// remove spa_tmp folder if it is exists
|
|
60
|
+
const spaTmpPath = path.join(__dirname, 'spa_tmp');
|
|
61
|
+
try {
|
|
62
|
+
await fs.promises.rm(spaTmpPath, { recursive: true });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async prepareSources({ filesUpdated, verbose = false }) {
|
|
69
|
+
const spaTmpPath = path.join(__dirname, 'spa_tmp');
|
|
70
|
+
|
|
71
|
+
const customFiles = [];
|
|
72
|
+
const icons = [];
|
|
73
|
+
let routes = '';
|
|
74
|
+
|
|
75
|
+
const collectAssetsFromMenu = (menu) => {
|
|
76
|
+
menu.forEach((item) => {
|
|
77
|
+
if (item.icon) {
|
|
78
|
+
icons.push(item.icon);
|
|
79
|
+
}
|
|
80
|
+
if (item.component) {
|
|
81
|
+
customFiles.push(item.component);
|
|
82
|
+
routes += `{
|
|
83
|
+
path: '${item.path}',
|
|
84
|
+
name: '${item.path}',
|
|
85
|
+
component: import('@/custom/${item.component}'),
|
|
86
|
+
},\n`
|
|
87
|
+
}
|
|
88
|
+
if (item.children) {
|
|
89
|
+
collectAssetsFromMenu(item.children);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
collectAssetsFromMenu(this.adminforth.config.menu);
|
|
94
|
+
|
|
95
|
+
if (this.adminforth.config.customization?.vueUsesFile) {
|
|
96
|
+
customFiles.push(this.adminforth.config.customization.vueUsesFile);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// create spa_tmp folder, or ignore if it exists
|
|
100
|
+
try {
|
|
101
|
+
await fs.promises.mkdir(spaTmpPath);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (filesUpdated) {
|
|
107
|
+
// copy only updated files
|
|
108
|
+
await Promise.all(filesUpdated.map(async (file) => {
|
|
109
|
+
const src = path.join(__dirname, 'spa', file);
|
|
110
|
+
const dest = path.join(spaTmpPath, file);
|
|
111
|
+
await fsExtra.copy(src, dest);
|
|
112
|
+
}));
|
|
113
|
+
} else {
|
|
114
|
+
await fsExtra.copy(path.join(__dirname, 'spa'), spaTmpPath, {
|
|
115
|
+
filter: (src) => {
|
|
116
|
+
return !src.includes('/node_modules') && !src.includes('/dist');
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// copy custom files
|
|
121
|
+
await Promise.all(customFiles.map(async (file) => {
|
|
122
|
+
const src = path.join(file);
|
|
123
|
+
const dest = path.join(spaTmpPath, 'src', 'custom', file);
|
|
124
|
+
await fsExtra.copy(src, dest);
|
|
125
|
+
}))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//collect all 'icon' fields from resources bulkActions
|
|
129
|
+
this.adminforth.config.resources.forEach((resource) => {
|
|
130
|
+
if (resource.options?.bulkActions) {
|
|
131
|
+
resource.options.bulkActions.forEach((action) => {
|
|
132
|
+
if (action.icon) {
|
|
133
|
+
icons.push(action.icon);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const uniqueIcons = Array.from(new Set(icons));
|
|
140
|
+
|
|
141
|
+
// icons are collectionName:iconName. Get list of all unique collection names:
|
|
142
|
+
const collections = new Set(icons.map((icon) => icon.split(':')[0]));
|
|
143
|
+
|
|
144
|
+
// package names @iconify-prerendered/vue-<collection name>
|
|
145
|
+
const packageNames = Array.from(collections).map((collection) => `@iconify-prerendered/vue-${collection}`);
|
|
146
|
+
|
|
147
|
+
// for each icon generate import statement
|
|
148
|
+
const iconImports = uniqueIcons.map((icon) => {
|
|
149
|
+
const [ collection, iconName ] = icon.split(':');
|
|
150
|
+
const PascalIconName = 'Icon' + iconName.split('-').map((part, index) => {
|
|
151
|
+
return part[0].toUpperCase() + part.slice(1);
|
|
152
|
+
}).join('');
|
|
153
|
+
return `import { ${PascalIconName} } from '@iconify-prerendered/vue-${collection}';`;
|
|
154
|
+
}).join('\n');
|
|
155
|
+
|
|
156
|
+
// Generate Vue.component statements for each icon
|
|
157
|
+
const iconComponents = uniqueIcons.map((icon) => {
|
|
158
|
+
const [ collection, iconName ] = icon.split(':');
|
|
159
|
+
const PascalIconName = 'Icon' + iconName.split('-').map((part, index) => {
|
|
160
|
+
return part[0].toUpperCase() + part.slice(1);
|
|
161
|
+
}).join('');
|
|
162
|
+
return `app.component('${PascalIconName}', ${PascalIconName});`;
|
|
163
|
+
}).join('\n');
|
|
164
|
+
|
|
165
|
+
let imports = iconImports + '\n';
|
|
166
|
+
|
|
167
|
+
if (this.adminforth.config.customization?.vueUsesFile) {
|
|
168
|
+
imports += `import addCustomUses from '@/custom/${this.adminforth.config.customization.vueUsesFile}';\n`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// inject that code into spa_tmp/src/App.vue
|
|
172
|
+
const appVuePath = path.join(spaTmpPath, 'src', 'main.ts');
|
|
173
|
+
let appVueContent = await fs.promises.readFile(appVuePath, 'utf-8');
|
|
174
|
+
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH IMPORTS */', imports);
|
|
175
|
+
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n' );
|
|
176
|
+
if (this.adminforth.config.customization?.vueUsesFile) {
|
|
177
|
+
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH CUSTOM USES */', 'addCustomUses(app);');
|
|
178
|
+
}
|
|
179
|
+
await fs.promises.writeFile(appVuePath, appVueContent);
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
/* generate custom rotes */
|
|
183
|
+
const routerVuePath = path.join(spaTmpPath, 'src', 'router', 'index.ts');
|
|
184
|
+
let routerVueContent = await fs.promises.readFile(routerVuePath, 'utf-8');
|
|
185
|
+
routerVueContent = routerVueContent.replace('/* IMPORTANT:ADMINFORTH ROUTES */', routes);
|
|
186
|
+
await fs.promises.writeFile(routerVuePath, routerVueContent);
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
/* hash checking */
|
|
190
|
+
const packageLockPath = path.join(spaTmpPath, 'package-lock.json');
|
|
191
|
+
const packageLock = JSON.parse(await fs.promises.readFile(packageLockPath, 'utf-8'));
|
|
192
|
+
const lockHash = hashify(packageLock);
|
|
193
|
+
/* customPackageLock */
|
|
194
|
+
const customPackagePath = path.join('./package.json');
|
|
195
|
+
const customPackage = JSON.parse(await fs.promises.readFile(customPackagePath, 'utf-8'));
|
|
196
|
+
const customPackageHash = hashify(customPackage);
|
|
197
|
+
|
|
198
|
+
const customLockPath = path.join('./package-lock.json');
|
|
199
|
+
const customLock = JSON.parse(await fs.promises.readFile(customLockPath, 'utf-8'));
|
|
200
|
+
const customLockHash = hashify(customLock);
|
|
201
|
+
|
|
202
|
+
const packagesNamesHash = hashify(packageNames);
|
|
203
|
+
|
|
204
|
+
const fullHash = hashify([lockHash, packagesNamesHash, customLockHash]);
|
|
205
|
+
const hashPath = path.join(spaTmpPath, 'node_modules', '.adminforth_hash');
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const existingHash = await fs.promises.readFile(hashPath, 'utf-8');
|
|
209
|
+
if (existingHash === fullHash) {
|
|
210
|
+
console.log('Hashes match, skipping npm ci/install');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// ignore
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
await this.runNpmShell({command: 'ci', verbose, cwd: spaTmpPath});
|
|
218
|
+
|
|
219
|
+
// get packages with version from customPackage
|
|
220
|
+
const customPackgeNames = [...Object.keys(customPackage.dependencies), ...Object.keys(customPackage.devDependencies || [])].reduce(
|
|
221
|
+
(acc, packageName) => {
|
|
222
|
+
const version = customLock.packages[`node_modules/${packageName}`].version;
|
|
223
|
+
acc.push(`${packageName}@${version}`);
|
|
224
|
+
return acc;
|
|
225
|
+
}, []);
|
|
226
|
+
|
|
227
|
+
if (packageNames.length) {
|
|
228
|
+
const npmInstallCommand = `install ${[...packageNames, ...customPackgeNames].join(' ')}`;
|
|
229
|
+
await this.runNpmShell({command: npmInstallCommand, cwd: spaTmpPath});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
await fs.promises.writeFile(hashPath, fullHash);
|
|
233
|
+
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async watchForReprepare() {
|
|
237
|
+
const spaPath = path.join(__dirname, 'spa');
|
|
238
|
+
// get list of all subdirectories in spa recursively
|
|
239
|
+
const directories = [];
|
|
240
|
+
const collectDirectories = async (dir) => {
|
|
241
|
+
const files = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
242
|
+
for (const file of files) {
|
|
243
|
+
if (file.isDirectory() && ['node_modules', 'dist'].indexOf(file.name) === -1) {
|
|
244
|
+
directories.push(path.join(dir, file.name));
|
|
245
|
+
await collectDirectories(path.join(dir, file.name));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
await collectDirectories(spaPath);
|
|
250
|
+
|
|
251
|
+
// console.log('👌👌Watching for changes in:', directories.join('\n '))
|
|
252
|
+
|
|
253
|
+
const watcher = filewatcher();
|
|
254
|
+
directories.forEach((dir) => {
|
|
255
|
+
watcher.add(dir);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
watcher.on(
|
|
259
|
+
'change',
|
|
260
|
+
async (file) => {
|
|
261
|
+
console.log(`File ${file} changed, preparing sources...`);
|
|
262
|
+
await this.prepareSources({ filesUpdated: [file.replace(spaPath + '/', '')] });
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
process.on('exit', () => {
|
|
266
|
+
watcher.removeAll();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async bundleNow({hotReload = false, verbose = false}) {
|
|
271
|
+
this.adminforth.config.runningHotReload = hotReload;
|
|
272
|
+
|
|
273
|
+
await this.prepareSources({ verbose });
|
|
274
|
+
await this.watchForReprepare();
|
|
275
|
+
console.log('AdminForth bundling');
|
|
276
|
+
|
|
277
|
+
const cwd = path.join(__dirname, 'spa_tmp');
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
if (!hotReload) {
|
|
282
|
+
await this.runNpmShell({command: 'run build', verbose, cwd});
|
|
283
|
+
} else {
|
|
284
|
+
const command = 'run dev';
|
|
285
|
+
console.time(`Running npm ${command}...`);
|
|
286
|
+
const nodeBinary = process.execPath;
|
|
287
|
+
const npmPath = path.join(path.dirname(nodeBinary), 'npm');
|
|
288
|
+
const env = {
|
|
289
|
+
VUE_APP_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl,
|
|
290
|
+
FORCE_COLOR: '1',
|
|
291
|
+
...process.env,
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const devServer = spawn(`${nodeBinary}`, [`${npmPath}`, ...command.split(' ')], {
|
|
295
|
+
cwd,
|
|
296
|
+
env,
|
|
297
|
+
});
|
|
298
|
+
devServer.stdout.on('data', (data) => {
|
|
299
|
+
console.log(`[AdminForth SPA]:`);
|
|
300
|
+
process.stdout.write(data);
|
|
301
|
+
});
|
|
302
|
+
devServer.stderr.on('data', (data) => {
|
|
303
|
+
console.error(`[AdminForth SPA ERR]:`);
|
|
304
|
+
process.stdout.write(data);
|
|
305
|
+
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
console.timeEnd(`Running npm ${command}...`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export default CodeInjector;
|
package/modules/utils.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export function guessLabelFromName(name) {
|
|
4
|
+
if (name.includes('_')) {
|
|
5
|
+
return name.split('_').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
6
|
+
} else if (name.includes('-')) {
|
|
7
|
+
return name.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
8
|
+
} else {
|
|
9
|
+
// split by capital letters
|
|
10
|
+
return name.split(/(?=[A-Z])/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adminforth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenSource Vue3 powered forth-generation admin panel",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"author": "devforth.io",
|
|
10
|
+
"license": "ISC",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"better-sqlite3": "^10.0.0",
|
|
14
|
+
"crypto": "^1.0.1",
|
|
15
|
+
"dayjs": "^1.11.11",
|
|
16
|
+
"filewatcher": "^3.0.1",
|
|
17
|
+
"fs-extra": "^11.2.0",
|
|
18
|
+
"jsonwebtoken": "^9.0.2",
|
|
19
|
+
"mongodb": "6.6",
|
|
20
|
+
"pg": "^8.11.5",
|
|
21
|
+
"uuid": "^9.0.1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function replaceAtStart(string, substring) {
|
|
11
|
+
if (string.startsWith(substring)) {
|
|
12
|
+
return string.slice(substring.length);
|
|
13
|
+
}
|
|
14
|
+
return string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function proxyTo(url, res) {
|
|
18
|
+
const r = await fetch(url);
|
|
19
|
+
const body = await r.text();
|
|
20
|
+
res.status(r.status);
|
|
21
|
+
r.headers.forEach((value, name) => {
|
|
22
|
+
res.setHeader(name, value);
|
|
23
|
+
});
|
|
24
|
+
res.send(body);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function parseExpressCookie(req) {
|
|
28
|
+
const cookies = req.headers.cookie;
|
|
29
|
+
if (!cookies) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
const parts = cookies.split('; ');
|
|
33
|
+
const result = {};
|
|
34
|
+
parts.forEach(part => {
|
|
35
|
+
const [key, value] = part.split('=');
|
|
36
|
+
result[key] = value;
|
|
37
|
+
});
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
const respondNoServer = (title, explanation) => {
|
|
44
|
+
return `
|
|
45
|
+
<!DOCTYPE html>
|
|
46
|
+
<html lang="en">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="UTF-8">
|
|
49
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
50
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
51
|
+
<title>AdminForth</title>
|
|
52
|
+
</head>
|
|
53
|
+
<body>
|
|
54
|
+
<div class="center">
|
|
55
|
+
<h1>Oops!</h1>
|
|
56
|
+
<h2>${title}</h2>
|
|
57
|
+
<p>${explanation}</p>
|
|
58
|
+
</div>
|
|
59
|
+
<style>
|
|
60
|
+
body {
|
|
61
|
+
font-family: Arial, sans-serif;
|
|
62
|
+
background-color: #f0f0f0;
|
|
63
|
+
margin: 0;
|
|
64
|
+
padding: 0;
|
|
65
|
+
}
|
|
66
|
+
.center {
|
|
67
|
+
display: flex;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
align-items: center;
|
|
70
|
+
height: 100vh;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
74
|
+
<script>
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
location.reload();
|
|
77
|
+
}, 1500);
|
|
78
|
+
</script>
|
|
79
|
+
</body>
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
class ExpressServer {
|
|
83
|
+
constructor(adminforth) {
|
|
84
|
+
this.adminforth = adminforth;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setupSpaServer() {
|
|
88
|
+
const prefix = this.adminforth.config.baseUrl
|
|
89
|
+
|
|
90
|
+
const slashedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
91
|
+
|
|
92
|
+
if (this.adminforth.config.runningHotReload) {
|
|
93
|
+
const handler = async (req, res) => {
|
|
94
|
+
// proxy using fetch to webpack dev server
|
|
95
|
+
try {
|
|
96
|
+
await proxyTo(`http://localhost:5173${req.url}`, res);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
res.status(500).send(respondNoServer('AdminForth SPA is not ready yet', 'Vite is still starting up. Please wait a moment...'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.expressApp.get(`${slashedPrefix}assets/*`, handler);
|
|
103
|
+
this.expressApp.get(`${prefix}*`, handler);
|
|
104
|
+
|
|
105
|
+
} else {
|
|
106
|
+
this.expressApp.get(`${slashedPrefix}assets/*`, (req, res) => {
|
|
107
|
+
res.sendFile(path.join(__dirname, '..', 'spa', 'dist', replaceAtStart(req.url, prefix)))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
this.expressApp.get(`${prefix}*`, async (req, res) => {
|
|
111
|
+
const fullPath = path.join(__dirname, '..', 'spa', 'dist', 'index.html');
|
|
112
|
+
|
|
113
|
+
let fileExists = true;
|
|
114
|
+
try {
|
|
115
|
+
await fs.promises.access(fullPath, fs.constants.F_OK);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
fileExists = false;
|
|
118
|
+
}
|
|
119
|
+
console.log('fileExists', fileExists);
|
|
120
|
+
if (!fileExists) {
|
|
121
|
+
res.status(500).send(respondNoServer(`${this.adminforth.config.brandName} is still warming up`, 'Please wait a moment...'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
res.sendFile(fullPath);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
serve(app) {
|
|
130
|
+
this.expressApp = app;
|
|
131
|
+
this.adminforth.setupEndpoints(this);
|
|
132
|
+
this.setupSpaServer();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
authorize(handler) {
|
|
136
|
+
return async (req, res, next) => {
|
|
137
|
+
const cookies = await parseExpressCookie(req);
|
|
138
|
+
const jwt = cookies['adminforth_jwt'];
|
|
139
|
+
if (!jwt) {
|
|
140
|
+
res.status(401).send('Unauthorized by AdminForth');
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
const adminforthUser = this.adminforth.auth.verify(jwt);
|
|
144
|
+
if (!adminforthUser) {
|
|
145
|
+
res.status(401).send('Unauthorized by AdminForth');
|
|
146
|
+
} else {
|
|
147
|
+
req.adminUser = adminforthUser;
|
|
148
|
+
handler(req, res, next);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
endpoint({ method='GET', path, handler, noAuth=false }) {
|
|
154
|
+
if (!path.startsWith('/')) {
|
|
155
|
+
throw new Error(`Path must start with /, got: ${path}`);
|
|
156
|
+
}
|
|
157
|
+
const fullPath = `${this.adminforth.config.baseUrl}/adminapi/v1${path}`;
|
|
158
|
+
|
|
159
|
+
const expressHandler = async (req, res) => {
|
|
160
|
+
let body = req.body || {};
|
|
161
|
+
if (typeof body === 'string') {
|
|
162
|
+
try {
|
|
163
|
+
body = JSON.parse(body);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error('Failed to parse body', e);
|
|
166
|
+
res.status(400).send('Invalid JSON body');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const query = req.query;
|
|
171
|
+
const adminUser = req.adminUser;
|
|
172
|
+
const headers = req.headers;
|
|
173
|
+
const cookies = await parseExpressCookie(req);
|
|
174
|
+
|
|
175
|
+
const response = {
|
|
176
|
+
headers: {},
|
|
177
|
+
status: 200,
|
|
178
|
+
message: undefined,
|
|
179
|
+
setHeader(name, value) {
|
|
180
|
+
this.headers[name] = value;
|
|
181
|
+
},
|
|
182
|
+
setStatus(code, message) {
|
|
183
|
+
this.status = code;
|
|
184
|
+
this.message = message;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const input = { body, query, headers, cookies, response, _raw_express_req: req, _raw_express_res: res};
|
|
188
|
+
|
|
189
|
+
let output;
|
|
190
|
+
try {
|
|
191
|
+
output = await handler(input);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.error('Error in handler', e);
|
|
194
|
+
// print full stack trace
|
|
195
|
+
console.error(e.stack);
|
|
196
|
+
|
|
197
|
+
res.status(500).send('Internal server error');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
Object.keys(response.headers).forEach((name) => {
|
|
201
|
+
res.setHeader(name, response.headers[name]);
|
|
202
|
+
})
|
|
203
|
+
const resp = res.status(response.status);
|
|
204
|
+
if (response.message) {
|
|
205
|
+
resp.send(response.message);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
res.json(output);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`Adding endpoint ${method} ${fullPath}`);
|
|
212
|
+
this.expressApp[method.toLowerCase()](fullPath, noAuth ? expressHandler : this.authorize(expressHandler));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default ExpressServer;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
require('@rushstack/eslint-patch/modern-module-resolution')
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
root: true,
|
|
6
|
+
'extends': [
|
|
7
|
+
'plugin:vue/vue3-essential',
|
|
8
|
+
'eslint:recommended',
|
|
9
|
+
'@vue/eslint-config-typescript'
|
|
10
|
+
],
|
|
11
|
+
parserOptions: {
|
|
12
|
+
ecmaVersion: 'latest'
|
|
13
|
+
}
|
|
14
|
+
}
|
package/spa/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# spa
|
|
2
|
+
|
|
3
|
+
This template should help get you started developing with Vue 3 in Vite.
|
|
4
|
+
|
|
5
|
+
## Recommended IDE Setup
|
|
6
|
+
|
|
7
|
+
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
|
8
|
+
|
|
9
|
+
## Type Support for `.vue` Imports in TS
|
|
10
|
+
|
|
11
|
+
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
|
12
|
+
|
|
13
|
+
## Customize configuration
|
|
14
|
+
|
|
15
|
+
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|
16
|
+
|
|
17
|
+
## Project Setup
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Compile and Hot-Reload for Development
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm run dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Type-Check, Compile and Minify for Production
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm run build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Lint with [ESLint](https://eslint.org/)
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm run lint
|
|
39
|
+
```
|
package/spa/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/spa/index.html
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<link rel="icon" href="/favicon.ico">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Vite App</title>
|
|
8
|
+
<!--
|
|
9
|
+
<script>
|
|
10
|
+
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
|
11
|
+
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
12
|
+
document.documentElement.classList.add('dark');
|
|
13
|
+
} else {
|
|
14
|
+
document.documentElement.classList.remove('dark')
|
|
15
|
+
}
|
|
16
|
+
</script> -->
|
|
17
|
+
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="app"></div>
|
|
21
|
+
<script type="module" src="/src/main.ts"></script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|