From 6155007303b2e2b0f3c3d3c314e78b228738dc6c Mon Sep 17 00:00:00 2001 From: Yachika Sharma Date: Fri, 19 Jun 2026 14:56:27 +0530 Subject: [PATCH 1/2] refactor(auth): replace any-cast authenticate fallback with typed preHandler - Remove (app as any).authenticate fallback chains in cards.ts, connect.ts, event.ts, nfc.ts - Use preHandler: [(req, reply) => app.authenticate(req, reply)] consistently - Fix TS generic types on route methods to resolve tsc errors Closes #554 --- apps/backend/src/routes/cards.ts | 17 ++++--------- apps/backend/src/routes/connect.ts | 36 ++++------------------------ apps/backend/src/routes/event.ts | 38 ++++++++++-------------------- apps/backend/src/routes/nfc.ts | 32 +++++-------------------- 4 files changed, 27 insertions(+), 96 deletions(-) diff --git a/apps/backend/src/routes/cards.ts b/apps/backend/src/routes/cards.ts index e5f98762..dff28789 100644 --- a/apps/backend/src/routes/cards.ts +++ b/apps/backend/src/routes/cards.ts @@ -49,16 +49,9 @@ interface _CardWithLinks { } export async function cardRoutes(app: FastifyInstance): Promise { - app.addHook('preHandler', async (request, reply) => { - const server = request.server as any; - if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } - if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (_e) { reply.status(401).send({ error: 'Unauthorized' }) } - }); - // ─── List Cards ─── - app.get('/', async (request: FastifyRequest, reply: FastifyReply): Promise => { + app.get('/',{preHandler: [(req, reply) => app.authenticate(req, reply)] }, async (request: FastifyRequest, reply: FastifyReply): Promise => { const userId = (request.user as { id: string }).id; try { return await cardService.listCards(app, userId) @@ -69,7 +62,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { // ─── Create Card ─── - app.post('/', async (request: FastifyRequest<{ Body: CreateCardBody }>, reply: FastifyReply): Promise => { + app.post<{ Body: CreateCardBody }>('/', { preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise => { const userId = (request.user as { id: string }).id; const parsed = createCardSchema.safeParse(request.body); @@ -88,7 +81,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { // ─── Update Card ─── - app.put('/:id', async (request: FastifyRequest<{ Params: CardParams; Body: UpdateCardBody }>, reply: FastifyReply): Promise => { + app.put<{ Params: CardParams; Body: UpdateCardBody }>('/:id', {preHandler: [(req, reply) => app.authenticate(req, reply)] }, async (request, reply): Promise => { const userId = (request.user as { id: string }).id; const { id } = request.params; @@ -106,7 +99,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { // ─── Delete Card ─── - app.delete('/:id', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise => { + app.delete<{ Params: CardParams }>('/:id', { preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise => { const userId = (request.user as { id: string }).id; const { id } = request.params; @@ -129,7 +122,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { // ─── Set Default Card ─── - app.put('/:id/default', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise => { + app.put<{ Params: CardParams }>('/:id/default', {preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise => { const userId = (request.user as { id: string }).id; const { id } = request.params; diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index e7e1a2be..6614b980 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -29,14 +29,7 @@ interface ParsedOAuthState { export async function connectRoutes(app: FastifyInstance): Promise { // ─── Status ─── - app.get('/status', { - preHandler: [async (request, reply) => { - const server = request.server as any; - if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } - if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } - }], - }, async (request: FastifyRequest, _reply: FastifyReply) => { + app.get('/status',{preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request: FastifyRequest, _reply: FastifyReply) => { const userId = (request.user as any).id; const tokens = await app.prisma.oAuthToken.findMany({ @@ -49,14 +42,7 @@ export async function connectRoutes(app: FastifyInstance): Promise { // ─── GitHub Connect ─── - app.get('/github', { - preHandler: [async (request, reply) => { - const server = request.server as any; - if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } - if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } - }], - }, async (request: FastifyRequest, reply: FastifyReply) => { + app.get('/github',{preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; const nonce = generateState(); @@ -173,14 +159,7 @@ export async function connectRoutes(app: FastifyInstance): Promise { } }); - app.get('/github/autodiscover', { - preHandler: [async (request, reply) => { - const server = request.server as any; - if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } - if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } - }], - }, async (request: FastifyRequest, reply: FastifyReply) => { + app.get('/github/autodiscover',{preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; const cacheKey = `github:autodiscover:${userId}`; @@ -262,14 +241,7 @@ export async function connectRoutes(app: FastifyInstance): Promise { // ─── Disconnect ─── - app.delete('/:platform', { - preHandler: [async (request, reply) => { - const server = request.server as any; - if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } - if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } - }], - }, async (request: FastifyRequest<{ Params: { platform: string } }>, reply: FastifyReply) => { + app.delete<{ Params: { platform: string } }>('/:platform', {preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply) => { const userId = (request.user as any).id; const { platform } = request.params; diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts index 4d4ee2d9..d058f131 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -1,8 +1,7 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { createEventSchema, joinEventSchema} from '../validations/event.validation'; - import {generateUniqueSlug} from '../utils/slug' +import { createEventSchema} from '../validations/event.validation'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; type EventDetails = { id: string; @@ -57,21 +56,8 @@ type EventWithAttendees = { }[]; } -export async function eventRoutes(app:FastifyInstance) { - app.post('/', { preHandler: [async (request, reply) => { - const server = request.server as any; - if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } - if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } - }] }, async (request: FastifyRequest<{ - Body: { - name: string, - description?: string, - startDate: string, - location: string, - endDate: string, - isPublic?: boolean - }}>, reply: FastifyReply) => { +export async function eventRoutes(app:FastifyInstance): Promise { + app.post<{Body: { name: string; description?: string; startDate: string; location: string; endDate: string; isPublic?: boolean; }; }>('/', {preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply) => { const userId = (request.user as any).id; const parsed = createEventSchema.safeParse(request.body); if(!parsed.success){ @@ -80,8 +66,8 @@ export async function eventRoutes(app:FastifyInstance) { const {name, description, startDate, endDate, isPublic ,location} = parsed.data - let finalSlug = await generateUniqueSlug(name, async(slug) => { - const existing = await app.prisma.event.findUnique({where: {slug : slug}}) + const finalSlug = await generateUniqueSlug(name, async(slug) => { + const existing = await app.prisma.event.findUnique({where: {slug}}) return !!existing }) @@ -95,7 +81,7 @@ export async function eventRoutes(app:FastifyInstance) { name, description, slug: finalSlug, - location: location, + location, startDate: startDateObj, endDate: endDateObj, isPublic: isPublic ?? true, @@ -104,7 +90,7 @@ export async function eventRoutes(app:FastifyInstance) { }) return reply.status(201).send(newEvent); - } catch (error) { + } catch (_error) { app.log.error('Failed to create event'); return reply.status(500).send({error: 'Failed to create event'}) } @@ -153,7 +139,7 @@ export async function eventRoutes(app:FastifyInstance) { return response; }) - app.post('/:slug/join', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + app.post<{ Params: { slug: string } }>('/:slug/join', {preHandler: [(req, reply) => app.authenticate(req, reply)]}, async(request, reply) => { const userId = (request.user as any).id; const paramsSlug = request.params.slug; @@ -171,7 +157,7 @@ export async function eventRoutes(app:FastifyInstance) { await app.prisma.eventAttendee.create({ data: { eventId: event.id, - userId: userId, + userId, joinedAt: new Date() } }) @@ -187,7 +173,7 @@ export async function eventRoutes(app:FastifyInstance) { }) - app.delete('/:slug/leave', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + app.delete<{Params: {slug: string}}>('/:slug/leave',{preHandler: [(req, reply) => app.authenticate(req, reply)]}, async(request, reply) => { const userId = (request.user as any).id; const paramsSlug = request.params.slug; @@ -205,7 +191,7 @@ export async function eventRoutes(app:FastifyInstance) { await app.prisma.eventAttendee.delete({ where: { userId_eventId: { - userId: userId, + userId, eventId: event.id } } diff --git a/apps/backend/src/routes/nfc.ts b/apps/backend/src/routes/nfc.ts index 5cf13f0c..c182f3cd 100644 --- a/apps/backend/src/routes/nfc.ts +++ b/apps/backend/src/routes/nfc.ts @@ -1,6 +1,7 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { z } from 'zod'; +import type { FastifyInstance } from 'fastify'; + type NfcPayloadResponse = { type: 'URI'; payload: string; @@ -10,33 +11,12 @@ const nfcQuerySchema = z.object({ card: z.string().uuid('Invalid card ID format').optional(), }); -export async function nfcRoutes(app: FastifyInstance) { - app.addHook('preHandler', async (request, reply) => { - const server = request.server as any; - if (typeof server?.authenticate === 'function') { - await server.authenticate(request, reply); - return; - } - if (typeof (app as any).authenticate === 'function') { - await (app as any).authenticate(request, reply); - return; - } - try { - await request.jwtVerify(); - } catch (e) { - reply.status(401).send({ error: 'Unauthorized' }); - } - }); - +export async function nfcRoutes(app: FastifyInstance) : Promise { // GET /api/nfc/payload — returns NDEF URI payload for user's default DevCard URL // GET /api/nfc/payload?card= — returns payload for a specific card - app.get( - '/payload', - async ( - request: FastifyRequest<{ Querystring: { card?: string } }>, - reply: FastifyReply - ) => { - const userId = (request.user as any).id; + app.get<{ Querystring: { card?: string } }>( + '/payload',{preHandler: [(req, reply) => app.authenticate(req, reply)]}, + async (request,reply ) => { const userId = (request.user as any).id; // Validate query params with Zod const parseResult = nfcQuerySchema.safeParse(request.query); From e794a64a0b8bc426200be6dff6dc19d08aa1f18d Mon Sep 17 00:00:00 2001 From: Yachika Sharma Date: Fri, 19 Jun 2026 15:20:25 +0530 Subject: [PATCH 2/2] fix(events): add missing organizerId to GET /api/events/:slug response and fix incomplete test mocks - Add organizerId field to EventDetails type and response object in event.ts - Uncomment organizerId assertion in event.test.ts - Add organizer object to GET /api/events/:slug mocks - Add _count object to GET /api/events/:slug/attendees mocks (was causing 500 crashes in 6 tests) Closes #606 --- apps/backend/src/__tests__/event.test.ts | 23 ++++++++++++++++++++--- apps/backend/src/routes/event.ts | 2 ++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 44806af1..5bcfca5f 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -3,6 +3,7 @@ import Fastify, { FastifyInstance } from 'fastify'; import { PrismaClient } from '@prisma/client'; import { eventRoutes } from '../routes/event'; + // ─── Shared mock data ──────────────────────────────────────────────────────── const MOCK_USER_ID = 'user-uuid-001'; @@ -77,6 +78,14 @@ async function buildApp(): Promise { app.decorateRequest('jwtVerify', function () { return mockJwtVerify(); }); + app.decorate('authenticate', async function (request, reply) { + try { + const payload = await request.jwtVerify(); + if (payload) { request.user = payload as typeof request.user; } + } catch { + return reply.status(401).send({ error: 'Unauthorized' }); + } + }); // Register with the same prefix used in production (app.ts) so that // tests exercise routes at their real paths — /api/events, /api/events/:slug, etc. @@ -251,6 +260,7 @@ describe('Events API', () => { it('200 — returns event info with attendee count', async () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + organizer: { username: 'johndoe', displayName: 'John Doe' }, _count: { attendees: 42 }, }); @@ -258,14 +268,14 @@ describe('Events API', () => { method: 'GET', url: '/api/events/devcard-conf-2025', }); - + console.log(JSON.stringify(res.json(), null, 2)); expect(res.statusCode).toBe(200); const body = res.json(); expect(body.slug).toBe('devcard-conf-2025'); expect(body.attendeesCount).toBe(42); expect(body.location).toBe('San Francisco, CA'); // organizerId is exposed (public info) - expect(body.organizerId).toBe(MOCK_USER_ID); + expect(body.organizerId).toBe(MOCK_USER_ID); }); it('404 — returns 404 for unknown slug', async () => { @@ -285,6 +295,7 @@ describe('Events API', () => { mockJwtVerify.mockRejectedValue(new Error('Should not be called')); prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, + organizer: { username: 'johndoe', displayName: 'John Doe' }, _count: { attendees: 0 }, }); @@ -495,13 +506,14 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: attendeeRows, + _count: { attendees: 2 }, }); const res = await app.inject({ method: 'GET', url: '/api/events/devcard-conf-2025/attendees', }); - + expect(res.statusCode).toBe(200); const body = res.json(); @@ -523,6 +535,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)], + _count: { attendees: 1 }, }); const res = await app.inject({ @@ -545,6 +558,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -561,6 +575,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -577,6 +592,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -594,6 +610,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [makeAttendeeRow(MOCK_USER_PROFILE)], + _count: { attendees: 1 }, }); const res = await app.inject({ diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts index d058f131..b2c398c9 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -9,6 +9,7 @@ type EventDetails = { slug: string; location: string; description: string | null; + organizerId: string; organizerUsername: string; organizerDisplayName: string; startDate: Date; @@ -128,6 +129,7 @@ export async function eventRoutes(app:FastifyInstance): Promise { slug: details.slug, description: details.description, location: details.location, + organizerId: details.organizerId, organizerUsername: details.organizer.username, organizerDisplayName: details.organizer.displayName, startDate: details.startDate,