diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index 6820072..6a61f51 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -3,7 +3,6 @@ import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; import type { PlatformLink } from '@devcard/shared'; import { getErrorMessage } from '../utils/error.util.js'; - // ── QR size bounds ──────────────────────────────────────────────────────────── // Enforced before any DB query or image allocation. Values outside this range // are rejected with 400 so a single unauthenticated request cannot trigger an @@ -19,7 +18,7 @@ type PublicProfileLink = { followed?: boolean; } -type UsernamePublicProfileResponse = { +type UsernamePublicProfileResponse = { username: string; displayName: string; bio: string | null; @@ -67,7 +66,6 @@ type UsernameCardPublicProfileResponse = { links: PublicProfileCardLink[] } -// Represents a CardLink record with the joined PlatformLink relation interface CardLinkWithPlatform { id: string; displayOrder: number; @@ -102,24 +100,26 @@ export async function publicRoutes(app: FastifyInstance) { // Try to extract viewer from Authorization header (soft auth) let viewerId: string | null = null; + let isSelfView = false; try { if (request.headers.authorization) { const decoded = (await request.jwtVerify()) as { id?: string }; - viewerId = decoded?.id ?? null; - } else { - viewerId = null; // Unauthenticated viewer + if (decoded?.id === user.id) { + isSelfView = true; + } else { + viewerId = decoded?.id ?? null; + } } } catch { // Ignored if invalid token } // Don't track if the owner is viewing their own profile - if (viewerId && viewerId !== user.id) { - // Background view tracking + if (!isSelfView && viewerId !== user.id) { app.prisma.cardView.create({ data: { ownerId: user.id, - cardId: null, // this is a profile view, not a card view + cardId: null, viewerId, viewerIp: request.ip || null, viewerAgent: request.headers['user-agent'] || null, @@ -176,7 +176,6 @@ export async function publicRoutes(app: FastifyInstance) { } return response; - }); /** @@ -230,7 +229,6 @@ export async function publicRoutes(app: FastifyInstance) { } return response; - }); // ─── Public Card View ─── @@ -272,16 +270,21 @@ export async function publicRoutes(app: FastifyInstance) { } let viewerId: string | null = null; + let isSelfView = false; try { if (request.headers.authorization) { - const decoded = (await request.jwtVerify()) as { id?: string }; - viewerId = decoded?.id ?? null; + const decoded = await request.jwtVerify() as any; + if (decoded?.id === user.id) { + isSelfView = true; + } else { + viewerId = decoded.id; + } } - } catch { + } catch (e) { // Ignored if invalid token } - if (viewerId && viewerId !== user.id) { + if (!isSelfView && viewerId !== user.id) { app.prisma.cardView.create({ data: { ownerId: user.id, @@ -294,7 +297,6 @@ export async function publicRoutes(app: FastifyInstance) { }).catch((err: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(err)}`)); } - const response: UsernameCardPublicProfileResponse = { title: card.title, owner: { @@ -323,7 +325,7 @@ export async function publicRoutes(app: FastifyInstance) { app.get('/:username/qr', { config: { rateLimit: { - max: 50, // Lower limit for QR generation as it's more resource intensive + max: 50, timeWindow: '1 minute' } } as FastifyContextConfig @@ -346,7 +348,6 @@ export async function publicRoutes(app: FastifyInstance) { }); } - // Verify user exists const user = await app.prisma.user.findUnique({ where: { username }, }); @@ -376,4 +377,4 @@ export async function publicRoutes(app: FastifyInstance) { return reply.status(500).send({ error: 'QR code generation failed' }); } }); -} +} \ No newline at end of file