From 20499097ea38f3fb6f5574a9829c0445db5c62aa Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Sat, 17 Jan 2026 18:02:51 +1100 Subject: [PATCH] fix: enforce contact book ownership --- apps/web/src/server/api/routers/contacts.ts | 34 +++++++++++++++-- .../public-api/api/contacts/delete-contact.ts | 18 +++++++-- .../public-api/api/contacts/update-contact.ts | 19 ++++++++-- .../web/src/server/service/contact-service.ts | 38 ++++++++++++++++++- 4 files changed, 95 insertions(+), 14 deletions(-) diff --git a/apps/web/src/server/api/routers/contacts.ts b/apps/web/src/server/api/routers/contacts.ts index 416b2913..68cafed7 100644 --- a/apps/web/src/server/api/routers/contacts.ts +++ b/apps/web/src/server/api/routers/contacts.ts @@ -1,4 +1,5 @@ import { CampaignStatus, Prisma } from "@prisma/client"; +import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { @@ -151,15 +152,40 @@ export const contactsRouter = createTRPCRouter({ subscribed: z.boolean().optional(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ ctx: { contactBook }, input }) => { const { contactId, ...contact } = input; - return contactService.updateContact(contactId, contact); + const updatedContact = await contactService.updateContactInContactBook( + contactId, + contactBook.id, + contact, + ); + + if (!updatedContact) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Contact not found", + }); + } + + return updatedContact; }), deleteContact: contactBookProcedure .input(z.object({ contactId: z.string() })) - .mutation(async ({ input }) => { - return contactService.deleteContact(input.contactId); + .mutation(async ({ ctx: { contactBook }, input }) => { + const deletedContact = await contactService.deleteContactInContactBook( + input.contactId, + contactBook.id, + ); + + if (!deletedContact) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Contact not found", + }); + } + + return deletedContact; }), exportContacts: contactBookProcedure diff --git a/apps/web/src/server/public-api/api/contacts/delete-contact.ts b/apps/web/src/server/public-api/api/contacts/delete-contact.ts index 4e54f43f..db270466 100644 --- a/apps/web/src/server/public-api/api/contacts/delete-contact.ts +++ b/apps/web/src/server/public-api/api/contacts/delete-contact.ts @@ -1,8 +1,8 @@ import { createRoute, z } from "@hono/zod-openapi"; import { PublicAPIApp } from "~/server/public-api/hono"; -import { getTeamFromToken } from "~/server/public-api/auth"; -import { deleteContact } from "~/server/service/contact-service"; +import { deleteContactInContactBook } from "~/server/service/contact-service"; import { getContactBook } from "../../api-utils"; +import { UnsendApiError } from "../../api-error"; const route = createRoute({ method: "delete", @@ -41,10 +41,20 @@ function deleteContactHandler(app: PublicAPIApp) { app.openapi(route, async (c) => { const team = c.var.team; - await getContactBook(c, team.id); + const contactBook = await getContactBook(c, team.id); const contactId = c.req.param("contactId"); - await deleteContact(contactId); + const deletedContact = await deleteContactInContactBook( + contactId, + contactBook.id, + ); + + if (!deletedContact) { + throw new UnsendApiError({ + code: "NOT_FOUND", + message: "Contact not found", + }); + } return c.json({ success: true }); }); diff --git a/apps/web/src/server/public-api/api/contacts/update-contact.ts b/apps/web/src/server/public-api/api/contacts/update-contact.ts index d1846bf7..734030fc 100644 --- a/apps/web/src/server/public-api/api/contacts/update-contact.ts +++ b/apps/web/src/server/public-api/api/contacts/update-contact.ts @@ -1,8 +1,8 @@ import { createRoute, z } from "@hono/zod-openapi"; import { PublicAPIApp } from "~/server/public-api/hono"; -import { getTeamFromToken } from "~/server/public-api/auth"; -import { updateContact } from "~/server/service/contact-service"; +import { updateContactInContactBook } from "~/server/service/contact-service"; import { getContactBook } from "../../api-utils"; +import { UnsendApiError } from "../../api-error"; const route = createRoute({ method: "patch", @@ -54,10 +54,21 @@ function updateContactInfo(app: PublicAPIApp) { app.openapi(route, async (c) => { const team = c.var.team; - await getContactBook(c, team.id); + const contactBook = await getContactBook(c, team.id); const contactId = c.req.param("contactId"); - const contact = await updateContact(contactId, c.req.valid("json")); + const contact = await updateContactInContactBook( + contactId, + contactBook.id, + c.req.valid("json"), + ); + + if (!contact) { + throw new UnsendApiError({ + code: "NOT_FOUND", + message: "Contact not found", + }); + } return c.json({ contactId: contact.id }); }); diff --git a/apps/web/src/server/service/contact-service.ts b/apps/web/src/server/service/contact-service.ts index 6d49eece..b2077fb8 100644 --- a/apps/web/src/server/service/contact-service.ts +++ b/apps/web/src/server/service/contact-service.ts @@ -63,10 +63,32 @@ export async function addOrUpdateContact( return createdContact; } -export async function updateContact( +export async function getContactInContactBook( contactId: string, + contactBookId: string, +) { + return db.contact.findFirst({ + where: { + id: contactId, + contactBookId, + }, + }); +} + +export async function updateContactInContactBook( + contactId: string, + contactBookId: string, contact: Partial, ) { + const existingContact = await getContactInContactBook( + contactId, + contactBookId, + ); + + if (!existingContact) { + return null; + } + return db.contact.update({ where: { id: contactId, @@ -75,7 +97,19 @@ export async function updateContact( }); } -export async function deleteContact(contactId: string) { +export async function deleteContactInContactBook( + contactId: string, + contactBookId: string, +) { + const existingContact = await getContactInContactBook( + contactId, + contactBookId, + ); + + if (!existingContact) { + return null; + } + return db.contact.delete({ where: { id: contactId,