From 74a454f6fe7626db73052dd3ed7ba31274ad712d Mon Sep 17 00:00:00 2001 From: edwh Date: Fri, 3 Jul 2026 22:50:14 +0100 Subject: [PATCH] Group view: fetch group + permission flags from /api/v2/groups/{id} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes Group 4.1 of the Blade-to-Vue plan. group/view.blade.php previously computed per-user permission flags server-side and passed them to as props; now GroupPage fetches the group (and those flags) from the v2 API instead. - getGroupv2 resolves the OPTIONAL user (session, then api-token guard) and adds a `permissions` object to the response via a groupPermissionsFor() helper that replicates the blade logic exactly: can_edit = admin || coordinatorForGroup || hostOfGroup can_demote = admin || coordinatorForGroup can_see_delete = admin can_perform_delete = can_see_delete && group->canDelete() can_perform_archive= admin || coordinatorForGroup With no resolved user, all flags are false. (Host = Fixometer::userHasEditGroupPermission, coordinator = User::isCoordinatorForGroup — the same calls the controller used.) - GroupPage.vue drops the hydrated group/permission props and fetches on mount, deriving the button flags as computed properties from the response. - group/view.blade.php stops computing/passing those props. - Note: these flags are UI show/hide only — the edit/delete/archive endpoints enforce their own authorization independently. Tests: APIv2GroupPermissionsTest covers the full matrix (anonymous / non-member / host / coordinator own-vs-other network / admin, and can_perform_delete following canDelete). GroupViewTest + InviteGroupTest updated for the removed props. --- app/Http/Controllers/API/GroupController.php | 54 +++- app/Http/Resources/Group.php | 36 +++ resources/js/components/GroupPage.vue | 92 ++++--- resources/views/group/view.blade.php | 14 +- .../Groups/APIv2GroupPermissionsTest.php | 233 ++++++++++++++++++ tests/Feature/Groups/GroupViewTest.php | 36 ++- tests/Feature/Groups/InviteGroupTest.php | 47 ++-- 7 files changed, 435 insertions(+), 77 deletions(-) create mode 100644 tests/Feature/Groups/APIv2GroupPermissionsTest.php diff --git a/app/Http/Controllers/API/GroupController.php b/app/Http/Controllers/API/GroupController.php index c4b5abfc21..feb077ef0b 100644 --- a/app/Http/Controllers/API/GroupController.php +++ b/app/Http/Controllers/API/GroupController.php @@ -383,7 +383,59 @@ public static function listTagsv2(Request $request) { */ public static function getGroupv2(Request $request, $idgroups) { $group = Group::findOrFail($idgroups); - return \App\Http\Resources\Group::make($group); + + // This endpoint is not behind auth:api, so the request may be anonymous. Try session auth first (the + // group view page is loaded via a normal browser session), then fall back to API token auth. If there is + // no authenticated user at all, every permission flag is false. + $user = Auth::user(); + if (! $user) { + $user = auth('api')->user(); + } + + $permissions = self::groupPermissionsFor($user, $group); + + return \App\Http\Resources\Group::make($group)->additional([ + 'data' => [ + 'permissions' => $permissions, + ], + ]); + } + + /** + * Compute the UI show/hide permission flags for a given (possibly null) user against a group. + * + * These flags are for UI purposes only - the actual edit/delete/archive endpoints enforce their own + * authorization independently. Mirrors the logic previously computed server-side in group/view.blade.php. + */ + private static function groupPermissionsFor(?User $user, Group $group): array + { + if (! $user) { + return [ + 'can_edit' => false, + 'can_demote' => false, + 'can_see_delete' => false, + 'can_perform_delete' => false, + 'can_perform_archive' => false, + ]; + } + + $isAdministrator = Fixometer::hasRole($user, 'Administrator'); + $isCoordinatorForGroup = $user->isCoordinatorForGroup($group); + $isHostOfGroup = Fixometer::userHasEditGroupPermission($group->idgroups, $user->id); + + $canEdit = $isAdministrator || $isCoordinatorForGroup || $isHostOfGroup; + $canDemote = $isAdministrator || $isCoordinatorForGroup; + $canSeeDelete = $isAdministrator; + $canPerformDelete = $canSeeDelete && $group->canDelete(); + $canPerformArchive = $isAdministrator || $isCoordinatorForGroup; + + return [ + 'can_edit' => $canEdit, + 'can_demote' => $canDemote, + 'can_see_delete' => $canSeeDelete, + 'can_perform_delete' => $canPerformDelete, + 'can_perform_archive' => $canPerformArchive, + ]; } /** diff --git a/app/Http/Resources/Group.php b/app/Http/Resources/Group.php index 7066ae67fc..fe42f444b3 100644 --- a/app/Http/Resources/Group.php +++ b/app/Http/Resources/Group.php @@ -260,6 +260,42 @@ * title="archived_at", * description="If present, this group has been archived and is no longer active.", * format="date-time", + * ), + * @OA\Property( + * property="permissions", + * title="permissions", + * description="UI show/hide permission flags for the currently authenticated user (only present on the single-group endpoint). These are for UI purposes only - the edit/delete/archive endpoints enforce their own authorization independently. When there is no authenticated user, every flag is false.", + * format="object", + * @OA\Property( + * property="can_edit", + * title="can_edit", + * description="Whether the user can see group editing controls (administrator, network coordinator for the group, or host of the group).", + * type="boolean", + * ), + * @OA\Property( + * property="can_demote", + * title="can_demote", + * description="Whether the user can demote/manage volunteer roles (administrator or network coordinator for the group).", + * type="boolean", + * ), + * @OA\Property( + * property="can_see_delete", + * title="can_see_delete", + * description="Whether the user can see the delete-group control (administrator only).", + * type="boolean", + * ), + * @OA\Property( + * property="can_perform_delete", + * title="can_perform_delete", + * description="Whether the group can actually be deleted by this user (can_see_delete and the group has no events with devices).", + * type="boolean", + * ), + * @OA\Property( + * property="can_perform_archive", + * title="can_perform_archive", + * description="Whether the user can archive the group (administrator or network coordinator for the group).", + * type="boolean", + * ), * ) * ) */ diff --git a/resources/js/components/GroupPage.vue b/resources/js/components/GroupPage.vue index 49b2a14360..7928b0442c 100644 --- a/resources/js/components/GroupPage.vue +++ b/resources/js/components/GroupPage.vue @@ -48,6 +48,9 @@ +
+
{{ __('partials.loading') }}...
+