API-ключи привязаны к ботам 1:1 (этап 1)

- Миграция 0064: organization_api_keys.bot_id (unique partial), task_audit_log.bot_id, file_uploads.bot_id
- POST/PUT /api/bots: apiAccess {enabled, mode, formIds, tableIds} — ключ создаётся/обновляется/деактивируется вместе с ботом, сырой ключ показывается один раз
- POST /api/bots/:id/api-key/regenerate; GET /api/bots возвращает apiKey summary
This commit is contained in:
2026-07-22 14:42:32 +03:00
parent 1fae441a09
commit 68153caef2
5 changed files with 187 additions and 4 deletions

View File

@@ -517,7 +517,7 @@ export class ContentStorage extends DataTablesStorage {
}
// Organization API Keys (MCP)
async createApiKey(organizationId: number, createdBy: number, label: string, scopes?: ApiKeyScopes | null): Promise<{ key: string; record: OrganizationApiKey }> {
async createApiKey(organizationId: number, createdBy: number, label: string, scopes?: ApiKeyScopes | null, botId?: number | null): Promise<{ key: string; record: OrganizationApiKey }> {
const rawKey = "wf_" + crypto.randomBytes(32).toString("base64url");
const keyHash = hashApiKey(rawKey);
const keyPrefix = rawKey.substring(0, 10);
@@ -525,10 +525,34 @@ export class ContentStorage extends DataTablesStorage {
organizationId, createdBy, label, keyHash, keyPrefix, isLegacy: false,
// NULL = полный доступ (legacy-поведение)
scopes: scopes ?? null,
botId: botId ?? null,
}).returning();
return { key: rawKey, record };
}
// Ключ, привязанный к боту (1:1), с проверкой принадлежности организации.
async getApiKeyByBotId(botId: number, organizationId: number): Promise<OrganizationApiKey | undefined> {
const [record] = await db.select().from(organizationApiKeys)
.where(and(eq(organizationApiKeys.botId, botId), eq(organizationApiKeys.organizationId, organizationId)));
return record || undefined;
}
// Активация/деактивация ключа (запись сохраняется).
async setApiKeyActive(id: number, organizationId: number, isActive: boolean): Promise<void> {
await db.update(organizationApiKeys)
.set({ isActive })
.where(and(eq(organizationApiKeys.id, id), eq(organizationApiKeys.organizationId, organizationId)));
}
// Отвязка ключа от бота (bot_id = NULL).
// Нужна при regenerate: частичный уникальный индекс по bot_id не даёт
// создать новый ключ бота, пока старый хранит привязку.
async detachApiKeyFromBot(id: number, organizationId: number): Promise<void> {
await db.update(organizationApiKeys)
.set({ botId: null })
.where(and(eq(organizationApiKeys.id, id), eq(organizationApiKeys.organizationId, organizationId)));
}
// Обновление label/scopes ключа с проверкой принадлежности организации.
// Возвращает undefined, если ключ не найден в этой организации.
async updateApiKey(id: number, organizationId: number, data: { label?: string; scopes?: ApiKeyScopes | null }): Promise<OrganizationApiKey | undefined> {