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:
263
server/trpc/team-router.ts
Normal file
263
server/trpc/team-router.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { protectedProcedure, TRPCError } from './router';
|
||||
import { z } from 'zod';
|
||||
import { eq, and, asc } from 'drizzle-orm';
|
||||
import type { DrizzleDB } from '../../src/db/config/migrations';
|
||||
import { teams, teamMembers } from '../../src/db/schema';
|
||||
|
||||
async function verifyTeamOwnership(
|
||||
db: DrizzleDB,
|
||||
teamId: string,
|
||||
userId: number
|
||||
) {
|
||||
const teamRows = await db.select({ id: teams.id, ownerId: teams.ownerId })
|
||||
.from(teams)
|
||||
.where(eq(teams.id, teamId));
|
||||
|
||||
const team = teamRows[0];
|
||||
if (!team) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Team ${teamId} not found` });
|
||||
}
|
||||
if (team.ownerId !== userId) {
|
||||
const memberRows = await db.select()
|
||||
.from(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, userId)));
|
||||
if (memberRows.length === 0) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to team ${teamId}` });
|
||||
}
|
||||
}
|
||||
return team;
|
||||
}
|
||||
|
||||
async function verifyTeamRole(
|
||||
db: DrizzleDB,
|
||||
teamId: string,
|
||||
userId: number,
|
||||
allowedRoles: string[]
|
||||
) {
|
||||
await verifyTeamOwnership(db, teamId, userId);
|
||||
|
||||
const teamRows = await db.select({ id: teams.id, ownerId: teams.ownerId })
|
||||
.from(teams)
|
||||
.where(eq(teams.id, teamId));
|
||||
const team = teamRows[0];
|
||||
if (!team) return;
|
||||
|
||||
if (team.ownerId === userId) return;
|
||||
|
||||
const memberRows = await db.select()
|
||||
.from(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, userId)));
|
||||
|
||||
const member = memberRows[0];
|
||||
if (!member || !allowedRoles.includes(member.role)) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Insufficient permissions' });
|
||||
}
|
||||
}
|
||||
|
||||
function generateTeamId(): string {
|
||||
return `team_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
export const teamRouter = {
|
||||
// Team CRUD
|
||||
listTeams: protectedProcedure.query(async ({ ctx }) => {
|
||||
const owned = await ctx.db!.select()
|
||||
.from(teams)
|
||||
.where(eq(teams.ownerId, ctx.userId!))
|
||||
.orderBy(asc(teams.createdAt));
|
||||
|
||||
const memberRows = await ctx.db!.select({ teamId: teamMembers.teamId })
|
||||
.from(teamMembers)
|
||||
.where(eq(teamMembers.userId, ctx.userId!));
|
||||
|
||||
const memberTeamIds = new Set(memberRows.map((r) => r.teamId));
|
||||
const memberTeams: typeof owned = [];
|
||||
|
||||
for (const tid of memberTeamIds) {
|
||||
const row = await ctx.db!.select()
|
||||
.from(teams)
|
||||
.where(eq(teams.id, tid))
|
||||
.then((r) => r[0]);
|
||||
if (row) memberTeams.push(row);
|
||||
}
|
||||
|
||||
const all = [...owned, ...memberTeams];
|
||||
const seen = new Set(all.map((t) => t.id));
|
||||
return all.filter((t) => seen.has(t.id));
|
||||
}),
|
||||
|
||||
getTeam: protectedProcedure
|
||||
.input(z.object({ id: z.string().min(1) }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
await verifyTeamOwnership(ctx.db!, input.id, ctx.userId!);
|
||||
const rows = await ctx.db!.select()
|
||||
.from(teams)
|
||||
.where(eq(teams.id, input.id));
|
||||
return rows[0];
|
||||
}),
|
||||
|
||||
createTeam: protectedProcedure
|
||||
.input(z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const teamId = generateTeamId();
|
||||
const result = await ctx.db!.insert(teams)
|
||||
.values({
|
||||
id: teamId,
|
||||
name: input.name,
|
||||
ownerId: ctx.userId!,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const team = result[0];
|
||||
if (!team) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Failed to create team' });
|
||||
}
|
||||
|
||||
await ctx.db!.insert(teamMembers)
|
||||
.values({
|
||||
teamId: team.id,
|
||||
userId: ctx.userId!,
|
||||
role: 'owner',
|
||||
});
|
||||
|
||||
return team;
|
||||
}),
|
||||
|
||||
updateTeam: protectedProcedure
|
||||
.input(z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await verifyTeamRole(ctx.db!, input.id, ctx.userId!, ['owner', 'admin']);
|
||||
|
||||
const updateData: Record<string, any> = { updatedAt: new Date() };
|
||||
if (input.name !== undefined) updateData.name = input.name;
|
||||
|
||||
const result = await ctx.db!.update(teams)
|
||||
.set(updateData)
|
||||
.where(eq(teams.id, input.id))
|
||||
.returning();
|
||||
return result[0];
|
||||
}),
|
||||
|
||||
deleteTeam: protectedProcedure
|
||||
.input(z.object({ id: z.string().min(1) }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await verifyTeamOwnership(ctx.db!, input.id, ctx.userId!);
|
||||
|
||||
const teamRows = await ctx.db!.select({ id: teams.id, ownerId: teams.ownerId })
|
||||
.from(teams)
|
||||
.where(eq(teams.id, input.id));
|
||||
if (teamRows[0]?.ownerId !== ctx.userId!) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Only the owner can delete a team' });
|
||||
}
|
||||
|
||||
await ctx.db!.delete(teamMembers)
|
||||
.where(eq(teamMembers.teamId, input.id));
|
||||
|
||||
await ctx.db!.delete(teams)
|
||||
.where(eq(teams.id, input.id));
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
// Team member management
|
||||
listMembers: protectedProcedure
|
||||
.input(z.object({ teamId: z.string().min(1) }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
await verifyTeamOwnership(ctx.db!, input.teamId, ctx.userId!);
|
||||
return await ctx.db!.select()
|
||||
.from(teamMembers)
|
||||
.where(eq(teamMembers.teamId, input.teamId))
|
||||
.orderBy(asc(teamMembers.joinedAt));
|
||||
}),
|
||||
|
||||
addMember: protectedProcedure
|
||||
.input(z.object({
|
||||
teamId: z.string().min(1),
|
||||
userId: z.number().int().positive(),
|
||||
role: z.enum(['owner', 'admin', 'editor', 'viewer']).default('editor'),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await verifyTeamRole(ctx.db!, input.teamId, ctx.userId!, ['owner', 'admin']);
|
||||
|
||||
const existing = await ctx.db!.select()
|
||||
.from(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, input.teamId), eq(teamMembers.userId, input.userId)));
|
||||
|
||||
if (existing.length > 0) {
|
||||
throw new TRPCError({ code: 'CONFLICT', message: 'User is already a member of this team' });
|
||||
}
|
||||
|
||||
const result = await ctx.db!.insert(teamMembers)
|
||||
.values({
|
||||
teamId: input.teamId,
|
||||
userId: input.userId,
|
||||
role: input.role,
|
||||
})
|
||||
.returning();
|
||||
return result[0];
|
||||
}),
|
||||
|
||||
updateMemberRole: protectedProcedure
|
||||
.input(z.object({
|
||||
teamId: z.string().min(1),
|
||||
userId: z.number().int().positive(),
|
||||
role: z.enum(['owner', 'admin', 'editor', 'viewer']),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await verifyTeamRole(ctx.db!, input.teamId, ctx.userId!, ['owner']);
|
||||
|
||||
const result = await ctx.db!.update(teamMembers)
|
||||
.set({ role: input.role })
|
||||
.where(and(eq(teamMembers.teamId, input.teamId), eq(teamMembers.userId, input.userId)))
|
||||
.returning();
|
||||
return result[0];
|
||||
}),
|
||||
|
||||
removeMember: protectedProcedure
|
||||
.input(z.object({
|
||||
teamId: z.string().min(1),
|
||||
userId: z.number().int().positive(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
await verifyTeamRole(ctx.db!, input.teamId, ctx.userId!, ['owner', 'admin']);
|
||||
|
||||
if (input.userId === ctx.userId!) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'You cannot remove yourself from this team' });
|
||||
}
|
||||
|
||||
const memberRows = await ctx.db!.select()
|
||||
.from(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, input.teamId), eq(teamMembers.userId, input.userId)));
|
||||
|
||||
if (memberRows[0]?.role === 'owner') {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Cannot remove the team owner' });
|
||||
}
|
||||
|
||||
await ctx.db!.delete(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, input.teamId), eq(teamMembers.userId, input.userId)));
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
leaveTeam: protectedProcedure
|
||||
.input(z.object({ teamId: z.string().min(1) }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const memberRows = await ctx.db!.select()
|
||||
.from(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, input.teamId), eq(teamMembers.userId, ctx.userId!)));
|
||||
|
||||
if (memberRows[0]?.role === 'owner') {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Owner cannot leave the team. Transfer ownership first.' });
|
||||
}
|
||||
|
||||
await ctx.db!.delete(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, input.teamId), eq(teamMembers.userId, ctx.userId!)));
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user