Skip to content

Commit 3eb6547

Browse files
committed
Fix issue when adding product & Allow upload image in the settings for hero banner.
1 parent 90dc380 commit 3eb6547

2 files changed

Lines changed: 100 additions & 30 deletions

File tree

server.js

Lines changed: 93 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const app = express();
5353
const databasePath = path.join(__dirname, "storage", "shop.db");
5454
const db = initializeDatabase(databasePath, env);
5555
const productUploadDir = path.join(__dirname, "public", "uploads", "products");
56+
const settingsUploadDir = path.join(__dirname, "public", "uploads", "settings");
5657
const stripe = env.STRIPE_SECRET_KEY ? new Stripe(env.STRIPE_SECRET_KEY) : null;
5758
const stripePublishableKey = env.STRIPE_PUBLISHABLE_KEY || "";
5859
const swissBitcoinPayApiUrl = (env.SWISS_BITCOIN_PAY_API_URL || "https://api.swiss-bitcoin-pay.ch").replace(/\/$/, "");
@@ -72,34 +73,39 @@ app.set("view engine", "ejs");
7273
app.set("views", path.join(__dirname, "views"));
7374
app.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

104110
function 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

444458
function 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+
/^\/admin\/products\/\d+\/edit$/.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+
460508
function 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+
19722036
app.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())

views/admin/settings.ejs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<a href="/admin" class="button button-secondary">Retour</a>
1313
</div>
1414

15-
<form action="/admin/settings" method="post" class="admin-form-grid">
15+
<form action="/admin/settings" method="post" class="admin-form-grid" enctype="multipart/form-data">
1616
<label>
1717
Nom de la boutique
1818
<input type="text" name="store_name" value="<%= settings.store_name %>" required>
@@ -49,6 +49,12 @@
4949
<small>URL de l'image de fond affichée derrière le texte du hero.</small>
5050
</label>
5151

52+
<label class="full-span">
53+
Importer une image hero
54+
<input type="file" name="hero_image_file" accept="image/*">
55+
<small>Remplace l'URL ci-dessus si une image est sélectionnée.</small>
56+
</label>
57+
5258
<label class="full-span">
5359
Points hero
5460
<textarea name="hero_points" rows="4"><%= settings.hero_points %></textarea>

0 commit comments

Comments
 (0)