Права доступа API-ключей: скоупы read/write/full и выборочный доступ к формам и справочникам

- organization_api_keys.scopes (jsonb, NULL = полный доступ для legacy-ключей)
- MCP: фильтрация инструментов по режиму и проверка formIds в handler'ах
- PATCH /api/mcp-keys/:id, валидация скоупов в server/utils/api-key.ts
- UI Settings: диалог создания/редактирования ключа с режимом и мультивыбором форм/справочников
This commit is contained in:
2026-07-21 16:12:55 +03:00
parent 08b0979ca2
commit a7733b4a03
8 changed files with 660 additions and 103 deletions

View File

@@ -1,5 +1,5 @@
import { forms, formTabs, fieldTemplates, customTabModules, taskTabValues, deviceTokens, userPresence, webPushSubscriptions, customPages, type User, type Organization, type FormTab, type Task, type FieldTemplate, type InsertFieldTemplate, type CustomTabModule, type InsertCustomTabModule, type TaskTabValue, type DeviceToken, type UserPresence, type WebPushSubscription, type CustomPage, type InsertCustomPage } from "@shared/schema";
import { organizationApiKeys, type OrganizationApiKey } from "@shared/schema";
import { organizationApiKeys, type OrganizationApiKey, type ApiKeyScopes } from "@shared/schema";
import { automations, type Automation, type InsertAutomation } from "@shared/schema";
import { systemConfig } from "@shared/schema";
import { db } from "../db";
@@ -517,16 +517,36 @@ export class ContentStorage extends DataTablesStorage {
}
// Organization API Keys (MCP)
async createApiKey(organizationId: number, createdBy: number, label: string): Promise<{ key: string; record: OrganizationApiKey }> {
async createApiKey(organizationId: number, createdBy: number, label: string, scopes?: ApiKeyScopes | null): Promise<{ key: string; record: OrganizationApiKey }> {
const rawKey = "wf_" + crypto.randomBytes(32).toString("base64url");
const keyHash = hashApiKey(rawKey);
const keyPrefix = rawKey.substring(0, 10);
const [record] = await db.insert(organizationApiKeys).values({
organizationId, createdBy, label, keyHash, keyPrefix, isLegacy: false,
// NULL = полный доступ (legacy-поведение)
scopes: scopes ?? null,
}).returning();
return { key: rawKey, record };
}
// Обновление label/scopes ключа с проверкой принадлежности организации.
// Возвращает undefined, если ключ не найден в этой организации.
async updateApiKey(id: number, organizationId: number, data: { label?: string; scopes?: ApiKeyScopes | null }): Promise<OrganizationApiKey | undefined> {
const updates: Partial<Pick<OrganizationApiKey, 'label' | 'scopes'>> = {};
if (data.label !== undefined) updates.label = data.label;
if (data.scopes !== undefined) updates.scopes = data.scopes;
if (Object.keys(updates).length === 0) {
const [existing] = await db.select().from(organizationApiKeys)
.where(and(eq(organizationApiKeys.id, id), eq(organizationApiKeys.organizationId, organizationId)));
return existing || undefined;
}
const [updated] = await db.update(organizationApiKeys)
.set(updates)
.where(and(eq(organizationApiKeys.id, id), eq(organizationApiKeys.organizationId, organizationId)))
.returning();
return updated || undefined;
}
async listApiKeys(organizationId: number): Promise<OrganizationApiKey[]> {
return await db.select().from(organizationApiKeys)
.where(eq(organizationApiKeys.organizationId, organizationId))