Skip to content

Commit 07a62c3

Browse files
committed
feat: websites query
1 parent bec70cf commit 07a62c3

File tree

1 file changed

+107
-4
lines changed

1 file changed

+107
-4
lines changed

apps/api/src/routes/query.ts

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { auth } from '@databuddy/auth';
2+
import { and, apikeyAccess, db, eq, isNull, websites } from '@databuddy/db';
23
import { filterOptions } from '@databuddy/shared';
34
import { Elysia, t } from 'elysia';
45
import { getApiKeyFromHeader, isApiKeyPresent } from '../lib/api-key';
56
import { getCachedWebsiteDomain, getWebsiteDomain } from '../lib/website-utils';
6-
// import { createRateLimitMiddleware } from '../middleware/rate-limit';
7-
import { websiteAuth } from '../middleware/website-auth';
87
import { compileQuery, executeQuery } from '../query';
98
import { QueryBuilders } from '../query/builders';
109
import type { QueryRequest } from '../query/types';
@@ -49,9 +48,113 @@ async function checkAuth(request: Request): Promise<Response | null> {
4948
);
5049
}
5150

51+
async function getAccessibleWebsites(request: Request) {
52+
const apiKeyPresent = isApiKeyPresent(request.headers);
53+
const apiKey = apiKeyPresent
54+
? await getApiKeyFromHeader(request.headers)
55+
: null;
56+
const session = await auth.api.getSession({ headers: request.headers });
57+
const sessionUser = session?.user ?? null;
58+
59+
const baseSelect = {
60+
id: websites.id,
61+
name: websites.name,
62+
domain: websites.domain,
63+
isPublic: websites.isPublic,
64+
createdAt: websites.createdAt,
65+
};
66+
67+
if (sessionUser) {
68+
return db
69+
.select(baseSelect)
70+
.from(websites)
71+
.where(
72+
and(
73+
eq(websites.userId, sessionUser.id),
74+
isNull(websites.organizationId)
75+
)
76+
)
77+
.orderBy((table) => table.createdAt);
78+
}
79+
80+
if (apiKey) {
81+
// Check for global access first
82+
const hasGlobalAccess = await db
83+
.select({ count: apikeyAccess.apikeyId })
84+
.from(apikeyAccess)
85+
.where(
86+
and(
87+
eq(apikeyAccess.apikeyId, apiKey.id),
88+
eq(apikeyAccess.resourceType, 'global')
89+
)
90+
)
91+
.limit(1);
92+
93+
if (hasGlobalAccess.length > 0) {
94+
// Global access - return all websites for the API key's scope
95+
const filter = apiKey.organizationId
96+
? eq(websites.organizationId, apiKey.organizationId)
97+
: apiKey.userId
98+
? and(
99+
eq(websites.userId, apiKey.userId),
100+
isNull(websites.organizationId)
101+
)
102+
: eq(websites.id, ''); // No matches if no user/org
103+
104+
return db
105+
.select(baseSelect)
106+
.from(websites)
107+
.where(filter)
108+
.orderBy((table) => table.createdAt);
109+
}
110+
111+
// Specific website access - join with access table
112+
return db
113+
.select(baseSelect)
114+
.from(websites)
115+
.innerJoin(
116+
apikeyAccess,
117+
and(
118+
eq(apikeyAccess.resourceId, websites.id),
119+
eq(apikeyAccess.resourceType, 'website'),
120+
eq(apikeyAccess.apikeyId, apiKey.id)
121+
)
122+
)
123+
.orderBy((table) => table.createdAt);
124+
}
125+
126+
return [];
127+
}
128+
52129
export const query = new Elysia({ prefix: '/v1/query' })
53-
// .use(createRateLimitMiddleware({ type: 'api' }))
54-
.use(websiteAuth())
130+
.get('/websites', async ({ request }: { request: Request }) => {
131+
const authResult = await checkAuth(request);
132+
if (authResult) {
133+
return authResult;
134+
}
135+
136+
try {
137+
const websites = await getAccessibleWebsites(request);
138+
return {
139+
success: true,
140+
websites,
141+
total: websites.length,
142+
};
143+
} catch (error) {
144+
return new Response(
145+
JSON.stringify({
146+
success: false,
147+
error:
148+
error instanceof Error ? error.message : 'Failed to fetch websites',
149+
code: 'INTERNAL_SERVER_ERROR',
150+
}),
151+
{
152+
status: 500,
153+
headers: { 'Content-Type': 'application/json' },
154+
}
155+
);
156+
}
157+
})
55158
.get(
56159
'/types',
57160
async ({

0 commit comments

Comments
 (0)