// server.js require('dotenv').config(); const express = require('express'); const cors = require('cors'); const { log } = require('./logger'); 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 { addMessage: addChatMessage, readChat } = require('./chatStore'); const app = express(); 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 }); }); // ── PUBLIC CHAT ENDPOINTS (for customer widget) ────────────────────────────── // widget.js MUST be before /chat/:shop — otherwise Express treats "widget.js" as the :shop param app.get('/chat/widget.js', (req, res) => { const shop = (req.query.shop || '').toLowerCase().trim(); const backendUrl = 'https://backend.data4autos.com'; res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=300'); res.send(`(function(){ if(!window.__d4aChat) window.__d4aChat={}; var SHOP='${shop}', BASE='${backendUrl}'; var VID=localStorage.getItem('d4a_vid')||'v'+(Math.random().toString(36).slice(2)); localStorage.setItem('d4a_vid',VID); var css=\` #d4a-fab{position:fixed;bottom:24px;right:24px;width:56px;height:56px;background:linear-gradient(135deg,#2563eb,#4f46e5);border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 4px 20px rgba(37,99,235,0.4);z-index:99999;transition:transform .15s;} #d4a-fab:hover{transform:scale(1.08);} #d4a-fab svg{width:26px;height:26px;fill:none;stroke:#fff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;} #d4a-badge{position:absolute;top:-4px;right:-4px;min-width:18px;height:18px;background:#ef4444;border-radius:9px;font-size:11px;font-weight:700;color:#fff;display:none;align-items:center;justify-content:center;padding:0 4px;border:2px solid #fff;} #d4a-win{position:fixed;bottom:92px;right:24px;width:360px;max-height:520px;background:#fff;border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,0.18);z-index:99998;display:none;flex-direction:column;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Inter',sans-serif;} #d4a-hdr{background:linear-gradient(135deg,#2563eb,#4f46e5);padding:16px 18px;display:flex;align-items:center;gap:10px;} #d4a-hdr-icon{width:36px;height:36px;background:rgba(255,255,255,.2);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;} #d4a-hdr-title{font-size:14px;font-weight:700;color:#fff;} #d4a-hdr-sub{font-size:12px;color:rgba(255,255,255,.75);margin-top:1px;} #d4a-msgs{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px;background:#f8fafc;min-height:200px;max-height:340px;} .d4a-bub{max-width:80%;padding:9px 13px;font-size:13px;line-height:1.5;border-radius:12px;} .d4a-bub.admin{background:#fff;color:#0f172a;border:1px solid #e2e8f0;align-self:flex-start;border-radius:12px 12px 12px 3px;} .d4a-bub.customer{background:#2563eb;color:#fff;align-self:flex-end;border-radius:12px 12px 3px 12px;} .d4a-time{font-size:10px;color:#94a3b8;margin-top:3px;} .d4a-time.r{text-align:right;} #d4a-inp-row{padding:12px;border-top:1px solid #e2e8f0;display:flex;gap:8px;background:#fff;} #d4a-inp{flex:1;border:1.5px solid #e2e8f0;border-radius:8px;padding:9px 12px;font-size:13px;font-family:inherit;outline:none;resize:none;} #d4a-inp:focus{border-color:#2563eb;} #d4a-send{background:#2563eb;border:none;border-radius:8px;color:#fff;padding:9px 16px;font-size:13px;font-weight:700;cursor:pointer;white-space:nowrap;} #d4a-send:hover{background:#1d4ed8;} \`; var el=document.createElement('style');el.textContent=css;document.head.appendChild(el); var fab=document.createElement('div');fab.id='d4a-fab'; fab.innerHTML='
'; document.body.appendChild(fab); var win=document.createElement('div');win.id='d4a-win'; win.innerHTML='
💬
Chat with us
We typically reply within a few hours
Send us a message and we\'ll get back to you!
'; document.body.appendChild(win); var open=false,unread=0,lastTs='',polling=null; fab.onclick=function(){ open=!open;win.style.display=open?'flex':'none'; if(open){unread=0;document.getElementById('d4a-badge').style.display='none';loadMsgs(true);} else if(polling){clearInterval(polling);polling=null;} if(open&&!polling) polling=setInterval(function(){loadMsgs(false);},3000); }; function loadMsgs(scroll){ fetch(BASE+'/chat/'+encodeURIComponent(SHOP)+'?vid='+VID).then(r=>r.json()).then(function(d){ var msgs=d.messages||[];var el=document.getElementById('d4a-msgs'); if(!msgs.length) return; var latest=msgs[msgs.length-1]?.timestamp||''; var isNew=latest&&latest!==lastTs;lastTs=latest; if(!isNew&&!scroll) return; el.innerHTML=msgs.map(function(m){ var isAdmin=m.from==='admin'; var t=new Date(m.timestamp).toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit'}); return '
'+esc(m.text)+'
'+t+'
'; }).join(''); if(scroll||el.scrollHeight-el.scrollTop-el.clientHeight<80) el.scrollTop=el.scrollHeight; if(!open&&isNew&&msgs[msgs.length-1]?.from==='admin'){ unread++;var b=document.getElementById('d4a-badge');b.textContent=unread>9?'9+':unread;b.style.display='flex'; } }).catch(function(){}); } window.__d4aChat.send=function(){ var inp=document.getElementById('d4a-inp');var text=inp.value.trim();if(!text) return; inp.value=''; fetch(BASE+'/chat/'+encodeURIComponent(SHOP),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:text,visitorId:VID})}).then(function(){loadMsgs(true);}); }; function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>');} loadMsgs(true); })();`); }); // One-time backfill: register chat widget ScriptTag on all already-installed shops // Hit: GET /chat/backfill-scripttags (admin use only — no sensitive data exposed) app.get('/chat/backfill-scripttags', async (req, res) => { const { getToken, listTokens } = require('./tokenStore'); const axios = require('axios'); const stores = listTokens(); const results = []; for (const [shop, record] of Object.entries(stores)) { if (!record.accessToken) { results.push({ shop, status: 'no-token' }); continue; } const WIDGET_SRC = `https://backend.data4autos.com/chat/widget.js?shop=${encodeURIComponent(shop)}`; const headers = { 'X-Shopify-Access-Token': record.accessToken, 'Content-Type': 'application/json' }; try { const ex = await axios.get(`https://${shop}/admin/api/2025-10/script_tags.json?src=${encodeURIComponent(WIDGET_SRC)}`, { headers }); if (ex.data?.script_tags?.length > 0) { results.push({ shop, status: 'already-exists' }); continue; } await axios.post(`https://${shop}/admin/api/2025-10/script_tags.json`, { script_tag: { event: 'onload', src: WIDGET_SRC } }, { headers }); results.push({ shop, status: 'registered' }); } catch (e) { results.push({ shop, status: 'error', error: e.response?.data || e.message }); } } res.json({ results }); }); // Health check app.get('/health', (req, res) => { res.json({ ok: true, uptime: process.uptime(), timestamp: new Date().toISOString() }); }); // Top-level job endpoints (mirrors manageproducts/jobs/* but at /jobs/*) app.get('/jobs', (req, res) => { const shop = req.query.shop || null; res.json({ jobs: listJobs(shop) }); }); app.get('/jobs/:jobId', (req, res) => { const job = getJob(req.params.jobId); if (!job) return res.status(404).json({ error: 'Job not found' }); res.json(job); }); app.post('/jobs/:jobId/cancel', (req, res) => { const job = cancelJob(req.params.jobId); if (!job) return res.status(404).json({ error: 'Job not found' }); res.json({ ok: true, job }); }); app.get('/shops', (req, res) => { try { const store = listTokens(); const shops = Object.keys(store).map(shop => ({ shop, savedAt: store[shop].savedAt, hasToken: !!store[shop].accessToken, hasLocation: !!store[shop].locationId, hasFulfillment: !!store[shop].fulfillmentService, })); res.json({ shops }); } catch { res.json({ shops: [] }); } }); app.get("/checkisshopdataexists/:shop", (req, res) => { const shop = req.params.shop; console.log("GET /checkisshopdataexists:", shop); const tokenRecord = getToken(shop); if (!tokenRecord) { return res.json({ status: 0, message: "Shop not found" }); } // Expected fields const expectedFields = [ "accessToken", "scope", "savedAt", "locationId", "fulfillmentService" ]; const result = {}; expectedFields.forEach((field) => { result[field] = tokenRecord[field] ? "present" : "missing"; }); res.json({ status: 1, shop, fields: result }); }); // 1) COMPLIANCE WEBHOOKS (raw body) — MUST be before any JSON body parser app.use('/webhooks', privacyLawWebhooks); // 2) OAuth / other routes app.use('/', auth); // 3) Body parsers for the rest of your app app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ limit: '10mb', extended: true })); // 4) Chat message endpoints (need body parser — must be after it) app.post('/chat/:shop', (req, res) => { const shop = decodeURIComponent(req.params.shop || '').toLowerCase().trim(); const { text, visitorId } = req.body; if (!shop || !text) return res.status(400).json({ error: 'shop and text required' }); const msg = addChatMessage(shop, 'customer', String(text).slice(0, 1000), visitorId || null); res.json({ ok: true, message: msg }); }); app.get('/chat/:shop', (req, res) => { const shop = decodeURIComponent(req.params.shop || '').toLowerCase().trim(); if (!shop) return res.status(400).json({ error: 'shop required' }); const chat = readChat(shop); res.json({ messages: chat.messages || [] }); }); // 5) Your other endpoints app.post('/fulfillment', (req, res) => { console.log('POST /fulfillment:', req.body); res.sendStatus(200); }); app.use('/managebrands', manageBrands); app.use('/manageproducts', manageProducts); app.use('/managepricing', managepricing); const server = app.listen(PORT, () => { log('general', `🖥️ Server listening on port ${PORT}`); console.log(`Server running on https://backend.data4autos.com/`); }); server.on('error', err => { if (err.code === 'EADDRINUSE') { console.error(`Port ${PORT} is already in use. Choose a different PORT or kill the process using it.`); process.exit(1); } else { console.error('Server error:', err); process.exit(1); } }); // // server.js // require('dotenv').config(); // const express = require('express'); // const { log } = require('./logger'); // // OAuth callback // const auth = require('./auth'); // // Your job-routes // const manageBrands = require('./routes/manageBrands'); // const manageProducts = require('./routes/manageProducts'); // const managepricing = require('./routes/managePricing'); // // const syncInventory = require('./routes/syncInventory'); // // const syncCustomers = require('./routes/syncCustomers'); // // // …etc, one per file in routes/ // const app = express(); // const PORT = process.env.PORT || 3002; // const cors = require('cors'); // app.use(express.json({ limit: '10mb' })); // app.use(express.urlencoded({ limit: '10mb', extended: true })); // app.use(express.json()); // // 1) OAuth // app.use('/', auth); // app.use(cors()); // // 2) Job endpoints (manually mapped) // app.post('/fulfillment', (req, res) => { // console.log('POST request received:', req.body); // Optional logging // res.sendStatus(200); // Sends 200 OK // }); // app.use('/managebrands', manageBrands); // app.use('/manageproducts', manageProducts); // app.use('/managepricing', managepricing); // const server = app.listen(PORT, () => { // log('general', `🖥️ Server listening on port ${PORT}`); // console.log(`Server running on https://backend.data4autos.com/`); // }); // server.on('error', err => { // if (err.code === 'EADDRINUSE') { // console.error(`Port ${PORT} is already in use. Choose a different PORT or kill the process using it.`); // process.exit(1); // } else { // console.error('Server error:', err); // process.exit(1); // } // }); // // app.use('/syncinventory', syncInventory); // // app.use('/synccustomers', syncCustomers); // // add more here as you create new files in routes/ // // app.listen(PORT, () => { // // log('general', `🖥️ Server listening on port ${PORT}`); // // console.log(`Server running on https://backend.dine360.ca/`); // // }); // // // server.js // // require('dotenv').config(); // // const express = require('express'); // // const auth = require('./auth'); // // const { log } = require('./logger'); // // const app = express(); // // const PORT = process.env.PORT || 3002; // // // mount the auth routes // // app.use('/', auth); // // app.listen(PORT, () => { // // log('general', `🖥️ Server listening on port ${PORT}`); // // console.log(`Server running on http://localhost:${PORT}`); // // }); // // const express = require('express'); // // const axios = require('axios'); // // const app = express(); // // const PORT = 3002; // // // Replace these with your app's credentials // // const CLIENT_ID = 'b7534c980967bad619cfdb9d3f837cfa'; // // const CLIENT_SECRET = 'ed6882a4fc5839df0677ad1bb3c92f2b'; // // app.get('/auth/callback', async (req, res) => { // // console.log('🔔 [Callback] Received OAuth callback'); // // const { shop, code } = req.query; // // if (!shop || !code) { // // console.warn('⚠️ [Callback] Missing shop or code in query:', req.query); // // return res.status(400).send('Missing shop or code parameter.'); // // } // // console.log(`🔍 [Callback] shop=${shop}, code=${code}`); // // try { // // console.log('🚀 [OAuth] Exchanging authorization code for access token...'); // // const tokenResponse = await axios.post( // // `https://${shop}/admin/oauth/access_token`, // // { // // client_id: CLIENT_ID, // // client_secret: CLIENT_SECRET, // // code: code, // // }, // // { // // headers: { 'Content-Type': 'application/json' }, // // } // // ); // // console.log('✅ [OAuth] Token endpoint responded:', tokenResponse.data); // // const { access_token, scope } = tokenResponse.data; // // console.log('🔑 [OAuth] Access Token:', access_token); // // console.log('📜 [OAuth] Granted Scopes:', scope); // // // TODO: Persist access_token securely in your database here // // console.log(`💾 [Store] Storing access token for shop ${shop} (simulate DB save)`); // // res.send('Access token received and logged. You can close this window.'); // // } catch (error) { // // console.error( // // '❌ [OAuth] Error exchanging code for access token:', // // error.response?.data || error.message // // ); // // res.status(500).send('Failed to get access token'); // // } // // }); // // app.listen(PORT, () => { // // console.log(`🖥️ [Server] Listening on http://localhost:${PORT}`); // // }); // // // 🔔 [Callback] Received OAuth callback // // // 🔍 [Callback] shop=veloxautomotive.myshopify.com, code=03f875ca02185dea8e60226c9263f3ba // // // 🚀 [OAuth] Exchanging authorization code for access token... // // // ✅ [OAuth] Token endpoint responded: { // // // access_token: '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', // // // scope: 'write_inventory,write_products,write_publications' // // // } // // // 🔑 [OAuth] Access Token: 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 // // // 📜 [OAuth] Granted Scopes: write_inventory,write_products,write_publications // // // 💾 [Store] Storing access token for shop veloxautomotive.myshopify.com (simulate DB save)