diff --git a/packages/api/src/routers/import.ts b/packages/api/src/routers/import.ts index 245be072..205e7459 100644 --- a/packages/api/src/routers/import.ts +++ b/packages/api/src/routers/import.ts @@ -12,6 +12,7 @@ import { colours } from "@kan/shared/constants"; import { generateUID } from "@kan/shared/utils"; import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { assertUserInWorkspace } from "../utils/auth"; const TRELLO_API_URL = "https://api.trello.com/1"; @@ -125,15 +126,6 @@ export const importRouter = createTRPCRouter({ code: "UNAUTHORIZED", }); - const newImport = await importRepo.create(ctx.db, { - source: "trello", - createdBy: userId, - }); - - const newImportId = newImport?.id; - - let boardsCreated = 0; - const workspace = await workspaceRepo.getByPublicId( ctx.db, input.workspacePublicId, @@ -145,6 +137,17 @@ export const importRouter = createTRPCRouter({ code: "NOT_FOUND", }); + await assertUserInWorkspace(ctx.db, userId, workspace.id); + + const newImport = await importRepo.create(ctx.db, { + source: "trello", + createdBy: userId, + }); + + const newImportId = newImport?.id; + + let boardsCreated = 0; + for (const boardId of input.boardIds) { const response = await fetch( `${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open&labels=all`, diff --git a/packages/api/src/routers/label.ts b/packages/api/src/routers/label.ts index ca4b72ec..777932e8 100644 --- a/packages/api/src/routers/label.ts +++ b/packages/api/src/routers/label.ts @@ -6,6 +6,7 @@ import * as cardRepo from "@kan/db/repository/card.repo"; import * as labelRepo from "@kan/db/repository/label.repo"; import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { assertUserInWorkspace } from "../utils/auth"; export const labelRouter = createTRPCRouter({ byPublicId: protectedProcedure @@ -22,7 +23,18 @@ export const labelRouter = createTRPCRouter({ .input(z.object({ labelPublicId: z.string().min(12) })) .output(z.custom>>()) .query(async ({ ctx, input }) => { - const label = await labelRepo.getByPublicId(ctx.db, input.labelPublicId); + const userId = ctx.user?.id; + + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + + const label = await labelRepo.getWorkspaceAndLabelIdByLabelPublicId( + ctx.db, + input.labelPublicId, + ); if (!label) throw new TRPCError({ @@ -30,7 +42,17 @@ export const labelRouter = createTRPCRouter({ code: "NOT_FOUND", }); - return label; + await assertUserInWorkspace(ctx.db, userId, label.workspaceId); + + const result = await labelRepo.getByPublicId(ctx.db, input.labelPublicId); + + if (!result) + throw new TRPCError({ + message: `Label with public ID ${input.labelPublicId} not found`, + code: "NOT_FOUND", + }); + + return result; }), create: protectedProcedure .meta({ @@ -60,7 +82,7 @@ export const labelRouter = createTRPCRouter({ code: "UNAUTHORIZED", }); - const board = await boardRepo.getIdByPublicId( + const board = await boardRepo.getWorkspaceAndBoardIdByBoardPublicId( ctx.db, input.boardPublicId, ); @@ -71,6 +93,8 @@ export const labelRouter = createTRPCRouter({ code: "NOT_FOUND", }); + await assertUserInWorkspace(ctx.db, userId, board.workspaceId); + const result = await labelRepo.create(ctx.db, { name: input.name, colourCode: input.colourCode, @@ -106,6 +130,27 @@ export const labelRouter = createTRPCRouter({ ) .output(z.custom>>()) .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; + + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + + const label = await labelRepo.getWorkspaceAndLabelIdByLabelPublicId( + ctx.db, + input.labelPublicId, + ); + + if (!label) + throw new TRPCError({ + message: `Label with public ID ${input.labelPublicId} not found`, + code: "NOT_FOUND", + }); + + await assertUserInWorkspace(ctx.db, userId, label.workspaceId); + const result = await labelRepo.update(ctx.db, input); return result; @@ -124,7 +169,18 @@ export const labelRouter = createTRPCRouter({ .input(z.object({ labelPublicId: z.string().min(12) })) .output(z.object({ success: z.boolean() })) .mutation(async ({ ctx, input }) => { - const label = await labelRepo.getByPublicId(ctx.db, input.labelPublicId); + const userId = ctx.user?.id; + + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + + const label = await labelRepo.getWorkspaceAndLabelIdByLabelPublicId( + ctx.db, + input.labelPublicId, + ); if (!label) throw new TRPCError({ @@ -132,6 +188,8 @@ export const labelRouter = createTRPCRouter({ code: "NOT_FOUND", }); + await assertUserInWorkspace(ctx.db, userId, label.workspaceId); + await cardRepo.hardDeleteAllCardLabelRelationships(ctx.db, label.id); await labelRepo.hardDelete(ctx.db, label.id); diff --git a/packages/api/src/routers/member.ts b/packages/api/src/routers/member.ts index 9be1fd7e..81feada2 100644 --- a/packages/api/src/routers/member.ts +++ b/packages/api/src/routers/member.ts @@ -7,6 +7,7 @@ import * as userRepo from "@kan/db/repository/user.repo"; import * as workspaceRepo from "@kan/db/repository/workspace.repo"; import { createTRPCRouter, protectedProcedure } from "../trpc"; +import { assertUserInWorkspace } from "../utils/auth"; export const memberRouter = createTRPCRouter({ invite: protectedProcedure @@ -47,6 +48,8 @@ export const memberRouter = createTRPCRouter({ code: "NOT_FOUND", }); + await assertUserInWorkspace(ctx.db, userId, workspace.id, "admin"); + const isInvitedEmailAlreadyMember = workspace.members.some( (member) => member.email === input.email, ); @@ -126,6 +129,8 @@ export const memberRouter = createTRPCRouter({ code: "NOT_FOUND", }); + await assertUserInWorkspace(ctx.db, userId, workspace.id, "admin"); + const member = await memberRepo.getByPublicId( ctx.db, input.memberPublicId, diff --git a/packages/api/src/routers/workspace.ts b/packages/api/src/routers/workspace.ts index 930ba67f..514cee69 100644 --- a/packages/api/src/routers/workspace.ts +++ b/packages/api/src/routers/workspace.ts @@ -6,6 +6,7 @@ import * as workspaceSlugRepo from "@kan/db/repository/workspaceSlug.repo"; import { generateUID } from "@kan/shared/utils"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; +import { assertUserInWorkspace } from "../utils/auth"; export const workspaceRouter = createTRPCRouter({ all: protectedProcedure @@ -54,6 +55,14 @@ export const workspaceRouter = createTRPCRouter({ >(), ) .query(async ({ ctx, input }) => { + const userId = ctx.user?.id; + + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + const result = await workspaceRepo.getByPublicIdWithMembers( ctx.db, input.workspacePublicId, @@ -65,6 +74,8 @@ export const workspaceRouter = createTRPCRouter({ code: "NOT_FOUND", }); + await assertUserInWorkspace(ctx.db, userId, result.id); + return result; }), bySlug: publicProcedure @@ -91,11 +102,27 @@ export const workspaceRouter = createTRPCRouter({ z.custom>>(), ) .query(async ({ ctx, input }) => { + const userId = ctx.user?.id; + + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + const result = await workspaceRepo.getBySlugWithBoards( ctx.db, input.workspaceSlug, ); + if (!result) + throw new TRPCError({ + message: `Workspace not found`, + code: "NOT_FOUND", + }); + + await assertUserInWorkspace(ctx.db, userId, result.id); + return result; }), create: protectedProcedure @@ -169,12 +196,28 @@ export const workspaceRouter = createTRPCRouter({ ) .output(z.custom>>()) .mutation(async ({ ctx, input }) => { - if (input.slug) { - const workspace = await workspaceRepo.getByPublicId( - ctx.db, - input.workspacePublicId, - ); + const userId = ctx.user?.id; + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + + const workspace = await workspaceRepo.getByPublicId( + ctx.db, + input.workspacePublicId, + ); + + if (!workspace) + throw new TRPCError({ + message: `Workspace not found`, + code: "NOT_FOUND", + }); + + await assertUserInWorkspace(ctx.db, userId, workspace.id, "admin"); + + if (input.slug) { const reservedOrPremiumWorkspaceSlug = await workspaceSlugRepo.getWorkspaceSlug(ctx.db, input.slug); @@ -183,7 +226,7 @@ export const workspaceRouter = createTRPCRouter({ if ( reservedOrPremiumWorkspaceSlug?.type === "reserved" || - (workspace?.plan !== "pro" && + (workspace.plan !== "pro" && reservedOrPremiumWorkspaceSlug?.type === "premium") || !isWorkspaceSlugAvailable ) { @@ -220,6 +263,27 @@ export const workspaceRouter = createTRPCRouter({ .input(z.object({ workspacePublicId: z.string().min(12) })) .output(z.custom>>()) .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; + + if (!userId) + throw new TRPCError({ + message: `User not authenticated`, + code: "UNAUTHORIZED", + }); + + const workspace = await workspaceRepo.getByPublicId( + ctx.db, + input.workspacePublicId, + ); + + if (!workspace) + throw new TRPCError({ + message: `Workspace not found`, + code: "NOT_FOUND", + }); + + await assertUserInWorkspace(ctx.db, userId, workspace.id, "admin"); + const result = await workspaceRepo.hardDelete( ctx.db, input.workspacePublicId, diff --git a/packages/api/src/utils/auth.ts b/packages/api/src/utils/auth.ts index 4173c60b..8e35b1cb 100644 --- a/packages/api/src/utils/auth.ts +++ b/packages/api/src/utils/auth.ts @@ -7,11 +7,13 @@ export async function assertUserInWorkspace( db: dbClient, userId: string, workspaceId: number, + role?: "admin" | "member", ) { const isMember = await workspaceRepo.isUserInWorkspace( db, userId, workspaceId, + role, ); if (!isMember) diff --git a/packages/db/src/repository/label.repo.ts b/packages/db/src/repository/label.repo.ts index 0d68c275..782c284d 100644 --- a/packages/db/src/repository/label.repo.ts +++ b/packages/db/src/repository/label.repo.ts @@ -106,3 +106,25 @@ export const hardDelete = async (db: dbClient, labelId: number) => { return result; }; + +export const getWorkspaceAndLabelIdByLabelPublicId = async ( + db: dbClient, + labelPublicId: string, +) => { + const result = await db.query.labels.findFirst({ + columns: { id: true }, + where: eq(labels.publicId, labelPublicId), + with: { + board: { + columns: { workspaceId: true }, + }, + }, + }); + + return result + ? { + id: result.id, + workspaceId: result.board.workspaceId, + } + : null; +}; diff --git a/packages/db/src/repository/workspace.repo.ts b/packages/db/src/repository/workspace.repo.ts index fe091c78..bb7eb888 100644 --- a/packages/db/src/repository/workspace.repo.ts +++ b/packages/db/src/repository/workspace.repo.ts @@ -132,6 +132,7 @@ export const getByPublicIdWithMembers = ( export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => { return db.query.workspaces.findFirst({ columns: { + id: true, publicId: true, name: true, description: true, @@ -225,6 +226,7 @@ export const isUserInWorkspace = async ( db: dbClient, userId: string, workspaceId: number, + role?: "admin" | "member", ) => { const result = await db.query.workspaceMembers.findFirst({ columns: { @@ -235,6 +237,7 @@ export const isUserInWorkspace = async ( eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.status, "active"), isNull(workspaceMembers.deletedAt), + role ? eq(workspaceMembers.role, role) : undefined, ), });