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

@@ -1714,6 +1714,14 @@ const accessPolicySchema = z.object({
userIds: z.array(z.number()).optional(),
});
// Настройки API-доступа бота (ключ organization_api_keys, привязанный к боту 1:1)
export const botApiAccessSchema = z.object({
enabled: z.boolean(),
mode: z.enum(['read', 'write', 'full']).optional(),
formIds: z.array(z.number().int()).nullable().optional(),
tableIds: z.array(z.number().int()).nullable().optional(),
});
// Create/Update schemas for bots
export const createBotSchema = z.object({
name: z.string().min(1, "Название бота обязательно").max(200),
@@ -1734,6 +1742,7 @@ export const createBotSchema = z.object({
sendSystemPrompt: z.boolean().optional(),
sendCardInfo: z.boolean().optional(),
sendChatHistory: z.boolean().optional(),
apiAccess: botApiAccessSchema.optional(),
});
export const updateBotSchema = z.object({
@@ -1755,6 +1764,7 @@ export const updateBotSchema = z.object({
sendSystemPrompt: z.boolean().optional(),
sendCardInfo: z.boolean().optional(),
sendChatHistory: z.boolean().optional(),
apiAccess: botApiAccessSchema.optional(),
}).partial();
export const createBotSubscriptionSchema = z.object({
@@ -2577,9 +2587,13 @@ export const organizationApiKeys = pgTable("organization_api_keys", {
isActive: boolean("is_active").notNull().default(true),
isLegacy: boolean("is_legacy").notNull().default(false),
scopes: jsonb("scopes").$type<ApiKeyScopes>(),
// Привязка ключа к боту (1:1); NULL — обычный/legacy-ключ без бота
botId: integer("bot_id").references(() => bots.id),
}, (table) => ({
orgIndex: index("api_keys_org_idx").on(table.organizationId),
hashIndex: index("api_keys_hash_idx").on(table.keyHash),
// Один ключ на бота (частичный индекс — NULL не ограничивается)
botUniqueIdx: uniqueIndex("organization_api_keys_bot_id_unique").on(table.botId).where(sql`${table.botId} IS NOT NULL`),
}));
export type OrganizationApiKey = typeof organizationApiKeys.$inferSelect;
@@ -2712,6 +2726,8 @@ export const taskAuditLog = pgTable("task_audit_log", {
newValue: jsonb("new_value"),
changedBy: integer("changed_by").references(() => users.id),
changedByName: text("changed_by_name"),
// Атрибуция действий бота (действие выполнено по API-ключу бота); NULL — действие пользователя
botId: integer("bot_id"),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
@@ -3192,6 +3208,8 @@ export const fileUploads = pgTable("file_uploads", {
sizeBytes: integer("size_bytes"),
taskId: integer("task_id"),
fieldId: integer("field_id"),
// Атрибуция загрузки бота (файл загружен по API-ключу бота); NULL — загрузка пользователя
botId: integer("bot_id"),
createdAt: timestamp("created_at").defaultNow(),
}, (table) => ({
orgIdx: index("file_uploads_org_idx").on(table.organizationId),