Skip to content

Commit 48408dd

Browse files
committed
fix(public): prevent owner self-views from inflating analytics when unauthenticated
1 parent a937047 commit 48408dd

1 file changed

Lines changed: 21 additions & 18 deletions

File tree

apps/backend/src/routes/public.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyRepl
22
import { generateQRBuffer, generateQRSvg } from '../utils/qr.js';
33
import type { PlatformLink } from '@devcard/shared';
44
import { getErrorMessage } from '../utils/error.util.js';
5+
56
type PublicProfileLink = {
67
id: string;
78
platform: string;
@@ -11,7 +12,7 @@ type PublicProfileLink = {
1112
followed?: boolean;
1213
}
1314

14-
type UsernamePublicProfileResponse = {
15+
type UsernamePublicProfileResponse = {
1516
username: string;
1617
displayName: string;
1718
bio: string | null;
@@ -59,7 +60,6 @@ type UsernameCardPublicProfileResponse = {
5960
links: PublicProfileCardLink[]
6061
}
6162

62-
// Represents a CardLink record with the joined PlatformLink relation
6363
interface CardLinkWithPlatform {
6464
id: string;
6565
displayOrder: number;
@@ -94,24 +94,26 @@ export async function publicRoutes(app: FastifyInstance) {
9494

9595
// Try to extract viewer from Authorization header (soft auth)
9696
let viewerId: string | null = null;
97+
let isSelfView = false;
9798
try {
9899
if (request.headers.authorization) {
99100
const decoded = (await request.jwtVerify()) as { id?: string };
100-
viewerId = decoded?.id ?? null;
101-
} else {
102-
viewerId = null; // Unauthenticated viewer
101+
if (decoded?.id === user.id) {
102+
isSelfView = true;
103+
} else {
104+
viewerId = decoded?.id ?? null;
105+
}
103106
}
104107
} catch {
105108
// Ignored if invalid token
106109
}
107110

108111
// Don't track if the owner is viewing their own profile
109-
if (viewerId && viewerId !== user.id) {
110-
// Background view tracking
112+
if (!isSelfView && viewerId !== user.id) {
111113
app.prisma.cardView.create({
112114
data: {
113115
ownerId: user.id,
114-
cardId: null, // this is a profile view, not a card view
116+
cardId: null,
115117
viewerId,
116118
viewerIp: request.ip || null,
117119
viewerAgent: request.headers['user-agent'] || null,
@@ -168,7 +170,6 @@ export async function publicRoutes(app: FastifyInstance) {
168170
}
169171

170172
return response;
171-
172173
});
173174

174175
/**
@@ -222,7 +223,6 @@ export async function publicRoutes(app: FastifyInstance) {
222223
}
223224

224225
return response;
225-
226226
});
227227

228228
// ─── Public Card View ───
@@ -264,16 +264,21 @@ export async function publicRoutes(app: FastifyInstance) {
264264
}
265265

266266
let viewerId: string | null = null;
267+
let isSelfView = false;
267268
try {
268269
if (request.headers.authorization) {
269-
const decoded = (await request.jwtVerify()) as { id?: string };
270-
viewerId = decoded?.id ?? null;
270+
const decoded = await request.jwtVerify() as any;
271+
if (decoded?.id === user.id) {
272+
isSelfView = true;
273+
} else {
274+
viewerId = decoded.id;
275+
}
271276
}
272-
} catch {
277+
} catch (e) {
273278
// Ignored if invalid token
274279
}
275280

276-
if (viewerId && viewerId !== user.id) {
281+
if (!isSelfView && viewerId !== user.id) {
277282
app.prisma.cardView.create({
278283
data: {
279284
ownerId: user.id,
@@ -286,7 +291,6 @@ export async function publicRoutes(app: FastifyInstance) {
286291
}).catch((err: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(err)}`));
287292
}
288293

289-
290294
const response: UsernameCardPublicProfileResponse = {
291295
title: card.title,
292296
owner: {
@@ -315,7 +319,7 @@ export async function publicRoutes(app: FastifyInstance) {
315319
app.get('/:username/qr', {
316320
config: {
317321
rateLimit: {
318-
max: 50, // Lower limit for QR generation as it's more resource intensive
322+
max: 50,
319323
timeWindow: '1 minute'
320324
}
321325
} as FastifyContextConfig
@@ -327,7 +331,6 @@ export async function publicRoutes(app: FastifyInstance) {
327331
const format = request.query.format || 'png';
328332
const size = parseInt(request.query.size || '400', 10);
329333

330-
// Verify user exists
331334
const user = await app.prisma.user.findUnique({
332335
where: { username },
333336
});
@@ -352,4 +355,4 @@ export async function publicRoutes(app: FastifyInstance) {
352355
.header('Content-Disposition', `inline; filename="devcard-${username}.png"`)
353356
.send(png);
354357
});
355-
}
358+
}

0 commit comments

Comments
 (0)