Добавлены API сайтов и операторов
Этот коммит содержится в:
@@ -3,9 +3,11 @@ import cors from '@fastify/cors';
|
||||
import rateLimit from '@fastify/rate-limit';
|
||||
import websocket from '@fastify/websocket';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { config } from './config.js';
|
||||
import {
|
||||
createVisitorMessage,
|
||||
db,
|
||||
getOrCreateOpenConversation,
|
||||
getSiteByWidgetKey,
|
||||
getStats,
|
||||
@@ -58,6 +60,21 @@ function getClientKey(siteId, visitorId) {
|
||||
return `${siteId || 'unknown_site'}:${visitorId || 'unknown_visitor'}`;
|
||||
}
|
||||
|
||||
function normalizeDomain(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/^www\./, '')
|
||||
.split('/')[0]
|
||||
.split(':')[0];
|
||||
}
|
||||
|
||||
function makeWidgetKey(domain) {
|
||||
const normalized = normalizeDomain(domain).replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
||||
return `site_${normalized || nanoid(8)}`;
|
||||
}
|
||||
|
||||
function sendJson(connection, payload) {
|
||||
try {
|
||||
connection.send(JSON.stringify(payload));
|
||||
@@ -175,6 +192,101 @@ app.get('/api/admin/messages', async (request) => {
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/admin/sites', async () => ({
|
||||
ok: true,
|
||||
sites: db.prepare(`
|
||||
SELECT
|
||||
sites.*,
|
||||
COUNT(DISTINCT visitors.id) AS visitors_count,
|
||||
COUNT(DISTINCT conversations.id) AS conversations_count,
|
||||
COUNT(DISTINCT site_operators.operator_id) AS operators_count
|
||||
FROM sites
|
||||
LEFT JOIN visitors ON visitors.site_id = sites.id
|
||||
LEFT JOIN conversations ON conversations.site_id = sites.id
|
||||
LEFT JOIN site_operators ON site_operators.site_id = sites.id
|
||||
GROUP BY sites.id
|
||||
ORDER BY sites.created_at DESC
|
||||
`).all()
|
||||
}));
|
||||
|
||||
app.post('/api/admin/sites', async (request, reply) => {
|
||||
const body = request.body || {};
|
||||
const domain = normalizeDomain(body.domain);
|
||||
const name = String(body.name || domain || '').trim();
|
||||
const widgetKey = String(body.widgetKey || makeWidgetKey(domain)).trim();
|
||||
|
||||
if (!domain) {
|
||||
reply.code(400);
|
||||
return { ok: false, error: 'domain is required' };
|
||||
}
|
||||
|
||||
const id = widgetKey;
|
||||
|
||||
try {
|
||||
db.prepare(`
|
||||
INSERT INTO sites (id, name, domain, widget_key, is_active)
|
||||
VALUES (?, ?, ?, ?, 1)
|
||||
`).run(id, name || domain, domain, widgetKey);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
site: db.prepare('SELECT * FROM sites WHERE id = ?').get(id)
|
||||
};
|
||||
} catch (error) {
|
||||
reply.code(400);
|
||||
return { ok: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/admin/operators', async () => ({
|
||||
ok: true,
|
||||
operators: db.prepare(`
|
||||
SELECT
|
||||
operators.*,
|
||||
GROUP_CONCAT(sites.domain, ', ') AS sites
|
||||
FROM operators
|
||||
LEFT JOIN site_operators ON site_operators.operator_id = operators.id
|
||||
LEFT JOIN sites ON sites.id = site_operators.site_id
|
||||
GROUP BY operators.id
|
||||
ORDER BY operators.updated_at DESC
|
||||
`).all()
|
||||
}));
|
||||
|
||||
app.post('/api/admin/site-operators', async (request, reply) => {
|
||||
const body = request.body || {};
|
||||
const siteId = String(body.siteId || '').trim();
|
||||
const operatorId = String(body.operatorId || '').trim();
|
||||
|
||||
if (!siteId || !operatorId) {
|
||||
reply.code(400);
|
||||
return { ok: false, error: 'siteId and operatorId are required' };
|
||||
}
|
||||
|
||||
const site = db.prepare('SELECT id FROM sites WHERE id = ? AND is_active = 1').get(siteId);
|
||||
const operator = db.prepare('SELECT id FROM operators WHERE id = ? AND is_active = 1').get(operatorId);
|
||||
|
||||
if (!site || !operator) {
|
||||
reply.code(404);
|
||||
return { ok: false, error: 'site or operator not found' };
|
||||
}
|
||||
|
||||
db.prepare(`
|
||||
INSERT OR IGNORE INTO site_operators (site_id, operator_id)
|
||||
VALUES (?, ?)
|
||||
`).run(siteId, operatorId);
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
app.delete('/api/admin/site-operators', async (request) => {
|
||||
const body = request.body || {};
|
||||
const siteId = String(body.siteId || '').trim();
|
||||
const operatorId = String(body.operatorId || '').trim();
|
||||
|
||||
db.prepare('DELETE FROM site_operators WHERE site_id = ? AND operator_id = ?').run(siteId, operatorId);
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
app.get('/api/admin/telegram/settings', async () => ({
|
||||
ok: true,
|
||||
settings: getTelegramSettings(),
|
||||
|
||||
Ссылка в новой задаче
Block a user