@@ -4,16 +4,10 @@ import { generateQRBuffer, generateQRSvg } from '../utils/qr.js';
44import type { FastifyContextConfig , FastifyInstance , FastifyRequest , FastifyReply } from 'fastify' ;
55
66// ── QR size bounds ────────────────────────────────────────────────────────────
7- // Enforced before any DB query or image allocation. Values outside this range
8- // are rejected with 400 so a single unauthenticated request cannot trigger an
9- // unbounded memory allocation in the QR rasteriser.
107const MIN_QR_SIZE = 1 ;
118const MAX_QR_SIZE = 2048 ;
129
1310// ── Cache constants ───────────────────────────────────────────────────────────
14- // Public profile cache TTL matches the Cache-Control max-age (5 minutes).
15- // The QR session JWT TTL is 10 minutes so an offline scan remains valid well
16- // beyond the HTTP cache window.
1711const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60' ;
1812
1913export async function publicRoutes ( app : FastifyInstance ) : Promise < void > {
@@ -32,21 +26,23 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
3226 } , async ( request : FastifyRequest < { Params : { username : string } } > , reply : FastifyReply ) => {
3327 const { username } = request . params ;
3428
35- // 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.
3632 let viewerId : string | null = null ;
33+ let authenticatedUserId : string | null = null ;
3734 try {
3835 if ( request . headers . authorization ) {
3936 const decoded = ( await request . jwtVerify ( ) ) as { id ?: string } ;
40- viewerId = decoded ?. id ?? null ;
41- } else {
42- viewerId = null ;
37+ authenticatedUserId = decoded ?. id ?? null ;
38+ viewerId = authenticatedUserId ;
4339 }
4440 } catch {
45- // ignored
41+ // ignored — treat as unauthenticated
4642 }
4743
4844 try {
49- const result = await publicService . getPublicProfile ( app , username , viewerId , request ) ;
45+ const result = await publicService . getPublicProfile ( app , username , viewerId , request , authenticatedUserId ) ;
5046 if ( ! result ) {
5147 return reply . status ( 404 ) . send ( { error : 'User not found' } ) ;
5248 }
@@ -121,17 +117,19 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
121117 const { username, cardId } = request . params ;
122118
123119 let viewerId : string | null = null ;
120+ let authenticatedUserId : string | null = null ;
124121 try {
125122 if ( request . headers . authorization ) {
126123 const decoded = ( await request . jwtVerify ( ) ) as { id ?: string } ;
127- viewerId = decoded ?. id ?? null ;
124+ authenticatedUserId = decoded ?. id ?? null ;
125+ viewerId = authenticatedUserId ;
128126 }
129127 } catch {
130128 // ignored
131129 }
132130
133131 try {
134- const result = await publicService . getUserCard ( app , username , cardId , viewerId , request ) ;
132+ const result = await publicService . getUserCard ( app , username , cardId , viewerId , request , authenticatedUserId ) ;
135133 if ( result . notFound ) {
136134 return reply . status ( 404 ) . send ( { error : 'User or card not found' } ) ;
137135 }
@@ -143,9 +141,6 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
143141 } ) ;
144142
145143 // ─── QR Session ──────────────────────────────────────────────────────────
146- // Returns a short-lived signed JWT encoding the public profile snapshot.
147- // Intended for native apps to generate QR codes that remain scannable when
148- // the device has no live network connectivity (offline QR mode, spec §5.9).
149144 app . get ( '/:username/qr-session' , {
150145 config : {
151146 rateLimit : {
@@ -157,7 +152,7 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
157152 const { username } = request . params ;
158153
159154 try {
160- const result = await publicService . getPublicProfile ( app , username , null , request ) ;
155+ const result = await publicService . getPublicProfile ( app , username , null , request , null ) ;
161156 if ( ! result ) {
162157 return reply . status ( 404 ) . send ( { error : 'User not found' } ) ;
163158 }
@@ -178,7 +173,7 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
178173 app . get ( '/:username/qr' , {
179174 config : {
180175 rateLimit : {
181- max : 50 , // Lower limit for QR generation as it's more resource intensive
176+ max : 50 ,
182177 timeWindow : '1 minute'
183178 }
184179 } as FastifyContextConfig
@@ -189,9 +184,6 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
189184 const { username } = request . params ;
190185 const format = ( request . query as any ) . format || 'png' ;
191186
192- // Parse and validate size before touching the DB or allocating any buffers.
193- // parseInt safely handles non-numeric strings (returns NaN) and ignores any
194- // trailing fractional part, so '400.9' → 400 which is within bounds.
195187 const rawSize = ( request . query as any ) . size ;
196188 const size = rawSize !== undefined ? parseInt ( rawSize , 10 ) : 400 ;
197189
@@ -201,7 +193,6 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
201193 } ) ;
202194 }
203195
204- // Verify user exists
205196 const user = await app . prisma . user . findUnique ( {
206197 where : { username } ,
207198 } ) ;
@@ -231,4 +222,4 @@ export async function publicRoutes(app: FastifyInstance): Promise<void> {
231222 return reply . status ( 500 ) . send ( { error : 'QR code generation failed' } ) ;
232223 }
233224 } ) ;
234- }
225+ }
0 commit comments