Skip to content

Commit db12e64

Browse files
authored
Merge pull request #456 from devforth/next
Next
2 parents 941974a + e1db74d commit db12e64

File tree

207 files changed

+4404
-5363
lines changed

Some content is hidden

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

207 files changed

+4404
-5363
lines changed

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Follow SOLID principles
2+
Use DRY and avoid duplication
3+
Keep the code KISS unless complexity is required
4+
Avoid unnecessary abstractions.
5+
Cover edge cases and clear error handling

README.md

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,44 +58,50 @@ npx adminforth create-app
5858

5959
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.
6060

61-
Fork repo, pull it and do next:
61+
To run dev demo:
62+
```sh
63+
cd dev-demo
6264

65+
npm run setup-dev-demo
66+
npm run migrate:all
6367

64-
```sh
65-
cd adminforth
66-
npm ci
67-
npm run build
68+
npm start
6869
```
6970

70-
To run dev demo:
71-
```sh
72-
cd dev-demo
73-
cp .env.sample .env
71+
## Adding columns to a database in dev-demo
7472

75-
# this will install all official plugins and link adminforth package, if plugin installed it will git pull and npm ci
76-
npm run install-plugins
73+
Open `./migrations` folder. There is prisma migration folder for the sqlite, postgres and mysql and `clickhouse_migrations` folder for the clickhouse:
7774

78-
# same for official adapters
79-
npm run install-adapters
75+
### Migrations for the MySQL, SQLite and Postgres
76+
To make migration add to the .prisma file in folder with database you need and add new tables or columns. Then run:
8077

81-
npm ci
8278

83-
./run_inventory.sh
79+
```
80+
npm run makemigration:sqlite -- --name init
81+
```
82+
83+
and
8484

85-
npm run migrate:local
86-
npm start
85+
```
86+
npm run migrate:sqlite
8787
```
8888

89-
## Adding columns to a database in dev-demo
89+
to apply migration
90+
91+
> use :sqlite, :mysql or :postgres for you case
92+
93+
### Migrations for the clickhouse
9094

91-
Open `.prisma` file, modify it, and run:
95+
In order to make migration for the clickhouse, go to the `./migrations/clickhouse_migrations` folder and add migration file to the folder.
9296

97+
Then run
9398
```
94-
npm run namemigration -- --name desctiption_of_changes
99+
npm run migrate:clickhouse
95100
```
96101

102+
to apply the migration.
97103

98-
### Testing CLI commands during development
104+
## Testing CLI commands during development
99105

100106

101107
Make sure you have not `adminforth` globally installed. If you have it, remove it:

adapters/install-adapters.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ adminforth-email-adapter-mailgun adminforth-google-oauth-adapter adminforth-gith
44
adminforth-facebook-oauth-adapter adminforth-keycloak-oauth-adapter adminforth-microsoft-oauth-adapter \
55
adminforth-twitch-oauth-adapter adminforth-image-generation-adapter-openai adminforth-storage-adapter-amazon-s3 \
66
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"
7+
adminforth-login-captcha-adapter-cloudflare adminforth-login-captcha-adapter-recaptcha adminforth-completion-adapter-google-gemini"
88

99
# for each
1010
install_adapter() {

adminforth/auth.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import jwt from 'jsonwebtoken';
33
import crypto from 'crypto';
44
import AdminForth from './index.js';
55
import { IAdminForthAuth } from './types/Back.js';
6+
import { afLogger } from './modules/logger.js';
7+
import is_ip_private from 'private-ip'
68

79
// Function to generate a password hash using PBKDF2
810
function calcPasswordHash(password, salt, iterations = 100000, keyLength = 64, digest = 'sha512') {
@@ -43,20 +45,22 @@ class AdminForthAuth implements IAdminForthAuth {
4345
this.adminforth = adminforth;
4446
}
4547

46-
getClientIp(headers: object) {
48+
getClientIp(headers: object) {
4749
const clientIpHeader = this.adminforth.config.auth.clientIpHeader;
4850

4951
const headersLower = Object.keys(headers).reduce((acc, key) => {
5052
acc[key.toLowerCase()] = headers[key];
5153
return acc;
5254
}, {});
55+
56+
let ip: string | null = null;
5357
if (clientIpHeader) {
54-
return headersLower[clientIpHeader.toLowerCase()] || 'unknown';
58+
ip = headersLower[clientIpHeader.toLowerCase()];
5559
} else {
5660
// first try common headers which can't bee spoofed, in other words
5761
// most common to nginx/traefik/apache
5862
// then fallback to less secure headers
59-
return headersLower['x-forwarded-for']?.split(',').shift().trim() ||
63+
ip = headersLower['x-forwarded-for']?.split(',').shift().trim() ||
6064
headersLower['x-real-ip'] ||
6165
headersLower['x-client-ip'] ||
6266
headersLower['x-cluster-client-ip'] ||
@@ -65,10 +69,14 @@ class AdminForthAuth implements IAdminForthAuth {
6569
headersLower['client-ip'] ||
6670
headersLower['client-address'] ||
6771
headersLower['client'] ||
68-
headersLower['x-host'] ||
69-
headersLower['host'] ||
70-
'unknown';
72+
headersLower['x-host'] ||
73+
null;
74+
}
75+
const isIpPrivate = is_ip_private(ip)
76+
if (isIpPrivate) {
77+
return null;
7178
}
79+
return ip;
7280
}
7381

7482
removeAuthCookie(response) {
@@ -98,15 +106,15 @@ class AdminForthAuth implements IAdminForthAuth {
98106
}
99107

100108
setCustomCookie({ response, payload }: {
101-
response: any, payload: { name: string, value: string, expiry: number | undefined, expirySeconds: number | undefined, httpOnly: boolean }
109+
response: any, payload: { name: string, value: string, expiry?: number | undefined, expirySeconds: number | undefined, httpOnly: boolean }
102110
}) {
103111
const {name, value, expiry, httpOnly, expirySeconds } = payload;
104112

105113
let expiryMs = 24 * 60 * 60 * 1000; // default 1 day
106114
if (expirySeconds !== undefined) {
107115
expiryMs = expirySeconds * 1000;
108116
} else if (expiry !== undefined) {
109-
console.warn('setCustomCookie: expiry(in ms) is deprecated, use expirySeconds instead (seconds), traceback:', new Error().stack);
117+
afLogger.warn(`setCustomCookie: expiry(in ms) is deprecated, use expirySeconds instead (seconds), traceback: ${new Error().stack}`);
110118
expiryMs = expiry;
111119
}
112120

@@ -146,23 +154,23 @@ class AdminForthAuth implements IAdminForthAuth {
146154
decoded = jwt.verify(jwtToken, secret);
147155
} catch (err) {
148156
if (err.name === 'TokenExpiredError') {
149-
console.error('Token expired:', err.message);
157+
afLogger.error(`Token expired: ${err.message}`);
150158
} else if (err.name === 'JsonWebTokenError') {
151-
console.error('Token error:', err.message);
159+
afLogger.error(`Token error: ${err.message}`);
152160
} else {
153-
console.error('Failed to verify JWT token', err);
161+
afLogger.error(`Failed to verify JWT token: ${err}`);
154162
}
155163
return null;
156164
}
157165
const { pk, t } = decoded;
158166
if (t !== mustHaveType) {
159-
console.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
167+
afLogger.error(`Invalid token type during verification: ${t}, must be ${mustHaveType}`);
160168
return null;
161169
}
162170
if (decodeUser !== false) {
163171
const dbUser = await this.adminforth.getUserByPk(pk);
164172
if (!dbUser) {
165-
console.error(`User with pk ${pk} not found in database`);
173+
afLogger.error(`User with pk ${pk} not found in database`);
166174
// will logout user which was deleted
167175
return null;
168176
}

adminforth/basePlugin.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import fs from 'fs';
66

77
import crypto from 'crypto';
88

9+
import { afLogger } from './modules/logger.js';
10+
911

1012
export default class AdminForthPlugin implements IAdminForthPlugin {
1113

@@ -24,7 +26,7 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
2426
this.pluginDir = currentFileDir(metaUrl);
2527
this.customFolderPath = path.join(this.pluginDir, this.customFolderName);
2628
this.pluginOptions = pluginOptions;
27-
process.env.HEAVY_DEBUG && console.log(`🪲 🪲 AdminForthPlugin.constructor`, this.constructor.name);
29+
afLogger.trace(`🪲 🪲 AdminForthPlugin.constructor ${this.constructor.name}`);
2830
this.className = this.constructor.name;
2931
}
3032

@@ -36,13 +38,17 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
3638
return 'non-uniquely-identified';
3739
}
3840

39-
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
41+
shouldHaveSingleInstancePerWholeApp(): boolean {
42+
return false;
43+
}
44+
45+
modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource, allPluginInstances?: {pi: AdminForthPlugin, resource: AdminForthResource}[]) {
4046
this.resourceConfig = resourceConfig;
4147
const uniqueness = this.instanceUniqueRepresentation(this.pluginOptions);
4248

4349
const seed = `af_pl_${this.constructor.name}_${resourceConfig.resourceId}_${uniqueness}`;
4450
this.pluginInstanceId = md5hash(seed);
45-
process.env.HEAVY_DEBUG && console.log(`🪲 AdminForthPlugin.modifyResourceConfig`, seed, 'id', this.pluginInstanceId);
51+
afLogger.trace(`🪲 AdminForthPlugin.modifyResourceConfig, ${seed}, 'id', ${this.pluginInstanceId}`);
4652
this.adminforth = adminforth;
4753
}
4854

adminforth/commands/callTsProxy.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ export function callTsProxy(tsCode, silent=false) {
3939
child.on("close", (code) => {
4040
if (code === 0) {
4141
try {
42-
const parsed = JSON.parse(stdout);
42+
const preparedStdout = stdout.slice(stdout.indexOf("{"), stdout.lastIndexOf("}") + 1);
43+
const parsed = JSON.parse(preparedStdout);
4344
if (!silent) {
4445
parsed.capturedLogs.forEach((log) => {
4546
console.log(...log);

adminforth/commands/createApp/templates/adminuser.ts.hbs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AdminForth, { AdminForthDataTypes } from 'adminforth';
22
import type { AdminForthResourceInput, AdminForthResource, AdminUser } from 'adminforth';
33
import { randomUUID } from 'crypto';
4+
import { logger } from 'adminforth';
45

56
async function allowedForSuperAdmin({ adminUser }: { adminUser: AdminUser }): Promise<boolean> {
67
return adminUser.dbUser.role === 'superadmin';
@@ -94,7 +95,7 @@ export default {
9495
},
9596
edit: {
9697
beforeSave: async ({ oldRecord, updates, adminUser, resource }: { oldRecord: any, updates: any, adminUser: AdminUser, resource: AdminForthResource }) => {
97-
console.log('Updating user', updates);
98+
logger.info(`Updating user, ${updates}`);
9899
if (oldRecord.id === adminUser.dbUser.id && updates.role) {
99100
return { ok: false, error: 'You cannot change your own role' };
100101
}

adminforth/commands/createApp/templates/index.ts.hbs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
55
import path from 'path';
66
import { Filters } from 'adminforth';
77
import { initApi } from './api.js';
8+
import { logger } from 'adminforth';
89

910
const ADMIN_BASE_URL = '';
1011

@@ -75,7 +76,7 @@ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
7576
const port = 3500;
7677

7778
admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development' }).then(() => {
78-
console.log('Bundling AdminForth SPA done.');
79+
logger.info('Bundling AdminForth SPA done.');
7980
});
8081

8182
admin.express.serve(app);
@@ -91,6 +92,6 @@ if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
9192
});
9293

9394
admin.express.listen(port, () => {
94-
console.log(`\n⚡ AdminForth is available at http://localhost:${port}${ADMIN_BASE_URL}\n`);
95+
logger.info(`\x1b[38;5;249m ⚡ AdminForth is available at\x1b[1m\x1b[38;5;46m http://localhost:${port}${ADMIN_BASE_URL}\x1b[0m\n`);
9596
});
9697
}

adminforth/commands/createCustomComponent/main.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,6 @@ async function handleCrudPageInjectionCreation(config, resources) {
188188
const injectionPosition = await select({
189189
message: 'Where exactly do you want to inject the component?',
190190
choices: [
191-
...(crudType === 'create' || crudType === 'edit'
192-
? [{ name: '💾 Save button on create/edit page', value: 'saveButton' }, new Separator()]
193-
: []),
194191
{ name: '⬆️ Before Breadcrumbs', value: 'beforeBreadcrumbs' },
195192
{ name: '➡️ Before Action Buttons', value: 'beforeActionButtons' },
196193
{ name: '⬇️ After Breadcrumbs', value: 'afterBreadcrumbs' },

adminforth/commands/createCustomComponent/templates/customCrud/afterBreadcrumbs.vue.hbs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
<script setup lang="ts">
1414
import { onMounted } from 'vue';
1515
import { useI18n } from 'vue-i18n';
16-
import adminforth from '@/adminforth';
16+
import { useAdminforth } from '@/adminforth';
1717
import { AdminForthResourceCommon, AdminUser } from "@/types/Common";
1818
1919
const { t } = useI18n();
2020
21+
const { alert } = useAdminforth();
22+
2123
const props = defineProps<{
2224
record: any
2325
resource: AdminForthResourceCommon
@@ -30,7 +32,7 @@ onMounted(() => {
3032
});
3133
3234
function handleClick() {
33-
adminforth.alert({
35+
alert({
3436
message: t('Confirmed'),
3537
variant: 'success',
3638
});

0 commit comments

Comments
 (0)