Skip to content

Commit 5a12e68

Browse files
rajesh-puripandaShantKhatri
authored andcommitted
style(backend): clean up lint violations and enforce explicit typings in public route (Dev-Card#447)
1 parent ab0cb32 commit 5a12e68

1 file changed

Lines changed: 79 additions & 125 deletions

File tree

apps/backend/src/routes/public.ts

Lines changed: 79 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
1+
import * as publicService from '../services/publicService';
22
import { generateQRBuffer, generateQRSvg } from '../utils/qr.js';
3-
import type { PlatformLink } from '@devcard/shared';
4-
import { getErrorMessage } from '../utils/error.util.js';
5-
import * as publicService from '../services/publicService'
63

4+
import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
75

86
// ── QR size bounds ────────────────────────────────────────────────────────────
97
// Enforced before any DB query or image allocation. Values outside this range
@@ -16,83 +14,11 @@ const MAX_QR_SIZE = 2048;
1614
// Public profile cache TTL matches the Cache-Control max-age (5 minutes).
1715
// The QR session JWT TTL is 10 minutes so an offline scan remains valid well
1816
// beyond the HTTP cache window.
19-
const PROFILE_CACHE_TTL = 300; // seconds (5 minutes)
2017
const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60';
2118

22-
type PublicProfileLink = {
23-
id: string;
24-
platform: string;
25-
username: string;
26-
url: string;
27-
displayOrder: number;
28-
followed?: boolean;
29-
}
30-
31-
type UsernamePublicProfileResponse = {
32-
username: string;
33-
displayName: string;
34-
bio: string | null;
35-
pronouns: string | null;
36-
role: string | null;
37-
company: string | null;
38-
avatarUrl: string | null;
39-
accentColor: string;
40-
links: PublicProfileLink[]
41-
}
42-
43-
type PublicProfileCardLink = {
44-
id: string;
45-
platform: string;
46-
username: string;
47-
url: string;
48-
followed?: boolean;
49-
}
50-
51-
type CardPublicProfileResponse = {
52-
id: string;
53-
title: string;
54-
owner: {
55-
username: string;
56-
displayName: string;
57-
bio: string | null;
58-
avatarUrl: string | null;
59-
accentColor: string;
60-
};
61-
links: PublicProfileCardLink[]
62-
}
63-
64-
type UsernameCardPublicProfileResponse = {
65-
title: string;
66-
owner: {
67-
username: string;
68-
displayName: string;
69-
bio: string | null;
70-
pronouns: string | null;
71-
role: string | null;
72-
company: string | null;
73-
avatarUrl: string | null;
74-
accentColor: string;
75-
};
76-
links: PublicProfileCardLink[]
77-
}
78-
79-
// Represents a CardLink record with the joined PlatformLink relation
80-
interface CardLinkWithPlatform {
81-
id: string;
82-
displayOrder: number;
83-
platformLink: PlatformLink;
84-
}
85-
86-
// ── Internal Redis cache shape ────────────────────────────────────────────────
87-
// Extends the public response with the owner's DB id so that background view
88-
// tracking can still fire on cache-HIT requests without an extra DB read.
89-
type CachedProfileEntry = UsernamePublicProfileResponse & { _userId: string };
90-
91-
92-
export async function publicRoutes(app: FastifyInstance) {
19+
export async function publicRoutes(app: FastifyInstance): Promise<void> {
9320
// ─── Public Profile ───────────────────────────────────────────────────────
94-
// ─── Public Profile ───
95-
/**
21+
/**
9622
* GET /api/u/:username
9723
* Returns the public profile information for a user.
9824
*/
@@ -105,29 +31,30 @@ export async function publicRoutes(app: FastifyInstance) {
10531
},
10632
}, async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => {
10733
const { username } = request.params;
108-
const cacheKey = `profile:${username}`;
10934

11035
// Try to extract viewer from Authorization header (soft auth).
111-
let viewerId: string | null = null
36+
let viewerId: string | null = null;
11237
try {
11338
if (request.headers.authorization) {
114-
const decoded = (await request.jwtVerify()) as { id?: string }
115-
viewerId = decoded?.id ?? null
39+
const decoded = (await request.jwtVerify()) as { id?: string };
40+
viewerId = decoded?.id ?? null;
11641
} else {
117-
viewerId = null
42+
viewerId = null;
11843
}
11944
} catch {
12045
// ignored
12146
}
12247

12348
try {
124-
const result = await publicService.getPublicProfile(app, username, viewerId, request)
125-
if (!result) return reply.status(404).send({ error: 'User not found' })
126-
reply.header('X-Cache', result.cached ? 'HIT' : 'MISS').header('Cache-Control', CACHE_CONTROL_HEADER)
127-
return result.data
128-
} catch (err: any) {
129-
app.log.error({ err }, 'Failed to fetch public profile')
130-
return reply.status(500).send({ error: 'Internal server error' })
49+
const result = await publicService.getPublicProfile(app, username, viewerId, request);
50+
if (!result) {
51+
return reply.status(404).send({ error: 'User not found' });
52+
}
53+
reply.header('X-Cache', result.cached ? 'HIT' : 'MISS').header('Cache-Control', CACHE_CONTROL_HEADER);
54+
return result.data;
55+
} catch (err: unknown) {
56+
app.log.error({ err }, 'Failed to fetch public profile');
57+
return reply.status(500).send({ error: 'Internal server error' });
13158
}
13259
});
13360

@@ -149,18 +76,35 @@ export async function publicRoutes(app: FastifyInstance) {
14976
const { cardId } = request.params;
15077

15178
try {
152-
const card = await publicService.getCardById(app, cardId)
153-
if (!card) return reply.status(404).send({ error: 'Card not found' })
154-
const response = { id: card.id, title: card.title, owner: { username: card.user.username, displayName: card.user.displayName, bio: card.user.bio, avatarUrl: card.user.avatarUrl, accentColor: card.user.accentColor }, links: card.cardLinks.map((cl: any) => ({ id: cl.platformLink.id, platform: cl.platformLink.platform, username: cl.platformLink.username, url: cl.platformLink.url })) }
155-
return response
156-
} catch (err: any) {
157-
app.log.error({ err }, 'Failed to fetch shared card')
158-
return reply.status(500).send({ error: 'Internal server error' })
79+
const card = await publicService.getCardById(app, cardId);
80+
if (!card) {
81+
return reply.status(404).send({ error: 'Card not found' });
82+
}
83+
const response = {
84+
id: card.id,
85+
title: card.title,
86+
owner: {
87+
username: card.user.username,
88+
displayName: card.user.displayName,
89+
bio: card.user.bio,
90+
avatarUrl: card.user.avatarUrl,
91+
accentColor: card.user.accentColor,
92+
},
93+
links: card.cardLinks.map((cl: any) => ({
94+
id: cl.platformLink.id,
95+
platform: cl.platformLink.platform,
96+
username: cl.platformLink.username,
97+
url: cl.platformLink.url,
98+
})),
99+
};
100+
return response;
101+
} catch (err: unknown) {
102+
app.log.error({ err }, 'Failed to fetch shared card');
103+
return reply.status(500).send({ error: 'Internal server error' });
159104
}
160105
});
161106

162107
// ─── Public Card View ─────────────────────────────────────────────────────
163-
// ─── Public Card View ───
164108
/**
165109
* GET /api/u/:username/card/:cardId
166110
* Returns full owner profile + specific card data.
@@ -176,23 +120,25 @@ export async function publicRoutes(app: FastifyInstance) {
176120
}, async (request: FastifyRequest<{ Params: { username: string; cardId: string } }>, reply: FastifyReply) => {
177121
const { username, cardId } = request.params;
178122

179-
let viewerId: string | null = null
123+
let viewerId: string | null = null;
180124
try {
181125
if (request.headers.authorization) {
182-
const decoded = (await request.jwtVerify()) as { id?: string }
183-
viewerId = decoded?.id ?? null
126+
const decoded = (await request.jwtVerify()) as { id?: string };
127+
viewerId = decoded?.id ?? null;
184128
}
185129
} catch {
186130
// ignored
187131
}
188132

189133
try {
190-
const result = await publicService.getUserCard(app, username, cardId, viewerId, request)
191-
if (result.notFound) return reply.status(404).send({ error: 'User or card not found' })
192-
return result.data
193-
} catch (err: any) {
194-
app.log.error({ err }, 'Failed to fetch user card')
195-
return reply.status(500).send({ error: 'Internal server error' })
134+
const result = await publicService.getUserCard(app, username, cardId, viewerId, request);
135+
if (result.notFound) {
136+
return reply.status(404).send({ error: 'User or card not found' });
137+
}
138+
return result.data;
139+
} catch (err: unknown) {
140+
app.log.error({ err }, 'Failed to fetch user card');
141+
return reply.status(500).send({ error: 'Internal server error' });
196142
}
197143
});
198144

@@ -209,20 +155,21 @@ export async function publicRoutes(app: FastifyInstance) {
209155
} as FastifyContextConfig
210156
}, async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => {
211157
const { username } = request.params;
212-
const cacheKey = `profile:${username}`;
213158

214159
try {
215-
const result = await publicService.getPublicProfile(app, username, null, request)
216-
if (!result) return reply.status(404).send({ error: 'User not found' })
217-
const snapshot = result.data
218-
const expiresIn = 600
219-
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString()
220-
const token = app.jwt.sign({ profile: snapshot, sub: username }, { expiresIn: '10m' })
221-
reply.header('Cache-Control', CACHE_CONTROL_HEADER)
222-
return { token, tokenType: 'JWT', expiresIn, expiresAt }
223-
} catch (err: any) {
224-
app.log.error({ err }, 'Failed to create qr-session')
225-
return reply.status(500).send({ error: 'Internal server error' })
160+
const result = await publicService.getPublicProfile(app, username, null, request);
161+
if (!result) {
162+
return reply.status(404).send({ error: 'User not found' });
163+
}
164+
const snapshot = result.data;
165+
const expiresIn = 600;
166+
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
167+
const token = app.jwt.sign({ profile: snapshot, sub: username }, { expiresIn: '10m' });
168+
reply.header('Cache-Control', CACHE_CONTROL_HEADER);
169+
return { token, tokenType: 'JWT', expiresIn, expiresAt };
170+
} catch (err: unknown) {
171+
app.log.error({ err }, 'Failed to create qr-session');
172+
return reply.status(500).send({ error: 'Internal server error' });
226173
}
227174
});
228175

@@ -267,14 +214,21 @@ export async function publicRoutes(app: FastifyInstance) {
267214

268215
try {
269216
if (format === 'svg') {
270-
const svg = await generateQRSvg(profileUrl, { width: size })
271-
return reply.header('Content-Type', 'image/svg+xml').header('Content-Disposition', `inline; filename="devcard-${username}.svg"`).send(svg)
217+
const svg = await generateQRSvg(profileUrl, { width: size });
218+
return reply
219+
.header('Content-Type', 'image/svg+xml')
220+
.header('Content-Disposition', `inline; filename="devcard-${username}.svg"`)
221+
.send(svg);
272222
}
273-
const png = await generateQRBuffer(profileUrl, { width: size })
274-
return reply.header('Content-Type', 'image/png').header('Content-Disposition', `inline; filename="devcard-${username}.png"`).send(png)
223+
224+
const png = await generateQRBuffer(profileUrl, { width: size });
225+
return reply
226+
.header('Content-Type', 'image/png')
227+
.header('Content-Disposition', `inline; filename="devcard-${username}.png"`)
228+
.send(png);
275229
} catch (error) {
276-
app.log.error({ error, username, size, format }, 'QR generation failed')
277-
return reply.status(500).send({ error: 'QR code generation failed' })
230+
app.log.error({ error, username, size, format }, 'QR generation failed');
231+
return reply.status(500).send({ error: 'QR code generation failed' });
278232
}
279233
});
280234
}

0 commit comments

Comments
 (0)