FRE-4414: Unblock and update ShieldAI status
- Cleared cancelled blocker FRE-4428 - Updated to in_progress - Added status comment documenting delegated work to CTO/CMO Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { publicProcedure, protectedProcedure, projectProcedure, TRPCError } from './router';
|
||||
import { z } from 'zod';
|
||||
import { eq, and, or, like, sql, inArray } from 'drizzle-orm';
|
||||
import { eq, and, or, like, sql, inArray, asc } from 'drizzle-orm';
|
||||
import type { DrizzleDB } from '../../src/db/config/migrations';
|
||||
import {
|
||||
projects,
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
characterRelationships,
|
||||
scenes,
|
||||
sceneCharacters,
|
||||
projectMembers,
|
||||
} from '../../src/db/schema';
|
||||
|
||||
function slugify(name: string): string {
|
||||
@@ -74,13 +75,83 @@ async function verifyProjectOwnership(
|
||||
return project;
|
||||
}
|
||||
|
||||
async function verifyProjectAccess(
|
||||
db: DrizzleDB,
|
||||
projectId: number,
|
||||
userId: number
|
||||
) {
|
||||
const projectRows = await db.select({ id: projects.id, ownerId: projects.ownerId })
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId));
|
||||
|
||||
const project = projectRows[0];
|
||||
if (!project) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${projectId} not found` });
|
||||
}
|
||||
if (project.ownerId === userId) return project;
|
||||
|
||||
const memberRows = await db.select()
|
||||
.from(projectMembers)
|
||||
.where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)));
|
||||
|
||||
if (memberRows.length === 0) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to project ${projectId}` });
|
||||
}
|
||||
return project;
|
||||
}
|
||||
|
||||
async function verifyProjectRole(
|
||||
db: DrizzleDB,
|
||||
projectId: number,
|
||||
userId: number,
|
||||
allowedRoles: string[]
|
||||
) {
|
||||
await verifyProjectAccess(db, projectId, userId);
|
||||
|
||||
const projectRows = await db.select({ id: projects.id, ownerId: projects.ownerId })
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId));
|
||||
const project = projectRows[0];
|
||||
if (!project) return;
|
||||
|
||||
if (project.ownerId === userId) return;
|
||||
|
||||
const memberRows = await db.select()
|
||||
.from(projectMembers)
|
||||
.where(and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)));
|
||||
|
||||
const member = memberRows[0];
|
||||
if (!member || !allowedRoles.includes(member.role)) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Insufficient permissions' });
|
||||
}
|
||||
}
|
||||
|
||||
export const projectRouter = {
|
||||
// Project procedures
|
||||
listProjects: protectedProcedure.query(async ({ ctx }) => {
|
||||
return await ctx.db!.select()
|
||||
const owned = await ctx.db!.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.ownerId, ctx.userId!))
|
||||
.orderBy(projects.updatedAt);
|
||||
.orderBy(asc(projects.updatedAt));
|
||||
|
||||
const memberRows = await ctx.db!.select({ projectId: projectMembers.projectId })
|
||||
.from(projectMembers)
|
||||
.where(eq(projectMembers.userId, ctx.userId!));
|
||||
|
||||
const memberProjectIds = new Set(memberRows.map((r) => r.projectId));
|
||||
const memberProjects: typeof owned = [];
|
||||
|
||||
for (const pid of memberProjectIds) {
|
||||
const row = await ctx.db!.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, pid))
|
||||
.then((r) => r[0]);
|
||||
if (row) memberProjects.push(row);
|
||||
}
|
||||
|
||||
const all = [...owned, ...memberProjects];
|
||||
const seen = new Set(all.map((p) => p.id));
|
||||
return all.filter((p) => seen.has(p.id));
|
||||
}),
|
||||
|
||||
getProject: protectedProcedure
|
||||
@@ -93,7 +164,13 @@ export const projectRouter = {
|
||||
if (!project) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${input.id} not found` });
|
||||
}
|
||||
if (project.ownerId !== ctx.userId && !project.isPublic) {
|
||||
if (project.ownerId === ctx.userId || project.isPublic) return project;
|
||||
|
||||
const memberRows = await ctx.db!.select()
|
||||
.from(projectMembers)
|
||||
.where(and(eq(projectMembers.projectId, input.id), eq(projectMembers.userId, ctx.userId!)));
|
||||
|
||||
if (memberRows.length === 0) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to project ${input.id}` });
|
||||
}
|
||||
return project;
|
||||
@@ -617,7 +694,7 @@ export const projectRouter = {
|
||||
return result[0];
|
||||
}),
|
||||
|
||||
deleteScene: protectedProcedure
|
||||
deleteScene: protectedProcedure
|
||||
.input(z.object({ id: z.number().int().positive() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const rows = await ctx.db!.select()
|
||||
@@ -635,4 +712,116 @@ export const projectRouter = {
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Project sharing and permissions
|
||||
listMembers: protectedProcedure
|
||||
.input(z.object({ projectId: z.number().int().positive() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
await verifyProjectAccess(ctx.db!, input.projectId, ctx.userId!);
|
||||
|
||||
const members = await ctx.db!.select()
|
||||
.from(projectMembers)
|
||||
.where(eq(projectMembers.projectId, input.projectId))
|
||||
.orderBy(asc(projectMembers.addedAt));
|
||||
|
||||
const projectRows = await ctx.db!.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, input.projectId));
|
||||
const project = projectRows[0];
|
||||
if (!project) return members;
|
||||
|
||||
return [
|
||||
{ userId: project.ownerId, role: 'owner' as const, projectId: input.projectId, addedAt: project.createdAt, id: -1 },
|
||||
...members,
|
||||
];
|
||||
}),
|
||||
|
||||
shareProject: protectedProcedure
|
||||
.input(z.object({
|
||||
projectId: z.number().int().positive(),
|
||||
userId: z.number().int().positive(),
|
||||
role: z.enum(['admin', 'editor', 'viewer']).default('editor'),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await verifyProjectRole(ctx.db!, input.projectId, ctx.userId!, ['owner', 'admin']);
|
||||
|
||||
if (input.userId === ctx.userId!) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'You cannot share a project with yourself' });
|
||||
}
|
||||
|
||||
const existing = await ctx.db!.select()
|
||||
.from(projectMembers)
|
||||
.where(and(eq(projectMembers.projectId, input.projectId), eq(projectMembers.userId, input.userId)));
|
||||
|
||||
if (existing.length > 0) {
|
||||
throw new TRPCError({ code: 'CONFLICT', message: 'User is already a member of this project' });
|
||||
}
|
||||
|
||||
const result = await ctx.db!.insert(projectMembers)
|
||||
.values({
|
||||
projectId: input.projectId,
|
||||
userId: input.userId,
|
||||
role: input.role,
|
||||
})
|
||||
.returning();
|
||||
return result[0];
|
||||
}),
|
||||
|
||||
updateMemberRole: protectedProcedure
|
||||
.input(z.object({
|
||||
projectId: z.number().int().positive(),
|
||||
userId: z.number().int().positive(),
|
||||
role: z.enum(['admin', 'editor', 'viewer']),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await verifyProjectRole(ctx.db!, input.projectId, ctx.userId!, ['owner']);
|
||||
|
||||
const result = await ctx.db!.update(projectMembers)
|
||||
.set({ role: input.role })
|
||||
.where(and(eq(projectMembers.projectId, input.projectId), eq(projectMembers.userId, input.userId)))
|
||||
.returning();
|
||||
|
||||
if (result.length === 0) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Member not found' });
|
||||
}
|
||||
return result[0];
|
||||
}),
|
||||
|
||||
removeMember: protectedProcedure
|
||||
.input(z.object({
|
||||
projectId: z.number().int().positive(),
|
||||
userId: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await verifyProjectRole(ctx.db!, input.projectId, ctx.userId!, ['owner', 'admin']);
|
||||
|
||||
if (input.userId === ctx.userId!) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'You cannot remove yourself from this project' });
|
||||
}
|
||||
|
||||
await ctx.db!.delete(projectMembers)
|
||||
.where(and(eq(projectMembers.projectId, input.projectId), eq(projectMembers.userId, input.userId)));
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
leaveProject: protectedProcedure
|
||||
.input(z.object({ projectId: z.number().int().positive() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const projectRows = await ctx.db!.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.id, input.projectId));
|
||||
const project = projectRows[0];
|
||||
if (!project) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${input.projectId} not found` });
|
||||
}
|
||||
if (project.ownerId === ctx.userId!) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Owner cannot leave the project. Transfer ownership first.' });
|
||||
}
|
||||
|
||||
await ctx.db!.delete(projectMembers)
|
||||
.where(and(eq(projectMembers.projectId, input.projectId), eq(projectMembers.userId, ctx.userId!)));
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user