Skip to content

Commit f392fe2

Browse files
fix: apply showMemberMiddleware to flowsheet write routes
The middleware was imported but never applied, allowing any authenticated DJ to modify flowsheet entries regardless of show membership. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 108d1bf commit f392fe2

4 files changed

Lines changed: 99 additions & 11 deletions

File tree

apps/backend/app.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { library_route } from './routes/library.route.js';
1010
import { schedule_route } from './routes/schedule.route.js';
1111
import { events_route } from './routes/events.route.js';
1212
import { request_line_route } from './routes/requestLine.route.js';
13-
import { showMemberMiddleware } from './middleware/checkShowMember.js';
1413
import { activeShow } from './middleware/checkActiveShow.js';
1514
import errorHandler from './middleware/errorHandler.js';
1615
import { requirePermissions } from '@wxyc/authentication';

apps/backend/middleware/checkShowMember.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ import { RequestHandler } from 'express';
22
import { getDJsInCurrentShow } from '../services/flowsheet.service.js';
33

44
export const showMemberMiddleware: RequestHandler = async (req, res, next) => {
5-
const show_djs = await getDJsInCurrentShow();
6-
// Get user ID from JWT - check both req.auth (from better-auth middleware) and res.locals (legacy)
7-
const user_id = req.auth?.id || req.auth?.sub || res.locals.decodedJWT?.id || res.locals.decodedJWT?.userId;
8-
const dj_in_show = show_djs.filter((dj) => {
9-
return dj.id === user_id;
10-
}).length;
5+
try {
6+
const show_djs = await getDJsInCurrentShow();
7+
const user_id = req.auth?.id || req.auth?.sub || res.locals.decodedJWT?.id || res.locals.decodedJWT?.userId;
8+
const dj_in_show = show_djs.some((dj) => dj.id === user_id);
119

12-
if (dj_in_show > 0) {
13-
next();
14-
} else {
15-
res.status(400).json({ message: 'Bad Request: DJ not a member of show' });
10+
if (dj_in_show) {
11+
next();
12+
} else {
13+
res.status(400).json({ message: 'Bad Request: DJ not a member of show' });
14+
}
15+
} catch {
16+
res.status(500).json({ message: 'Internal server error checking show membership' });
1617
}
1718
};

apps/backend/routes/flowsheet.route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Router } from 'express';
33
import * as flowsheetController from '../controllers/flowsheet.controller';
44
import { flowsheetMirror } from '../middleware/legacy/flowsheet.mirror';
55
import { conditionalGet } from '../middleware/conditionalGet';
6+
import { showMemberMiddleware } from '../middleware/checkShowMember';
67

78
export const flowsheet_route = Router();
89

@@ -11,20 +12,23 @@ flowsheet_route.get('/', conditionalGet, flowsheetMirror.getEntries, flowsheetCo
1112
flowsheet_route.post(
1213
'/',
1314
requirePermissions({ flowsheet: ['write'] }),
15+
showMemberMiddleware,
1416
flowsheetMirror.addEntry,
1517
flowsheetController.addEntry
1618
);
1719

1820
flowsheet_route.patch(
1921
'/',
2022
requirePermissions({ flowsheet: ['write'] }),
23+
showMemberMiddleware,
2124
flowsheetMirror.updateEntry,
2225
flowsheetController.updateEntry
2326
);
2427

2528
flowsheet_route.delete(
2629
'/',
2730
requirePermissions({ flowsheet: ['write'] }),
31+
showMemberMiddleware,
2832
flowsheetMirror.deleteEntry,
2933
flowsheetController.deleteEntry
3034
);
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { jest } from '@jest/globals';
2+
import type { Request, Response, NextFunction } from 'express';
3+
4+
const mockGetDJsInCurrentShow = jest.fn<() => Promise<{ id: string }[]>>();
5+
6+
jest.mock('../../../apps/backend/services/flowsheet.service', () => ({
7+
getDJsInCurrentShow: mockGetDJsInCurrentShow,
8+
}));
9+
10+
import { showMemberMiddleware } from '../../../apps/backend/middleware/checkShowMember';
11+
12+
function createMockReqResNext(userId: string) {
13+
const req = {
14+
auth: { id: userId },
15+
} as unknown as Request;
16+
17+
const res = {
18+
status: jest.fn().mockReturnThis(),
19+
json: jest.fn().mockReturnThis(),
20+
locals: {},
21+
} as unknown as Response;
22+
23+
const next = jest.fn() as unknown as NextFunction;
24+
25+
return { req, res, next };
26+
}
27+
28+
describe('showMemberMiddleware', () => {
29+
it('rejects a DJ who is not in the current show', async () => {
30+
mockGetDJsInCurrentShow.mockResolvedValue([
31+
{ id: 'dj-alice' },
32+
{ id: 'dj-bob' },
33+
]);
34+
35+
const { req, res, next } = createMockReqResNext('dj-charlie');
36+
37+
await showMemberMiddleware(req, res, next);
38+
39+
expect(res.status).toHaveBeenCalledWith(400);
40+
expect(res.json).toHaveBeenCalledWith({
41+
message: 'Bad Request: DJ not a member of show',
42+
});
43+
expect(next).not.toHaveBeenCalled();
44+
});
45+
46+
it('allows a DJ who is in the current show', async () => {
47+
mockGetDJsInCurrentShow.mockResolvedValue([
48+
{ id: 'dj-alice' },
49+
{ id: 'dj-bob' },
50+
]);
51+
52+
const { req, res, next } = createMockReqResNext('dj-alice');
53+
54+
await showMemberMiddleware(req, res, next);
55+
56+
expect(next).toHaveBeenCalled();
57+
expect(res.status).not.toHaveBeenCalled();
58+
});
59+
60+
it('rejects when there are no DJs in the current show', async () => {
61+
mockGetDJsInCurrentShow.mockResolvedValue([]);
62+
63+
const { req, res, next } = createMockReqResNext('dj-alice');
64+
65+
await showMemberMiddleware(req, res, next);
66+
67+
expect(res.status).toHaveBeenCalledWith(400);
68+
expect(next).not.toHaveBeenCalled();
69+
});
70+
71+
it('returns 500 when getDJsInCurrentShow throws', async () => {
72+
mockGetDJsInCurrentShow.mockRejectedValue(new Error('DB connection lost'));
73+
74+
const { req, res, next } = createMockReqResNext('dj-alice');
75+
76+
await showMemberMiddleware(req, res, next);
77+
78+
expect(res.status).toHaveBeenCalledWith(500);
79+
expect(res.json).toHaveBeenCalledWith({
80+
message: 'Internal server error checking show membership',
81+
});
82+
expect(next).not.toHaveBeenCalled();
83+
});
84+
});

0 commit comments

Comments
 (0)