From 8b52ea2082422885bddcf57720d263be6c085cb7 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 22 Jun 2026 16:41:27 -0300 Subject: [PATCH] fix(permissions): viewers open posts read-only instead of hitting the editor A viewer clicking a draft/scheduled post landed on the editor route (authorizes update) and got a 403. The post list/calendar routed every editable post to the edit page, and PostController@show redirected draft/scheduled posts to the editor for everyone. - show only redirects to the editor when the user can update the post; viewers get the read-only Show page - posts index + calendar route to show (not edit) when the user cannot create posts - cover viewer-sees-show, member-redirected-to-editor, and viewer-403-on-direct-edit in WorkspaceRolePermissionsTest --- app/Http/Controllers/App/PostController.php | 3 ++- resources/js/pages/posts/Calendar.vue | 2 +- resources/js/pages/posts/Index.vue | 6 +++--- .../WorkspaceRolePermissionsTest.php | 18 ++++++++++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/App/PostController.php b/app/Http/Controllers/App/PostController.php index 65576b9c..5dff497b 100644 --- a/app/Http/Controllers/App/PostController.php +++ b/app/Http/Controllers/App/PostController.php @@ -212,7 +212,8 @@ public function show(Request $request, Post $post): Response|RedirectResponse $this->authorize('view', $post); - if (in_array($post->status, [PostStatus::Draft, PostStatus::Scheduled], true)) { + if ($request->user()->can('update', $post) + && in_array($post->status, [PostStatus::Draft, PostStatus::Scheduled], true)) { return redirect()->route('app.posts.edit', $post); } diff --git a/resources/js/pages/posts/Calendar.vue b/resources/js/pages/posts/Calendar.vue index 4e2c04e6..162b45c2 100644 --- a/resources/js/pages/posts/Calendar.vue +++ b/resources/js/pages/posts/Calendar.vue @@ -237,7 +237,7 @@ const getStatusColor = (status: string): string => { const EDITABLE_STATUSES: readonly string[] = [PostStatus.Draft, PostStatus.Scheduled]; const getPostUrl = (post: Post): string => { - return EDITABLE_STATUSES.includes(post.status) + return canCreatePost.value && EDITABLE_STATUSES.includes(post.status) ? editPost.url(post.id) : showPost.url(post.id); }; diff --git a/resources/js/pages/posts/Index.vue b/resources/js/pages/posts/Index.vue index ab7ec15e..26f23204 100644 --- a/resources/js/pages/posts/Index.vue +++ b/resources/js/pages/posts/Index.vue @@ -143,13 +143,13 @@ const DELETABLE_STATUSES: readonly string[] = [PostStatus.Draft, PostStatus.Sche const canEdit = (post: Post): boolean => EDITABLE_STATUSES.includes(post.status); const canDelete = (post: Post): boolean => DELETABLE_STATUSES.includes(post.status); +const { canCreatePost } = useWorkspaceRole(); + const postUrl = (post: Post): string => - canEdit(post) ? editPost.url(post.id) : showPost.url(post.id); + canCreatePost.value && canEdit(post) ? editPost.url(post.id) : showPost.url(post.id); const deleteModal = ref | null>(null); -const { canCreatePost } = useWorkspaceRole(); - const handleDelete = (post: Post) => { deleteModal.value?.open({ url: destroyPost.url(post.id), diff --git a/tests/Feature/Permissions/WorkspaceRolePermissionsTest.php b/tests/Feature/Permissions/WorkspaceRolePermissionsTest.php index 296d7ae6..16165df3 100644 --- a/tests/Feature/Permissions/WorkspaceRolePermissionsTest.php +++ b/tests/Feature/Permissions/WorkspaceRolePermissionsTest.php @@ -60,3 +60,21 @@ 'user_id' => $this->viewer->id, ]); }); + +test('a viewer opening a draft post sees the read-only page instead of the editor', function () { + $this->actingAs($this->viewer) + ->get(route('app.posts.show', $this->post)) + ->assertOk(); +}); + +test('a member opening a draft post is redirected to the editor', function () { + $this->actingAs($this->member) + ->get(route('app.posts.show', $this->post)) + ->assertRedirect(route('app.posts.edit', $this->post)); +}); + +test('a viewer cannot open the post editor directly', function () { + $this->actingAs($this->viewer) + ->get(route('app.posts.edit', $this->post)) + ->assertForbidden(); +});