[object Object]

← back to Patterndesignlab

chore: lint+refactor hardening (open-redirect guard, image path containment, double-submit guard, adminUrl from SHOP), v0.2.0 (session close)

22fb9696bff9305437533a30a46089d589ab120a · 2026-07-22 21:03:59 -0700 · Steve

Files touched

Diff

commit 22fb9696bff9305437533a30a46089d589ab120a
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 22 21:03:59 2026 -0700

    chore: lint+refactor hardening (open-redirect guard, image path containment, double-submit guard, adminUrl from SHOP), v0.2.0 (session close)
---
 package-lock.json       | 4 ++--
 package.json            | 2 +-
 public/admin-login.html | 2 +-
 public/design.html      | 4 +++-
 server.js               | 5 +++--
 5 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index f69e5a8..e3ae99c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "patterndesignlab",
-  "version": "0.1.0",
+  "version": "0.2.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "patterndesignlab",
-      "version": "0.1.0",
+      "version": "0.2.0",
       "dependencies": {
         "bcryptjs": "^3.0.3",
         "cookie": "^0.7.2",
diff --git a/package.json b/package.json
index 3f50c35..e6fc79c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "patterndesignlab",
-  "version": "0.1.0",
+  "version": "0.2.0",
   "private": true,
   "description": "patterndesignlab.com — a modern seamless-pattern licensing marketplace (a nicer Patternbank). OWNED content only.",
   "main": "server.js",
diff --git a/public/admin-login.html b/public/admin-login.html
index d4b0641..21347d4 100644
--- a/public/admin-login.html
+++ b/public/admin-login.html
@@ -31,7 +31,7 @@ document.getElementById('f').onsubmit = async (e) => {
     body: JSON.stringify({ user: document.getElementById('u').value, pass: document.getElementById('p').value }) });
   if (r.ok) {
     const next = new URLSearchParams(location.search).get('next');
-    location.href = (next && next.startsWith('/')) ? next : '/admin';
+    location.href = (next && /^\/(?!\/)/.test(next)) ? next : '/admin'; // "/x" only — "//host" is an open redirect
   } else { msg.textContent = 'Invalid credentials.'; msg.style.color = '#b3261e'; }
 };
 </script>
diff --git a/public/design.html b/public/design.html
index 504b9ea..aceb7e0 100644
--- a/public/design.html
+++ b/public/design.html
@@ -134,6 +134,7 @@ async function renderAdminBar(){
 }
 async function pushShopify(){
   const note = document.getElementById('adm-note');
+  const btn = event?.target; if(btn){ if(btn.disabled) return; btn.disabled = true; } // double-click = duplicate product
   note.textContent = 'Pushing to Shopify…'; note.style.color = '';
   try{
     const r = await fetch('/api/admin/designs/'+encodeURIComponent(design.id)+'/shopify', {
@@ -148,8 +149,9 @@ async function pushShopify(){
     } else {
       note.textContent = '✗ ' + (j.error||'push failed') + (j.detail?(' — '+JSON.stringify(j.detail).slice(0,200)):'');
       note.style.color = '#b3261e';
+      if(btn) btn.disabled = false;
     }
-  }catch(e){ note.textContent = '✗ '+e.message; note.style.color = '#b3261e'; }
+  }catch(e){ note.textContent = '✗ '+e.message; note.style.color = '#b3261e'; if(btn) btn.disabled = false; }
 }
 load();
 </script>
diff --git a/server.js b/server.js
index 5f2378a..be48e7e 100644
--- a/server.js
+++ b/server.js
@@ -371,7 +371,7 @@ app.post('/api/admin/designs/:id/shopify', requireAdmin, async (req, res) => {
     // Attach the master pattern image from local disk (base64) when it exists.
     try {
       const imgPath = path.join(__dirname, 'public', String(design.img || '').replace(/^\//, ''));
-      if (design.img && fs.existsSync(imgPath)) {
+      if (design.img && imgPath.startsWith(path.join(__dirname, 'public') + path.sep) && fs.existsSync(imgPath)) {
         payload.product.images = [{ attachment: fs.readFileSync(imgPath).toString('base64'), filename: path.basename(imgPath) }];
       }
     } catch (e) { console.error('image attach skipped:', e.message); }
@@ -390,8 +390,9 @@ app.post('/api/admin/designs/:id/shopify', requireAdmin, async (req, res) => {
     await pool.query(
       'UPDATE designs SET shopify_product_id=$1, shopify_sku=$2, shopify_pushed_at=now() WHERE id=$3',
       [String(j.product.id), sku, design.id]);
+    const storeHandle = SHOP.replace('.myshopify.com', '');
     res.json({ ok: true, productId: String(j.product.id), sku, status: j.product.status,
-      adminUrl: `https://admin.shopify.com/store/designer-laboratory-sandbox/products/${j.product.id}` });
+      adminUrl: `https://admin.shopify.com/store/${storeHandle}/products/${j.product.id}` });
   } catch (e) { console.error('shopify push', e); res.status(500).json({ error: 'push failed', detail: e.message }); }
 });
 

← 350abca pin cookie@0.7.2 explicitly (Kamatera npm pulled v1.x with c  ·  back to Patterndesignlab  ·  (newest)