@@ -15,6 +15,7 @@ import { randomUUID } from 'crypto';
1515import { listify } from '../modules/utils.js' ;
1616import { afLogger } from '../modules/logger.js' ;
1717import * as z from 'zod' ;
18+ import multer from 'multer' ;
1819
1920function replaceAtStart ( string , substring ) {
2021 if ( string . startsWith ( substring ) ) {
@@ -62,6 +63,7 @@ const EXPRESS_REGEXP_PARAM_CAPTURE_RE = /\(\?:\(\[\^\\\/]\+\?\)\)/g;
6263const EXPRESS_REGEXP_ESCAPED_SLASH_RE = / \\ \/ / g;
6364const EXPRESS_REGEXP_TRAILING_DOLLAR_RE = / \$ $ / ;
6465const EXPRESS_REGEXP_LEADING_CARET_RE = / ^ \^ / ;
66+ type MulterParser = ( req : any , res : any , callback : ( error ?: unknown ) => void ) => void ;
6567
6668type RegisteredExpressRouteSchema = IAdminForthExpressRouteSchema & {
6769 request ?: AnySchemaObject ;
@@ -154,9 +156,13 @@ class ExpressServer implements IExpressHttpServer {
154156 adminforth : IAdminForth ;
155157 server : http . Server ;
156158 schemaAwareRouteRegistrationPatched = false ;
159+ uploadParser : MulterParser ;
157160
158161 constructor ( adminforth : IAdminForth ) {
159162 this . adminforth = adminforth ;
163+ this . uploadParser = multer ( {
164+ storage : multer . memoryStorage ( ) ,
165+ } ) . any ( ) ;
160166 }
161167
162168 setupSpaServer ( ) {
@@ -543,6 +549,7 @@ class ExpressServer implements IExpressHttpServer {
543549 request_schema,
544550 response_schema,
545551 responce_schema,
552+ target= 'json'
546553 } = options ;
547554 if ( ! path . startsWith ( '/' ) ) {
548555 throw new Error ( `Path must start with /, got: ${ path } ` ) ;
@@ -572,17 +579,39 @@ class ExpressServer implements IExpressHttpServer {
572579 // AdminForth API endpoints accept only application/json for POST, PUT, PATCH, DELETE
573580 // If you need other content types, use a custom server endpoint.
574581 const method = ( req . method || '' ) . toUpperCase ( ) ;
582+ const contentTypeHeader = ( req . headers ?. [ 'content-type' ] || '' ) . toString ( ) ;
575583 if ( [ "POST" , "PUT" , "PATCH" , "DELETE" ] . includes ( method ) ) {
576- const contentTypeHeader = ( req . headers ?. [ 'content-type' ] || '' ) . toString ( ) ;
577- const isJson = contentTypeHeader . toLowerCase ( ) . startsWith ( 'application/json' ) ;
578- if ( ! isJson ) {
584+ const expectedContentType = target === 'upload' ? 'multipart/form-data' : 'application/json' ;
585+ const hasExpectedContentType = contentTypeHeader . toLowerCase ( ) . startsWith ( expectedContentType ) ;
586+ if ( ! hasExpectedContentType ) {
579587 const passed = contentTypeHeader || 'undefined' ;
580- res . status ( 415 ) . send ( `AdminForth API endpoints support only requests with Content/Type: application/json, when you passed: ${ passed } . Please use custom server endpoint if you really need this content type` ) ;
588+ res . status ( 415 ) . send ( `AdminForth API endpoint supports only requests with Content-Type: ${ expectedContentType } , when you passed: ${ passed } . Please use custom server endpoint if you really need this content type` ) ;
589+ return ;
590+ }
591+ }
592+ if ( target === 'upload' ) {
593+ try {
594+ await new Promise < void > ( ( resolve , reject ) => {
595+ this . uploadParser ( req , res , ( error ?: unknown ) => {
596+ if ( error ) {
597+ reject ( error ) ;
598+ return ;
599+ }
600+
601+ resolve ( ) ;
602+ } ) ;
603+ } ) ;
604+ if ( ! ( req as any ) . file && Array . isArray ( ( req as any ) . files ) && ( req as any ) . files . length ) {
605+ ( req as any ) . file = ( req as any ) . files [ 0 ] ;
606+ }
607+ } catch ( error ) {
608+ afLogger . error ( `Failed to parse multipart form-data body, ${ error } ` ) ;
609+ res . status ( 400 ) . send ( 'Invalid multipart/form-data body' ) ;
581610 return ;
582611 }
583612 }
584613 let body = req . body || { } ;
585- if ( typeof body === 'string' ) {
614+ if ( typeof body === 'string' && target === 'json' ) {
586615 try {
587616 body = JSON . parse ( body ) ;
588617 } catch ( e ) {
0 commit comments