Права доступа 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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user