diff --git a/api/_cors.js b/api/_cors.js new file mode 100644 index 0000000..b4868fe --- /dev/null +++ b/api/_cors.js @@ -0,0 +1,38 @@ +const LOCALHOST_ORIGIN_PREFIXES = ['http://localhost:', 'http://127.0.0.1:']; + +export function isAllowedOrigin(origin) { + if (!origin) return false; + + if (LOCALHOST_ORIGIN_PREFIXES.some(prefix => origin.startsWith(prefix))) { + return true; + } + + try { + const url = new URL(origin); + if (url.hostname === 'craze-data-check.vercel.app') return true; + if (url.hostname.endsWith('.vercel.app')) return true; + if (process.env.APP_URL) { + const appUrl = new URL(process.env.APP_URL); + if (url.hostname === appUrl.hostname) return true; + } + if (process.env.VERCEL_URL) { + const vercelHost = process.env.VERCEL_URL.replace(/^https?:\/\//, ''); + if (url.hostname === vercelHost) return true; + } + } catch { + return false; + } + + return false; +} + +export function applyCors(req, res, methods) { + const origin = req.headers.origin; + if (isAllowedOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + } + res.setHeader('Access-Control-Allow-Methods', methods); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, apikey'); + res.setHeader('Access-Control-Max-Age', '86400'); + res.setHeader('Vary', 'Origin'); +} diff --git a/api/bc-export.js b/api/bc-export.js index 8ba26e9..f402639 100644 --- a/api/bc-export.js +++ b/api/bc-export.js @@ -1,21 +1,9 @@ +import { applyCors, isAllowedOrigin } from './_cors.js'; import * as XLSX from 'xlsx'; import { getBcConfig, getBCToken, fetchAllItems, buildWorkbook } from '../bc-runtime.js'; -const ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost:4173', - 'http://localhost:5173', - 'https://craze-data-check.vercel.app', -]; - function setCors(req, res) { - const origin = req.headers.origin; - if (origin && ALLOWED_ORIGINS.includes(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin); - } - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - res.setHeader('Vary', 'Origin'); + applyCors(req, res, 'GET, OPTIONS'); } export default async function handler(req, res) { @@ -24,7 +12,7 @@ export default async function handler(req, res) { if (req.method === 'OPTIONS') return res.status(204).end(); const origin = req.headers.origin; - if (origin && !ALLOWED_ORIGINS.includes(origin)) { + if (origin && !isAllowedOrigin(origin)) { return res.status(403).json({ error: 'Forbidden' }); } diff --git a/api/bc-proxy.js b/api/bc-proxy.js index 1d40a9e..0807b44 100644 --- a/api/bc-proxy.js +++ b/api/bc-proxy.js @@ -1,20 +1,8 @@ +import { applyCors, isAllowedOrigin } from './_cors.js'; import { getBcConfig, getBCToken, findItem, patchItemCpnpNo } from '../bc-runtime.js'; -const ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost:4173', - 'http://localhost:5173', - 'https://craze-data-check.vercel.app', -]; - function setCors(req, res) { - const origin = req.headers.origin; - if (origin && ALLOWED_ORIGINS.includes(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin); - } - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - res.setHeader('Vary', 'Origin'); + applyCors(req, res, 'POST, OPTIONS'); } export default async function handler(req, res) { @@ -23,7 +11,7 @@ export default async function handler(req, res) { if (req.method === 'OPTIONS') return res.status(204).end(); const origin = req.headers.origin; - if (origin && !ALLOWED_ORIGINS.includes(origin)) { + if (origin && !isAllowedOrigin(origin)) { return res.status(403).json({ error: 'Forbidden' }); } diff --git a/api/bc-sync-apply.js b/api/bc-sync-apply.js index a656cfe..40fcaa4 100644 --- a/api/bc-sync-apply.js +++ b/api/bc-sync-apply.js @@ -1,21 +1,9 @@ +import { applyCors, isAllowedOrigin } from './_cors.js'; import { getBcConfig, getBCToken } from '../bc-runtime.js'; import { applyBusinessCentralCpnp, applyBusinessCentralSync } from '../bc-sync-runtime.js'; -const ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost:4173', - 'http://localhost:5173', - 'https://craze-data-check.vercel.app', -]; - function setCors(req, res) { - const origin = req.headers.origin; - if (origin && ALLOWED_ORIGINS.includes(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin); - } - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - res.setHeader('Vary', 'Origin'); + applyCors(req, res, 'POST, OPTIONS'); } export default async function handler(req, res) { @@ -24,7 +12,7 @@ export default async function handler(req, res) { if (req.method === 'OPTIONS') return res.status(204).end(); const origin = req.headers.origin; - if (origin && !ALLOWED_ORIGINS.includes(origin)) { + if (origin && !isAllowedOrigin(origin)) { return res.status(403).json({ error: 'Forbidden' }); } diff --git a/api/bc-sync-preview.js b/api/bc-sync-preview.js index f69083e..5e5d4fc 100644 --- a/api/bc-sync-preview.js +++ b/api/bc-sync-preview.js @@ -1,21 +1,9 @@ +import { applyCors, isAllowedOrigin } from './_cors.js'; import { getBcConfig, getBCToken } from '../bc-runtime.js'; import { previewBusinessCentralCpnp, previewBusinessCentralSync } from '../bc-sync-runtime.js'; -const ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://localhost:4173', - 'http://localhost:5173', - 'https://craze-data-check.vercel.app', -]; - function setCors(req, res) { - const origin = req.headers.origin; - if (origin && ALLOWED_ORIGINS.includes(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin); - } - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - res.setHeader('Vary', 'Origin'); + applyCors(req, res, 'POST, OPTIONS'); } export default async function handler(req, res) { @@ -24,7 +12,7 @@ export default async function handler(req, res) { if (req.method === 'OPTIONS') return res.status(204).end(); const origin = req.headers.origin; - if (origin && !ALLOWED_ORIGINS.includes(origin)) { + if (origin && !isAllowedOrigin(origin)) { return res.status(403).json({ error: 'Forbidden' }); } diff --git a/api/dropbox-proxy.js b/api/dropbox-proxy.js index b4e40d7..a7599af 100644 --- a/api/dropbox-proxy.js +++ b/api/dropbox-proxy.js @@ -1,17 +1,15 @@ +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 ALLOWED_ORIGIN = 'https://craze-data-check.vercel.app'; +const DROPBOX_SHARED_URL = + process.env.DROPBOX_SHARED_URL || + process.env.DROPBOX_FILE_URL || + 'https://www.dropbox.com/scl/fi/usa8me7ywgylrij2bt6hj/Data-Matrix.xlsx?rlkey=tsec8csrhye54u1fdvk15ped1&dl=1'; 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'); + applyCors(req, res, 'GET, OPTIONS'); } async function getAccessToken() { @@ -40,7 +38,7 @@ export default async function handler(req, res) { } const origin = req.headers.origin; - if (origin && origin !== ALLOWED_ORIGIN) { + if (origin && !isAllowedOrigin(origin)) { return res.status(403).json({ error: 'Forbidden' }); } @@ -58,8 +56,7 @@ export default async function handler(req, res) { } try { - const sharingUrl = 'https://www.dropbox.com/scl/fi/usa8me7ywgylrij2bt6hj/Data-Matrix.xlsx?rlkey=tsec8csrhye54u1fdvk15ped1&dl=1'; - const upstream = await fetch(sharingUrl, { + const upstream = await fetch(DROPBOX_SHARED_URL, { method: 'GET', headers: { 'Cache-Control': 'no-cache', @@ -76,7 +73,11 @@ export default async function handler(req, res) { if (!upstream.ok) { const errText = await upstream.text(); console.error('Dropbox URL error:', upstream.status, errText); - return res.status(upstream.status).send('Dropbox error: ' + errText); + return res.status(upstream.status).send( + 'Dropbox error: ' + + errText + + '\n\nSet DROPBOX_SHARED_URL in Vercel if the sharing link changed.' + ); } const buffer = await upstream.arrayBuffer(); diff --git a/api/dropbox-sync.js b/api/dropbox-sync.js index d577f40..2508983 100644 --- a/api/dropbox-sync.js +++ b/api/dropbox-sync.js @@ -1,19 +1,13 @@ +import { applyCors, isAllowedOrigin } from './_cors.js'; 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'); + applyCors(req, res, 'POST, OPTIONS'); } export default async function handler(req, res) { @@ -24,7 +18,7 @@ export default async function handler(req, res) { } const origin = req.headers.origin; - if (origin && origin !== ALLOWED_ORIGIN) { + if (origin && !isAllowedOrigin(origin)) { return res.status(403).json({ error: 'Forbidden' }); } diff --git a/api/users-admin.js b/api/users-admin.js index 4b338dc..3714e47 100644 --- a/api/users-admin.js +++ b/api/users-admin.js @@ -1,7 +1,8 @@ +import { applyCors, isAllowedOrigin } from './_cors.js'; + const SUPABASE_URL = process.env.SUPABASE_URL || 'https://hwithddwaapyhnfwcesj.supabase.co'; const SUPABASE_SERVICE_KEY = process.env.SUPABASE_SERVICE_KEY; const SUPABASE_ANON_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv'; -const ALLOWED_ORIGIN = 'https://craze-data-check.vercel.app'; const MASTER_USERS = new Set([ 'christian.vidal@craze-group.com', @@ -9,14 +10,7 @@ const MASTER_USERS = new Set([ ]); function setCors(req, res) { - const origin = req.headers.origin; - if (origin === ALLOWED_ORIGIN || (origin && (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')))) { - res.setHeader('Access-Control-Allow-Origin', origin); - } - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, apikey'); - res.setHeader('Access-Control-Max-Age', '86400'); - res.setHeader('Vary', 'Origin'); + applyCors(req, res, 'POST, OPTIONS'); } export default async function handler(req, res) { @@ -26,6 +20,11 @@ export default async function handler(req, res) { return res.status(204).end(); } + const origin = req.headers.origin; + if (origin && !isAllowedOrigin(origin)) { + return res.status(403).json({ error: 'Forbidden' }); + } + try { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' });