Skip to content

Commit fe6e839

Browse files
committed
fix: docs
1 parent 7490068 commit fe6e839

File tree

4 files changed

+165
-156
lines changed

4 files changed

+165
-156
lines changed

apps/api/src/index.ts

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
const UNAUTHORIZED_RE = /unauthorized/i;
2-
const FORBIDDEN_RE = /forbidden/i;
31
import './polyfills/compression';
42
import { auth } from '@databuddy/auth';
53
import { appRouter, createTRPCContext } from '@databuddy/rpc';
@@ -57,54 +55,6 @@ const app = new Elysia()
5755
const errorMessage = error instanceof Error ? error.message : String(error);
5856
logger.error(errorMessage, { error });
5957

60-
if (
61-
error instanceof Error &&
62-
(UNAUTHORIZED_RE.test(error.message) || error.message === 'Unauthorized')
63-
) {
64-
return new Response(
65-
JSON.stringify({
66-
success: false,
67-
error: 'Authentication required',
68-
code: 'AUTH_REQUIRED',
69-
}),
70-
{
71-
status: 401,
72-
headers: { 'Content-Type': 'application/json' },
73-
}
74-
);
75-
}
76-
77-
if (
78-
error instanceof Error &&
79-
(FORBIDDEN_RE.test(error.message) || error.message === 'Forbidden')
80-
) {
81-
return new Response(
82-
JSON.stringify({
83-
success: false,
84-
error: 'Insufficient permissions',
85-
code: 'FORBIDDEN',
86-
}),
87-
{
88-
status: 403,
89-
headers: { 'Content-Type': 'application/json' },
90-
}
91-
);
92-
}
93-
94-
if (error instanceof Error && error.message === 'Website not found') {
95-
return new Response(
96-
JSON.stringify({
97-
success: false,
98-
error: 'Website not found',
99-
code: 'NOT_FOUND',
100-
}),
101-
{
102-
status: 404,
103-
headers: { 'Content-Type': 'application/json' },
104-
}
105-
);
106-
}
107-
10858
return new Response(
10959
JSON.stringify({
11060
success: false,

apps/api/src/middleware/website-auth.ts

Lines changed: 92 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,71 +13,29 @@ function json(status: number, body: unknown) {
1313
export function websiteAuth() {
1414
return new Elysia()
1515
.onRequest(async ({ request }) => {
16-
const url = new URL(request.url);
17-
const websiteId = url.searchParams.get('website_id');
18-
19-
const apiKeySecret = request.headers.get('x-api-key');
20-
const apiKey = apiKeySecret
21-
? await getApiKeyFromHeader(request.headers)
22-
: null;
23-
24-
const session = await auth.api.getSession({ headers: request.headers });
25-
const hasSession = Boolean(session?.user);
26-
27-
// No website_id: require either session or valid API key
28-
if (!websiteId) {
29-
if (hasSession || apiKey) {
30-
return;
31-
}
32-
return json(401, {
33-
success: false,
34-
error: 'Authentication required',
35-
code: 'AUTH_REQUIRED',
36-
});
37-
}
38-
39-
const website = await getCachedWebsite(websiteId);
40-
if (!website) {
41-
return json(404, {
42-
success: false,
43-
error: 'Website not found',
44-
code: 'NOT_FOUND',
45-
});
46-
}
47-
48-
if (website.isPublic) {
16+
if (isPreflight(request)) {
4917
return;
5018
}
5119

52-
// Private website: allow if session exists OR key has read:data for website
53-
if (hasSession) {
54-
return;
20+
const debug = shouldDebug();
21+
const rid = Math.random().toString(36).slice(2, 8);
22+
if (debug) {
23+
console.time(`websiteAuth:${rid}`);
5524
}
5625

57-
if (!apiKeySecret) {
58-
return json(401, {
59-
success: false,
60-
error: 'Authentication required',
61-
code: 'AUTH_REQUIRED',
62-
});
63-
}
26+
const url = new URL(request.url);
27+
const websiteId = url.searchParams.get('website_id');
28+
const { sessionUser, apiKey, apiKeyPresent } =
29+
await getAuthContext(request);
6430

65-
if (!apiKey) {
66-
return json(401, {
67-
success: false,
68-
error: 'Invalid or expired API key',
69-
code: 'AUTH_REQUIRED',
70-
});
71-
}
31+
const outcome = websiteId
32+
? await checkWebsiteAuth(websiteId, sessionUser, apiKey, apiKeyPresent)
33+
: checkNoWebsiteAuth(sessionUser, apiKey);
7234

73-
const canRead = await hasWebsiteScope(apiKey, websiteId, 'read:data');
74-
if (!canRead) {
75-
return json(403, {
76-
success: false,
77-
error: 'Insufficient permissions',
78-
code: 'FORBIDDEN',
79-
});
35+
if (debug) {
36+
console.timeEnd(`websiteAuth:${rid}`);
8037
}
38+
return outcome;
8139
})
8240
.derive(async ({ request }) => {
8341
const url = new URL(request.url);
@@ -95,3 +53,80 @@ export function websiteAuth() {
9553
} as const;
9654
});
9755
}
56+
57+
function isPreflight(request: Request): boolean {
58+
return request.method === 'OPTIONS' || request.method === 'HEAD';
59+
}
60+
61+
function shouldDebug(): boolean {
62+
return process.env.NODE_ENV === 'development';
63+
}
64+
65+
async function getAuthContext(request: Request) {
66+
const apiKeyPresent = request.headers.get('x-api-key') != null;
67+
const apiKey = apiKeyPresent
68+
? await getApiKeyFromHeader(request.headers)
69+
: null;
70+
const session = await auth.api.getSession({ headers: request.headers });
71+
const sessionUser = session?.user ?? null;
72+
return { sessionUser, apiKey, apiKeyPresent } as const;
73+
}
74+
75+
function checkNoWebsiteAuth(
76+
sessionUser: unknown,
77+
apiKey: unknown
78+
): Response | null {
79+
if (sessionUser || apiKey) {
80+
return null;
81+
}
82+
return json(401, {
83+
success: false,
84+
error: 'Authentication required',
85+
code: 'AUTH_REQUIRED',
86+
});
87+
}
88+
89+
async function checkWebsiteAuth(
90+
websiteId: string,
91+
sessionUser: unknown,
92+
apiKey: Parameters<typeof hasWebsiteScope>[0] | null,
93+
apiKeyPresent: boolean
94+
): Promise<Response | null> {
95+
const website = await getCachedWebsite(websiteId);
96+
if (!website) {
97+
return json(404, {
98+
success: false,
99+
error: 'Website not found',
100+
code: 'NOT_FOUND',
101+
});
102+
}
103+
if (website.isPublic) {
104+
return null;
105+
}
106+
if (sessionUser) {
107+
return null;
108+
}
109+
if (!apiKeyPresent) {
110+
return json(401, {
111+
success: false,
112+
error: 'Authentication required',
113+
code: 'AUTH_REQUIRED',
114+
});
115+
}
116+
if (!apiKey) {
117+
return json(401, {
118+
success: false,
119+
error: 'Invalid or expired API key',
120+
code: 'AUTH_REQUIRED',
121+
});
122+
}
123+
const ok = await hasWebsiteScope(apiKey, websiteId, 'read:data');
124+
if (!ok) {
125+
return json(403, {
126+
success: false,
127+
error: 'Insufficient permissions',
128+
code: 'FORBIDDEN',
129+
});
130+
}
131+
return null;
132+
}

apps/docs/content/docs/Integrations/framer.mdx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,27 @@ description: Add privacy-first analytics to your Framer site
77

88
Databuddy enables you to gather privacy-first analytics on user behavior, capture custom events, track performance metrics, and more within your Framer site - all while maintaining GDPR compliance without cookies.
99

10+
> TL;DR: Paste the inline loader below into Framer → Site Settings → General → Custom Code (Start of <head> tag). Replace `YOUR_CLIENT_ID` with your client ID. Publish, then check the dashboard.
11+
1012
## How to Add Databuddy to Framer
1113

1214
### 1. Get Your Tracking Script
1315

1416
Navigate to your [Databuddy dashboard](https://app.databuddy.cc) and copy your tracking code snippet:
1517

1618
```html
17-
<script
18-
src="https://cdn.databuddy.cc/databuddy.js"
19-
data-site-id="YOUR_SITE_ID"
20-
async
21-
></script>
19+
<script>
20+
(function () {
21+
var script = document.createElement("script");
22+
script.async = true;
23+
script.src = "https://cdn.databuddy.cc/databuddy.js";
24+
script.setAttribute("data-client-id", "YOUR_CLIENT_ID");
25+
script.setAttribute("data-track-screen-views", "true");
26+
script.setAttribute("data-track-attributes", "true");
27+
script.setAttribute("data-track-errors", "true");
28+
document.head.appendChild(script);
29+
})();
30+
</script>
2231
```
2332

2433
### 2. Add the Script to Your Framer Project
@@ -46,9 +55,7 @@ Track custom interactions in your Framer components using data attributes:
4655

4756
```html
4857
<!-- Track button clicks -->
49-
<button data-track="cta_clicked" data-button-type="primary">
50-
Get Started
51-
</button>
58+
<button data-track="cta_clicked" data-button-type="primary">Get Started</button>
5259

5360
<!-- Track form submissions -->
5461
<form data-track="newsletter_signup" data-form-location="header">
@@ -61,13 +68,19 @@ Track custom interactions in your Framer components using data attributes:
6168
Enable performance tracking by updating your script configuration:
6269

6370
```html
64-
<script
65-
src="https://cdn.databuddy.cc/databuddy.js"
66-
data-site-id="YOUR_SITE_ID"
67-
data-track-performance="true"
68-
data-track-web-vitals="true"
69-
async
70-
></script>
71+
<script>
72+
(function () {
73+
var script = document.createElement("script");
74+
script.async = true;
75+
script.src = "https://cdn.databuddy.cc/databuddy.js";
76+
script.setAttribute("data-client-id", "YOUR_CLIENT_ID");
77+
script.setAttribute("data-track-screen-views", "true");
78+
script.setAttribute("data-track-performance", "true");
79+
script.setAttribute("data-track-web-vitals", "true");
80+
script.setAttribute("data-track-errors", "true");
81+
document.head.appendChild(script);
82+
})();
83+
</script>
7184
```
7285

7386
## Benefits for Framer Sites
@@ -98,4 +111,4 @@ If analytics data isn't showing:
98111
3. Ensure you're viewing the correct date range
99112
4. Try visiting your site in an incognito window
100113

101-
Need help? Contact our support team at [[email protected]](mailto:[email protected]).
114+
Need help? Contact our support team at [[email protected]](mailto:[email protected]).

0 commit comments

Comments
 (0)