FRE-592: Fix 4 code review blockers (security + correctness)

- Add project ownership verification to relationship mutations (createRelationship,
  updateRelationship, deleteRelationship, getRelationshipsForCharacter)
- Add project ownership verification to getCharacter and getScene
- Add ownership check to projectProcedure middleware (hasProjectAccess)
- Fix searchCharacters filter combination bug (accumulate conditions instead of
  overwriting)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-24 10:13:49 -04:00
parent 79d153f75a
commit 0ba20e5b31
2 changed files with 55 additions and 22 deletions

View File

@@ -192,6 +192,7 @@ export const projectRouter = {
if (!character) {
throw new Error(`Character ${input.id} not found`);
}
await verifyProjectOwnership(ctx.db!, character.projectId, ctx.userId!);
return character;
}),
@@ -332,12 +333,11 @@ export const projectRouter = {
.query(async ({ input, ctx }) => {
await verifyProjectOwnership(ctx.db!, input.projectId, ctx.userId!);
let conditions: import('drizzle-orm').SQL[] = [eq(characters.projectId, input.projectId)];
const conditions: import('drizzle-orm').SQL[] = [eq(characters.projectId, input.projectId)];
if (input.query) {
const q = `%${input.query.toLowerCase()}%`;
conditions = [
eq(characters.projectId, input.projectId),
conditions.push(
or(
like(sql`LOWER(${characters.name})`, q),
like(sql`LOWER(COALESCE(${characters.description}, ''))`, q),
@@ -345,21 +345,15 @@ export const projectRouter = {
like(sql`LOWER(COALESCE(${characters.traits}, ''))`, q),
like(sql`LOWER(COALESCE(${characters.motivation}, ''))`, q)
)!,
];
);
}
if (input.role) {
conditions = [
eq(characters.projectId, input.projectId),
eq(characters.role, input.role),
];
conditions.push(eq(characters.role, input.role));
}
if (input.arcType) {
conditions = [
eq(characters.projectId, input.projectId),
eq(characters.arcType, input.arcType),
];
conditions.push(eq(characters.arcType, input.arcType));
}
return await ctx.db!.select()
@@ -421,6 +415,7 @@ export const projectRouter = {
if (!rows[0]) {
throw new Error(`Character ${input.characterId} not found`);
}
await verifyProjectOwnership(ctx.db!, rows[0].projectId, ctx.userId!);
return await ctx.db!.select()
.from(characterRelationships)
.where(
@@ -454,6 +449,7 @@ export const projectRouter = {
if (!charARows[0] || !charBRows[0]) {
throw new Error('Both characters must exist');
}
await verifyProjectOwnership(ctx.db!, charARows[0].projectId, ctx.userId!);
const existing = await ctx.db!.select()
.from(characterRelationships)
@@ -495,13 +491,21 @@ export const projectRouter = {
isAntagonistic: z.boolean().optional(),
}))
.mutation(async ({ input, ctx }) => {
const rows = await ctx.db!.select()
const relRows = await ctx.db!.select()
.from(characterRelationships)
.where(eq(characterRelationships.id, input.id));
if (!rows[0]) {
if (!relRows[0]) {
throw new Error(`Relationship ${input.id} not found`);
}
const charARows = await ctx.db!.select()
.from(characters)
.where(eq(characters.id, relRows[0].characterIdA));
if (!charARows[0]) {
throw new Error('Character not found');
}
await verifyProjectOwnership(ctx.db!, charARows[0].projectId, ctx.userId!);
const updateData: Record<string, any> = { updatedAt: new Date() };
if (input.relationshipType !== undefined) updateData.relationshipType = input.relationshipType;
if (input.description !== undefined) updateData.description = input.description ?? null;
@@ -518,13 +522,21 @@ export const projectRouter = {
deleteRelationship: protectedProcedure
.input(z.object({ id: z.number().int().positive() }))
.mutation(async ({ input, ctx }) => {
const rows = await ctx.db!.select()
const relRows = await ctx.db!.select()
.from(characterRelationships)
.where(eq(characterRelationships.id, input.id));
if (!rows[0]) {
if (!relRows[0]) {
throw new Error(`Relationship ${input.id} not found`);
}
const charARows = await ctx.db!.select()
.from(characters)
.where(eq(characters.id, relRows[0].characterIdA));
if (!charARows[0]) {
throw new Error('Character not found');
}
await verifyProjectOwnership(ctx.db!, charARows[0].projectId, ctx.userId!);
await ctx.db!.delete(characterRelationships)
.where(eq(characterRelationships.id, input.id));
@@ -552,6 +564,7 @@ export const projectRouter = {
if (!scene) {
throw new Error(`Scene ${input.id} not found`);
}
await verifyProjectOwnership(ctx.db!, scene.projectId, ctx.userId!);
return scene;
}),