Skip to content

Commit 7a00738

Browse files
committed
fix(public): add missing return types to publicService functions
1 parent cddea77 commit 7a00738

2 files changed

Lines changed: 93 additions & 195 deletions

File tree

apps/backend/src/routes/public.ts

Lines changed: 62 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,21 @@
1-
import { getErrorMessage } from '../utils/error.util.js';
1+
import * as publicService from '../services/publicService.js';
22
import { generateQRBuffer, generateQRSvg } from '../utils/qr.js';
33

4-
import type { PlatformLink } from '@devcard/shared';
54
import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
65

7-
type PublicProfileLink = {
8-
id: string;
9-
platform: string;
10-
username: string;
11-
url: string;
12-
displayOrder: number;
13-
followed?: boolean;
14-
}
15-
16-
type UsernamePublicProfileResponse = {
17-
username: string;
18-
displayName: string;
19-
bio: string | null;
20-
pronouns: string | null;
21-
role: string | null;
22-
company: string | null;
23-
avatarUrl: string | null;
24-
accentColor: string;
25-
links: PublicProfileLink[]
26-
}
27-
28-
type PublicProfileCardLink = {
29-
id: string;
30-
platform: string;
31-
username: string;
32-
url: string;
33-
followed?: boolean;
34-
}
35-
36-
type CardPublicProfileResponse = {
37-
id: string;
38-
title: string;
39-
owner: {
40-
username: string;
41-
displayName: string;
42-
bio: string | null;
43-
avatarUrl: string | null;
44-
accentColor: string;
45-
};
46-
links: PublicProfileCardLink[]
47-
}
48-
49-
type UsernameCardPublicProfileResponse = {
50-
title: string;
51-
owner: {
52-
username: string;
53-
displayName: string;
54-
bio: string | null;
55-
pronouns: string | null;
56-
role: string | null;
57-
company: string | null;
58-
avatarUrl: string | null;
59-
accentColor: string;
60-
};
61-
links: PublicProfileCardLink[]
62-
}
63-
64-
// Represents a CardLink record with the joined PlatformLink relation
65-
interface CardLinkWithPlatform {
66-
id: string;
67-
displayOrder: number;
68-
platformLink: PlatformLink;
69-
}
6+
// ── QR size bounds ────────────────────────────────────────────────────────────
7+
const MIN_QR_SIZE = 1;
8+
const MAX_QR_SIZE = 2048;
709

10+
// ── Cache constants ───────────────────────────────────────────────────────────
11+
const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60';
7112

7213
export async function publicRoutes(app: FastifyInstance): Promise<void> {
73-
// ─── Public Profile ───
14+
// ─── Public Profile ───────────────────────────────────────────────────────
15+
/**
16+
* GET /api/u/:username
17+
* Returns the public profile information for a user.
18+
*/
7419
app.get('/:username', {
7520
config: {
7621
rateLimit: {
@@ -81,69 +26,23 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
8126
}, async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => {
8227
const { username } = request.params;
8328

84-
// Try to extract viewer from Authorization header (soft auth).
29+
// Soft auth: extract viewer id if token present.
30+
// authenticatedUserId is used to detect self-views; viewerId is only set
31+
// for other authenticated users so the service knows who is viewing.
8532
let viewerId: string | null = null;
86-
let isSelfView = false;
33+
let authenticatedUserId: string | null = null;
8734
try {
8835
if (request.headers.authorization) {
8936
const decoded = (await request.jwtVerify()) as { id?: string };
90-
if (decoded?.id === user.id) {
91-
isSelfView = true;
92-
} else {
93-
viewerId = decoded?.id ?? null;
94-
}
95-
} else {
96-
viewerId = null;
37+
authenticatedUserId = decoded?.id ?? null;
38+
viewerId = authenticatedUserId;
9739
}
9840
} catch {
99-
// Ignored if invalid token
100-
}
101-
102-
// Don't track if the owner is viewing their own profile
103-
if (!isSelfView && viewerId !== user.id) {
104-
// Background view tracking
105-
app.prisma.cardView.create({
106-
data: {
107-
ownerId: user.id,
108-
cardId: null, // this is a profile view, not a card view
109-
viewerId,
110-
viewerIp: request.ip || null,
111-
viewerAgent: request.headers['user-agent'] || null,
112-
source: request.query?.source || 'link',
113-
},
114-
}).catch((err: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(err)}`));
115-
}
116-
117-
// Fetch viewer's successful follow logs for this profile's links
118-
let followedLinkIds: string[] = [];
119-
if (viewerId && user.platformLinks.length > 0) {
120-
const successfulFollows = await app.prisma.followLog.findMany({
121-
where: {
122-
followerId: viewerId,
123-
status: 'success',
124-
OR: user.platformLinks.map((link: PlatformLink) => ({
125-
platform: link.platform,
126-
targetUsername: link.username,
127-
})),
128-
},
129-
select: {
130-
platform: true,
131-
targetUsername: true,
132-
},
133-
});
134-
135-
followedLinkIds = user.platformLinks
136-
.filter((link: PlatformLink) =>
137-
successfulFollows.some((f: { platform: string; targetUsername: string }) =>
138-
f.platform === link.platform &&
139-
f.targetUsername.toLowerCase() === link.username.toLowerCase()
140-
)
141-
)
142-
.map((link: PlatformLink) => link.id);
41+
// ignored — treat as unauthenticated
14342
}
14443

14544
try {
146-
const result = await publicService.getPublicProfile(app, username, viewerId, request);
45+
const result = await publicService.getPublicProfile(app, username, viewerId, request, authenticatedUserId);
14746
if (!result) {
14847
return reply.status(404).send({ error: 'User not found' });
14948
}
@@ -169,44 +68,36 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
16968
timeWindow: '1 minute'
17069
}
17170
} as FastifyContextConfig
172-
}, async (request: FastifyRequest<{ Params: { cardId: string } }>, _reply: FastifyReply) => {
71+
}, async (request: FastifyRequest<{ Params: { cardId: string } }>, reply: FastifyReply) => {
17372
const { cardId } = request.params;
17473

175-
const card = await app.prisma.card.findUnique({
176-
where: { id: cardId },
177-
include: {
178-
user: true,
179-
cardLinks: {
180-
include: { platformLink: true },
181-
orderBy: { displayOrder: 'asc' },
74+
try {
75+
const card = await publicService.getCardById(app, cardId);
76+
if (!card) {
77+
return reply.status(404).send({ error: 'Card not found' });
78+
}
79+
const response = {
80+
id: card.id,
81+
title: card.title,
82+
owner: {
83+
username: card.user.username,
84+
displayName: card.user.displayName,
85+
bio: card.user.bio,
86+
avatarUrl: card.user.avatarUrl,
87+
accentColor: card.user.accentColor,
18288
},
183-
},
184-
});
185-
186-
if (!card) {
187-
return _reply.status(404).send({ error: 'Card not found' });
188-
}
189-
190-
const response: CardPublicProfileResponse = {
191-
id: card.id,
192-
title: card.title,
193-
owner: {
194-
username: card.user.username,
195-
displayName: card.user.displayName,
196-
bio: card.user.bio,
197-
avatarUrl: card.user.avatarUrl,
198-
accentColor: card.user.accentColor,
199-
},
200-
links: card.cardLinks.map((cl: CardLinkWithPlatform) => ({
201-
id: cl.platformLink.id,
202-
platform: cl.platformLink.platform,
203-
username: cl.platformLink.username,
204-
url: cl.platformLink.url,
205-
})),
89+
links: card.cardLinks.map((cl: any) => ({
90+
id: cl.platformLink.id,
91+
platform: cl.platformLink.platform,
92+
username: cl.platformLink.username,
93+
url: cl.platformLink.url,
94+
})),
95+
};
96+
return response;
97+
} catch (err: unknown) {
98+
app.log.error({ err }, 'Failed to fetch shared card');
99+
return reply.status(500).send({ error: 'Internal server error' });
206100
}
207-
208-
return response;
209-
210101
});
211102

212103
// ─── Public Card View ─────────────────────────────────────────────────────
@@ -226,38 +117,30 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
226117
const { username, cardId } = request.params;
227118

228119
let viewerId: string | null = null;
229-
let isSelfView = false;
120+
let authenticatedUserId: string | null = null;
230121
try {
231122
if (request.headers.authorization) {
232123
const decoded = (await request.jwtVerify()) as { id?: string };
233-
if (decoded?.id === user.id) {
234-
isSelfView = true;
235-
} else {
236-
viewerId = decoded?.id ?? null;
237-
}
124+
authenticatedUserId = decoded?.id ?? null;
125+
viewerId = authenticatedUserId;
238126
}
239-
} catch (_e) {
240-
// Ignored if invalid token
127+
} catch {
128+
// ignored
241129
}
242130

243-
if (!isSelfView && viewerId !== user.id) {
244-
app.prisma.cardView.create({
245-
data: {
246-
ownerId: user.id,
247-
cardId: card.id,
248-
viewerId,
249-
viewerIp: request.ip || null,
250-
viewerAgent: request.headers['user-agent'] || null,
251-
source: request.query?.source || 'qr',
252-
},
253-
}).catch((err: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(err)}`));
131+
try {
132+
const result = await publicService.getUserCard(app, username, cardId, viewerId, request, authenticatedUserId);
133+
if (result.notFound) {
134+
return reply.status(404).send({ error: 'User or card not found' });
135+
}
136+
return result.data;
137+
} catch (err: unknown) {
138+
app.log.error({ err }, 'Failed to fetch user card');
139+
return reply.status(500).send({ error: 'Internal server error' });
254140
}
255141
});
256142

257143
// ─── QR Session ──────────────────────────────────────────────────────────
258-
// Returns a short-lived signed JWT encoding the public profile snapshot.
259-
// Intended for native apps to generate QR codes that remain scannable when
260-
// the device has no live network connectivity (offline QR mode, spec §5.9).
261144
app.get('/:username/qr-session', {
262145
config: {
263146
rateLimit: {
@@ -269,7 +152,7 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
269152
const { username } = request.params;
270153

271154
try {
272-
const result = await publicService.getPublicProfile(app, username, null, request);
155+
const result = await publicService.getPublicProfile(app, username, null, request, null);
273156
if (!result) {
274157
return reply.status(404).send({ error: 'User not found' });
275158
}
@@ -290,7 +173,7 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
290173
app.get('/:username/qr', {
291174
config: {
292175
rateLimit: {
293-
max: 50, // Lower limit for QR generation as it's more resource intensive
176+
max: 50,
294177
timeWindow: '1 minute'
295178
}
296179
} as FastifyContextConfig
@@ -301,9 +184,6 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
301184
const { username } = request.params;
302185
const format = (request.query as any).format || 'png';
303186

304-
// Parse and validate size before touching the DB or allocating any buffers.
305-
// parseInt safely handles non-numeric strings (returns NaN) and ignores any
306-
// trailing fractional part, so '400.9' → 400 which is within bounds.
307187
const rawSize = (request.query as any).size;
308188
const size = rawSize !== undefined ? parseInt(rawSize, 10) : 400;
309189

@@ -313,7 +193,6 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
313193
});
314194
}
315195

316-
// Verify user exists
317196
const user = await app.prisma.user.findUnique({
318197
where: { username },
319198
});
@@ -343,4 +222,4 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
343222
return reply.status(500).send({ error: 'QR code generation failed' });
344223
}
345224
});
346-
}
225+
}

0 commit comments

Comments
 (0)