- ApiScopesEditor — общий редактор скоупов (режим + формы/справочники) - Bots.tsx: секция «Доступ к API и MCP» при создании и в настройках бота, показ ключа один раз, перевыпуск с подтверждением - Settings.tsx: управление ключами убрано, read-only список legacy-ключей - TaskHistory/TaskDetail: иконка бота + бейдж канала MCP/API у записей аудита - GET /api/bots/:id возвращает apiKey
624 lines
27 KiB
TypeScript
624 lines
27 KiB
TypeScript
import express from 'express';
|
||
import { z } from 'zod';
|
||
import crypto from 'crypto';
|
||
import { storage } from "../storage";
|
||
import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware";
|
||
import { tenantIsolation } from "../middleware/tenant.middleware";
|
||
import { validateRequest } from "../middleware/validation.middleware";
|
||
import {
|
||
createBotSchema, updateBotSchema,
|
||
botLoginSchema, mcpServerSchema,
|
||
type ApiKeyScopes,
|
||
} from "@shared/schema";
|
||
import { generateBotLoginTokens } from "../utils/jwt";
|
||
import { verifyPassword } from "../utils/password";
|
||
import { authLimiter } from "./shared";
|
||
import { encrypt, decrypt } from "../crypto";
|
||
import { parseApiKeyScopesInput, normalizeApiKeyScopes } from "../utils/api-key";
|
||
|
||
// Валидация apiAccess из тела запроса → ApiKeyScopes (дефолт mode='read').
|
||
// Возвращает { ok: true, scopes } либо { ok: false, error } с русским сообщением.
|
||
function parseBotApiAccess(apiAccess: { enabled: boolean; mode?: 'read' | 'write' | 'full'; formIds?: number[] | null; tableIds?: number[] | null }):
|
||
{ ok: true; scopes: ApiKeyScopes } | { ok: false; error: string } {
|
||
return parseApiKeyScopesInput({
|
||
mode: apiAccess.mode ?? 'read',
|
||
formIds: apiAccess.formIds ?? null,
|
||
tableIds: apiAccess.tableIds ?? null,
|
||
});
|
||
}
|
||
|
||
const CYRILLIC_TO_LATIN: Record<string, string> = {
|
||
а:'a',б:'b',в:'v',г:'g',д:'d',е:'e',ё:'yo',ж:'zh',з:'z',и:'i',й:'y',к:'k',л:'l',м:'m',н:'n',
|
||
о:'o',п:'p',р:'r',с:'s',т:'t',у:'u',ф:'f',х:'kh',ц:'ts',ч:'ch',ш:'sh',щ:'shch',ъ:'',ы:'y',
|
||
ь:'',э:'e',ю:'yu',я:'ya',
|
||
};
|
||
|
||
function transliterate(str: string): string {
|
||
return str
|
||
.toLowerCase()
|
||
.split('')
|
||
.map(c => CYRILLIC_TO_LATIN[c] ?? c)
|
||
.join('')
|
||
.replace(/[^a-z0-9]+/g, '_')
|
||
.replace(/^_+|_+$/g, '')
|
||
.slice(0, 50);
|
||
}
|
||
|
||
async function generateBotLogin(name: string, organizationId: number): Promise<string> {
|
||
const base = transliterate(name) || 'bot';
|
||
let login = base;
|
||
let counter = 1;
|
||
while (await storage.getBotByLogin(login, organizationId)) {
|
||
const suffix = `_${counter}`;
|
||
login = base.slice(0, 50 - suffix.length) + suffix;
|
||
counter++;
|
||
if (counter > 9999) {
|
||
login = `${base}_${Date.now()}`;
|
||
break;
|
||
}
|
||
}
|
||
return login;
|
||
}
|
||
|
||
export function registerBotCrudRoutes(app: import("express").Express): void {
|
||
// Bot authentication
|
||
app.post('/api/bot/auth/login',
|
||
authLimiter,
|
||
validateRequest(botLoginSchema),
|
||
async (req, res) => {
|
||
try {
|
||
const { login, password, organizationSlug } = req.body;
|
||
|
||
const organization = await storage.getOrganizationBySlug(organizationSlug);
|
||
if (!organization) {
|
||
return res.status(401).json({ success: false, error: 'Организация не найдена' });
|
||
}
|
||
|
||
const bot = await storage.getBotByLogin(login, organization.id);
|
||
if (!bot) {
|
||
return res.status(401).json({ success: false, error: 'Неверный логин или пароль' });
|
||
}
|
||
|
||
if (!bot.isActive) {
|
||
return res.status(401).json({ success: false, error: 'Бот деактивирован' });
|
||
}
|
||
|
||
const isValidPassword = await verifyPassword(password, bot.passwordHash);
|
||
if (!isValidPassword) {
|
||
return res.status(401).json({ success: false, error: 'Неверный логин или пароль' });
|
||
}
|
||
|
||
// Отдельный тип токена bot_login: НЕ пользовательский JWT — бот не может
|
||
// войти как пользователь с совпадающим числовым id (закрыта коллизия id).
|
||
const tokens = generateBotLoginTokens(bot.id, organization.id);
|
||
|
||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||
await storage.createBotSession({
|
||
botId: bot.id,
|
||
refreshToken: tokens.refreshToken,
|
||
expiresAt,
|
||
ipAddress: req.ip || null,
|
||
userAgent: req.headers['user-agent'] || null
|
||
});
|
||
|
||
res.json({
|
||
success: true,
|
||
bot: { id: bot.id, name: bot.name, login: bot.login },
|
||
organization: { id: organization.id, name: organization.name, slug: organization.slug },
|
||
tokens
|
||
});
|
||
} catch (error) {
|
||
console.error('Bot login error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка авторизации бота' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Get all bots for organization (admin only)
|
||
app.get('/api/bots',
|
||
authenticateToken,
|
||
requirePermission('bots.manage'),
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const bots = await storage.getBotsByOrganization(req.organizationId!);
|
||
// Ключи организации одним запросом; по bot_id находим ключ каждого бота
|
||
const apiKeys = await storage.listApiKeys(req.organizationId!);
|
||
const keyByBotId = new Map(
|
||
apiKeys.filter((k) => k.botId != null).map((k) => [k.botId as number, k])
|
||
);
|
||
|
||
const safeBots = bots.map(bot => ({
|
||
id: bot.id,
|
||
name: bot.name,
|
||
description: bot.description,
|
||
avatarUrl: bot.avatarUrl,
|
||
login: bot.login,
|
||
webhookUrl: bot.webhookUrl,
|
||
webhookEnabled: bot.webhookEnabled,
|
||
isActive: bot.isActive,
|
||
createdAt: bot.createdAt,
|
||
type: bot.type ?? 'webhook',
|
||
ragEnabled: bot.ragEnabled ?? false,
|
||
mcpServers: bot.mcpServers
|
||
? bot.mcpServers.map(s => ({ url: s.url, name: s.name }))
|
||
: null,
|
||
// Сырой ключ и keyHash никогда не отдаём
|
||
apiKey: (() => {
|
||
const k = keyByBotId.get(bot.id);
|
||
return k ? { id: k.id, keyPrefix: k.keyPrefix, scopes: k.scopes, isActive: k.isActive, lastUsedAt: k.lastUsedAt } : null;
|
||
})(),
|
||
}));
|
||
|
||
res.json({ success: true, bots: safeBots });
|
||
} catch (error) {
|
||
console.error('Get bots error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при получении ботов' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Get bot by id (admin only)
|
||
app.get('/api/bots/:id',
|
||
authenticateToken,
|
||
requirePermission('bots.manage'),
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const botId = parseInt(req.params.id);
|
||
if (isNaN(botId)) {
|
||
return res.status(400).json({ success: false, error: 'Некорректный ID бота' });
|
||
}
|
||
|
||
const bot = await storage.getBotWithSubscriptions(botId, req.organizationId!);
|
||
if (!bot) {
|
||
return res.status(404).json({ success: false, error: 'Бот не найден' });
|
||
}
|
||
|
||
// Привязанный API-ключ бота (без сырого ключа и keyHash)
|
||
const botApiKey = await storage.getApiKeyByBotId(botId, req.organizationId!);
|
||
|
||
res.json({
|
||
success: true,
|
||
bot: {
|
||
id: bot.id,
|
||
name: bot.name,
|
||
description: bot.description,
|
||
avatarUrl: bot.avatarUrl,
|
||
login: bot.login,
|
||
webhookUrl: bot.webhookUrl,
|
||
webhookSecret: bot.webhookSecret ? '***' : null,
|
||
webhookEnabled: bot.webhookEnabled,
|
||
isActive: bot.isActive,
|
||
createdAt: bot.createdAt,
|
||
subscriptions: bot.subscriptions,
|
||
type: bot.type ?? 'webhook',
|
||
systemPrompt: bot.systemPrompt ?? null,
|
||
ragEnabled: bot.ragEnabled ?? false,
|
||
ragTopK: bot.ragTopK ?? 5,
|
||
mcpServers: bot.mcpServers
|
||
? bot.mcpServers.map(s => ({ url: s.url, name: s.name, apiKey: s.apiKey ? '***' : undefined }))
|
||
: null,
|
||
accessPolicy: bot.accessPolicy ?? null,
|
||
llmProviderId: bot.llmProviderId ?? null,
|
||
llmModel: bot.llmModel ?? null,
|
||
sendSystemPrompt: bot.sendSystemPrompt ?? true,
|
||
sendCardInfo: bot.sendCardInfo ?? true,
|
||
sendChatHistory: bot.sendChatHistory ?? true,
|
||
apiKey: botApiKey
|
||
? { id: botApiKey.id, keyPrefix: botApiKey.keyPrefix, scopes: botApiKey.scopes, isActive: botApiKey.isActive, lastUsedAt: botApiKey.lastUsedAt }
|
||
: null,
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Get bot error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при получении бота' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Create bot (admin only)
|
||
app.post('/api/bots',
|
||
authenticateToken,
|
||
requirePermission('bots.manage'),
|
||
tenantIsolation,
|
||
validateRequest(createBotSchema),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const { name, description, avatarUrl, webhookUrl, webhookSecret, type, systemPrompt, ragEnabled, ragTopK, mcpServers, accessPolicy, llmProviderId, llmModel } = req.body;
|
||
|
||
const isAiBot = type === 'ai_assistant';
|
||
const login: string = req.body.login || await generateBotLogin(name, req.organizationId!);
|
||
const password: string = req.body.password || crypto.randomBytes(16).toString('hex');
|
||
|
||
const existingBot = await storage.getBotByLogin(login, req.organizationId!);
|
||
if (existingBot) {
|
||
return res.status(400).json({ success: false, error: 'Бот с таким логином уже существует' });
|
||
}
|
||
|
||
// apiAccess валидируем ДО создания бота, чтобы не оставлять бота без ключа при ошибке
|
||
let apiKeyScopes: ApiKeyScopes | null = null;
|
||
if (req.body.apiAccess?.enabled) {
|
||
const parsed = parseBotApiAccess(req.body.apiAccess);
|
||
if (!parsed.ok) {
|
||
return res.status(400).json({ success: false, error: parsed.error });
|
||
}
|
||
apiKeyScopes = parsed.scopes;
|
||
}
|
||
|
||
if (llmProviderId) {
|
||
const numProv = Number(llmProviderId);
|
||
if (!Number.isInteger(numProv) || numProv <= 0) {
|
||
return res.status(400).json({ success: false, error: "Некорректный llmProviderId" });
|
||
}
|
||
const prov = await storage.getLlmProvider(numProv, req.organizationId!);
|
||
if (!prov || !prov.isActive) {
|
||
return res.status(400).json({ success: false, error: "Провайдер LLM не найден или неактивен" });
|
||
}
|
||
if (llmModel && prov.enabledModels && prov.enabledModels.length > 0 && !prov.enabledModels.includes(llmModel)) {
|
||
return res.status(400).json({ success: false, error: `Модель "${llmModel}" не входит в список разрешённых моделей провайдера` });
|
||
}
|
||
}
|
||
|
||
const bcrypt = await import('bcrypt');
|
||
const passwordHash = await bcrypt.hash(password, 12);
|
||
const finalWebhookSecret = webhookSecret || crypto.randomBytes(32).toString('hex');
|
||
const encryptedWebhookSecret = encrypt(finalWebhookSecret);
|
||
|
||
const encryptedMcpServers = mcpServers != null
|
||
? mcpServers.map((s: { url: string; apiKey?: string; name?: string }) => ({
|
||
...s,
|
||
...(s.apiKey ? { apiKey: encrypt(s.apiKey) } : {}),
|
||
}))
|
||
: undefined;
|
||
|
||
const bot = await storage.createBot({
|
||
organizationId: req.organizationId!,
|
||
name,
|
||
description: description || null,
|
||
avatarUrl: avatarUrl || null,
|
||
login,
|
||
passwordHash,
|
||
webhookUrl: webhookUrl || null,
|
||
webhookSecret: encryptedWebhookSecret,
|
||
webhookEnabled: true,
|
||
isActive: true,
|
||
createdBy: req.user!.id,
|
||
type: type ?? 'webhook',
|
||
...(systemPrompt != null && { systemPrompt }),
|
||
...(ragEnabled !== undefined && { ragEnabled }),
|
||
...(ragTopK !== undefined && { ragTopK }),
|
||
...(encryptedMcpServers != null && { mcpServers: encryptedMcpServers }),
|
||
...(accessPolicy != null && { accessPolicy }),
|
||
...(llmProviderId != null && { llmProviderId: Number(llmProviderId) }),
|
||
llmModel: llmProviderId != null ? (llmModel || null) : null,
|
||
});
|
||
|
||
// API-ключ бота (1:1): создаём только при apiAccess.enabled.
|
||
// СЫРОЙ КЛЮЧ возвращается один раз — только в этом ответе.
|
||
let apiKeyResponse: { key: string; keyPrefix: string } | null = null;
|
||
if (apiKeyScopes) {
|
||
const { key, record } = await storage.createApiKey(
|
||
req.organizationId!, req.user!.id, bot.name, apiKeyScopes, bot.id
|
||
);
|
||
apiKeyResponse = { key, keyPrefix: record.keyPrefix };
|
||
}
|
||
|
||
res.status(201).json({
|
||
success: true,
|
||
message: 'Бот создан',
|
||
bot: {
|
||
id: bot.id,
|
||
name: bot.name,
|
||
login: bot.login,
|
||
password,
|
||
webhookSecret: finalWebhookSecret,
|
||
type: bot.type ?? 'webhook',
|
||
createdAt: bot.createdAt
|
||
},
|
||
apiKey: apiKeyResponse
|
||
});
|
||
} catch (error) {
|
||
console.error('Create bot error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при создании бота' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Update bot (admin only)
|
||
app.put('/api/bots/:id',
|
||
authenticateToken,
|
||
requirePermission('bots.manage'),
|
||
tenantIsolation,
|
||
validateRequest(updateBotSchema),
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const botId = parseInt(req.params.id);
|
||
if (isNaN(botId)) {
|
||
return res.status(400).json({ success: false, error: 'Некорректный ID бота' });
|
||
}
|
||
|
||
const existingBot = await storage.getBot(botId, req.organizationId!);
|
||
if (!existingBot) {
|
||
return res.status(404).json({ success: false, error: 'Бот не найден' });
|
||
}
|
||
|
||
const updates: Record<string, unknown> = {};
|
||
if (req.body.name !== undefined) updates.name = req.body.name;
|
||
if (req.body.description !== undefined) updates.description = req.body.description;
|
||
if (req.body.avatarUrl !== undefined) updates.avatarUrl = req.body.avatarUrl;
|
||
if (req.body.webhookUrl !== undefined) updates.webhookUrl = req.body.webhookUrl;
|
||
if (req.body.webhookSecret !== undefined) updates.webhookSecret = encrypt(req.body.webhookSecret);
|
||
if (req.body.webhookEnabled !== undefined) updates.webhookEnabled = req.body.webhookEnabled;
|
||
if (req.body.isActive !== undefined) updates.isActive = req.body.isActive;
|
||
if (req.body.type !== undefined) updates.type = req.body.type;
|
||
if (req.body.systemPrompt !== undefined) updates.systemPrompt = req.body.systemPrompt;
|
||
if (req.body.ragEnabled !== undefined) updates.ragEnabled = req.body.ragEnabled;
|
||
if (req.body.ragTopK !== undefined) updates.ragTopK = req.body.ragTopK;
|
||
if (req.body.mcpServers !== undefined) {
|
||
const incoming: Array<{ url: string; apiKey?: string; name?: string }> = req.body.mcpServers ?? [];
|
||
const existing: Array<{ url: string; apiKey?: string; name?: string }> = existingBot.mcpServers ?? [];
|
||
updates.mcpServers = incoming.map(srv => {
|
||
if (!srv.apiKey || srv.apiKey === '***') {
|
||
const prev = existing.find(e => e.url === srv.url);
|
||
return { url: srv.url, name: srv.name, ...(prev?.apiKey ? { apiKey: prev.apiKey } : {}) };
|
||
}
|
||
return { url: srv.url, name: srv.name, apiKey: encrypt(srv.apiKey) };
|
||
});
|
||
}
|
||
if (req.body.accessPolicy !== undefined) updates.accessPolicy = req.body.accessPolicy;
|
||
if (req.body.llmProviderId !== undefined) {
|
||
if (req.body.llmProviderId) {
|
||
const n = Number(req.body.llmProviderId);
|
||
if (!Number.isInteger(n) || n <= 0) return res.status(400).json({ success: false, error: "Некорректный llmProviderId" });
|
||
updates.llmProviderId = n;
|
||
} else {
|
||
updates.llmProviderId = null;
|
||
}
|
||
}
|
||
if (req.body.sendSystemPrompt !== undefined) updates.sendSystemPrompt = req.body.sendSystemPrompt;
|
||
if (req.body.sendCardInfo !== undefined) updates.sendCardInfo = req.body.sendCardInfo;
|
||
if (req.body.sendChatHistory !== undefined) updates.sendChatHistory = req.body.sendChatHistory;
|
||
if (req.body.llmModel !== undefined) updates.llmModel = req.body.llmModel || null;
|
||
if (updates.llmProviderId === null) updates.llmModel = null;
|
||
|
||
const effectiveProviderId = updates.llmProviderId !== undefined ? (updates.llmProviderId as number | null) : existingBot.llmProviderId;
|
||
if (!effectiveProviderId && updates.llmModel !== null) updates.llmModel = null;
|
||
const effectiveModel = updates.llmModel !== undefined ? (updates.llmModel as string | null) : existingBot.llmModel;
|
||
if (effectiveProviderId) {
|
||
const prov = await storage.getLlmProvider(effectiveProviderId, req.organizationId!);
|
||
if (!prov || !prov.isActive) {
|
||
return res.status(400).json({ success: false, error: "Провайдер LLM не найден или неактивен" });
|
||
}
|
||
if (effectiveModel && prov.enabledModels && prov.enabledModels.length > 0 && !prov.enabledModels.includes(effectiveModel)) {
|
||
return res.status(400).json({ success: false, error: `Модель "${effectiveModel}" не входит в список разрешённых моделей провайдера` });
|
||
}
|
||
}
|
||
|
||
const updatedBot = await storage.updateBot(botId, req.organizationId!, updates as Parameters<typeof storage.updateBot>[2]);
|
||
|
||
// Управление API-ключом бота (1:1) через apiAccess:
|
||
// enabled без ключа → создать (сырой ключ — только в этом ответе);
|
||
// enabled с ключом → обновить scopes (+реактивировать, если был выключен);
|
||
// disabled с активным ключом → деактивировать (запись сохраняется).
|
||
let apiKeyResponse: { key: string; keyPrefix: string } | null = null;
|
||
if (req.body.apiAccess !== undefined) {
|
||
const apiAccess = req.body.apiAccess;
|
||
const existingKey = await storage.getApiKeyByBotId(botId, req.organizationId!);
|
||
if (apiAccess.enabled) {
|
||
const parsed = parseBotApiAccess(apiAccess);
|
||
if (!parsed.ok) {
|
||
return res.status(400).json({ success: false, error: parsed.error });
|
||
}
|
||
if (existingKey) {
|
||
await storage.updateApiKey(existingKey.id, req.organizationId!, { scopes: parsed.scopes });
|
||
if (!existingKey.isActive) {
|
||
await storage.setApiKeyActive(existingKey.id, req.organizationId!, true);
|
||
}
|
||
} else {
|
||
const { key, record } = await storage.createApiKey(
|
||
req.organizationId!, req.user!.id, updatedBot.name, parsed.scopes, botId
|
||
);
|
||
apiKeyResponse = { key, keyPrefix: record.keyPrefix };
|
||
}
|
||
} else if (existingKey?.isActive) {
|
||
await storage.setApiKeyActive(existingKey.id, req.organizationId!, false);
|
||
}
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
message: 'Бот обновлен',
|
||
bot: {
|
||
id: updatedBot.id,
|
||
name: updatedBot.name,
|
||
description: updatedBot.description,
|
||
avatarUrl: updatedBot.avatarUrl,
|
||
login: updatedBot.login,
|
||
webhookUrl: updatedBot.webhookUrl,
|
||
webhookEnabled: updatedBot.webhookEnabled,
|
||
isActive: updatedBot.isActive
|
||
},
|
||
apiKey: apiKeyResponse
|
||
});
|
||
} catch (error) {
|
||
console.error('Update bot error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при обновлении бота' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Regenerate bot API key (admin only)
|
||
app.post('/api/bots/:id/api-key/regenerate',
|
||
authenticateToken,
|
||
requirePermission('bots.manage'),
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const botId = parseInt(req.params.id);
|
||
if (isNaN(botId)) {
|
||
return res.status(400).json({ success: false, error: 'Некорректный ID бота' });
|
||
}
|
||
|
||
const bot = await storage.getBot(botId, req.organizationId!);
|
||
if (!bot) {
|
||
return res.status(404).json({ success: false, error: 'Бот не найден' });
|
||
}
|
||
|
||
const existingKey = await storage.getApiKeyByBotId(botId, req.organizationId!);
|
||
// Скоупы нового ключа = скоупы старого; без старого ключа — безопасный дефолт 'read'
|
||
const scopes: ApiKeyScopes = existingKey
|
||
? normalizeApiKeyScopes(existingKey.scopes)
|
||
: { mode: 'read', formIds: null, tableIds: null };
|
||
|
||
if (existingKey) {
|
||
// Деактивируем и отвязываем от бота: частичный уникальный индекс по bot_id
|
||
// не позволит создать новый ключ, пока старый хранит привязку.
|
||
await storage.setApiKeyActive(existingKey.id, req.organizationId!, false);
|
||
await storage.detachApiKeyFromBot(existingKey.id, req.organizationId!);
|
||
}
|
||
|
||
const { key, record } = await storage.createApiKey(
|
||
req.organizationId!, req.user!.id, bot.name, scopes, botId
|
||
);
|
||
|
||
// СЫРОЙ КЛЮЧ возвращается один раз — только в этом ответе
|
||
res.json({
|
||
success: true,
|
||
message: 'API-ключ бота перевыпущен',
|
||
apiKey: { key, keyPrefix: record.keyPrefix }
|
||
});
|
||
} catch (error) {
|
||
console.error('Regenerate bot API key error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при перевыпуске API-ключа' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Regenerate bot password (admin only)
|
||
app.post('/api/bots/:id/regenerate-password',
|
||
authenticateToken,
|
||
requirePermission('bots.manage'),
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const botId = parseInt(req.params.id);
|
||
if (isNaN(botId)) {
|
||
return res.status(400).json({ success: false, error: 'Некорректный ID бота' });
|
||
}
|
||
|
||
const bot = await storage.getBot(botId, req.organizationId!);
|
||
if (!bot) {
|
||
return res.status(404).json({ success: false, error: 'Бот не найден' });
|
||
}
|
||
|
||
const newPassword = crypto.randomBytes(16).toString('base64');
|
||
const bcrypt = await import('bcrypt');
|
||
const passwordHash = await bcrypt.hash(newPassword, 12);
|
||
await storage.updateBot(botId, req.organizationId!, { passwordHash });
|
||
await storage.deleteBotSessions(botId);
|
||
|
||
res.json({ success: true, message: 'Пароль бота обновлен', newPassword });
|
||
} catch (error) {
|
||
console.error('Regenerate bot password error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при обновлении пароля бота' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Regenerate bot webhook secret (admin only)
|
||
app.post('/api/bots/:id/regenerate-webhook-secret',
|
||
authenticateToken,
|
||
requirePermission('bots.manage'),
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const botId = parseInt(req.params.id);
|
||
if (isNaN(botId)) {
|
||
return res.status(400).json({ success: false, error: 'Некорректный ID бота' });
|
||
}
|
||
|
||
const bot = await storage.getBot(botId, req.organizationId!);
|
||
if (!bot) {
|
||
return res.status(404).json({ success: false, error: 'Бот не найден' });
|
||
}
|
||
|
||
const newWebhookSecret = crypto.randomBytes(32).toString('hex');
|
||
await storage.updateBot(botId, req.organizationId!, { webhookSecret: encrypt(newWebhookSecret) });
|
||
|
||
res.json({ success: true, message: 'Webhook secret обновлен', webhookSecret: newWebhookSecret });
|
||
} catch (error) {
|
||
console.error('Regenerate webhook secret error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при обновлении webhook secret' });
|
||
}
|
||
}
|
||
);
|
||
|
||
// Test MCP servers connectivity for a bot (admin only)
|
||
app.post('/api/bots/:id/mcp/test',
|
||
authenticateToken,
|
||
requirePermission('bots.manage'),
|
||
tenantIsolation,
|
||
async (req: AuthenticatedRequest, res) => {
|
||
try {
|
||
const botId = parseInt(req.params.id);
|
||
if (isNaN(botId)) {
|
||
return res.status(400).json({ success: false, error: 'Некорректный ID бота' });
|
||
}
|
||
|
||
const bot = await storage.getBot(botId, req.organizationId!);
|
||
if (!bot) {
|
||
return res.status(404).json({ success: false, error: 'Бот не найден' });
|
||
}
|
||
|
||
let mcpServers: Array<{ url: string; apiKey?: string; name?: string }> | null = null;
|
||
if (req.body && Array.isArray(req.body.servers) && req.body.servers.length > 0) {
|
||
const parsed = z.array(mcpServerSchema).safeParse(req.body.servers);
|
||
if (!parsed.success) {
|
||
return res.status(400).json({ success: false, error: 'Некорректные данные серверов MCP', details: parsed.error.flatten() });
|
||
}
|
||
const existing: Array<{ url: string; apiKey?: string; name?: string }> = bot.mcpServers ?? [];
|
||
mcpServers = parsed.data.map(srv => {
|
||
if (!srv.apiKey || srv.apiKey === '***') {
|
||
const prev = existing.find(e => e.url === srv.url);
|
||
const decryptedApiKey = prev?.apiKey ? decrypt(prev.apiKey) : undefined;
|
||
return { url: srv.url, name: srv.name, ...(decryptedApiKey ? { apiKey: decryptedApiKey } : {}) };
|
||
}
|
||
return srv;
|
||
});
|
||
} else {
|
||
mcpServers = (bot.mcpServers ?? []).map(srv => ({
|
||
...srv,
|
||
apiKey: srv.apiKey ? decrypt(srv.apiKey) : undefined,
|
||
}));
|
||
}
|
||
|
||
if (!mcpServers || mcpServers.length === 0) {
|
||
return res.json({ success: true, results: [] });
|
||
}
|
||
|
||
const { McpClient } = await import('../utils/mcp-client');
|
||
const results = await Promise.all(
|
||
mcpServers.map(async (srv) => {
|
||
const client = new McpClient(srv.url, srv.apiKey, 8000);
|
||
const ping = await client.ping();
|
||
let toolCount = 0;
|
||
if (ping.ok) {
|
||
try {
|
||
const tools = await client.listTools();
|
||
toolCount = tools.length;
|
||
} catch {}
|
||
}
|
||
return { url: srv.url, name: srv.name, ok: ping.ok, error: ping.error, toolCount };
|
||
})
|
||
);
|
||
|
||
res.json({ success: true, results });
|
||
} catch (error) {
|
||
console.error('MCP test error:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка при проверке MCP-серверов' });
|
||
}
|
||
}
|
||
);
|
||
}
|