Skip to content

Commit 9ddc1dc

Browse files
committed
update annotations
1 parent 43b21d3 commit 9ddc1dc

File tree

1 file changed

+99
-16
lines changed

1 file changed

+99
-16
lines changed

packages/rpc/src/routers/annotations.ts

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { and, annotations, desc, eq, isNull } from "@databuddy/db";
1+
import { websitesApi } from "@databuddy/auth";
2+
import { and, annotations, desc, eq, isNull, or, type SQL } from "@databuddy/db";
23
import { createDrizzleCache, redis } from "@databuddy/redis";
34
import { ORPCError } from "@orpc/server";
45
import { z } from "zod";
6+
import type { Context } from "../orpc";
57
import { protectedProcedure, publicProcedure } from "../orpc";
68
import { authorizeWebsiteAccess } from "../utils/auth";
79
import { getCacheAuthContext } from "../utils/cache-keys";
@@ -12,6 +14,23 @@ const annotationsCache = createDrizzleCache({
1214
});
1315
const CACHE_TTL = 300; // 5 minutes
1416

17+
/**
18+
* Check if a user has update permission for a website (ownership check)
19+
*/
20+
async function hasWebsiteUpdatePermission(
21+
context: Context & { user: NonNullable<Context["user"]> },
22+
website: { organizationId: string | null; userId: string | null }
23+
): Promise<boolean> {
24+
if (website.organizationId) {
25+
const { success } = await websitesApi.hasPermission({
26+
headers: context.headers,
27+
body: { permissions: { website: ["update"] } },
28+
});
29+
return success;
30+
}
31+
return website.userId === context.user.id;
32+
}
33+
1534
const chartContextSchema = z.object({
1635
dateRange: z.object({
1736
start_date: z.string(),
@@ -51,18 +70,44 @@ export const annotationsRouter = {
5170
ttl: CACHE_TTL,
5271
tables: ["annotations"],
5372
queryFn: async () => {
54-
await authorizeWebsiteAccess(context, input.websiteId, "read");
73+
const website = await authorizeWebsiteAccess(
74+
context,
75+
input.websiteId,
76+
"read"
77+
);
78+
79+
// For public websites, filter annotations to only show:
80+
// 1. Public annotations (isPublic: true)
81+
// 2. Annotations created by the current user (if authenticated)
82+
// For non-public websites, show all annotations (user has access via authorizeWebsiteAccess)
83+
const baseConditions = [
84+
eq(annotations.websiteId, input.websiteId),
85+
eq(annotations.chartType, input.chartType),
86+
isNull(annotations.deletedAt),
87+
];
88+
89+
let visibilityCondition: SQL<unknown> | undefined;
90+
if (website.isPublic) {
91+
if (context.user) {
92+
// Show public annotations OR user's own annotations
93+
visibilityCondition = or(
94+
eq(annotations.isPublic, true),
95+
eq(annotations.createdBy, context.user.id)
96+
);
97+
} else {
98+
// Unauthenticated users on public websites only see public annotations
99+
visibilityCondition = eq(annotations.isPublic, true);
100+
}
101+
}
102+
103+
const whereCondition = visibilityCondition
104+
? and(...baseConditions, visibilityCondition)
105+
: and(...baseConditions);
55106

56107
return context.db
57108
.select()
58109
.from(annotations)
59-
.where(
60-
and(
61-
eq(annotations.websiteId, input.websiteId),
62-
eq(annotations.chartType, input.chartType),
63-
isNull(annotations.deletedAt)
64-
)
65-
)
110+
.where(whereCondition)
66111
.orderBy(desc(annotations.createdAt));
67112
},
68113
});
@@ -141,7 +186,21 @@ export const annotationsRouter = {
141186
})
142187
)
143188
.handler(async ({ context, input }) => {
144-
await authorizeWebsiteAccess(context, input.websiteId, "update");
189+
const website = await authorizeWebsiteAccess(
190+
context,
191+
input.websiteId,
192+
"update"
193+
);
194+
195+
if (website.isPublic) {
196+
const hasPermission = await hasWebsiteUpdatePermission(context, website);
197+
if (!hasPermission) {
198+
throw new ORPCError("FORBIDDEN", {
199+
message:
200+
"You cannot create annotations on public websites unless you own them.",
201+
});
202+
}
203+
}
145204

146205
const annotationId = crypto.randomUUID();
147206
const [newAnnotation] = await context.db
@@ -193,12 +252,24 @@ export const annotationsRouter = {
193252
});
194253
}
195254

196-
await authorizeWebsiteAccess(
255+
const annotation = existingAnnotation[0];
256+
257+
// Users can only update their own annotations, unless they own the website
258+
const website = await authorizeWebsiteAccess(
197259
context,
198-
existingAnnotation[0].websiteId,
199-
"update"
260+
annotation.websiteId,
261+
"read"
200262
);
201263

264+
const hasPermission = await hasWebsiteUpdatePermission(context, website);
265+
266+
// If user doesn't own website, they can only update their own annotations
267+
if (!hasPermission && annotation.createdBy !== context.user.id) {
268+
throw new ORPCError("FORBIDDEN", {
269+
message: "You can only update your own annotations.",
270+
});
271+
}
272+
202273
const [updatedAnnotation] = await context.db
203274
.update(annotations)
204275
.set({
@@ -230,12 +301,24 @@ export const annotationsRouter = {
230301
});
231302
}
232303

233-
await authorizeWebsiteAccess(
304+
const annotation = existingAnnotation[0];
305+
306+
// Users can only delete their own annotations, unless they own the website
307+
const website = await authorizeWebsiteAccess(
234308
context,
235-
existingAnnotation[0].websiteId,
236-
"update"
309+
annotation.websiteId,
310+
"read"
237311
);
238312

313+
const hasPermission = await hasWebsiteUpdatePermission(context, website);
314+
315+
// If user doesn't own website, they can only delete their own annotations
316+
if (!hasPermission && annotation.createdBy !== context.user.id) {
317+
throw new ORPCError("FORBIDDEN", {
318+
message: "You can only delete your own annotations.",
319+
});
320+
}
321+
239322
await context.db
240323
.update(annotations)
241324
.set({ deletedAt: new Date() })

0 commit comments

Comments
 (0)