From 1fbc19ad4f67df83e1d99a6b6f520171c91231a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rnyi=20M=C3=A1rk?= Date: Tue, 31 Mar 2026 23:45:11 +0200 Subject: [PATCH] fix: add missing permission checks to file routes and map context menu - Add checkPermission to 6 unprotected file endpoints (star, restore, permanent delete, empty trash, link, unlink) - Gate map right-click place creation with place_edit permission - Use file_upload permission for collab note file uploads --- client/src/pages/TripPlannerPage.tsx | 1 + server/src/routes/collab.ts | 4 ++-- server/src/routes/files.ts | 12 ++++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx index 4de73c1..eff748f 100644 --- a/client/src/pages/TripPlannerPage.tsx +++ b/client/src/pages/TripPlannerPage.tsx @@ -169,6 +169,7 @@ export default function TripPlannerPage(): React.ReactElement | null { }, []) const handleMapContextMenu = useCallback(async (e) => { + if (!can('place_edit', trip)) return e.originalEvent?.preventDefault() const { lat, lng } = e.latlng setPrefillCoords({ lat, lng }) diff --git a/server/src/routes/collab.ts b/server/src/routes/collab.ts index 18e51b6..98be75a 100644 --- a/server/src/routes/collab.ts +++ b/server/src/routes/collab.ts @@ -206,8 +206,8 @@ router.post('/notes/:id/files', authenticate, noteUpload.single('file'), (req: R const { tripId, id } = req.params; const access = verifyTripAccess(Number(tripId), authReq.user.id); if (!access) return res.status(404).json({ error: 'Trip not found' }); - if (!checkPermission('collab_edit', authReq.user.role, access.user_id, authReq.user.id, access.user_id !== authReq.user.id)) - return res.status(403).json({ error: 'No permission' }); + if (!checkPermission('file_upload', authReq.user.role, access.user_id, authReq.user.id, access.user_id !== authReq.user.id)) + return res.status(403).json({ error: 'No permission to upload files' }); if (!req.file) return res.status(400).json({ error: 'No file uploaded' }); const note = db.prepare('SELECT id FROM collab_notes WHERE id = ? AND trip_id = ?').get(id, tripId); diff --git a/server/src/routes/files.ts b/server/src/routes/files.ts index fdf2912..6da1b0a 100644 --- a/server/src/routes/files.ts +++ b/server/src/routes/files.ts @@ -226,6 +226,8 @@ router.patch('/:id/star', authenticate, (req: Request, res: Response) => { const trip = verifyTripOwnership(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); + if (!checkPermission('file_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id)) + return res.status(403).json({ error: 'No permission' }); const file = db.prepare('SELECT * FROM trip_files WHERE id = ? AND trip_id = ?').get(id, tripId) as TripFile | undefined; if (!file) return res.status(404).json({ error: 'File not found' }); @@ -263,6 +265,8 @@ router.post('/:id/restore', authenticate, (req: Request, res: Response) => { const trip = verifyTripOwnership(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); + if (!checkPermission('file_delete', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id)) + return res.status(403).json({ error: 'No permission' }); const file = db.prepare('SELECT * FROM trip_files WHERE id = ? AND trip_id = ? AND deleted_at IS NOT NULL').get(id, tripId) as TripFile | undefined; if (!file) return res.status(404).json({ error: 'File not found in trash' }); @@ -281,6 +285,8 @@ router.delete('/:id/permanent', authenticate, (req: Request, res: Response) => { const trip = verifyTripOwnership(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); + if (!checkPermission('file_delete', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id)) + return res.status(403).json({ error: 'No permission' }); const file = db.prepare('SELECT * FROM trip_files WHERE id = ? AND trip_id = ? AND deleted_at IS NOT NULL').get(id, tripId) as TripFile | undefined; if (!file) return res.status(404).json({ error: 'File not found in trash' }); @@ -302,6 +308,8 @@ router.delete('/trash/empty', authenticate, (req: Request, res: Response) => { const trip = verifyTripOwnership(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); + if (!checkPermission('file_delete', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id)) + return res.status(403).json({ error: 'No permission' }); const trashed = db.prepare('SELECT * FROM trip_files WHERE trip_id = ? AND deleted_at IS NOT NULL').all(tripId) as TripFile[]; for (const file of trashed) { @@ -323,6 +331,8 @@ router.post('/:id/link', authenticate, (req: Request, res: Response) => { const trip = verifyTripOwnership(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); + if (!checkPermission('file_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id)) + return res.status(403).json({ error: 'No permission' }); const file = db.prepare('SELECT * FROM trip_files WHERE id = ? AND trip_id = ?').get(id, tripId); if (!file) return res.status(404).json({ error: 'File not found' }); @@ -346,6 +356,8 @@ router.delete('/:id/link/:linkId', authenticate, (req: Request, res: Response) = const trip = verifyTripOwnership(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); + if (!checkPermission('file_edit', authReq.user.role, trip.user_id, authReq.user.id, trip.user_id !== authReq.user.id)) + return res.status(403).json({ error: 'No permission' }); db.prepare('DELETE FROM file_links WHERE id = ? AND file_id = ?').run(linkId, id); res.json({ success: true });