157 lines
4.2 KiB
JavaScript
157 lines
4.2 KiB
JavaScript
const express = require('express');
|
|
const helmet = require('helmet');
|
|
const rateLimit = require('express-rate-limit');
|
|
const crypto = require('crypto');
|
|
|
|
const app = express();
|
|
|
|
const PORT = Number(process.env.PORT || 3001);
|
|
const INTERNAL_TOKEN = process.env.INTERNAL_WEBHOOK_TOKEN || '';
|
|
const TEXT_WEBHOOK = process.env.N8N_TEXT_SUBMIT_WEBHOOK || '';
|
|
const SCENARIO_WEBHOOK = process.env.N8N_SCENARIO_WEBHOOK || '';
|
|
const ALLOWED_SCENARIOS = new Set(
|
|
(process.env.ALLOWED_SCENARIOS || 'demo-1,demo-2')
|
|
.split(',')
|
|
.map(s => s.trim())
|
|
.filter(Boolean)
|
|
);
|
|
|
|
app.use(helmet({ contentSecurityPolicy: false }));
|
|
app.use(express.json({ limit: '2mb' }));
|
|
app.use(rateLimit({
|
|
windowMs: 60 * 1000,
|
|
max: 60,
|
|
standardHeaders: true,
|
|
legacyHeaders: false
|
|
}));
|
|
|
|
function ok(res, data = {}, message = 'OK') {
|
|
return res.json({ ok: true, data, message });
|
|
}
|
|
|
|
function fail(res, status, error, message, details = null) {
|
|
return res.status(status).json({ ok: false, error, message, details });
|
|
}
|
|
|
|
async function postToN8N(url, payload) {
|
|
if (!url) {
|
|
throw new Error('Webhook URL is empty');
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Internal-Token': INTERNAL_TOKEN
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const raw = await response.text();
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = raw ? JSON.parse(raw) : {};
|
|
} catch {
|
|
parsed = { raw };
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const err = new Error(parsed.message || raw || `n8n returned ${response.status}`);
|
|
err.status = response.status;
|
|
err.payload = parsed;
|
|
throw err;
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
app.get('/api/health', (req, res) => {
|
|
return ok(res, {
|
|
service: 'uno-bff',
|
|
nowTs: Date.now()
|
|
}, 'BFF is healthy');
|
|
});
|
|
|
|
app.post('/api/forms/text-submit', async (req, res) => {
|
|
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : '';
|
|
|
|
if (!text) {
|
|
return fail(res, 400, 'VALIDATION_ERROR', 'Поле text обязательно');
|
|
}
|
|
|
|
if (text.length > 500) {
|
|
return fail(res, 400, 'VALIDATION_ERROR', 'Поле text должно быть не длиннее 500 символов');
|
|
}
|
|
|
|
const payload = {
|
|
requestId: req.body?.requestId || crypto.randomUUID(),
|
|
source: 'uno-click-site',
|
|
text,
|
|
ts: Date.now(),
|
|
clientIp: req.headers['x-forwarded-for'] || req.socket.remoteAddress || '',
|
|
userAgent: req.headers['user-agent'] || ''
|
|
};
|
|
|
|
try {
|
|
const result = await postToN8N(TEXT_WEBHOOK, payload);
|
|
return ok(res, result, 'Текст успешно отправлен');
|
|
} catch (error) {
|
|
console.error('text-submit error:', error);
|
|
return fail(
|
|
res,
|
|
502,
|
|
'N8N_ERROR',
|
|
'Не удалось обработать запрос в n8n',
|
|
error.payload || error.message
|
|
);
|
|
}
|
|
});
|
|
|
|
app.post('/api/actions/run-scenario', async (req, res) => {
|
|
const scenarioId = typeof req.body?.scenarioId === 'string' ? req.body.scenarioId.trim() : '';
|
|
const payload = (req.body?.payload && typeof req.body.payload === 'object' && !Array.isArray(req.body.payload))
|
|
? req.body.payload
|
|
: {};
|
|
|
|
if (!scenarioId) {
|
|
return fail(res, 400, 'VALIDATION_ERROR', 'Поле scenarioId обязательно');
|
|
}
|
|
|
|
if (!ALLOWED_SCENARIOS.has(scenarioId)) {
|
|
return fail(res, 400, 'VALIDATION_ERROR', `Сценарий ${scenarioId} не разрешен`);
|
|
}
|
|
|
|
const body = {
|
|
requestId: req.body?.requestId || crypto.randomUUID(),
|
|
source: 'uno-click-site',
|
|
scenarioId,
|
|
payload,
|
|
ts: Date.now(),
|
|
clientIp: req.headers['x-forwarded-for'] || req.socket.remoteAddress || '',
|
|
userAgent: req.headers['user-agent'] || ''
|
|
};
|
|
|
|
try {
|
|
const result = await postToN8N(SCENARIO_WEBHOOK, body);
|
|
return ok(res, result, 'Сценарий успешно запущен');
|
|
} catch (error) {
|
|
console.error('run-scenario error:', error);
|
|
return fail(
|
|
res,
|
|
502,
|
|
'N8N_ERROR',
|
|
'Не удалось запустить сценарий в n8n',
|
|
error.payload || error.message
|
|
);
|
|
}
|
|
});
|
|
|
|
app.use((req, res) => {
|
|
return fail(res, 404, 'NOT_FOUND', 'Маршрут не найден');
|
|
});
|
|
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`uno-bff listening on port ${PORT}`);
|
|
});
|