Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions apps/backend/src/routes/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,7 +18,7 @@ type PublicProfileLink = {
followed?: boolean;
}

type UsernamePublicProfileResponse = {
type UsernamePublicProfileResponse = {
username: string;
displayName: string;
bio: string | null;
Expand Down Expand Up @@ -67,7 +66,6 @@ type UsernameCardPublicProfileResponse = {
links: PublicProfileCardLink[]
}

// Represents a CardLink record with the joined PlatformLink relation
interface CardLinkWithPlatform {
id: string;
displayOrder: number;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -176,7 +176,6 @@ export async function publicRoutes(app: FastifyInstance) {
}

return response;

});

/**
Expand Down Expand Up @@ -230,7 +229,6 @@ export async function publicRoutes(app: FastifyInstance) {
}

return response;

});

// ─── Public Card View ───
Expand Down Expand Up @@ -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,
Expand All @@ -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: {
Expand Down Expand Up @@ -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
Expand All @@ -346,7 +348,6 @@ export async function publicRoutes(app: FastifyInstance) {
});
}

// Verify user exists
const user = await app.prisma.user.findUnique({
where: { username },
});
Expand Down Expand Up @@ -376,4 +377,4 @@ export async function publicRoutes(app: FastifyInstance) {
return reply.status(500).send({ error: 'QR code generation failed' });
}
});
}
}