From fda765e4f2d7420c92024a0224d6fbc27c391c40 Mon Sep 17 00:00:00 2001 From: Milan Lazic Date: Thu, 25 Sep 2025 20:56:22 +0200 Subject: [PATCH 1/2] Add back-end support for conversations RBAC --- .../conversations/[conversationId]/route.ts | 47 ++++++++++++------- .../snapshots/[id]/rewind/route.ts | 15 ++++-- .../[conversationId]/snapshots/[id]/route.ts | 18 +++---- .../snapshots/latest/route.ts | 18 +++---- .../[conversationId]/snapshots/route.ts | 11 +++-- app/api/conversations/route.ts | 17 +++++-- app/lib/casl/prisma-helpers.ts | 6 ++- app/lib/casl/user-ability.ts | 15 +++++- app/lib/services/conversationService.ts | 32 +++++++------ app/lib/services/roleService.ts | 10 ++-- .../migration.sql | 14 ++++++ prisma/schema.prisma | 31 +++++++----- prisma/seed.ts | 1 + 13 files changed, 151 insertions(+), 84 deletions(-) create mode 100644 prisma/migrations/20250925173354_add_conversation_rbac/migration.sql diff --git a/app/api/conversations/[conversationId]/route.ts b/app/api/conversations/[conversationId]/route.ts index f81dc91a..fb06e83a 100644 --- a/app/api/conversations/[conversationId]/route.ts +++ b/app/api/conversations/[conversationId]/route.ts @@ -4,7 +4,9 @@ import { z } from 'zod'; import { conversationService } from '~/lib/services/conversationService'; import { logger } from '~/utils/logger'; import { StorageServiceFactory } from '~/lib/services/storage/storage-service-factory'; -import { requireUserId } from '~/auth/session'; +import { requireUserAbility } from '~/auth/session'; +import { PermissionAction } from '@prisma/client'; +import { subject } from '@casl/ability'; export async function GET(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { const { conversationId } = await params; @@ -13,11 +15,15 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Conversation ID is required' }, { status: 400 }); } - const userId = await requireUserId(request); + const { userAbility } = await requireUserAbility(request); try { - const conversation = await prisma.conversation.findUnique({ - where: { id: conversationId, userId }, + if (userAbility.cannot(PermissionAction.read, subject('Conversation', { id: conversationId }))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } + + const conversationWithMessages = await prisma.conversation.findUnique({ + where: { id: conversationId }, include: { messages: { include: { @@ -34,11 +40,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ }, }); - if (!conversation) { + if (!conversationWithMessages) { return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); } - return NextResponse.json(conversation); + return NextResponse.json(conversationWithMessages); } catch (error) { logger.error('Error fetching conversation:', error); return NextResponse.json( @@ -57,9 +63,13 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise return NextResponse.json({ error: 'Conversation ID is required' }, { status: 400 }); } - const userId = await requireUserId(request); + const { userAbility } = await requireUserAbility(request); + + if (userAbility.cannot(PermissionAction.delete, subject('Conversation', { id: conversationId }))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } - return handleDelete(conversationId, userId); + return handleDelete(conversationId); } export async function PATCH(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { @@ -69,17 +79,18 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise< return NextResponse.json({ error: 'Conversation ID is required' }, { status: 400 }); } - const userId = await requireUserId(request); + const { userAbility } = await requireUserAbility(request); + + if (userAbility.cannot(PermissionAction.update, subject('Conversation', { id: conversationId }))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } - return handlePatch(conversationId, userId, request); + return handlePatch(conversationId, request); } -async function handleDelete(conversationId: string, userId: string) { +async function handleDelete(conversationId: string) { try { - const conversation = await prisma.conversation.findUnique({ - where: { id: conversationId, userId }, - select: { id: true }, - }); + const conversation = await conversationService.getConversation(conversationId); if (!conversation) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); @@ -89,7 +100,7 @@ async function handleDelete(conversationId: string, userId: string) { logger.info(`Deleting conversation ${conversationId}`); - await conversationService.deleteConversation(conversationId, userId); + await conversationService.deleteConversation(conversationId); await storageService.deleteAll(`snapshots/${conversationId}`); logger.info(`Deleted conversation ${conversationId}`); @@ -112,12 +123,12 @@ const UPDATE_CONVERSATION_SCHEMA = z.object({ dataSourceId: z.string().optional(), }); -async function handlePatch(conversationId: string, userId: string, request: NextRequest) { +async function handlePatch(conversationId: string, request: NextRequest) { try { const body = await request.json(); const updateData = UPDATE_CONVERSATION_SCHEMA.parse(body); - const updatedConversation = await conversationService.updateConversation(conversationId, userId, updateData); + const updatedConversation = await conversationService.updateConversation(conversationId, updateData); if (!updatedConversation) { return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); diff --git a/app/api/conversations/[conversationId]/snapshots/[id]/rewind/route.ts b/app/api/conversations/[conversationId]/snapshots/[id]/rewind/route.ts index 41335d3c..8ae81a2b 100644 --- a/app/api/conversations/[conversationId]/snapshots/[id]/rewind/route.ts +++ b/app/api/conversations/[conversationId]/snapshots/[id]/rewind/route.ts @@ -4,10 +4,13 @@ import { snapshotService } from '~/lib/services/snapshotService'; import { prisma } from '~/lib/prisma'; import { logger } from '~/utils/logger'; import { messageService } from '~/lib/services/messageService'; -import { requireUserId } from '~/auth/session'; +import { requireUserAbility } from '~/auth/session'; import { getTelemetry } from '~/lib/telemetry/telemetry-manager'; import { TelemetryEventType } from '~/lib/telemetry/telemetry-types'; import { userService } from '~/lib/services/userService'; +import { PermissionAction, Prisma } from '@prisma/client'; +import { subject } from '@casl/ability'; +import { buildResourceWhereClause } from '~/lib/casl/prisma-helpers'; export async function POST( request: NextRequest, @@ -19,10 +22,16 @@ export async function POST( return NextResponse.json({ error: 'Invalid request' }, { status: 400 }); } - const userId = await requireUserId(request); + const { userAbility, userId } = await requireUserAbility(request); + + if (userAbility.cannot(PermissionAction.update, subject('Conversation', { id: conversationId }))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } + + const permissionWhere = buildResourceWhereClause<'Conversation'>(userAbility, PermissionAction.read, 'Conversation'); const conversation = await prisma.conversation.findUnique({ - where: { id: conversationId, userId }, + where: permissionWhere as Prisma.ConversationWhereUniqueInput, include: { messages: { select: { diff --git a/app/api/conversations/[conversationId]/snapshots/[id]/route.ts b/app/api/conversations/[conversationId]/snapshots/[id]/route.ts index f8436a0e..3bde82a2 100644 --- a/app/api/conversations/[conversationId]/snapshots/[id]/route.ts +++ b/app/api/conversations/[conversationId]/snapshots/[id]/route.ts @@ -1,9 +1,10 @@ import { NextRequest, NextResponse } from 'next/server'; -import { conversationService } from '~/lib/services/conversationService'; import { StorageServiceFactory } from '~/lib/services/storage/storage-service-factory'; import { snapshotService } from '~/lib/services/snapshotService'; import { logger } from '~/utils/logger'; -import { requireUserId } from '~/auth/session'; +import { requireUserAbility } from '~/auth/session'; +import { PermissionAction } from '@prisma/client'; +import { subject } from '@casl/ability'; export async function GET( request: NextRequest, @@ -15,17 +16,10 @@ export async function GET( return NextResponse.json({ error: 'Conversation ID and Snapshot ID are required' }, { status: 400 }); } - const userId = await requireUserId(request); + const { userAbility } = await requireUserAbility(request); - const conversation = await conversationService.getConversation(conversationId); - - if (!conversation) { - return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); - } - - // Check if the conversation belongs to the authenticated user - if (conversation.userId !== userId) { - return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); + if (userAbility.cannot(PermissionAction.read, subject('Conversation', { id: conversationId }))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); } try { diff --git a/app/api/conversations/[conversationId]/snapshots/latest/route.ts b/app/api/conversations/[conversationId]/snapshots/latest/route.ts index c4f7eb32..baa7672f 100644 --- a/app/api/conversations/[conversationId]/snapshots/latest/route.ts +++ b/app/api/conversations/[conversationId]/snapshots/latest/route.ts @@ -3,7 +3,9 @@ import { conversationService } from '~/lib/services/conversationService'; import { StorageServiceFactory } from '~/lib/services/storage/storage-service-factory'; import { snapshotService } from '~/lib/services/snapshotService'; import { logger } from '~/utils/logger'; -import { requireUserId } from '~/auth/session'; +import { requireUserAbility } from '~/auth/session'; +import { PermissionAction } from '@prisma/client'; +import { subject } from '@casl/ability'; export async function GET(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { const { conversationId } = await params; @@ -12,7 +14,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Conversation ID is required' }, { status: 400 }); } - const userId = await requireUserId(request); + const { userAbility } = await requireUserAbility(request); const conversation = await conversationService.getConversation(conversationId); @@ -20,9 +22,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); } - // Check if the conversation belongs to the authenticated user - if (conversation.userId !== userId) { - return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); + if (userAbility.cannot(PermissionAction.read, subject('Conversation', conversation))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); } try { @@ -58,7 +59,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Conversation ID is required' }, { status: 400 }); } - const userId = await requireUserId(request); + const { userAbility } = await requireUserAbility(request); const conversation = await conversationService.getConversation(conversationId); @@ -66,9 +67,8 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); } - // Check if the conversation belongs to the authenticated user - if (conversation.userId !== userId) { - return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); + if (userAbility.cannot(PermissionAction.update, subject('Conversation', conversation))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); } try { diff --git a/app/api/conversations/[conversationId]/snapshots/route.ts b/app/api/conversations/[conversationId]/snapshots/route.ts index e43e9b3d..31eae8cd 100644 --- a/app/api/conversations/[conversationId]/snapshots/route.ts +++ b/app/api/conversations/[conversationId]/snapshots/route.ts @@ -5,8 +5,10 @@ import type { FileMap } from '~/lib/stores/files'; import { createId } from '@paralleldrive/cuid2'; import { snapshotService } from '~/lib/services/snapshotService'; import { logger } from '~/utils/logger'; -import { requireUserId } from '~/auth/session'; +import { requireUserAbility } from '~/auth/session'; import { prisma } from '~/lib/prisma'; +import { PermissionAction } from '@prisma/client'; +import { subject } from '@casl/ability'; export async function POST(request: NextRequest, { params }: { params: Promise<{ conversationId: string }> }) { const { conversationId } = await params; @@ -15,7 +17,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Conversation ID is required' }, { status: 400 }); } - const userId = await requireUserId(request); + const { userAbility } = await requireUserAbility(request); const conversation = await conversationService.getConversation(conversationId); @@ -23,9 +25,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); } - // Check if the conversation belongs to the authenticated user - if (conversation.userId !== userId) { - return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); + if (userAbility.cannot(PermissionAction.create, subject('Conversation', conversation))) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); } try { diff --git a/app/api/conversations/route.ts b/app/api/conversations/route.ts index 1f53da73..629d753b 100644 --- a/app/api/conversations/route.ts +++ b/app/api/conversations/route.ts @@ -1,15 +1,20 @@ import { NextRequest, NextResponse } from 'next/server'; import { conversationService } from '~/lib/services/conversationService'; import type { Message } from '@prisma/client'; +import { PermissionAction } from '@prisma/client'; import { snapshotService } from '~/lib/services/snapshotService'; import { messageService } from '~/lib/services/messageService'; import { createId } from '@paralleldrive/cuid2'; import { logger } from '~/utils/logger'; -import { requireUserId } from '~/auth/session'; +import { requireUserAbility } from '~/auth/session'; import { prisma } from '~/lib/prisma'; export async function POST(request: NextRequest) { - const userId = await requireUserId(request); + const { userId, userAbility } = await requireUserAbility(request); + + if (userAbility.cannot(PermissionAction.create, 'Conversation')) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } try { const body = (await request.json()) as { @@ -92,10 +97,14 @@ export async function POST(request: NextRequest) { } export async function GET(request: NextRequest) { - const userId = await requireUserId(request); + const { userAbility } = await requireUserAbility(request); + + if (userAbility.cannot(PermissionAction.read, 'Conversation')) { + return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + } try { - const conversations = await conversationService.getAllConversations(userId); + const conversations = await conversationService.getAllConversationsWithPermissions(userAbility); return NextResponse.json(conversations); } catch (error) { logger.error('Error fetching conversations:', error); diff --git a/app/lib/casl/prisma-helpers.ts b/app/lib/casl/prisma-helpers.ts index e9734123..367987a6 100644 --- a/app/lib/casl/prisma-helpers.ts +++ b/app/lib/casl/prisma-helpers.ts @@ -2,7 +2,7 @@ import { Prisma, PermissionAction } from '@prisma/client'; import { accessibleBy } from '@casl/prisma'; import type { AppAbility } from './user-ability'; -export type PrismaResources = 'Environment' | 'DataSource' | 'Website' | 'EnvironmentVariable'; +export type PrismaResources = 'Environment' | 'DataSource' | 'Website' | 'EnvironmentVariable' | 'Conversation'; type WhereInputForResource = T extends 'DataSource' ? Prisma.DataSourceWhereInput @@ -12,7 +12,9 @@ type WhereInputForResource = T extends 'DataSource' ? Prisma.WebsiteWhereInput : T extends 'EnvironmentVariable' ? Prisma.EnvironmentVariableWhereInput - : never; + : T extends 'Conversation' + ? Prisma.ConversationWhereInput + : never; /** * Builds a generic Prisma WHERE clause for a given resource based on a user's abilities. diff --git a/app/lib/casl/user-ability.ts b/app/lib/casl/user-ability.ts index 45e1c684..e9732195 100644 --- a/app/lib/casl/user-ability.ts +++ b/app/lib/casl/user-ability.ts @@ -3,7 +3,7 @@ import { PureAbility, AbilityBuilder } from '@casl/ability'; import { createPrismaAbility } from '@casl/prisma'; import { PermissionResource, PermissionAction } from '@prisma/client'; import type { PrismaQuery, Subjects } from '@casl/prisma'; -import type { DataSource, Environment, EnvironmentVariable, Permission, Website } from '@prisma/client'; +import type { DataSource, Environment, EnvironmentVariable, Permission, Website, Conversation } from '@prisma/client'; import type { PrismaResources } from './prisma-helpers'; import { getUserPermissions } from '~/lib/services/permissionService'; import { logger } from '~/utils/logger'; @@ -19,6 +19,7 @@ type PrismaSubjects = Subjects<{ DataSource: Partial; Website: Partial; EnvironmentVariable: Partial; + Conversation: Partial; }>; type NonPrismaSubjects = Exclude; @@ -30,7 +31,7 @@ export function createAbilityForUser(userId: string, permissions: Permission[]): const { can, build } = new AbilityBuilder(createPrismaAbility); permissions.forEach((permission) => { - const { action, resource, environmentId, dataSourceId, websiteId } = permission as Permission; + const { action, resource, environmentId, dataSourceId, websiteId, conversationId } = permission as Permission; // Handle different permission scenarios switch (resource) { @@ -75,6 +76,15 @@ export function createAbilityForUser(userId: string, permissions: Permission[]): break; + case PermissionResource.Conversation: + if (conversationId) { + can(action, PermissionResource.Conversation, { id: conversationId }); + } else { + can(action, PermissionResource.Conversation); + } + + break; + // Add other resource types as needed... default: logger.warn(`User ability: Unknown resource type '${resource}' for action '${action}'`); @@ -86,6 +96,7 @@ export function createAbilityForUser(userId: string, permissions: Permission[]): can(PermissionAction.manage, PermissionResource.DataSource, { createdById: userId }); can(PermissionAction.manage, PermissionResource.Website, { createdById: userId }); can(PermissionAction.manage, PermissionResource.EnvironmentVariable, { createdById: userId }); + can(PermissionAction.manage, PermissionResource.Conversation, { userId }); return build(); } diff --git a/app/lib/services/conversationService.ts b/app/lib/services/conversationService.ts index 4f62a7c0..1f83b173 100644 --- a/app/lib/services/conversationService.ts +++ b/app/lib/services/conversationService.ts @@ -1,7 +1,9 @@ import { prisma } from '~/lib/prisma'; -import { type Conversation, type Prisma } from '@prisma/client'; +import { type Conversation, PermissionAction, type Prisma } from '@prisma/client'; import { StarterPluginManager } from '~/lib/plugins/starter/starter-plugin-manager'; import { getEnvironmentDataSource } from '~/lib/services/datasourceService'; +import { buildResourceWhereClause } from '~/lib/casl/prisma-helpers'; +import type { AppAbility } from '~/lib/casl/user-ability'; export const conversationService = { async getConversation(conversationId: string): Promise { @@ -52,18 +54,16 @@ export const conversationService = { }); }, - async updateConversation( - conversationId: string, - userId: string, - data: Partial, - ): Promise { + async updateConversation(conversationId: string, data: Partial): Promise { return await prisma.conversation.update({ - where: { id: conversationId, userId }, + where: { id: conversationId }, data, }); }, - async getAllConversations(userId: string): Promise<(Conversation & { editedAt: Date | null })[]> { + async getAllConversationsWithPermissions(ability: AppAbility): Promise<(Conversation & { editedAt: Date | null })[]> { + const permissionWhere = buildResourceWhereClause<'Conversation'>(ability, PermissionAction.read, 'Conversation'); + const conversations = await prisma.conversation.findMany({ select: { id: true, @@ -86,10 +86,14 @@ export const conversationService = { environmentId: true, }, where: { - userId, - snapshots: { - some: {}, - }, + AND: [ + permissionWhere as Prisma.ConversationWhereInput, + { + snapshots: { + some: {}, + }, + }, + ], }, orderBy: { createdAt: 'desc', @@ -106,9 +110,9 @@ export const conversationService = { })); }, - async deleteConversation(conversationId: string, userId: string): Promise { + async deleteConversation(conversationId: string): Promise { await prisma.conversation.delete({ - where: { id: conversationId, userId }, + where: { id: conversationId }, }); }, }; diff --git a/app/lib/services/roleService.ts b/app/lib/services/roleService.ts index e3c0ec5c..7c5d31fd 100644 --- a/app/lib/services/roleService.ts +++ b/app/lib/services/roleService.ts @@ -1,7 +1,6 @@ import { prisma } from '~/lib/prisma'; -import { RoleScope, Prisma, PrismaClient } from '@prisma/client'; -import { type Role, PermissionAction, PermissionResource } from '@prisma/client'; -import { permissionLevels, type PermissionLevel } from '~/lib/services/permissionService'; +import { PermissionAction, PermissionResource, Prisma, PrismaClient, type Role, RoleScope } from '@prisma/client'; +import { type PermissionLevel, permissionLevels } from '~/lib/services/permissionService'; export type ResourceRoleScope = Exclude; @@ -9,6 +8,7 @@ export const roleScopeResourceMap: Record [RoleScope.DATA_SOURCE]: PermissionResource.DataSource, [RoleScope.ENVIRONMENT]: PermissionResource.Environment, [RoleScope.WEBSITE]: PermissionResource.Website, + [RoleScope.CONVERSATION]: PermissionResource.Conversation, }; export async function getRole(id: string): Promise { @@ -121,6 +121,7 @@ export async function findOrCreateResourceRole( dataSourceId?: string; environmentId?: string; websiteId?: string; + conversationId?: string; } = { action: permissionDetails.action, resource: roleScopeResourceMap[scope], @@ -136,6 +137,9 @@ export async function findOrCreateResourceRole( case RoleScope.WEBSITE: permissionData.websiteId = resourceId; break; + case RoleScope.CONVERSATION: + permissionData.conversationId = resourceId; + break; } return prismaClient.role.create({ diff --git a/prisma/migrations/20250925173354_add_conversation_rbac/migration.sql b/prisma/migrations/20250925173354_add_conversation_rbac/migration.sql new file mode 100644 index 00000000..688e20f6 --- /dev/null +++ b/prisma/migrations/20250925173354_add_conversation_rbac/migration.sql @@ -0,0 +1,14 @@ +-- AlterEnum +ALTER TYPE "public"."PermissionResource" ADD VALUE 'Conversation'; + +-- AlterEnum +ALTER TYPE "public"."RoleScope" ADD VALUE 'CONVERSATION'; + +-- AlterTable +ALTER TABLE "public"."permission" ADD COLUMN "conversation_id" TEXT; + +-- CreateIndex +CREATE INDEX "idx_permission_conversation_id" ON "public"."permission"("conversation_id"); + +-- AddForeignKey +ALTER TABLE "public"."permission" ADD CONSTRAINT "permission_conversation_id_fkey" FOREIGN KEY ("conversation_id") REFERENCES "public"."conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dfd4ffe1..510c51f0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -181,6 +181,8 @@ model Conversation { User User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String + directPermissions Permission[] + @@map("conversation") } @@ -207,6 +209,7 @@ enum RoleScope { ENVIRONMENT DATA_SOURCE WEBSITE + CONVERSATION } model Role { @@ -277,6 +280,7 @@ enum PermissionResource { BuilderApp AdminApp EnvironmentVariable + Conversation } enum DataSourceType { @@ -290,23 +294,26 @@ enum DataSourceType { } model Permission { - id String @id @default(cuid()) - roleId String @map("role_id") - role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) - action PermissionAction - resource PermissionResource - environmentId String? @map("environment_id") - environment Environment? @relation(fields: [environmentId], references: [id], onDelete: Cascade) - dataSourceId String? @map("data_source_id") - dataSource DataSource? @relation(fields: [dataSourceId], references: [id], onDelete: Cascade) - websiteId String? @map("website_id") - website Website? @relation(fields: [websiteId], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) + id String @id @default(cuid()) + roleId String @map("role_id") + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + action PermissionAction + resource PermissionResource + environmentId String? @map("environment_id") + environment Environment? @relation(fields: [environmentId], references: [id], onDelete: Cascade) + dataSourceId String? @map("data_source_id") + dataSource DataSource? @relation(fields: [dataSourceId], references: [id], onDelete: Cascade) + websiteId String? @map("website_id") + website Website? @relation(fields: [websiteId], references: [id], onDelete: Cascade) + conversationId String? @map("conversation_id") + conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) @@index([roleId], name: "idx_permission_role_id") @@index([environmentId], name: "idx_permission_environment_id") @@index([dataSourceId], name: "idx_permission_data_source_id") @@index([websiteId], name: "idx_permission_website_id") + @@index([conversationId], name: "idx_permission_conversation_id") @@map("permission") } diff --git a/prisma/seed.ts b/prisma/seed.ts index 87ca050f..fbd8a7bd 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -212,6 +212,7 @@ async function seedBuilderRole(): Promise { { resource: PermissionResource.Environment, action: PermissionAction.manage }, { resource: PermissionResource.DataSource, action: PermissionAction.manage }, { resource: PermissionResource.Website, action: PermissionAction.manage }, + { resource: PermissionResource.Conversation, action: PermissionAction.manage }, { resource: PermissionResource.BuilderApp, action: PermissionAction.manage }, ]; await seedPermissions(builderRole.id, permissions); From ddbba9f395fecd0f42bdf5b0705048fb53bac153 Mon Sep 17 00:00:00 2001 From: Milan Lazic Date: Thu, 25 Sep 2025 21:06:49 +0200 Subject: [PATCH 2/2] Change permission acction on POST snapshot endpoint --- app/api/conversations/[conversationId]/snapshots/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/conversations/[conversationId]/snapshots/route.ts b/app/api/conversations/[conversationId]/snapshots/route.ts index 31eae8cd..0f2cede4 100644 --- a/app/api/conversations/[conversationId]/snapshots/route.ts +++ b/app/api/conversations/[conversationId]/snapshots/route.ts @@ -25,7 +25,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Conversation not found' }, { status: 404 }); } - if (userAbility.cannot(PermissionAction.create, subject('Conversation', conversation))) { + if (userAbility.cannot(PermissionAction.update, subject('Conversation', conversation))) { return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); }