Files

209 lines
6.7 KiB
TypeScript
Raw Permalink Normal View History

import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import {defineConfig, loadEnv} from 'vite';
import { getBcConfig, getBCToken, findItem, patchItemCpnpNo, fetchAllItems, buildWorkbook } from './bc-runtime.js';
import * as XLSX from 'xlsx';
// @ts-ignore
import dropboxProxyHandler from './api/dropbox-proxy.js';
// @ts-ignore
import dropboxSyncHandler from './api/dropbox-sync.js';
// @ts-ignore
import backupHandler from './api/backup.js';
// @ts-ignore
import usersAdminHandler from './api/users-admin.js';
async function readJsonBody(req: any): Promise<any> {
return await new Promise((resolve, reject) => {
let raw = '';
req.on('data', (chunk: Buffer) => { raw += chunk.toString('utf8'); });
req.on('end', () => {
if (!raw.trim()) return resolve({});
try {
resolve(JSON.parse(raw));
} catch (err) {
reject(err);
}
});
req.on('error', reject);
});
}
2026-05-21 14:08:41 +02:00
function createLocalApiMiddleware(env: Record<string, string>, prodEnv: Record<string, string>) {
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, '.', '');
2026-05-21 13:58:52 +02:00
const prodEnv = loadEnv('production', '.', '');
2026-05-21 14:08:41 +02:00
const localApiMiddleware = createLocalApiMiddleware(env, prodEnv);
return {
plugins: [
react(),
tailwindcss(),
{
name: 'bc-local-api',
configureServer(server) {
2026-05-21 14:08:41 +02:00
server.middlewares.use(localApiMiddleware);
},
configurePreviewServer(server) {
server.middlewares.use(localApiMiddleware);
},
},
],
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
server: {
// HMR is disabled in AI Studio via DISABLE_HMR env var.
// Do not modify - file watching is disabled to prevent flickering during agent edits.
hmr: process.env.DISABLE_HMR !== 'true',
proxy: {
'/dropbox-file': {
target: 'https://www.dropbox.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/dropbox-file/, ''),
},
},
},
};
});