← 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
M package-lock.jsonM package.jsonM public/admin-login.htmlM public/design.htmlM server.js
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)