Skip to content

Commit 6c740e2

Browse files
committed
Merge branch 'main' of https://github.com/devforth/adminforth into fix-mongo-id-filter
2 parents 5ba0c0e + c0c481f commit 6c740e2

File tree

336 files changed

+25092
-6775
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

336 files changed

+25092
-6775
lines changed

README.md

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# AdminForth - fully free Node.js admin panel framework on Vue & Tailwind
1+
# AdminForth - free powerfull Node.js admin panel framework on Vue & Tailwind
22

33

44
<a href="https://adminforth.dev"><img src="https://img.shields.io/badge/website-adminforth.dev-blue" style="height:24px"/></a> <a href="https://adminforth.dev"><img src="https://img.shields.io/npm/dw/adminforth" style="height:24px"/></a> <a href="https://devforth.io"><img src="https://raw.githubusercontent.com/devforth/OnLogs/e97944fffc24fec0ce2347b205c9bda3be8de5c5/.assets/df_powered_by.svg" style="height:28px"/></a>
@@ -20,12 +20,15 @@
2020
<br/>
2121

2222
Why AdminForth:
23-
* AdminForth is always free and open-source (no paid versions, no cloud subscriptions sh*t)
24-
* Init AdminForth with your database URL in Node.js file, easily describe the tables you wish to see in admin, and get fully functional UI for your data (filter, create, edit, remove)
25-
* Define Vue components to change look of various parts of admin (place in data cell, instead of row, add something above the table, inject something to header or sidebar, add custom page with charts or custom components)
26-
* Rich build-in Components library (AdminForth AFCL) with premade easy-to-use build-blocks which follow your theme
23+
24+
* Init AdminForth project with `npx adminforth create-app` and pass your database URL, import the tables you wish to see in admin using `npx adminforth resource`, and get fully functional UI for your data (filter, create, edit, remove)
25+
* Modern look and simple Tailwind-ish ability to adjust it
26+
* Supports Postgres, MySQL, Mongo, SQLite, Clickhouse
27+
* Define Vue components to change look of various parts of admin using `npx adminforth component` (edit data cells, edit fields, add something above the table, inject something to header or sidebar, add custom page with charts or custom components)
28+
* Build-in Components library (AdminForth AFCL) with premade easy-to-use build-blocks which follow your theme
2729
* Define express APIs and call them from your components and pages
28-
* Use various modern back-office-must-have plugins like audit log, files/image upload, TOTP 2FA, I18N, Copilot-style AI writing and image generation
30+
* Use various modern back-office-must-have plugins like audit log, files/image upload, TOTP 2FA, I18N, Copilot-style AI writing and image generation and many more
31+
* AdminForth is always free and open-source (no paid versions, no cloud subscriptions sh*t)
2932

3033

3134
## Project initialisation
@@ -54,7 +57,6 @@ npx adminforth create-app
5457

5558

5659

57-
5860
# For developers
5961

6062
The most convenient way to add new features or fixes is using `dev-demo`. It imports the source code of the repository and plugins so you can edit them and see changes on the fly.
@@ -66,20 +68,24 @@ Fork repo, pull it and do next:
6668
cd adminforth
6769
npm ci
6870
npm run build
71+
```
72+
73+
To run dev demo:
74+
```sh
75+
cd dev-demo
76+
cp .env.sample .env
6977

7078
# this will install all official plugins and link adminforth package, if plugin installed it will git pull and npm ci
7179
npm run install-plugins
7280

7381
# same for official adapters
7482
npm run install-adapters
75-
```
7683

77-
To run dev demo:
78-
```sh
79-
cd dev-demo
80-
cp .env.sample .env
8184
npm ci
82-
npm run migrate
85+
86+
./run_inventory.sh
87+
88+
npm run migrate:local
8389
npm start
8490
```
8591

@@ -92,3 +98,29 @@ npm run namemigration -- --name desctiption_of_changes
9298
```
9399

94100

101+
### Testing CLI commands during development
102+
103+
104+
Make sure you have not `adminforth` globally installed. If you have it, remove it:
105+
106+
107+
```sh
108+
npm uninstall -g adminforth
109+
```
110+
111+
Then, in the root of the project, run once:
112+
113+
```
114+
cd adminforth/adminforth
115+
npm run build
116+
```
117+
118+
This will automatically make an npm link to the `adminforth` package in the root of the project.
119+
120+
Then, go to testing app, e.g. created with CLI, and use next command:
121+
122+
```
123+
npx -g adminforth <your command under development>
124+
```
125+
126+
This will always run latest version of adminforth package.

adapters/install-adapters.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#!/usr/bin/env bash
2-
ADAPTERS="adminforth-completion-adapter-open-ai-chat-gpt adminforth-email-adapter-aws-ses adminforth-google-oauth-adapter adminforth-github-oauth-adapter adminforth-facebook-oauth-adapter adminforth-keycloak-oauth-adapter adminforth-microsoft-oauth-adapter"
2+
ADAPTERS="adminforth-completion-adapter-open-ai-chat-gpt adminforth-email-adapter-aws-ses \
3+
adminforth-email-adapter-mailgun adminforth-google-oauth-adapter adminforth-github-oauth-adapter \
4+
adminforth-facebook-oauth-adapter adminforth-keycloak-oauth-adapter adminforth-microsoft-oauth-adapter \
5+
adminforth-twitch-oauth-adapter adminforth-image-generation-adapter-openai adminforth-storage-adapter-amazon-s3 \
6+
adminforth-storage-adapter-local adminforth-image-vision-adapter-openai adminforth-key-value-adapter-ram \
7+
adminforth-login-captcha-adapter-cloudflare adminforth-login-captcha-adapter-recaptcha"
38

49
# for each
510
install_adapter() {

adminforth/auth.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,18 @@ class AdminForthAuth implements IAdminForthAuth {
7676
response.setHeader('Set-Cookie', `adminforth_${brandSlug}_jwt=; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=Thu, 01 Jan 1970 00:00:00 GMT`);
7777
}
7878

79-
setAuthCookie({ expireInDays, response, username, pk}: {
80-
expireInDays?: number,
79+
setAuthCookie({ expireInDuration, response, username, pk}: {
80+
expireInDuration?: string,
8181
response: any,
8282
username: string,
8383
pk: string | null
8484
}) {
85-
const expiresIn: string = expireInDays ? `${expireInDays}d` : (process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h');
85+
const expiresIn: string = expireInDuration || (process.env.ADMINFORTH_AUTH_EXPIRESIN || '24h');
8686
// might be h,m,d in string
8787
const expiresInSec = parseTimeToSeconds(expiresIn);
8888

89-
const token = this.issueJWT({ username, pk}, 'auth', expiresIn);
89+
const token = this.issueJWT({ username, pk}, 'auth', expiresInSec);
9090
const expiresCookieFormat = new Date(Date.now() + expiresInSec * 1000).toUTCString();
91-
9291
const brandSlug = this.adminforth.config.customization.brandNameSlug;
9392
response.setHeader('Set-Cookie', `adminforth_${brandSlug}_jwt=${token}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${expiresCookieFormat}`);
9493
}
@@ -99,16 +98,32 @@ class AdminForthAuth implements IAdminForthAuth {
9998
}
10099

101100
setCustomCookie({ response, payload }: {
102-
response: any, payload: {name: string, value: string, expiry: number, httpOnly: boolean}
101+
response: any, payload: { name: string, value: string, expiry: number | undefined, expirySeconds: number | undefined, httpOnly: boolean }
103102
}) {
104-
const {name, value, expiry, httpOnly} = payload;
103+
const {name, value, expiry, httpOnly, expirySeconds } = payload;
104+
105+
let expiryMs = 24 * 60 * 60 * 1000; // default 1 day
106+
if (expirySeconds !== undefined) {
107+
expiryMs = expirySeconds * 1000;
108+
} else if (expiry !== undefined) {
109+
console.warn('setCustomCookie: expiry(in ms) is deprecated, use expirySeconds instead (seconds), traceback:', new Error().stack);
110+
expiryMs = expiry;
111+
}
112+
105113
const brandSlug = this.adminforth.config.customization.brandNameSlug;
106114
response.setHeader('Set-Cookie', `adminforth_${brandSlug}_${name}=${value}; Path=${this.adminforth.config.baseUrl || '/'};${
107115
httpOnly ? ' HttpOnly;' : ''
108-
} SameSite=Strict; Expires=${new Date(Date.now() + expiry).toUTCString() } `);
116+
} SameSite=Strict; Expires=${new Date(Date.now() + expiryMs).toUTCString() } `);
117+
}
118+
119+
getCustomCookie({ cookies, name }: {
120+
cookies: {key: string, value: string}[], name: string
121+
}): string | null {
122+
const brandSlug = this.adminforth.config.customization.brandNameSlug;
123+
return cookies.find((cookie) => cookie.key === `adminforth_${brandSlug}_${name}`)?.value || null;
109124
}
110125

111-
issueJWT(payload: Object, type: string, expiresIn: string = '24h'): string {
126+
issueJWT(payload: Object, type: string, expiresIn: string | number = '24h'): string {
112127
// read ADMINFORH_SECRET from environment if not drop error
113128
const secret = process.env.ADMINFORTH_SECRET;
114129
if (!secret) {

adminforth/commands/bundle.js

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,22 @@
1-
import fs from "fs";
2-
import { getInstance } from "./utils.js";
1+
import { callTsProxy, findAdminInstance } from "./callTsProxy.js";
2+
33

44
async function bundle() {
5-
const currentDirectory = process.cwd();
6-
const files = fs.readdirSync(currentDirectory);
7-
let instanceFound = false;
5+
console.log("Bundling admin SPA...");
6+
const instance = await findAdminInstance();
7+
8+
9+
try {
10+
await callTsProxy(`
11+
import { admin } from './${instance.file}.js';
812
9-
for (const file of files) {
10-
if (file.endsWith(".js") || file.endsWith(".ts")) {
11-
try {
12-
const instance = await getInstance(file, currentDirectory);
13-
if (instance) {
14-
await instance.bundleNow({ hotReload: false });
15-
instanceFound = true;
16-
break;
17-
}
18-
} catch (error) {
19-
console.error(`Error: Could not bundle '${file}'`, error);
13+
export async function exec() {
14+
return await admin.bundleNow({ hotReload: false });
2015
}
21-
}
22-
}
23-
if (!instanceFound) {
24-
console.error("Error: No valid instance found to bundle.");
25-
return;
16+
`);
17+
18+
} catch (e) {
19+
console.log(`Running budndle failed`, e);
2620
}
2721
}
2822

adminforth/commands/callTsProxy.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// callTsProxy.js
2+
import { spawn } from "child_process";
3+
import path from "path";
4+
import fs from "fs";
5+
import chalk from "chalk";
6+
import dotenv from "dotenv";
7+
8+
const currentFilePath = import.meta.url;
9+
const currentFileFolder = path.dirname(currentFilePath).replace("file:", "");
10+
11+
export function callTsProxy(tsCode, silent=false) {
12+
13+
const currentDirectory = process.cwd();
14+
const envPath = path.resolve(currentDirectory, ".env");
15+
const envLocalPath = path.resolve(currentDirectory, ".env.local");
16+
if (fs.existsSync(envLocalPath)) {
17+
dotenv.config({ path: envLocalPath, override: true });
18+
}
19+
if (fs.existsSync(envPath)) {
20+
dotenv.config({ path: envPath, override: true });
21+
}
22+
23+
process.env.HEAVY_DEBUG && console.log("🌐 Calling tsproxy with code:", path.join(currentFileFolder, "proxy.ts"));
24+
return new Promise((resolve, reject) => {
25+
const child = spawn("tsx", [path.join(currentFileFolder, "proxy.ts")], {
26+
env: process.env,
27+
});
28+
let stdout = "";
29+
let stderr = "";
30+
31+
child.stdout.on("data", (data) => {
32+
stdout += data;
33+
});
34+
35+
child.stderr.on("data", (data) => {
36+
stderr += data;
37+
});
38+
39+
child.on("close", (code) => {
40+
if (code === 0) {
41+
try {
42+
const parsed = JSON.parse(stdout);
43+
if (!silent) {
44+
parsed.capturedLogs.forEach((log) => {
45+
console.log(...log);
46+
});
47+
}
48+
49+
if (parsed.error) {
50+
reject(new Error(`${parsed.error}\n${parsed.stack}`));
51+
}
52+
resolve(parsed.result);
53+
} catch (e) {
54+
reject(new Error("Invalid JSON from tsproxy: " + stdout));
55+
}
56+
} else {
57+
console.error(`tsproxy exited with non-0, this should never happen, stdout: ${stdout}, stderr: ${stderr}`);
58+
reject(new Error(stderr));
59+
}
60+
});
61+
62+
process.env.HEAVY_DEBUG && console.log("🪲 Writing to tsproxy stdin...\n'''", tsCode, "'''");
63+
child.stdin.write(tsCode);
64+
child.stdin.end();
65+
});
66+
}
67+
68+
export async function findAdminInstance() {
69+
process.env.HEAVY_DEBUG && console.log("🌐 Finding admin instance...");
70+
const currentDirectory = process.cwd();
71+
72+
let files = fs.readdirSync(currentDirectory);
73+
let instanceFound = {
74+
file: null,
75+
version: null,
76+
};
77+
// try index.ts first
78+
if (files.includes("index.ts")) {
79+
files = files.filter((file) => file !== "index.ts");
80+
files.unshift("index.ts");
81+
}
82+
83+
for (const file of files) {
84+
if (file.endsWith(".ts")) {
85+
const fileNoTs = file.replace(/\.ts$/, "");
86+
process.env.HEAVY_DEBUG && console.log(`🪲 Trying bundleing ${file}...`);
87+
try {
88+
const res = await callTsProxy(`
89+
import { admin } from './${fileNoTs}.js';
90+
91+
export async function exec() {
92+
return admin.formatAdminForth();
93+
}
94+
`, true);
95+
instanceFound.file = fileNoTs;
96+
instanceFound.version = res;
97+
break;
98+
99+
} catch (e) {
100+
// do our best to guess that this file has a good chance to be admin instance
101+
// and show the error so user can fix it
102+
const fileContent = fs.readFileSync(file, "utf-8");
103+
if (fileContent.includes("export const admin")) {
104+
console.error(chalk.red(`Error running ${file}:`, e));
105+
process.exit(1);
106+
}
107+
process.env.HEAVY_DEBUG && console.log(`🪲 File ${file} failed`, e);
108+
}
109+
}
110+
}
111+
if (!instanceFound.file) {
112+
console.error(
113+
chalk.red(
114+
`Error: No valid instance found to bundle.\n` +
115+
`Make sure you have a file in the current directory with a .ts extension, and it exports an ` +
116+
chalk.cyan.bold('admin') +
117+
` instance like:\n\n` +
118+
chalk.yellow('export const admin = new AdminForth({...})') +
119+
`\n\nFor example, adminforth CLI creates an index.ts file which exports the admin instance.`
120+
)
121+
);
122+
process.exit(1);
123+
}
124+
return instanceFound;
125+
}
126+
127+
// Example usage:
128+
// callTsProxy(`
129+
// import admin from './admin';
130+
// function exec() {
131+
// return admin.doX();
132+
// }
133+
// `).then(console.log).catch(console.error);

0 commit comments

Comments
 (0)