diff --git a/apps/backend/app.ts b/apps/backend/app.ts index e06f510..38feb40 100644 --- a/apps/backend/app.ts +++ b/apps/backend/app.ts @@ -10,7 +10,6 @@ import { library_route } from './routes/library.route.js'; import { schedule_route } from './routes/schedule.route.js'; import { events_route } from './routes/events.route.js'; import { request_line_route } from './routes/requestLine.route.js'; -import { showMemberMiddleware } from './middleware/checkShowMember.js'; import { activeShow } from './middleware/checkActiveShow.js'; import errorHandler from './middleware/errorHandler.js'; import { requirePermissions } from '@wxyc/authentication'; diff --git a/apps/backend/middleware/checkShowMember.ts b/apps/backend/middleware/checkShowMember.ts index 8d07269..703d885 100644 --- a/apps/backend/middleware/checkShowMember.ts +++ b/apps/backend/middleware/checkShowMember.ts @@ -2,16 +2,21 @@ import { RequestHandler } from 'express'; import { getDJsInCurrentShow } from '../services/flowsheet.service.js'; export const showMemberMiddleware: RequestHandler = async (req, res, next) => { - const show_djs = await getDJsInCurrentShow(); - // Get user ID from JWT - check both req.auth (from better-auth middleware) and res.locals (legacy) - const user_id = req.auth?.id || req.auth?.sub || res.locals.decodedJWT?.id || res.locals.decodedJWT?.userId; - const dj_in_show = show_djs.filter((dj) => { - return dj.id === user_id; - }).length; + if (process.env.AUTH_BYPASS === 'true') { + return next(); + } + + try { + const show_djs = await getDJsInCurrentShow(); + const user_id = req.auth?.id || req.auth?.sub || res.locals.decodedJWT?.id || res.locals.decodedJWT?.userId; + const dj_in_show = show_djs.some((dj) => dj.id === user_id); - if (dj_in_show > 0) { - next(); - } else { - res.status(400).json({ message: 'Bad Request: DJ not a member of show' }); + if (dj_in_show) { + next(); + } else { + res.status(400).json({ message: 'Bad Request: DJ not a member of show' }); + } + } catch { + res.status(500).json({ message: 'Internal server error checking show membership' }); } }; diff --git a/apps/backend/routes/flowsheet.route.ts b/apps/backend/routes/flowsheet.route.ts index 38385dc..07f968e 100644 --- a/apps/backend/routes/flowsheet.route.ts +++ b/apps/backend/routes/flowsheet.route.ts @@ -3,6 +3,7 @@ import { Router } from 'express'; import * as flowsheetController from '../controllers/flowsheet.controller'; import { flowsheetMirror } from '../middleware/legacy/flowsheet.mirror'; import { conditionalGet } from '../middleware/conditionalGet'; +import { showMemberMiddleware } from '../middleware/checkShowMember'; export const flowsheet_route = Router(); @@ -11,6 +12,7 @@ flowsheet_route.get('/', conditionalGet, flowsheetMirror.getEntries, flowsheetCo flowsheet_route.post( '/', requirePermissions({ flowsheet: ['write'] }), + showMemberMiddleware, flowsheetMirror.addEntry, flowsheetController.addEntry ); @@ -18,6 +20,7 @@ flowsheet_route.post( flowsheet_route.patch( '/', requirePermissions({ flowsheet: ['write'] }), + showMemberMiddleware, flowsheetMirror.updateEntry, flowsheetController.updateEntry ); @@ -25,6 +28,7 @@ flowsheet_route.patch( flowsheet_route.delete( '/', requirePermissions({ flowsheet: ['write'] }), + showMemberMiddleware, flowsheetMirror.deleteEntry, flowsheetController.deleteEntry ); diff --git a/tests/unit/middleware/checkShowMember.test.ts b/tests/unit/middleware/checkShowMember.test.ts new file mode 100644 index 0000000..8e82599 --- /dev/null +++ b/tests/unit/middleware/checkShowMember.test.ts @@ -0,0 +1,106 @@ +import { jest } from '@jest/globals'; +import type { Request, Response, NextFunction } from 'express'; + +const mockGetDJsInCurrentShow = jest.fn<() => Promise<{ id: string }[]>>(); + +jest.mock('../../../apps/backend/services/flowsheet.service', () => ({ + getDJsInCurrentShow: mockGetDJsInCurrentShow, +})); + +import { showMemberMiddleware } from '../../../apps/backend/middleware/checkShowMember'; + +function createMockReqResNext(userId: string) { + const req = { + auth: { id: userId }, + } as unknown as Request; + + const statusMock = jest.fn().mockReturnThis(); + const jsonMock = jest.fn().mockReturnThis(); + const res = { + status: statusMock, + json: jsonMock, + locals: {}, + } as unknown as Response; + + const next = jest.fn() as unknown as NextFunction; + + return { req, res, next, statusMock, jsonMock }; +} + +describe('showMemberMiddleware', () => { + const originalAuthBypass = process.env.AUTH_BYPASS; + + beforeEach(() => { + delete process.env.AUTH_BYPASS; + }); + + afterAll(() => { + if (originalAuthBypass !== undefined) { + process.env.AUTH_BYPASS = originalAuthBypass; + } else { + delete process.env.AUTH_BYPASS; + } + }); + + it('skips show membership check when AUTH_BYPASS is true', async () => { + process.env.AUTH_BYPASS = 'true'; + + const { req, res, next, statusMock } = createMockReqResNext('unknown-user'); + + await showMemberMiddleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(statusMock).not.toHaveBeenCalled(); + expect(mockGetDJsInCurrentShow).not.toHaveBeenCalled(); + }); + + it('rejects a DJ who is not in the current show', async () => { + mockGetDJsInCurrentShow.mockResolvedValue([{ id: 'dj-alice' }, { id: 'dj-bob' }]); + + const { req, res, next, statusMock, jsonMock } = createMockReqResNext('dj-charlie'); + + await showMemberMiddleware(req, res, next); + + expect(statusMock).toHaveBeenCalledWith(400); + expect(jsonMock).toHaveBeenCalledWith({ + message: 'Bad Request: DJ not a member of show', + }); + expect(next).not.toHaveBeenCalled(); + }); + + it('allows a DJ who is in the current show', async () => { + mockGetDJsInCurrentShow.mockResolvedValue([{ id: 'dj-alice' }, { id: 'dj-bob' }]); + + const { req, res, next, statusMock } = createMockReqResNext('dj-alice'); + + await showMemberMiddleware(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(statusMock).not.toHaveBeenCalled(); + }); + + it('rejects when there are no DJs in the current show', async () => { + mockGetDJsInCurrentShow.mockResolvedValue([]); + + const { req, res, next, statusMock } = createMockReqResNext('dj-alice'); + + await showMemberMiddleware(req, res, next); + + expect(statusMock).toHaveBeenCalledWith(400); + expect(next).not.toHaveBeenCalled(); + }); + + it('returns 500 when getDJsInCurrentShow throws', async () => { + mockGetDJsInCurrentShow.mockRejectedValue(new Error('DB connection lost')); + + const { req, res, next, statusMock, jsonMock } = createMockReqResNext('dj-alice'); + + await showMemberMiddleware(req, res, next); + + expect(statusMock).toHaveBeenCalledWith(500); + expect(jsonMock).toHaveBeenCalledWith({ + message: 'Internal server error checking show membership', + }); + expect(next).not.toHaveBeenCalled(); + }); +});