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:
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user