From 3d3b03ee8294da2945a46d59609e26c8dc278093 Mon Sep 17 00:00:00 2001 From: Koda Date: Thu, 23 Apr 2026 20:56:39 +0000 Subject: [PATCH] Fix PR52 API author visibility and ticket-option user scoping --- .../BuildProjectTicketOptionsPayload.php | 1 + .../Api/ProjectTicketController.php | 21 +++++- tests/Feature/Api/TicketApiTest.php | 71 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) diff --git a/app/Actions/Projects/BuildProjectTicketOptionsPayload.php b/app/Actions/Projects/BuildProjectTicketOptionsPayload.php index a22aea1..7c6a307 100644 --- a/app/Actions/Projects/BuildProjectTicketOptionsPayload.php +++ b/app/Actions/Projects/BuildProjectTicketOptionsPayload.php @@ -38,6 +38,7 @@ class BuildProjectTicketOptionsPayload 'ticketTypes' => TicketTypeResource::collection($project->ticketTypes()->get())->resolve(), 'users' => UserResource::collection( User::query() + ->visibleToProject($project) ->orderBy('name') ->get(), )->resolve(), diff --git a/app/Http/Controllers/Api/ProjectTicketController.php b/app/Http/Controllers/Api/ProjectTicketController.php index 02409e5..337fda9 100644 --- a/app/Http/Controllers/Api/ProjectTicketController.php +++ b/app/Http/Controllers/Api/ProjectTicketController.php @@ -123,7 +123,8 @@ class ProjectTicketController extends ApiController ? "{$ticket->ticket_key} was updated." : 'No ticket changes were detected.'; - return TicketResource::make($ticket->fresh()->load([ + $canManageProject = $this->permissionService->allows($user, Permission::ProjectAdmin, $project); + $displayTicket = $ticket->fresh([ 'assignee', 'author', 'fieldValues.ticketFieldDefinition', @@ -132,7 +133,23 @@ class ProjectTicketController extends ApiController 'ticketStatus.boardColumn', 'ticketType', 'worklogEntries.actor', - ])) + ]) ?? $ticket->load([ + 'assignee', + 'author', + 'fieldValues.ticketFieldDefinition', + 'fieldValues.userValue', + 'ticketPriority', + 'ticketStatus.boardColumn', + 'ticketType', + 'worklogEntries.actor', + ]); + + if (! $project->show_author && ! $canManageProject) { + $displayTicket->setRelation('author', null); + $displayTicket->author_user_id = null; + } + + return TicketResource::make($displayTicket) ->additional([ 'meta' => [ 'message' => $statusMessage, diff --git a/tests/Feature/Api/TicketApiTest.php b/tests/Feature/Api/TicketApiTest.php index 3a4f044..e02880a 100644 --- a/tests/Feature/Api/TicketApiTest.php +++ b/tests/Feature/Api/TicketApiTest.php @@ -81,6 +81,77 @@ class TicketApiTest extends TestCase ->assertJsonPath('data.ticket.story_points', 5); } + public function test_ticket_update_hides_author_for_non_admins_when_author_visibility_is_disabled(): void + { + $writer = User::factory()->create(); + $author = User::factory()->create([ + 'name' => 'Author User', + ]); + $project = Project::factory()->create([ + 'key' => 'CORE', + 'show_author' => false, + ]); + + app(InitializeProjectTicketMetadata::class)->handle($project); + + $priority = $project->ticketPriorities()->firstOrFail(); + $status = $project->ticketStatuses()->firstOrFail(); + $type = $project->ticketTypes()->where('is_parent', false)->firstOrFail(); + + $ticket = Ticket::factory()->create([ + 'author_user_id' => $author->id, + 'project_id' => $project->id, + 'sequence_number' => 1, + 'ticket_key' => 'CORE-1', + 'ticket_priority_id' => $priority->id, + 'ticket_status_id' => $status->id, + 'ticket_type_id' => $type->id, + 'title' => 'API ticket', + ]); + + $this->syncProjectPermissions($project, $writer, ['write', 'view']); + + $this->actingAs($writer) + ->patchJson(route('api.projects.tickets.update', [$project, $ticket]), [ + 'acceptance_criteria' => $ticket->acceptance_criteria, + 'blocked_comment' => $ticket->blocked_comment, + 'description' => $ticket->description, + 'is_blocked' => $ticket->is_blocked, + 'parent_ticket_id' => $ticket->parent_ticket_id, + 'story_points' => $ticket->story_points, + 'ticket_priority_id' => $priority->id, + 'ticket_type_id' => $type->id, + 'title' => 'Updated API ticket', + ]) + ->assertOk() + ->assertJsonPath('data.author', null) + ->assertJsonPath('data.author_user_id', null); + } + + public function test_ticket_options_only_return_project_visible_users(): void + { + [$user, $project] = $this->projectContext(); + $projectViewer = User::factory()->create(); + $outsider = User::factory()->create(); + + $this->syncProjectPermissions($project, $projectViewer, ['view']); + + $response = $this->actingAs($user) + ->getJson(route('api.projects.ticket-options', [ + 'project' => $project, + 'source' => 'backlog', + ])) + ->assertOk(); + + $userIds = collect($response->json('data.users')) + ->pluck('id') + ->all(); + + $this->assertContains($user->id, $userIds); + $this->assertContains($projectViewer->id, $userIds); + $this->assertNotContains($outsider->id, $userIds); + } + public function test_tickets_can_return_status_targets_and_move_statuses(): void { [$user, $project] = $this->projectContext();