diff --git a/apps/backend/app.ts b/apps/backend/app.ts index e6abbba2..c2b3ddc2 100644 --- a/apps/backend/app.ts +++ b/apps/backend/app.ts @@ -12,7 +12,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 { requestIdMiddleware } from './middleware/requestId.js'; diff --git a/apps/backend/controllers/flowsheet.controller.ts b/apps/backend/controllers/flowsheet.controller.ts index 9ac09f27..7b892fcd 100644 --- a/apps/backend/controllers/flowsheet.controller.ts +++ b/apps/backend/controllers/flowsheet.controller.ts @@ -354,11 +354,8 @@ export const leaveShow: RequestHandler = asy res.status(404).json({ message: 'Bad Request: No active show session found.' }); } else { try { - // Catch case where DJ is not in show, but attempting to hit this endpoint - const show_djs = await flowsheet_service.getDJsInCurrentShow(); - if (!show_djs.map((dj) => dj.id).includes(req.body.dj_id)) { - res.status(400).json({ message: 'Bad Request: DJ not in current show' }); - } else if (req.body.dj_id === currentShow.primary_dj_id) { + // Show membership is verified by showMemberMiddleware on the route + if (req.body.dj_id === currentShow.primary_dj_id) { const finalizedShow: Show = await flowsheet_service.endShow(currentShow); res.status(200).json(finalizedShow); } else { diff --git a/apps/backend/middleware/checkShowMember.ts b/apps/backend/middleware/checkShowMember.ts index 8d07269f..703d885e 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 38385dc5..bcf8c7f3 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 ); @@ -32,6 +36,7 @@ flowsheet_route.delete( flowsheet_route.patch( '/play-order', requirePermissions({ flowsheet: ['write'] }), + showMemberMiddleware, /*flowsheetMirror.changeOrder,*/ flowsheetController.changeOrder ); @@ -48,6 +53,7 @@ flowsheet_route.post( flowsheet_route.post( '/end', requirePermissions({ flowsheet: ['write'] }), + showMemberMiddleware, flowsheetMirror.endShow, flowsheetController.leaveShow ); diff --git a/apps/backend/services/flowsheet.service.ts b/apps/backend/services/flowsheet.service.ts index 3dafb34d..e25293f9 100644 --- a/apps/backend/services/flowsheet.service.ts +++ b/apps/backend/services/flowsheet.service.ts @@ -1,4 +1,5 @@ import { sql, desc, eq, and, lte, gte, inArray } from 'drizzle-orm'; +import WxycError from '../utils/error.js'; import { db, FSEntry, @@ -419,7 +420,7 @@ export const leaveShow = async (dj_id: string, currentShow: Show): Promise 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', () => { + 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(); + }); +});