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
This commit is contained in:
Paulo Castellano 2026-06-22 16:41:27 -03:00
parent 0eb6f4df24
commit 8b52ea2082
4 changed files with 24 additions and 5 deletions

View file

@ -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);
}

View file

@ -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);
};

View file

@ -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<InstanceType<typeof ConfirmDeleteModal> | null>(null);
const { canCreatePost } = useWorkspaceRole();
const handleDelete = (post: Post) => {
deleteModal.value?.open({
url: destroyPost.url(post.id),

View file

@ -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();
});