From 35b3f98d5f4f970fa2b23fe3f7bfde12ca5178cd Mon Sep 17 00:00:00 2001 From: MOHAN Date: Thu, 11 Jun 2026 13:19:06 +0530 Subject: [PATCH] feat: free-access whitelist + admin panel - freeAccessStore.js: JSON-persisted whitelist of shops with optional expiry dates; isShopAllowed(), addShop(), removeShop(), listShops() - routes/adminPanel.js: password-protected single-page admin dashboard served at /d4a-admin; cookie-based session auth (no extra deps); add / remove / list shops with expiry dates and notes - server.js: mount /d4a-admin panel; expose GET /free-access/:shop public API for frontend loaders; import freeAccessStore Credentials: d4a-admin / Data4autos@2026. Migrates racewerksengg.myshopify.com from hardcode into the JSON store. Co-Authored-By: Claude Sonnet 4.6 --- freeAccessStore.js | 70 ++++++++ routes/adminPanel.js | 369 +++++++++++++++++++++++++++++++++++++++++++ server.js | 12 ++ 3 files changed, 451 insertions(+) create mode 100644 freeAccessStore.js create mode 100644 routes/adminPanel.js diff --git a/freeAccessStore.js b/freeAccessStore.js new file mode 100644 index 0000000..a2bb3da --- /dev/null +++ b/freeAccessStore.js @@ -0,0 +1,70 @@ +// freeAccessStore.js — persistent whitelist of shops granted free access +const fs = require('fs'); +const path = require('path'); + +const dataFile = path.resolve(__dirname, 'data', 'freeAccess.json'); +const dataDir = path.dirname(dataFile); + +if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }); +if (!fs.existsSync(dataFile)) { + // seed with the shop that was previously hardcoded + fs.writeFileSync(dataFile, JSON.stringify({ + "racewerksengg.myshopify.com": { + grantedAt: new Date().toISOString(), + expiresAt: null, + note: "Internal test shop (migrated from hardcode)" + } + }, null, 2), 'utf8'); +} + +function readStore() { + try { return JSON.parse(fs.readFileSync(dataFile, 'utf8')); } + catch { return {}; } +} + +function saveStore(store) { + fs.writeFileSync(dataFile, JSON.stringify(store, null, 2), 'utf8'); +} + +function isShopAllowed(shop) { + if (!shop) return false; + const store = readStore(); + const entry = store[shop.toLowerCase().trim()]; + if (!entry) return false; + if (!entry.expiresAt) return true; // permanent grant + return new Date(entry.expiresAt) > new Date(); +} + +function addShop(shop, expiresAt, note = '') { + const store = readStore(); + const key = shop.toLowerCase().trim(); + store[key] = { + grantedAt: new Date().toISOString(), + expiresAt: expiresAt || null, + note: note || '', + }; + saveStore(store); + return store[key]; +} + +function removeShop(shop) { + const store = readStore(); + const key = shop.toLowerCase().trim(); + delete store[key]; + saveStore(store); +} + +function listShops() { + const store = readStore(); + const now = new Date(); + return Object.entries(store).map(([shop, entry]) => ({ + shop, + grantedAt: entry.grantedAt, + expiresAt: entry.expiresAt, + note: entry.note || '', + expired: entry.expiresAt ? new Date(entry.expiresAt) <= now : false, + permanent: !entry.expiresAt, + })); +} + +module.exports = { isShopAllowed, addShop, removeShop, listShops }; diff --git a/routes/adminPanel.js b/routes/adminPanel.js new file mode 100644 index 0000000..3aed792 --- /dev/null +++ b/routes/adminPanel.js @@ -0,0 +1,369 @@ +// routes/adminPanel.js — password-protected admin panel for free-access shop management +const express = require('express'); +const crypto = require('crypto'); +const { isShopAllowed, addShop, removeShop, listShops } = require('../freeAccessStore'); + +const router = express.Router(); + +const ADMIN_USER = 'd4a-admin'; +const ADMIN_PASS = 'Data4autos@2026.'; +const COOKIE_NAME = 'd4a_admin_tok'; +const COOKIE_MAX_AGE = 8 * 60 * 60 * 1000; // 8 hours + +// In-memory valid tokens (cleared on server restart) +const validTokens = new Set(); + +// ── auth middleware ───────────────────────────────────────────────────────── +function requireAuth(req, res, next) { + const tok = req.cookies?.[COOKIE_NAME] || req.headers['x-admin-token']; + if (tok && validTokens.has(tok)) return next(); + if (req.path.startsWith('/api/')) return res.status(401).json({ error: 'Unauthorised' }); + res.redirect('/d4a-admin'); +} + +// ── cookie parser (lightweight, no dependency) ────────────────────────────── +router.use((req, _res, next) => { + const raw = req.headers.cookie || ''; + req.cookies = Object.fromEntries( + raw.split(';').map(c => c.trim().split('=').map(decodeURIComponent)) + .filter(([k]) => k).map(([k, ...v]) => [k, v.join('=')]) + ); + next(); +}); + +router.use(express.urlencoded({ extended: false })); +router.use(express.json()); + +// ── login ──────────────────────────────────────────────────────────────────── +router.post('/login', (req, res) => { + const { username, password } = req.body; + if (username === ADMIN_USER && password === ADMIN_PASS) { + const token = crypto.randomBytes(32).toString('hex'); + validTokens.add(token); + setTimeout(() => validTokens.delete(token), COOKIE_MAX_AGE); + res.setHeader('Set-Cookie', `${COOKIE_NAME}=${encodeURIComponent(token)}; HttpOnly; Path=/; Max-Age=${COOKIE_MAX_AGE / 1000}; SameSite=Lax`); + return res.json({ ok: true }); + } + res.status(401).json({ error: 'Invalid credentials' }); +}); + +// ── logout ─────────────────────────────────────────────────────────────────── +router.post('/logout', (req, res) => { + const tok = req.cookies?.[COOKIE_NAME]; + if (tok) validTokens.delete(tok); + res.setHeader('Set-Cookie', `${COOKIE_NAME}=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax`); + res.json({ ok: true }); +}); + +// ── check-auth ─────────────────────────────────────────────────────────────── +router.get('/api/check-auth', (req, res) => { + const tok = req.cookies?.[COOKIE_NAME]; + if (tok && validTokens.has(tok)) return res.json({ ok: true }); + res.status(401).json({ ok: false }); +}); + +// ── shops API (protected) ──────────────────────────────────────────────────── +router.get('/api/shops', requireAuth, (_req, res) => { + res.json({ shops: listShops() }); +}); + +router.post('/api/shops', requireAuth, (req, res) => { + const { shop, expiresAt, note } = req.body; + if (!shop) return res.status(400).json({ error: 'shop is required' }); + const entry = addShop(shop.trim(), expiresAt || null, note || ''); + res.json({ ok: true, shop: shop.trim(), entry }); +}); + +router.delete('/api/shops/:shop', requireAuth, (req, res) => { + removeShop(decodeURIComponent(req.params.shop)); + res.json({ ok: true }); +}); + +// ── serve admin HTML (always — login gate is client-side) ──────────────────── +router.get('/', (_req, res) => { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.send(adminHtml()); +}); + +// ── HTML ───────────────────────────────────────────────────────────────────── +function adminHtml() { + return ` + + + + +Data4Autos Admin + + + + + +
+ +
+ + +
+
+
+
🛡️ Data4Autos — Free Access Manager
+
Manage shops with free / bypassed subscription access
+
+ +
+ +
+ + +
+

➕ Grant Free Access to a Shop

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
✅ Shop added successfully!
+
+
+ + +
+
+

🏪 Whitelisted Shops

+ 0 shops +
+
+
+
+
+ +
+
+ + + +`; +} + +module.exports = router; diff --git a/server.js b/server.js index 12b16f0..c6eeb75 100755 --- a/server.js +++ b/server.js @@ -8,10 +8,12 @@ const auth = require('./auth'); const manageBrands = require('./routes/manageBrands'); const manageProducts = require('./routes/manageProducts'); const managepricing = require('./routes/managePricing'); +const adminPanel = require('./routes/adminPanel'); const privacyLawWebhooks = require('./routes/privacyLawWebhooks'); const { getToken, listTokens } = require('./tokenStore'); const { listJobs, getJob, cancelJob, getLatestJobForShop } = require('./jobStore'); +const { isShopAllowed } = require('./freeAccessStore'); const app = express(); const PORT = process.env.PORT || 3002; @@ -19,6 +21,16 @@ const PORT = process.env.PORT || 3002; // 0) CORS (safe before everything) app.use(cors()); +// Admin panel (mounted before JSON body parser — it handles its own parsing) +app.use('/d4a-admin', adminPanel); + +// Public free-access check used by the Shopify frontend loaders +app.get('/free-access/:shop', (req, res) => { + const shop = decodeURIComponent(req.params.shop || '').toLowerCase().trim(); + const allowed = isShopAllowed(shop); + res.json({ shop, allowed }); +}); + // Health check app.get('/health', (req, res) => { res.json({ ok: true, uptime: process.uptime(), timestamp: new Date().toISOString() });