Skip to content
Closed
Show file tree
Hide file tree
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
23 changes: 20 additions & 3 deletions apps/backend/src/__tests__/event.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';

Check failure on line 2 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

`fastify` import should occur before import of `vitest`

Check failure on line 2 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Imports "FastifyInstance" are only used as type
import { PrismaClient } from '@prisma/client';

Check failure on line 3 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

`@prisma/client` import should occur before import of `vitest`

Check failure on line 3 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

There should be at least one empty line between import groups

Check failure on line 3 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

All imports in the declaration are only used as types. Use `import type`
import { eventRoutes } from '../routes/event';


// ─── Shared mock data ────────────────────────────────────────────────────────

const MOCK_USER_ID = 'user-uuid-001';
Expand Down Expand Up @@ -64,7 +65,7 @@
//
// This mirrors the real app setup without touching a real DB or real JWT keys.

let mockJwtVerify = vi.fn();

Check failure on line 68 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

'mockJwtVerify' is never reassigned. Use 'const' instead

async function buildApp(): Promise<FastifyInstance> {
const app = Fastify({ logger: false });
Expand All @@ -77,6 +78,14 @@
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.
Expand All @@ -93,7 +102,7 @@
}

/** Injects a POST /api/events request */
async function createEvent(

Check warning on line 105 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
app: FastifyInstance,
body: Record<string, unknown>,
authenticated = true,
Expand Down Expand Up @@ -251,21 +260,22 @@
it('200 — returns event info with attendee count', async () => {
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
organizer: { username: 'johndoe', displayName: 'John Doe' },
_count: { attendees: 42 },
});

const res = await app.inject({
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 () => {
Expand All @@ -285,6 +295,7 @@
mockJwtVerify.mockRejectedValue(new Error('Should not be called'));
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
organizer: { username: 'johndoe', displayName: 'John Doe' },
_count: { attendees: 0 },
});

Expand Down Expand Up @@ -474,7 +485,7 @@

describe('GET /api/events/:slug/attendees — paginated attendee list', () => {
/** Builds a raw EventAttendee row as Prisma returns it (with nested user) */
function makeAttendeeRow(

Check warning on line 488 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
profile: typeof MOCK_USER_PROFILE | typeof MOCK_OTHER_USER_PROFILE,
) {
return {
Expand All @@ -495,13 +506,14 @@
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();

Expand All @@ -523,6 +535,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)],
_count: { attendees: 1 },
});

const res = await app.inject({
Expand All @@ -545,6 +558,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -561,6 +575,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -577,6 +592,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [],
_count: { attendees: 0 },
});

const res = await app.inject({
Expand All @@ -594,6 +610,7 @@
prismaMock.event.findUnique.mockResolvedValue({
...MOCK_EVENT,
attendees: [makeAttendeeRow(MOCK_USER_PROFILE)],
_count: { attendees: 1 },
});

const res = await app.inject({
Expand Down
17 changes: 5 additions & 12 deletions apps/backend/src/routes/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,9 @@
}

export async function cardRoutes(app: FastifyInstance): Promise<void> {
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<CardResponse[] | void> => {
app.get('/',{preHandler: [(req, reply) => app.authenticate(req, reply)] }, async (request: FastifyRequest, reply: FastifyReply): Promise<CardResponse[] | void> => {
const userId = (request.user as { id: string }).id;
try {
return await cardService.listCards(app, userId)
Expand All @@ -69,7 +62,7 @@

// ─── Create Card ───

app.post('/', async (request: FastifyRequest<{ Body: CreateCardBody }>, reply: FastifyReply): Promise<Card | void> => {
app.post<{ Body: CreateCardBody }>('/', { preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise<Card | void> => {

Check warning on line 65 in apps/backend/src/routes/cards.ts

View workflow job for this annotation

GitHub Actions / backend-ci

'Card' is an 'error' type that acts as 'any' and overrides all other types in this union type
const userId = (request.user as { id: string }).id;
const parsed = createCardSchema.safeParse(request.body);

Expand All @@ -88,7 +81,7 @@

// ─── Update Card ───

app.put('/:id', async (request: FastifyRequest<{ Params: CardParams; Body: UpdateCardBody }>, reply: FastifyReply): Promise<CardResponse> => {
app.put<{ Params: CardParams; Body: UpdateCardBody }>('/:id', {preHandler: [(req, reply) => app.authenticate(req, reply)] }, async (request, reply): Promise<CardResponse> => {
const userId = (request.user as { id: string }).id;
const { id } = request.params;

Expand All @@ -106,7 +99,7 @@

// ─── Delete Card ───

app.delete('/:id', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise<void> => {
app.delete<{ Params: CardParams }>('/:id', { preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise<void> => {
const userId = (request.user as { id: string }).id;
const { id } = request.params;

Expand All @@ -129,7 +122,7 @@

// ─── Set Default Card ───

app.put('/:id/default', async (request: FastifyRequest<{ Params: CardParams }>, reply: FastifyReply): Promise<object | void> => {
app.put<{ Params: CardParams }>('/:id/default', {preHandler: [(req, reply) => app.authenticate(req, reply)]}, async (request, reply): Promise<object | void> => {
const userId = (request.user as { id: string }).id;
const { id } = request.params;

Expand Down
36 changes: 4 additions & 32 deletions apps/backend/src/routes/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,7 @@ interface ParsedOAuthState {
export async function connectRoutes(app: FastifyInstance): Promise<void> {
// ─── 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({
Expand All @@ -49,14 +42,7 @@ export async function connectRoutes(app: FastifyInstance): Promise<void> {

// ─── 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();

Expand Down Expand Up @@ -173,14 +159,7 @@ export async function connectRoutes(app: FastifyInstance): Promise<void> {
}
});

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}`;

Expand Down Expand Up @@ -262,14 +241,7 @@ export async function connectRoutes(app: FastifyInstance): Promise<void> {

// ─── 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;

Expand Down
42 changes: 15 additions & 27 deletions apps/backend/src/routes/event.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
import { createEventSchema, joinEventSchema} from '../validations/event.validation.js';

import {generateUniqueSlug} from '../utils/slug.js'
import {generateUniqueSlug} from '../utils/slug'
import { createEventSchema} from '../validations/event.validation';

import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

type EventDetails = {
id: string;
name: string;
slug: string;
location: string;
description: string | null;
organizerId: string;
organizerUsername: string;
organizerDisplayName: string;
startDate: Date;
Expand Down Expand Up @@ -57,21 +57,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<void> {
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){
Expand All @@ -80,8 +67,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
})
Expand All @@ -95,7 +82,7 @@ export async function eventRoutes(app:FastifyInstance) {
name,
description,
slug: finalSlug,
location: location,
location,
startDate: startDateObj,
endDate: endDateObj,
isPublic: isPublic ?? true,
Expand All @@ -104,7 +91,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'})
}
Expand Down Expand Up @@ -142,6 +129,7 @@ export async function eventRoutes(app:FastifyInstance) {
slug: details.slug,
description: details.description,
location: details.location,
organizerId: details.organizerId,
organizerUsername: details.organizer.username,
organizerDisplayName: details.organizer.displayName,
startDate: details.startDate,
Expand All @@ -153,7 +141,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;

Expand All @@ -171,7 +159,7 @@ export async function eventRoutes(app:FastifyInstance) {
await app.prisma.eventAttendee.create({
data: {
eventId: event.id,
userId: userId,
userId,
joinedAt: new Date()
}
})
Expand All @@ -187,7 +175,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;

Expand All @@ -205,7 +193,7 @@ export async function eventRoutes(app:FastifyInstance) {
await app.prisma.eventAttendee.delete({
where: {
userId_eventId: {
userId: userId,
userId,
eventId: event.id
}
}
Expand Down
32 changes: 6 additions & 26 deletions apps/backend/src/routes/nfc.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<void> {
// GET /api/nfc/payload — returns NDEF URI payload for user's default DevCard URL
// GET /api/nfc/payload?card=<cardId> — 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);
Expand Down
Loading