9baa0f0959
- services/yukassa.js: createPayment, handleWebhook createPayment → создаёт платёж + сохраняет в payment_orders handleWebhook → activates plan + charges credits on payment.succeeded - routes/billing.js: POST /checkout, POST /webhook (публичный) - DB: payment_orders table - index.js: /api/billing/webhook публичный (до auth middleware)
126 lines
5.1 KiB
JavaScript
126 lines
5.1 KiB
JavaScript
const express = require('express');
|
|
const config = require('./src/config');
|
|
const { migrate } = require('./src/config/db');
|
|
|
|
// Routes
|
|
const generateRoutes = require('./src/routes/generate');
|
|
const channelsRoutes = require('./src/routes/channels');
|
|
const postsRoutes = require('./src/routes/posts');
|
|
const articlesRoutes = require('./src/routes/articles');
|
|
const statsRoutes = require('./src/routes/stats');
|
|
const notesRoutes = require('./src/routes/notes');
|
|
const seriesRoutes = require('./src/routes/series');
|
|
const categoriesRoutes = require('./src/routes/categories');
|
|
const autogenRoutes = require('./src/routes/autogen');
|
|
const userPostsRoutes = require('./src/routes/userPosts');
|
|
const settingsRoutes = require('./src/routes/settings');
|
|
const photoSearchRoutes = require('./src/routes/photo-search');
|
|
const scheduledPostsRoutes = require('./src/routes/scheduledPosts');
|
|
const channelStatsRoutes = require('./src/routes/channelStats');
|
|
const calendarRoutes = require('./src/routes/calendar');
|
|
const metricsRoutes = require('./src/routes/metrics');
|
|
const usageRoutes = require('./src/routes/usage');
|
|
|
|
// Start queue worker
|
|
require('./src/workers/generation');
|
|
// Metrics collector
|
|
require('./src/services/metricsCollector').startAutoCollect();
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
|
|
// Раздача загруженных файлов (обложки статей и т.п.)
|
|
const path = require('path');
|
|
const UPLOADS_DIR = process.env.UPLOADS_DIR || '/var/www/zeropost-uploads';
|
|
require('fs').mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
|
|
// Public uploads — ДО auth-middleware, без секрета
|
|
app.use('/uploads', express.static(UPLOADS_DIR, { maxAge: '7d', immutable: true }));
|
|
|
|
|
|
// Публичные роуты (без auth)
|
|
app.get('/api/billing/plans', async (req, res) => {
|
|
const { query: q } = require('./src/config/db');
|
|
const { rows: plans } = await q('SELECT * FROM plans WHERE is_active=true ORDER BY sort_order');
|
|
const { rows: costs } = await q('SELECT * FROM credit_costs ORDER BY operation');
|
|
res.json({ plans, costs });
|
|
});
|
|
|
|
// ЮKassa webhook — публичный, без internal secret
|
|
app.post('/api/billing/webhook',
|
|
express.json({ type: '*/*' }),
|
|
require('./src/routes/billing').handle || ((req, res, next) => {
|
|
require('./src/services/yukassa').handleWebhook(req.body)
|
|
.then(r => res.json({ ok: true, ...r }))
|
|
.catch(err => res.status(500).json({ error: err.message }));
|
|
})
|
|
);
|
|
|
|
// Simple internal auth middleware
|
|
app.use((req, res, next) => {
|
|
const secret = req.headers['x-internal-secret'];
|
|
if (secret !== config.internalSecret) {
|
|
return res.status(401).json({ error: 'Unauthorized' });
|
|
}
|
|
next();
|
|
});
|
|
|
|
// AI usage context — приклеивает к каждому запросу service + user_id,
|
|
// чтобы сервисы (ai.js, covers.js, postImages.js, articleAutoSeries.js)
|
|
// логировали расход без явного проброса параметров.
|
|
const aiContext = require('./src/lib/aiContext');
|
|
app.use((req, res, next) => {
|
|
let service = 'zeropost-other';
|
|
// Блог-сторона zeropost.ru: статьи, серии, авто-публикация, темы.
|
|
if (/^\/api\/(articles|autogen|series|notes|categories|stats|posts|scheduled-posts|generate)/.test(req.path)) {
|
|
service = 'zeropost-blog';
|
|
// SaaS-сторона app.zeropost.ru: пользовательские посты, каналы, календарь, аналитика.
|
|
} else if (/^\/api\/(user-posts|calendar|channels|channel-stats|metrics|photo-search)/.test(req.path)) {
|
|
service = 'zeropost-tool';
|
|
}
|
|
const userIdRaw = req.headers['x-user-id'];
|
|
const userId = userIdRaw ? parseInt(userIdRaw, 10) || null : null;
|
|
aiContext.run({ service, userId }, () => next());
|
|
});
|
|
|
|
app.use('/api/generate', generateRoutes);
|
|
app.use('/api/channels', channelsRoutes);
|
|
app.use('/api/posts', postsRoutes);
|
|
app.use('/api/articles', articlesRoutes);
|
|
app.use('/api/stats', statsRoutes);
|
|
app.use('/api/notes', notesRoutes);
|
|
app.use('/api/series', seriesRoutes);
|
|
app.use('/api/categories', categoriesRoutes);
|
|
app.use('/api/autogen', autogenRoutes);
|
|
app.use('/api/user-posts', userPostsRoutes);
|
|
app.use('/api/settings', settingsRoutes);
|
|
app.use('/api/photo-search', photoSearchRoutes);
|
|
app.use('/api/scheduled-posts', scheduledPostsRoutes);
|
|
app.use('/api/channel-stats', channelStatsRoutes);
|
|
app.use('/api/calendar', calendarRoutes);
|
|
app.use('/api/metrics', metricsRoutes);
|
|
app.use('/api/usage', usageRoutes);
|
|
app.use('/api/billing', require('./src/routes/billing'));
|
|
|
|
app.get('/health', (req, res) => {
|
|
res.json({ ok: true, service: 'zeropost-engine', time: new Date() });
|
|
});
|
|
|
|
const start = async () => {
|
|
await migrate();
|
|
await config.reloadAi();
|
|
console.log('[Engine] AI config loaded from app_settings: text=' + config.ai.baseUrl + ', images=routerai.ru (' + (config.ai.routeraiModel || 'gpt-5-image-mini') + ')');
|
|
|
|
// Автоматический ретрай SVG-заглушек
|
|
require('./src/services/coverRetry').start();
|
|
|
|
app.listen(config.port, () => {
|
|
console.log(`[Engine] Running on port ${config.port}`);
|
|
});
|
|
};
|
|
|
|
start().catch(err => {
|
|
console.error('[Engine] Failed to start:', err);
|
|
process.exit(1);
|
|
});
|