From 2e4ab35f371b5821e9d6703f9c4efc828f42153a Mon Sep 17 00:00:00 2001 From: Christian Vidal Wolf Date: Thu, 23 Apr 2026 10:02:47 +0200 Subject: [PATCH] Security: Restrict API endpoints to allowed origin (CORS) Both Vercel serverless functions now enforce CORS, returning 403 for requests from any origin other than craze-data-check.vercel.app. Co-Authored-By: Claude Sonnet 4.6 --- api/dropbox-proxy.js | 40 ++++++++++++++++++++++++++++------------ api/dropbox-sync.js | 42 ++++++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/api/dropbox-proxy.js b/api/dropbox-proxy.js index b44ea56..89396bc 100644 --- a/api/dropbox-proxy.js +++ b/api/dropbox-proxy.js @@ -2,6 +2,18 @@ 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 ALLOWED_ORIGIN = 'https://craze-data-check.vercel.app'; + +function setCors(req, res) { + const origin = req.headers.origin; + if (origin === ALLOWED_ORIGIN) { + res.setHeader('Access-Control-Allow-Origin', ALLOWED_ORIGIN); + } + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.setHeader('Vary', 'Origin'); +} + async function getAccessToken() { const response = await fetch('https://api.dropboxapi.com/oauth2/token', { method: 'POST', @@ -21,24 +33,28 @@ async function getAccessToken() { } export default async function handler(req, res) { - let accessToken; + setCors(req, res); + + if (req.method === 'OPTIONS') { + return res.status(204).end(); + } + + const origin = req.headers.origin; + if (origin && origin !== ALLOWED_ORIGIN) { + return res.status(403).json({ error: 'Forbidden' }); + } + try { - accessToken = await getAccessToken(); + await getAccessToken(); } catch (err) { console.warn('Token refresh optional failure (sharing link might still work):', err.message); } if (req.method === 'GET' && req.query.info === '1') { - try { - // In this mode, we'll just return a placeholder rev since we're using a sharing link now - // This satisfies the frontend check without needing a specific API path - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); - return res.json({ rev: 'new-url-v1', size: 0, server_modified: new Date().toISOString() }); - } catch (err) { - return res.status(500).json({ error: err.message }); - } + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + return res.json({ rev: 'new-url-v1', size: 0, server_modified: new Date().toISOString() }); } try { diff --git a/api/dropbox-sync.js b/api/dropbox-sync.js index 9e751ab..28ae667 100644 --- a/api/dropbox-sync.js +++ b/api/dropbox-sync.js @@ -2,25 +2,47 @@ import { createClient } from '@supabase/supabase-js'; const SUPABASE_URL = process.env.SUPABASE_URL || 'https://hwithddwaapyhnfwcesj.supabase.co'; const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY || 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv'; +const ALLOWED_ORIGIN = 'https://craze-data-check.vercel.app'; const supabase = createClient(SUPABASE_URL, SUPABASE_KEY); +function setCors(req, res) { + const origin = req.headers.origin; + if (origin === ALLOWED_ORIGIN) { + res.setHeader('Access-Control-Allow-Origin', ALLOWED_ORIGIN); + } + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + res.setHeader('Vary', 'Origin'); +} + export default async function handler(req, res) { + setCors(req, res); + + if (req.method === 'OPTIONS') { + return res.status(204).end(); + } + + const origin = req.headers.origin; + if (origin && origin !== ALLOWED_ORIGIN) { + return res.status(403).json({ error: 'Forbidden' }); + } + try { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } - const { rows, fileMeta } = req.body; - + const { rows } = req.body; + if (!rows || !Array.isArray(rows)) { return res.status(400).json({ error: 'Missing rows data' }); } const articleNoIdx = 0; - + const productsToUpsert = []; - + for (const row of rows) { const productId = String(row[articleNoIdx]); if (productId && productId.trim() !== '') { @@ -37,9 +59,9 @@ export default async function handler(req, res) { const { error } = await supabase .from('products') - .upsert(productsToUpsert, { + .upsert(productsToUpsert, { onConflict: 'product_id', - ignoreDuplicates: true + ignoreDuplicates: true }); if (error) { @@ -47,12 +69,12 @@ export default async function handler(req, res) { return res.status(500).json({ error: error.message, detail: 'Failed to upsert products' }); } - return res.json({ - success: true, - syncedCount: productsToUpsert.length + return res.json({ + success: true, + syncedCount: productsToUpsert.length }); } catch (err) { console.error('Handler error:', err); res.status(500).json({ error: err.message }); } -} \ No newline at end of file +}