@@ -4,6 +4,7 @@ const path = require("path");
44const crypto = require ( "crypto" ) ;
55const express = require ( "express" ) ;
66const session = require ( "express-session" ) ;
7+ const multer = require ( "multer" ) ;
78const nodemailer = require ( "nodemailer" ) ;
89const Stripe = require ( "stripe" ) ;
910const { verifyPassword } = require ( "./lib/auth" ) ;
@@ -50,6 +51,7 @@ const env = process.env;
5051const app = express ( ) ;
5152const databasePath = path . join ( __dirname , "storage" , "shop.db" ) ;
5253const db = initializeDatabase ( databasePath , env ) ;
54+ const productUploadDir = path . join ( __dirname , "public" , "uploads" , "products" ) ;
5355const stripe = env . STRIPE_SECRET_KEY ? new Stripe ( env . STRIPE_SECRET_KEY ) : null ;
5456const stripePublishableKey = env . STRIPE_PUBLISHABLE_KEY || "" ;
5557const swissBitcoinPayApiUrl = ( env . SWISS_BITCOIN_PAY_API_URL || "https://api.swiss-bitcoin-pay.ch" ) . replace ( / \/ $ / , "" ) ;
@@ -69,6 +71,35 @@ app.set("view engine", "ejs");
6971app . set ( "views" , path . join ( __dirname , "views" ) ) ;
7072app . use ( "/static" , express . static ( path . join ( __dirname , "public" ) ) ) ;
7173
74+ const productImageUpload = multer ( {
75+ storage : multer . diskStorage ( {
76+ destination : ( req , file , callback ) => {
77+ callback ( null , productUploadDir ) ;
78+ } ,
79+ filename : ( req , file , callback ) => {
80+ const extensionByMimeType = {
81+ "image/jpeg" : ".jpg" ,
82+ "image/png" : ".png" ,
83+ "image/webp" : ".webp" ,
84+ "image/gif" : ".gif" ,
85+ } ;
86+ const extension = extensionByMimeType [ file . mimetype ] || ".img" ;
87+ callback ( null , `${ Date . now ( ) } -${ crypto . randomBytes ( 8 ) . toString ( "hex" ) } ${ extension } ` ) ;
88+ } ,
89+ } ) ,
90+ limits : {
91+ fileSize : 8 * 1024 * 1024 ,
92+ files : 13 ,
93+ } ,
94+ fileFilter : ( req , file , callback ) => {
95+ if ( ! [ "image/jpeg" , "image/png" , "image/webp" , "image/gif" ] . includes ( file . mimetype ) ) {
96+ return callback ( new Error ( "Seules les images JPG, PNG, WebP ou GIF peuvent être importées." ) ) ;
97+ }
98+
99+ callback ( null , true ) ;
100+ } ,
101+ } ) ;
102+
72103function formatMoney ( cents , currency = "CHF" ) {
73104 return new Intl . NumberFormat ( "fr-CH" , {
74105 style : "currency" ,
@@ -397,6 +428,53 @@ function absoluteUrl(req, value) {
397428 return `${ origin } ${ input . startsWith ( "/" ) ? "" : "/" } ${ input } ` ;
398429}
399430
431+ function productUploadUrl ( file ) {
432+ if ( ! file ?. filename ) {
433+ return "" ;
434+ }
435+
436+ return `/static/uploads/products/${ file . filename } ` ;
437+ }
438+
439+ function withProductUploads ( req , res , next ) {
440+ fs . mkdirSync ( productUploadDir , { recursive : true } ) ;
441+
442+ productImageUpload . fields ( [
443+ { name : "image_file" , maxCount : 1 } ,
444+ { name : "gallery_files" , maxCount : 12 } ,
445+ ] ) ( req , res , ( error ) => {
446+ if ( error ) {
447+ setFlash ( req , "error" , error . message || "L'import des images a échoué." ) ;
448+ return saveSessionAndRedirect ( req , res , req . originalUrl ) ;
449+ }
450+
451+ return next ( ) ;
452+ } ) ;
453+ }
454+
455+ function productInputWithUploads ( req ) {
456+ const input = { ...req . body } ;
457+ const primaryUpload = productUploadUrl ( req . files ?. image_file ?. [ 0 ] ) ;
458+ const galleryUploads = ( req . files ?. gallery_files || [ ] ) . map ( productUploadUrl ) . filter ( Boolean ) ;
459+ const existingGalleryUrls = String ( input . image_gallery_urls || "" ) . trim ( ) ;
460+
461+ if ( primaryUpload ) {
462+ input . image_url = primaryUpload ;
463+ }
464+
465+ if ( ! input . image_url && galleryUploads . length ) {
466+ input . image_url = galleryUploads . shift ( ) ;
467+ }
468+
469+ if ( galleryUploads . length ) {
470+ input . image_gallery_urls = [ existingGalleryUrls , ...galleryUploads ]
471+ . filter ( Boolean )
472+ . join ( "\n" ) ;
473+ }
474+
475+ return input ;
476+ }
477+
400478function setPublicApiHeaders ( res ) {
401479 res . set ( "Access-Control-Allow-Origin" , "*" ) ;
402480 res . set ( "Access-Control-Allow-Methods" , "GET, OPTIONS" ) ;
@@ -2780,8 +2858,8 @@ app.get("/admin/products/new", requireAdmin, (req, res) => {
27802858 } ) ;
27812859} ) ;
27822860
2783- app . post ( "/admin/products/new" , requireAdmin , ( req , res ) => {
2784- createProduct ( db , req . body ) ;
2861+ app . post ( "/admin/products/new" , requireAdmin , withProductUploads , ( req , res ) => {
2862+ createProduct ( db , productInputWithUploads ( req ) ) ;
27852863 setFlash ( req , "success" , "Produit créé." ) ;
27862864 saveSessionAndRedirect ( req , res , "/admin" ) ;
27872865} ) ;
@@ -2799,8 +2877,8 @@ app.get("/admin/products/:id/edit", requireAdmin, (req, res) => {
27992877 } ) ;
28002878} ) ;
28012879
2802- app . post ( "/admin/products/:id/edit" , requireAdmin , ( req , res ) => {
2803- const product = updateProduct ( db , Number . parseInt ( req . params . id , 10 ) , req . body ) ;
2880+ app . post ( "/admin/products/:id/edit" , requireAdmin , withProductUploads , ( req , res ) => {
2881+ const product = updateProduct ( db , Number . parseInt ( req . params . id , 10 ) , productInputWithUploads ( req ) ) ;
28042882 if ( ! product ) {
28052883 return res . status ( 404 ) . render ( "not-found" , { title : "Produit introuvable" } ) ;
28062884 }
0 commit comments