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" ;
23import { createDrizzleCache , redis } from "@databuddy/redis" ;
34import { ORPCError } from "@orpc/server" ;
45import { z } from "zod" ;
6+ import type { Context } from "../orpc" ;
57import { protectedProcedure , publicProcedure } from "../orpc" ;
68import { authorizeWebsiteAccess } from "../utils/auth" ;
79import { getCacheAuthContext } from "../utils/cache-keys" ;
@@ -12,6 +14,23 @@ const annotationsCache = createDrizzleCache({
1214} ) ;
1315const 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+
1534const 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