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 Free Access Manager
+