From 9c3e26c523cec3ad754f6cbcb0b9f3915b712cc5 Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Thu, 21 May 2026 14:08:41 +0200 Subject: [PATCH] fix(dropbox): use direct raw fallback --- api/dropbox-proxy.js | 51 ++++++-- src/App.tsx | 16 ++- vite.config.ts | 274 ++++++++++++++++++++++--------------------- 3 files changed, 187 insertions(+), 154 deletions(-) diff --git a/api/dropbox-proxy.js b/api/dropbox-proxy.js index dce0e24..1c7eedd 100644 --- a/api/dropbox-proxy.js +++ b/api/dropbox-proxy.js @@ -1,23 +1,47 @@ import { applyCors, isAllowedOrigin } from './_cors.js'; -const DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY; -const DROPBOX_APP_SECRET = process.env.DROPBOX_APP_SECRET; -const DROPBOX_REFRESH_TOKEN = process.env.DROPBOX_REFRESH_TOKEN; -const DROPBOX_SHARED_URL = 'https://www.dropbox.com/scl/fi/usa8me7ywgylrij2bt6hj/Data-Matrix.xlsx?rlkey=tsec8csrhye54u1fdvk15ped1&st=qbxxs4cn&dl=0'; +const DEFAULT_DROPBOX_SHARED_URL = 'https://www.dropbox.com/scl/fi/usa8me7ywgylrij2bt6hj/Data-Matrix.xlsx?rlkey=tsec8csrhye54u1fdvk15ped1&st=qbxxs4cn&dl=0'; + +function getDropboxConfig() { + return { + appKey: process.env.DROPBOX_APP_KEY, + appSecret: process.env.DROPBOX_APP_SECRET, + refreshToken: process.env.DROPBOX_REFRESH_TOKEN, + sharedUrl: process.env.DROPBOX_SHARED_URL || DEFAULT_DROPBOX_SHARED_URL, + }; +} + +function normalizeDropboxSharedUrl(sharedUrl) { + const url = new URL(sharedUrl); + url.searchParams.delete('dl'); + url.searchParams.delete('raw'); + url.searchParams.delete('st'); + return url.toString(); +} + +function buildDropboxDownloadUrl(sharedUrl) { + const url = new URL(normalizeDropboxSharedUrl(sharedUrl)); + url.searchParams.set('raw', '1'); + return url.toString(); +} function setCors(req, res) { applyCors(req, res, 'GET, OPTIONS'); } async function getAccessToken() { + const { appKey, appSecret, refreshToken } = getDropboxConfig(); + if (!appKey || !appSecret || !refreshToken) { + throw new Error('Dropbox auth env vars are not configured.'); + } const response = await fetch('https://api.dropboxapi.com/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', - refresh_token: DROPBOX_REFRESH_TOKEN, - client_id: DROPBOX_APP_KEY, - client_secret: DROPBOX_APP_SECRET, + refresh_token: refreshToken, + client_id: appKey, + client_secret: appSecret, }) }); const data = await response.json(); @@ -46,11 +70,12 @@ export default async function handler(req, res) { return res.json({ rev: 'new-url-v1', size: 0, server_modified: new Date().toISOString() }); } - // Strip dl=1 for the authenticated API call (browser-redirect hint, not needed for API). - const sharingUrlForApi = DROPBOX_SHARED_URL.replace(/[&?]dl=1/, ''); + const { sharedUrl } = getDropboxConfig(); + const sharingUrlForApi = normalizeDropboxSharedUrl(sharedUrl); + const downloadUrl = buildDropboxDownloadUrl(sharedUrl); // Try authenticated Dropbox API first — bypasses CDN cache so we always get the latest version. - // Falls back to DROPBOX_SHARED_URL if credentials are not configured. + // Falls back to the shared URL if credentials are not configured. let upstream = null; let usedAuth = false; @@ -73,12 +98,12 @@ export default async function handler(req, res) { console.warn('Dropbox API download failed, falling back to sharing link:', apiRes.status, errText.substring(0, 200)); } } catch (authErr) { - console.warn('Dropbox auth unavailable, falling back to sharing link:', authErr.message); + console.warn('Dropbox auth unavailable, falling back to sharing link:', authErr?.message || String(authErr)); } try { if (!upstream) { - upstream = await fetch(DROPBOX_SHARED_URL, { + upstream = await fetch(downloadUrl, { method: 'GET', headers: { 'Cache-Control': 'no-cache', @@ -110,6 +135,6 @@ export default async function handler(req, res) { res.send(Buffer.from(buffer)); } catch (err) { console.error('Dropbox proxy error:', err); - res.status(500).send('Proxy error: ' + err.message); + res.status(500).send('Proxy error: ' + (err?.message || String(err))); } } diff --git a/src/App.tsx b/src/App.tsx index 9a988f9..162fe35 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -56,6 +56,8 @@ const FORCED_ZERO_STOCK_SKUS = new Set([ const BC_SYNC_QUEUE_STORAGE_KEY = 'craze_bc_sync_queue'; const LEGACY_CPNP_INDEX = 77; +const DROPBOX_PROXY_URL = '/api/dropbox-proxy'; +const DROPBOX_FILE_URL = '/dropbox-file/scl/fi/usa8me7ywgylrij2bt6hj/Data-Matrix.xlsx?rlkey=tsec8csrhye54u1fdvk15ped1&st=qbxxs4cn&dl=0'; type BcSyncStatus = 'queued' | 'previewed' | 'preview_only' | 'syncing' | 'synced' | 'failed'; @@ -216,19 +218,15 @@ export default function App() { return arrayBuffer.byteLength; }; - const dropboxSources = import.meta.env.DEV - ? [ - '/api/dropbox-proxy', - `/dropbox-file/scl/fi/usa8me7ywgylrij2bt6hj/Data-Matrix.xlsx?rlkey=tsec8csrhye54u1fdvk15ped1&st=qbxxs4cn&dl=0&t=${Date.now()}` - ] - : [ - '/api/dropbox-proxy' - ]; + const dropboxSources = [ + DROPBOX_PROXY_URL, + `${DROPBOX_FILE_URL}&t=${Date.now()}` + ]; let lastError: unknown = null; for (const source of dropboxSources) { try { - console.log(`Fetching Data-Matrix.xlsx from ${source.includes('/api/') ? 'proxy' : 'Dropbox'}...`); + console.log(`Fetching Data-Matrix.xlsx from ${source}...`); const size = await loadWorkbookFromUrl(source); fileMeta = { rev: source, size }; lastError = null; diff --git a/vite.config.ts b/vite.config.ts index c5fbff4..4ee4aac 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -29,9 +29,147 @@ async function readJsonBody(req: any): Promise { }); } +function createLocalApiMiddleware(env: Record, prodEnv: Record) { + return async (req: any, res: any, next: any) => { + try { + const url = new URL(req.url || '/', 'http://localhost'); + const config = getBcConfig(env); + + // Populate process.env with loaded env variables for serverless handlers. + // The Dropbox proxy reads env at request time so preview/dev/production + // can share the same handler implementation. + Object.assign(process.env, prodEnv, env); + + // Helper to adapt Node.js req/res to Vercel signature. + const adaptVercelHandler = async (handler: any) => { + (req as any).query = Object.fromEntries(url.searchParams.entries()); + try { + (req as any).body = await readJsonBody(req); + } catch { + (req as any).body = {}; + } + (res as any).status = (code: number) => { + res.statusCode = code; + return res; + }; + (res as any).json = (data: any) => { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(data)); + }; + (res as any).send = (data: any) => { + if (data instanceof Buffer) { + res.end(data); + } else if (typeof data === 'object') { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(data)); + } else { + res.end(data); + } + }; + await handler(req, res); + }; + + if (url.pathname === '/api/dropbox-proxy') { + await adaptVercelHandler(dropboxProxyHandler); + return; + } + + if (url.pathname === '/api/dropbox-sync') { + await adaptVercelHandler(dropboxSyncHandler); + return; + } + + if (url.pathname === '/api/backup') { + await adaptVercelHandler(backupHandler); + return; + } + + if (url.pathname === '/api/users-admin') { + await adaptVercelHandler(usersAdminHandler); + return; + } + + if (url.pathname === '/api/bc-proxy') { + if (req.method === 'OPTIONS') { + res.statusCode = 204; + res.end(); + return; + } + if (req.method !== 'POST') { + res.statusCode = 405; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Method not allowed' })); + return; + } + + const body = await readJsonBody(req); + const { articleNo, cpnpNo } = body || {}; + if (!articleNo || cpnpNo === undefined) { + res.statusCode = 400; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Missing articleNo or cpnpNo' })); + return; + } + + const token = await getBCToken(config); + const item = await findItem(config, token, articleNo); + await patchItemCpnpNo(config, token, item, String(cpnpNo)); + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ success: true, articleNo, cpnpNo })); + return; + } + + if (url.pathname === '/api/bc-export') { + if (req.method === 'OPTIONS') { + res.statusCode = 204; + res.end(); + return; + } + if (req.method !== 'GET') { + res.statusCode = 405; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ error: 'Method not allowed' })); + return; + } + + const token = await getBCToken(config); + const items = await fetchAllItems(config, token); + + if (url.searchParams.get('format') === 'json') { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ success: true, count: items.length, items })); + return; + } + + const workbook = buildWorkbook(items); + const buffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }); + const dateStr = new Date().toISOString().split('T')[0]; + res.statusCode = 200; + res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + res.setHeader('Content-Disposition', `attachment; filename="BusinessCentral_Items_${dateStr}.xlsx"`); + res.setHeader('Cache-Control', 'no-store'); + res.end(buffer); + return; + } + } catch (err: any) { + const message = err?.message || String(err); + if ((req.url || '').startsWith('/api/bc-')) { + res.statusCode = 500; + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ success: false, error: message })); + return; + } + console.error('[bc-local-api]', message); + } + + next(); + }; +} + export default defineConfig(({mode}) => { const env = loadEnv(mode, '.', ''); const prodEnv = loadEnv('production', '.', ''); + const localApiMiddleware = createLocalApiMiddleware(env, prodEnv); return { plugins: [ react(), @@ -39,138 +177,10 @@ export default defineConfig(({mode}) => { { name: 'bc-local-api', configureServer(server) { - server.middlewares.use(async (req, res, next) => { - try { - const url = new URL(req.url || '/', 'http://localhost'); - const config = getBcConfig(env); - - // Populate process.env with loaded env variables for serverless handlers - Object.assign(process.env, prodEnv, env); - - // Helper to adapt Node.js req/res to Vercel signature - const adaptVercelHandler = async (handler: any) => { - (req as any).query = Object.fromEntries(url.searchParams.entries()); - try { - (req as any).body = await readJsonBody(req); - } catch { - (req as any).body = {}; - } - (res as any).status = (code: number) => { - res.statusCode = code; - return res; - }; - (res as any).json = (data: any) => { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(data)); - }; - (res as any).send = (data: any) => { - if (data instanceof Buffer) { - res.end(data); - } else if (typeof data === 'object') { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(data)); - } else { - res.end(data); - } - }; - await handler(req, res); - }; - - if (url.pathname === '/api/dropbox-proxy') { - await adaptVercelHandler(dropboxProxyHandler); - return; - } - - if (url.pathname === '/api/dropbox-sync') { - await adaptVercelHandler(dropboxSyncHandler); - return; - } - - if (url.pathname === '/api/backup') { - await adaptVercelHandler(backupHandler); - return; - } - - if (url.pathname === '/api/users-admin') { - await adaptVercelHandler(usersAdminHandler); - return; - } - - if (url.pathname === '/api/bc-proxy') { - if (req.method === 'OPTIONS') { - res.statusCode = 204; - res.end(); - return; - } - if (req.method !== 'POST') { - res.statusCode = 405; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: 'Method not allowed' })); - return; - } - - const body = await readJsonBody(req); - const { articleNo, cpnpNo } = body || {}; - if (!articleNo || cpnpNo === undefined) { - res.statusCode = 400; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: 'Missing articleNo or cpnpNo' })); - return; - } - - const token = await getBCToken(config); - const item = await findItem(config, token, articleNo); - await patchItemCpnpNo(config, token, item, String(cpnpNo)); - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ success: true, articleNo, cpnpNo })); - return; - } - - if (url.pathname === '/api/bc-export') { - if (req.method === 'OPTIONS') { - res.statusCode = 204; - res.end(); - return; - } - if (req.method !== 'GET') { - res.statusCode = 405; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ error: 'Method not allowed' })); - return; - } - - const token = await getBCToken(config); - const items = await fetchAllItems(config, token); - - if (url.searchParams.get('format') === 'json') { - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ success: true, count: items.length, items })); - return; - } - - const workbook = buildWorkbook(items); - const buffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' }); - const dateStr = new Date().toISOString().split('T')[0]; - res.statusCode = 200; - res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - res.setHeader('Content-Disposition', `attachment; filename="BusinessCentral_Items_${dateStr}.xlsx"`); - res.setHeader('Cache-Control', 'no-store'); - res.end(buffer); - return; - } - } catch (err: any) { - const message = err?.message || String(err); - if ((req.url || '').startsWith('/api/bc-')) { - res.statusCode = 500; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({ success: false, error: message })); - return; - } - console.error('[bc-local-api]', message); - } - - next(); - }); + server.middlewares.use(localApiMiddleware); + }, + configurePreviewServer(server) { + server.middlewares.use(localApiMiddleware); }, }, ],