Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion app/Http/Controllers/API/GroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
];
}

/**
Expand Down
36 changes: 36 additions & 0 deletions app/Http/Resources/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -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",
* ),
* )
* )
*/
Expand Down
92 changes: 52 additions & 40 deletions resources/js/components/GroupPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@

<GroupDevicesBreakdown :idgroups="idgroups" :cluster-stats="clusterStats" />
</div>
<div v-else class="vue-placeholder vue-placeholder-large">
<div class="vue-placeholder-content">{{ __('partials.loading') }}...</div>
</div>
</template>
<script>
import GroupHeading from './GroupHeading.vue'
Expand All @@ -60,6 +63,7 @@ import GroupDevicesMostRepaired from './GroupDevicesMostRepaired.vue'
import GroupDevicesBreakdown from './GroupDevicesBreakdown.vue'
import AlertBanner from './AlertBanner.vue'
import auth from '../mixins/auth'
import axios from 'axios'

export default {
components: {
Expand All @@ -79,39 +83,10 @@ export default {
type: Number,
required: true
},
initialGroup: {
type: Object,
required: true
},
events: {
type: Array,
required: true
},
canedit: {
type: Boolean,
required: false,
default: false
},
candemote: {
type: Boolean,
required: false,
default: false
},
canSeeDelete: {
type: Boolean,
required: false,
default: false
},
canPerformDelete: {
type: Boolean,
required: false,
default: false
},
canPerformArchive: {
type: Boolean,
required: false,
default: false
},
ingroup: {
type: Boolean,
required: false,
Expand Down Expand Up @@ -163,21 +138,58 @@ export default {
name: this.group.name,
link: '/group/view/' + this.group.id
})
},
permissions() {
return (this.group && this.group.permissions) || {}
},
canedit() {
return this.permissions.can_edit || false
},
candemote() {
return this.permissions.can_demote || false
},
canSeeDelete() {
return this.permissions.can_see_delete || false
},
canPerformDelete() {
return this.permissions.can_perform_delete || false
},
canPerformArchive() {
return this.permissions.can_perform_archive || false
}
},
mounted () {
// Data is passed from the blade template to us via props. We put it in the store for all components to use,
// and so that as/when it changes then reactivity updates all the views.
async mounted () {
// Fetch the group - including this user's UI permission flags - from the API, rather than relying on
// server-hydrated Blade props. We put it in the store for all components to use, and so that as/when it
// changes then reactivity updates all the views. While the fetch is in flight, `group` in the store is
// undefined and the template shows a loading placeholder.
//
// Further down the line this may change so that the data is obtained via an AJAX call and perhaps SSR.
// TODO LATER We add some properties to the group before adding it to the store. These should move into
// computed properties once we have good access to the session on the client.
this.initialGroup.idgroups = this.idgroups
this.initialGroup.canedit = this.canedit
this.initialGroup.candemote = this.candemote
this.initialGroup.ingroup = this.ingroup
// We fetch directly here (rather than via the groups/fetch action) so that we can add the old-style field
// aliases some sibling components still expect (see below) before the group ever lands in the store, and
// so we commit it exactly once - the shared groups/fetch action is also used by components which want the
// raw API shape as-is (e.g. GroupAddEdit.vue), so we don't want to change what it commits.
try {
const ret = await axios.get('/api/v2/groups/' + this.idgroups)
const group = ret.data.data

// A handful of sibling components (GroupVolunteers, GroupEvents, ...) read canedit/candemote/ingroup off
// the stored group object itself via the `group` mixin, rather than as props - and some still expect
// old-style (Eloquent) field names. Add compatibility aliases rather than changing those components
// (out of scope here) - mirrors the newToOld() shim already used elsewhere in the groups store.
group.idgroups = this.idgroups
group.canedit = group.permissions ? group.permissions.can_edit : false
group.candemote = group.permissions ? group.permissions.can_demote : false
group.ingroup = this.ingroup
group.free_text = group.description

this.$store.dispatch('groups/set', this.initialGroup)
if (group.image) {
group.group_image = { image: { path: group.image } }
}

this.$store.dispatch('groups/set', group)
} catch (e) {
console.error('Group fetch failed', e)
}

this.events.forEach(e => {
this.$store.dispatch('events/setStats', {
Expand Down
14 changes: 3 additions & 11 deletions resources/views/group/view.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,9 @@
$group_image->image->path;
}

$can_edit_group = App\Helpers\Fixometer::hasRole($user, 'Administrator') || $isCoordinatorForGroup || $is_host_of_group;
$can_demote = App\Helpers\Fixometer::hasRole($user, 'Administrator') || $isCoordinatorForGroup;
$can_see_delete = App\Helpers\Fixometer::hasRole($user, 'Administrator');
$can_perform_delete = $can_see_delete && $group->canDelete();
$can_perform_archive = App\Helpers\Fixometer::hasRole($user, 'Administrator') || $isCoordinatorForGroup;
// The per-user UI permission flags (can_edit, can_demote, can_see_delete, can_perform_delete,
// can_perform_archive) are no longer computed here. GroupPage.vue fetches them, along with the rest
// of the group data, from GET /api/v2/groups/{id} (see API\GroupController::getGroupv2()).

$showCalendar = Auth::check() && (($group && $group->isVolunteer()) || App\Helpers\Fixometer::hasRole(Auth::user(), 'Administrator'));

Expand Down Expand Up @@ -143,17 +141,11 @@
<GroupPage
csrf="{{ csrf_token() }}"
:idgroups="{{ $group->idgroups }}"
:initial-group="{{ $group }}"
:group-stats="{{ json_encode($group_stats, JSON_INVALID_UTF8_IGNORE) }}"
:device-stats="{{ json_encode($device_stats, JSON_INVALID_UTF8_IGNORE) }}"
:cluster-stats="{{ json_encode($cluster_stats, JSON_INVALID_UTF8_IGNORE) }}"
:top-devices="{{ json_encode($top, JSON_INVALID_UTF8_IGNORE) }}"
:events="{{ json_encode($expanded_events, JSON_INVALID_UTF8_IGNORE) }}"
:canedit="{{ $can_edit_group ? 'true' : 'false' }}"
:candemote="{{ $can_demote ? 'true' : 'false' }}"
:can-see-delete="{{ $can_see_delete ? 'true' : 'false' }}"
:can-perform-delete="{{ $can_perform_delete ? 'true' : 'false' }}"
:can-perform-archive="{{ $can_perform_archive ? 'true' : 'false' }}"
calendar-copy-url="{{ $showCalendar ? url("/calendar/group/{$group->idgroups}") : '' }}"
calendar-edit-url="{{ $showCalendar ? url("/profile/edit/{$user->id}#list-calendar-links") : '' }}"
:ingroup="{{ $in_group ? 'true' : 'false' }}"
Expand Down
Loading