@@ -53,6 +53,7 @@ const app = express();
5353const databasePath = path . join ( __dirname , "storage" , "shop.db" ) ;
5454const db = initializeDatabase ( databasePath , env ) ;
5555const productUploadDir = path . join ( __dirname , "public" , "uploads" , "products" ) ;
56+ const settingsUploadDir = path . join ( __dirname , "public" , "uploads" , "settings" ) ;
5657const stripe = env . STRIPE_SECRET_KEY ? new Stripe ( env . STRIPE_SECRET_KEY ) : null ;
5758const stripePublishableKey = env . STRIPE_PUBLISHABLE_KEY || "" ;
5859const swissBitcoinPayApiUrl = ( env . SWISS_BITCOIN_PAY_API_URL || "https://api.swiss-bitcoin-pay.ch" ) . replace ( / \/ $ / , "" ) ;
@@ -72,34 +73,39 @@ app.set("view engine", "ejs");
7273app . set ( "views" , path . join ( __dirname , "views" ) ) ;
7374app . use ( "/static" , express . static ( path . join ( __dirname , "public" ) ) ) ;
7475
75- const productImageUpload = multer ( {
76- storage : multer . diskStorage ( {
77- destination : ( req , file , callback ) => {
78- callback ( null , productUploadDir ) ;
76+ function createImageUpload ( uploadDir , maxFiles ) {
77+ return multer ( {
78+ storage : multer . diskStorage ( {
79+ destination : ( req , file , callback ) => {
80+ callback ( null , uploadDir ) ;
81+ } ,
82+ filename : ( req , file , callback ) => {
83+ const extensionByMimeType = {
84+ "image/jpeg" : ".jpg" ,
85+ "image/png" : ".png" ,
86+ "image/webp" : ".webp" ,
87+ "image/gif" : ".gif" ,
88+ } ;
89+ const extension = extensionByMimeType [ file . mimetype ] || ".img" ;
90+ callback ( null , `${ Date . now ( ) } -${ crypto . randomBytes ( 8 ) . toString ( "hex" ) } ${ extension } ` ) ;
91+ } ,
92+ } ) ,
93+ limits : {
94+ fileSize : 8 * 1024 * 1024 ,
95+ files : maxFiles ,
7996 } ,
80- filename : ( req , file , callback ) => {
81- const extensionByMimeType = {
82- "image/jpeg" : ".jpg" ,
83- "image/png" : ".png" ,
84- "image/webp" : ".webp" ,
85- "image/gif" : ".gif" ,
86- } ;
87- const extension = extensionByMimeType [ file . mimetype ] || ".img" ;
88- callback ( null , `${ Date . now ( ) } -${ crypto . randomBytes ( 8 ) . toString ( "hex" ) } ${ extension } ` ) ;
97+ fileFilter : ( req , file , callback ) => {
98+ if ( ! [ "image/jpeg" , "image/png" , "image/webp" , "image/gif" ] . includes ( file . mimetype ) ) {
99+ return callback ( new Error ( "Seules les images JPG, PNG, WebP ou GIF peuvent être importées." ) ) ;
100+ }
101+
102+ callback ( null , true ) ;
89103 } ,
90- } ) ,
91- limits : {
92- fileSize : 8 * 1024 * 1024 ,
93- files : 13 ,
94- } ,
95- fileFilter : ( req , file , callback ) => {
96- if ( ! [ "image/jpeg" , "image/png" , "image/webp" , "image/gif" ] . includes ( file . mimetype ) ) {
97- return callback ( new Error ( "Seules les images JPG, PNG, WebP ou GIF peuvent être importées." ) ) ;
98- }
104+ } ) ;
105+ }
99106
100- callback ( null , true ) ;
101- } ,
102- } ) ;
107+ const productImageUpload = createImageUpload ( productUploadDir , 13 ) ;
108+ const settingsImageUpload = createImageUpload ( settingsUploadDir , 1 ) ;
103109
104110function formatMoney ( cents , currency = "CHF" ) {
105111 return new Intl . NumberFormat ( "fr-CH" , {
@@ -433,15 +439,27 @@ function absoluteUrl(req, value) {
433439 return `${ origin } ${ input . startsWith ( "/" ) ? "" : "/" } ${ input } ` ;
434440}
435441
436- function productUploadUrl ( file ) {
442+ function uploadUrl ( file , folder ) {
437443 if ( ! file ?. filename ) {
438444 return "" ;
439445 }
440446
441- return `/static/uploads/products/${ file . filename } ` ;
447+ return `/static/uploads/${ folder } /${ file . filename } ` ;
448+ }
449+
450+ function productUploadUrl ( file ) {
451+ return uploadUrl ( file , "products" ) ;
452+ }
453+
454+ function settingsUploadUrl ( file ) {
455+ return uploadUrl ( file , "settings" ) ;
442456}
443457
444458function withProductUploads ( req , res , next ) {
459+ if ( req . productUploadsParsed ) {
460+ return next ( ) ;
461+ }
462+
445463 fs . mkdirSync ( productUploadDir , { recursive : true } ) ;
446464
447465 productImageUpload . fields ( [
@@ -453,10 +471,40 @@ function withProductUploads(req, res, next) {
453471 return saveSessionAndRedirect ( req , res , req . originalUrl ) ;
454472 }
455473
474+ req . productUploadsParsed = true ;
456475 return next ( ) ;
457476 } ) ;
458477}
459478
479+ function withSettingsUpload ( req , res , next ) {
480+ if ( req . settingsUploadParsed ) {
481+ return next ( ) ;
482+ }
483+
484+ fs . mkdirSync ( settingsUploadDir , { recursive : true } ) ;
485+
486+ settingsImageUpload . single ( "hero_image_file" ) ( req , res , ( error ) => {
487+ if ( error ) {
488+ setFlash ( req , "error" , error . message || "L'import de l'image a échoué." ) ;
489+ return saveSessionAndRedirect ( req , res , req . originalUrl ) ;
490+ }
491+
492+ req . settingsUploadParsed = true ;
493+ return next ( ) ;
494+ } ) ;
495+ }
496+
497+ function isProductUploadRequest ( req ) {
498+ return req . method === "POST" && (
499+ req . path === "/admin/products/new" ||
500+ / ^ \/ a d m i n \/ p r o d u c t s \/ \d + \/ e d i t $ / . test ( req . path )
501+ ) && req . is ( "multipart/form-data" ) ;
502+ }
503+
504+ function isSettingsUploadRequest ( req ) {
505+ return req . method === "POST" && req . path === "/admin/settings" && req . is ( "multipart/form-data" ) ;
506+ }
507+
460508function productInputWithUploads ( req ) {
461509 const input = { ...req . body } ;
462510 const primaryUpload = productUploadUrl ( req . files ?. image_file ?. [ 0 ] ) ;
@@ -1969,6 +2017,22 @@ app.use((req, res, next) => {
19692017 next ( ) ;
19702018} ) ;
19712019
2020+ app . use ( ( req , res , next ) => {
2021+ if ( ! req . currentAdmin ) {
2022+ return next ( ) ;
2023+ }
2024+
2025+ if ( isProductUploadRequest ( req ) ) {
2026+ return withProductUploads ( req , res , next ) ;
2027+ }
2028+
2029+ if ( isSettingsUploadRequest ( req ) ) {
2030+ return withSettingsUpload ( req , res , next ) ;
2031+ }
2032+
2033+ return next ( ) ;
2034+ } ) ;
2035+
19722036app . use ( ( req , res , next ) => {
19732037 if ( ! [ "POST" , "PUT" , "PATCH" , "DELETE" ] . includes ( req . method ) || req . path . startsWith ( "/webhooks/" ) ) {
19742038 return next ( ) ;
@@ -3077,15 +3141,15 @@ app.get("/admin/settings", requireAdmin, (req, res) => {
30773141 } ) ;
30783142} ) ;
30793143
3080- app . post ( "/admin/settings" , requireAdmin , ( req , res ) => {
3144+ app . post ( "/admin/settings" , requireAdmin , withSettingsUpload , ( req , res ) => {
30813145 const currentSettings = getSettings ( db ) ;
30823146 const nextSmtpPassword = String ( req . body . smtp_password || "" ) . trim ( ) ;
30833147 saveSettings ( db , {
30843148 store_name : String ( req . body . store_name || "" ) . trim ( ) ,
30853149 tagline : String ( req . body . tagline || "" ) . trim ( ) ,
30863150 hero_title : String ( req . body . hero_title || "" ) . trim ( ) ,
30873151 hero_text : String ( req . body . hero_text || "" ) . trim ( ) ,
3088- hero_image_url : String ( req . body . hero_image_url || "" ) . trim ( ) ,
3152+ hero_image_url : settingsUploadUrl ( req . file ) || String ( req . body . hero_image_url || "" ) . trim ( ) ,
30893153 hero_points : String ( req . body . hero_points || "" )
30903154 . split ( / \r ? \n / )
30913155 . map ( ( point ) => point . trim ( ) )
0 commit comments