Skip to content

Commit b0dd4ae

Browse files
authored
Merge branch 'next' into feature/AdminForth/1181/we-need-to-limit-error-length
2 parents 7dca36a + bec462b commit b0dd4ae

File tree

26 files changed

+456
-213
lines changed

26 files changed

+456
-213
lines changed

adapters/install-adapters.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ 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 adminforth-completion-adapter-google-gemini"
7+
adminforth-login-captcha-adapter-cloudflare adminforth-login-captcha-adapter-recaptcha adminforth-completion-adapter-google-gemini \
8+
adminforth-key-value-adapter-redis adminforth-key-value-adapter-leveldb"
89

910
# for each
1011
install_adapter() {

adminforth/commands/createApp/templates/.env.local.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
ADMINFORTH_SECRET=123
22
NODE_ENV=development
3+
DEBUG_LEVEL=info
4+
AF_DEBUG_LEVEL=info
5+
DB_DEBUG_LEVEL=info
36
DATABASE_URL={{{dbUrl}}}
47
{{#if prismaDbUrl}}
58
PRISMA_DATABASE_URL={{{prismaDbUrl}}}

adminforth/documentation/docs/tutorial/05-ListOfAdapters.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,49 @@ The RAM adapter is a simplest in-memory key-value storage. Stores data in proces
244244

245245
Pros:
246246
* Simplest in use - does not reqauire any external daemon.
247+
247248
Cones:
248249
* In production sutable for single-process installations only
249250

250251

252+
### Redis adapter
253+
254+
```bash
255+
npm i @adminforth/key-value-adapter-redis
256+
```
257+
258+
Redis adapter uses redis database.
259+
260+
```ts
261+
import RedisKeyValueAdapter from '@adminforth/key-value-adapter-redis';
262+
263+
const adapter = new RedisKeyValueAdapter({
264+
redisUrl: '127.0.0.1:6379'
265+
})
266+
267+
adapeter.set('test-key', 'test-value', 120); //expiry in 120 seconds
268+
269+
```
270+
271+
### LevelDB adapter
272+
273+
```bash
274+
npm i @adminforth/key-value-adapter-leveldb
275+
```
276+
277+
LebelDB uses local storage for storing keys.
278+
279+
```ts
280+
import LevelDBKeyValueAdapter from '@adminforth/key-value-adapter-leveldb'
281+
282+
const adapter = new LevelDBKeyValueAdapter({
283+
dbPath: './testdb'
284+
});
285+
286+
adapeter.set('test-key', 'test-value', 120); //expiry in 120 seconds
287+
288+
```
289+
251290
## 🤖Captcha adapters
252291

253292
Used to add capthca to the login screen

adminforth/documentation/docs/tutorial/08-Plugins/05-upload.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,73 @@ new UploadPlugin({
305305
> adminServeBaseUrl defines the public path prefix. If your AdminForth base URL is /admin, files will be accessible under /admin/static/source/<key>.
306306
307307
308+
## API
309+
310+
### uploadFromBufferToNewRecord
311+
312+
In some cases you may want to upload a file directly from your backend (for example, a file generated by a background job or received from a webhook) without going through the browser. For this, the Upload plugin exposes the `uploadFromBufferToNewRecord` method.
313+
314+
You can code it as custom logic or you can simply reuse Upload plugin for this purpose as well.
315+
316+
This method uploads a file from a Node.js `Buffer`, automatically creates a record in the corresponding resource, and returns both the stored file path and a preview URL.
317+
318+
```ts title="./some-backend-service.ts"
319+
import { admin } from './admin'; // your AdminForth instance
320+
321+
...
322+
plugins: [
323+
new UploadPlugin({
324+
id: 'my_reports_plugin', // unique identifier for your plugin instance
325+
....
326+
})
327+
]
328+
...
329+
330+
const plugin = admin.getPluginById('my_reports_plugin');
331+
332+
const { path, previewUrl } = await plugin.uploadFromBufferToNewRecord({
333+
filename: 'report.pdf',
334+
contentType: 'application/pdf',
335+
buffer, // Node.js Buffer with file content
336+
adminUser, // current admin user or system user
337+
recordAttributes: {
338+
title: 'Generated report',
339+
listed: false,
340+
},
341+
});
342+
```
343+
344+
- `uploadFromBufferToNewRecord` uses the configured storage adapter (S3, local, etc.) to store the file.
345+
- It automatically creates a new record in the resource and stores the file path into the column defined by `pathColumnName`, together with any extra `recordAttributes` you pass.
346+
- It returns an object `{ path, previewUrl }`, where `previewUrl` is the same URL that would be used for previews inside AdminForth.
347+
348+
> ⚠️ It is not recommended to upload large files from the backend using `uploadFromBuffer`, because the entire file must go through your server memory and network. For large uploads you should prefer frontend presigned uploads directly to storage. You can find an example of presigned upload flow using upload plugin in the Rich editor plugin source code (Rich editor actually uses Upload plugin to upload images in edited content).
349+
350+
351+
### uploadFromBufferToExistingRecord
352+
353+
If you already have a record and just want to replace the file referenced in its `pathColumnName` field, you can use the `uploadFromBufferToExistingRecord` method. It uploads a file from a Node.js `Buffer`, updates the existing record, and returns the new file path and preview URL.
354+
355+
```ts title="./some-backend-service.ts"
356+
const plugin = admin.getPluginById('my_reports_plugin');
357+
358+
const { path, previewUrl } = await plugin.uploadFromBufferToExistingRecord({
359+
recordId: existingRecordId, // primary key of the record to update
360+
filename: 'report.pdf',
361+
contentType: 'application/pdf',
362+
buffer, // Node.js Buffer with file content
363+
adminUser, // current admin user or system user
364+
extra: {}, // optional extra meta for your hooks / audit
365+
});
366+
```
367+
368+
- Uses the same storage adapter and validation rules as `uploadFromBufferToNewRecord` (file extension whitelist, `maxFileSize`, `filePath` callback, etc.).
369+
- Does not create a new record – it only updates the existing one identified by `recordId`, replacing the value in `pathColumnName` with the new storage path.
370+
- If the generated `filePath` would be the same as the current value in the record, it throws an error to help you avoid CDN/browser caching issues. To force a refresh, make sure your `filePath` callback produces a different key (for example, include a timestamp or random UUID).
371+
372+
> ⚠️ The same recommendation about large files applies here: avoid using `uploadFromBufferToExistingRecord` for very large uploads; prefer a presigned upload flow from the frontend instead.
373+
374+
308375
309376
## Image generation
310377

adminforth/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -568,17 +568,21 @@ class AdminForth implements IAdminForth {
568568
response,
569569
extra,
570570
});
571-
if (!resp || (typeof resp.ok !== 'boolean' && (!resp.error && !resp.newRecordId))) {
571+
if (resp.newRecordId) {
572+
afLogger.warn(`Deprecation warning: beforeSave hook returned 'newRecordId'. Since AdminForth v1.2.9 use 'redirectToRecordId' instead. 'newRecordId' will be removed in v2.0.0`);
573+
}
574+
if (!resp || (typeof resp.ok !== 'boolean' && (!resp.error && !resp.newRecordId && !resp.redirectToRecordId))) {
572575
throw new Error(
573-
`Invalid return value from beforeSave hook. Expected: { ok: boolean, error?: string | null, newRecordId?: any }.\n` +
574-
`Note: Return { ok: false, error: null, newRecordId } to stop creation and redirect to an existing record.`
576+
`Invalid return value from beforeSave hook. Expected: { ok: boolean, error?: string | null, newRecordId?: any, redirectToRecordId?: any }.\n` +
577+
`Note: Return { ok: false, error: null, redirectToRecordId } (preferred) or { ok: false, error: null, newRecordId } (deprecated) to stop creation and redirect to an existing record.`
575578
);
576579
}
577580
if (resp.ok === false && !resp.error) {
578-
const { error, ok, newRecordId } = resp;
581+
const { error, ok, newRecordId, redirectToRecordId } = resp;
579582
return {
580583
error: error ?? 'Operation aborted by hook',
581-
newRecordId: newRecordId
584+
newRecordId: redirectToRecordId ? redirectToRecordId : newRecordId,
585+
redirectToRecordId: redirectToRecordId
582586
};
583587
}
584588
if (resp.error) {

adminforth/modules/logger.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,21 @@ const baseLogger = pino({
66
options: {
77
colorize: true,
88
ignore: 'pid,hostname',
9-
hideObject: true,
10-
messageFormat: '{layer}: {msg}',
119
}
1210
},
1311
});
1412

1513
export const logger = baseLogger.child(
16-
{ layer: 'user_logs' },
17-
{ level: process.env.HEAVY_DEBUG ? 'trace' : ( process.env.DEBUG_LEVEL || 'info' ) }
14+
{ name: 'User logs' },
15+
{ level: process.env.HEAVY_DEBUG ? 'trace' : ( process.env.DEBUG_LEVEL || 'info' ) },
1816
);
1917

2018
export const afLogger = baseLogger.child(
21-
{ layer: 'af' },
19+
{ name: 'AF' },
2220
{ level: process.env.HEAVY_DEBUG ? 'trace' : ( process.env.AF_DEBUG_LEVEL || 'info' ) }
2321
);
2422

2523
export const dbLogger = baseLogger.child(
26-
{ layer: 'db' },
24+
{ name: 'DB' },
2725
{ level: process.env.HEAVY_DEBUG_QUERY ? 'trace' : (process.env.DB_DEBUG_LEVEL|| 'info') }
2826
);

adminforth/modules/restApi.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1280,7 +1280,11 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI {
12801280
extra: { body, query, headers, cookies, requestUrl, response }
12811281
});
12821282
if (createRecordResponse.error) {
1283-
return { error: createRecordResponse.error, ok: false, newRecordId: createRecordResponse.newRecordId };
1283+
return {
1284+
error: createRecordResponse.error,
1285+
ok: false,
1286+
newRecordId: createRecordResponse.redirectToRecordId ? createRecordResponse.redirectToRecordId :createRecordResponse.newRecordId,
1287+
redirectToRecordId: createRecordResponse.redirectToRecordId };
12841288
}
12851289
const connector = this.adminforth.connectors[resource.dataSource];
12861290

adminforth/servers/express.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ class ExpressServer implements IExpressHttpServer {
217217
this.server.listen(...args);
218218
}
219219

220-
async processAuthorizeCallbacks(adminUser: AdminUser, toReturn: { error?: string, allowed: boolean }, response: Response, extra: any) {
220+
async processAuthorizeCallbacks(adminUser: AdminUser, toReturn: { error?: string, allowed: boolean }, response: Response, extra: HttpExtra) {
221221
const adminUserAuthorize = this.adminforth.config.auth.adminUserAuthorize as (AdminUserAuthorizeFunction[] | undefined);
222222

223223
for (const hook of listify(adminUserAuthorize)) {
@@ -269,7 +269,15 @@ class ExpressServer implements IExpressHttpServer {
269269
} else {
270270
req.adminUser = adminforthUser;
271271
const toReturn: { error?: string, allowed: boolean } = { allowed: true };
272-
await this.processAuthorizeCallbacks(adminforthUser, toReturn, res, {});
272+
await this.processAuthorizeCallbacks(adminforthUser, toReturn, res, {
273+
body: req.body,
274+
query: req.query,
275+
headers: req.headers,
276+
cookies: cookies as any,
277+
requestUrl: req.url,
278+
meta: {},
279+
response: res
280+
});
273281
if (!toReturn.allowed) {
274282
res.status(401).send('Unauthorized by AdminForth');
275283
} else {

adminforth/spa/src/components/Toast.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>
2424
<div class="flex flex-col items-center justify-center break-all overflow-hidden [display:-webkit-box] [-webkit-box-orient:vertical] [-webkit-line-clamp:70]">
2525
{{toast.message}}
26-
<div v-if="toast.buttons" class="flex justify-center mt-2 gap-2">
27-
<div v-for="button in toast.buttons" class="af-toast-button rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
28-
<button @click="onButtonClick(button.value)" class="px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/10">
29-
{{ button.label }}
30-
</button>
31-
</div>
26+
</div>
27+
<div v-if="toast.buttons" class="flex mt-2 gap-2 w-full ml-6">
28+
<div v-for="button in toast.buttons" class="af-toast-button rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
29+
<button @click="onButtonClick(button.value)" class="px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/10">
30+
{{ button.label }}
31+
</button>
3232
</div>
3333
</div>
3434
</div>

adminforth/spa/src/utils/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './utils';
2+
export * from './listUtils';

0 commit comments

Comments
 (0)