From ff3a7ddbf0b739c4c8ff7fa94023522190cc5a1c Mon Sep 17 00:00:00 2001 From: Marek Maslowski Date: Fri, 24 Apr 2026 18:50:22 +0200 Subject: [PATCH] adding permission check for creation and delete of links --- .../Planner/DayPlanSidebar.test.tsx | 26 ++++++++++++++- .../src/components/Planner/DayPlanSidebar.tsx | 25 +++++++++------ client/src/i18n/translations/en.ts | 1 + server/src/routes/trips.ts | 9 +++++- server/tests/integration/trips.test.ts | 32 +++++++++++++++++++ 5 files changed, 81 insertions(+), 12 deletions(-) diff --git a/client/src/components/Planner/DayPlanSidebar.test.tsx b/client/src/components/Planner/DayPlanSidebar.test.tsx index 9cd1b432..114d800e 100644 --- a/client/src/components/Planner/DayPlanSidebar.test.tsx +++ b/client/src/components/Planner/DayPlanSidebar.test.tsx @@ -24,6 +24,10 @@ const mockDayNotesState = vi.hoisted(() => ({ moveNote: vi.fn(), })) +const mockPermissionsState = vi.hoisted(() => ({ + canDo: true, +})) + // ── Module mocks ──────────────────────────────────────────────────────────── vi.mock('../../api/client', async (importOriginal) => { @@ -79,7 +83,7 @@ vi.mock('../../store/permissionsStore', async (importOriginal) => { const actual = await importOriginal() as any return { ...actual, - useCanDo: () => () => true, + useCanDo: () => () => mockPermissionsState.canDo, } }) @@ -125,6 +129,7 @@ beforeEach(() => { // Reset mutable day-notes state mockDayNotesState.noteUi = {} mockDayNotesState.dayNotes = {} + mockPermissionsState.canDo = true seedStore(useAuthStore, { user: buildUser(), isAuthenticated: true }) seedStore(useTripStore, { trip: buildTrip({ id: 1 }) }) seedStore(useSettingsStore, { settings: { time_format: '24h', temperature_unit: 'celsius' } } as any) @@ -1007,6 +1012,25 @@ describe('DayPlanSidebar', () => { revokeObjURL.mockRestore() }) + it('FE-PLANNER-DAYPLAN-099: ICS dialog hides delete link button without share_manage permission', async () => { + const user = userEvent.setup() + mockPermissionsState.canDo = false + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ token: 'existing-token' }), + } as any) + + render() + await user.click(screen.getByText('ICS').closest('button')!) + + await waitFor(() => expect(fetchSpy).toHaveBeenCalledWith('/api/trips/1/subscribe.ics', expect.any(Object))) + expect(await screen.findByDisplayValue(`${window.location.origin}/api/shared/existing-token/calendar.ics`)).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Delete link' })).not.toBeInTheDocument() + + fetchSpy.mockRestore() + }) + // ── openAddNote button click ────────────────────────────────────────── it('FE-PLANNER-DAYPLAN-059: clicking Add Note button calls openAddNote', async () => { diff --git a/client/src/components/Planner/DayPlanSidebar.tsx b/client/src/components/Planner/DayPlanSidebar.tsx index 4edcd1d9..9d2f8d07 100644 --- a/client/src/components/Planner/DayPlanSidebar.tsx +++ b/client/src/components/Planner/DayPlanSidebar.tsx @@ -225,6 +225,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({ const tripActions = useRef(useTripStore.getState()).current const can = useCanDo() const canEditDays = can('day_edit', trip) + const canManageShare = can('share_manage', trip) const { noteUi, setNoteUi, noteInputRef, dayNotes, openAddNote: _openAddNote, openEditNote: _openEditNote, cancelNote, saveNote, deleteNote: _deleteNote, moveNote: _moveNote } = useDayNotes(tripId) @@ -2270,23 +2271,27 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({ {icsCopied ? <> {t('common.copied')} : <> {t('common.copy')}} - + {canManageShare && ( + + )} ) : ( - )}