standardization of policies/permissions
Some checks failed
linter / quality (push) Failing after 1m3s
tests / ci (8.3) (push) Failing after 48s
tests / ci (8.4) (push) Failing after 1m5s
tests / ci (8.5) (push) Failing after 1m4s

This commit is contained in:
Daan Meijer 2026-06-22 16:13:32 +02:00
parent c60033b365
commit 188c4435cb
11 changed files with 166 additions and 162 deletions

View File

@ -13,6 +13,13 @@ class MutationResource extends BaseResource
*/ */
public function toArray(Request $request): array public function toArray(Request $request): array
{ {
return parent::toArray($request); $data = parent::toArray($request);
$data['can'] = [
'update' => $request->user()?->can('update', $this->resource) ?? false,
'void' => $request->user()?->can('void', $this->resource) ?? false,
];
return $data;
} }
} }

View File

@ -24,8 +24,9 @@ class MutationPolicy
public function update(User $user, Mutation $mutation): bool public function update(User $user, Mutation $mutation): bool
{ {
$dynamic = $mutation->ledger->dynamic; $dynamic = $mutation->ledger->dynamic;
$isOwner = $dynamic->participants()->where('user_id', $user->id)->where('role', 'owner')->exists();
return $dynamic->participants()->where('user_id', $user->id)->where('role', 'owner')->exists(); return $isOwner && $mutation->status === 'pending';
} }
/** /**
@ -34,7 +35,8 @@ class MutationPolicy
public function void(User $user, Mutation $mutation): bool public function void(User $user, Mutation $mutation): bool
{ {
$dynamic = $mutation->ledger->dynamic; $dynamic = $mutation->ledger->dynamic;
$isOwner = $dynamic->participants()->where('user_id', $user->id)->where('role', 'owner')->exists();
return $dynamic->participants()->where('user_id', $user->id)->where('role', 'owner')->exists(); return $isOwner && $mutation->status !== 'voided';
} }
} }

View File

@ -17,13 +17,16 @@ const props = defineProps<{
created_at: string; created_at: string;
chat: any; chat: any;
media?: Array<{ id: number; url: string; mime_type: string }>; media?: Array<{ id: number; url: string; mime_type: string }>;
can: {
update: boolean;
void: boolean;
};
}>; }>;
participants?: Array<{ participants?: Array<{
id: number; id: number;
name: string; name: string;
pivot?: { role: string }; pivot?: { role: string };
}>; }>;
isOwner: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -167,25 +170,25 @@ function getAmountClass(amount: number): string {
<!-- Owner Approve/Reject Actions --> <!-- Owner Approve/Reject Actions -->
<div <div
v-if="isOwner && (mutation.status === 'pending' || mutation.status === 'approved')" v-if="mutation.can.update || mutation.can.void"
class="c-mutation-list__actions" class="c-mutation-list__actions"
> >
<button <button
v-if="mutation.status === 'pending'" v-if="mutation.can.update"
@click="updateStatus(mutation.id, 'approved')" @click="updateStatus(mutation.id, 'approved')"
class="c-mutation-list__approve-btn" class="c-mutation-list__approve-btn"
> >
Approve Approve
</button> </button>
<button <button
v-if="mutation.status === 'pending'" v-if="mutation.can.update"
@click="updateStatus(mutation.id, 'rejected')" @click="updateStatus(mutation.id, 'rejected')"
class="c-mutation-list__reject-btn" class="c-mutation-list__reject-btn"
> >
Reject Reject
</button> </button>
<button <button
v-if="mutation.status !== 'voided'" v-if="mutation.can.void"
@click="voidMutation(mutation.id)" @click="voidMutation(mutation.id)"
class="c-mutation-list__void-btn" class="c-mutation-list__void-btn"
> >

View File

@ -2,22 +2,26 @@
import { Head, useForm } from '@inertiajs/vue3'; import { Head, useForm } from '@inertiajs/vue3';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
defineOptions({
layout: {
breadcrumbs: [
{
title: 'Dynamics',
href: route('dynamics.index'),
},
{
title: 'Create',
href: route('dynamics.create'),
},
],
},
});
const form = useForm({ const form = useForm({
name: '', name: '',
rules: '', rules: '',
}); });
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
{
name: 'Create',
href: route('dynamics.create'),
},
];
function submit() { function submit() {
form.post(route('dynamics.store')); form.post(route('dynamics.store'));
} }

View File

@ -2,6 +2,17 @@
import { Head, Link } from '@inertiajs/vue3'; import { Head, Link } from '@inertiajs/vue3';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
defineOptions({
layout: {
breadcrumbs: [
{
title: 'Dynamics',
href: route('dynamics.index'),
},
],
},
});
defineProps<{ defineProps<{
dynamics: Array<{ dynamics: Array<{
id: string; id: string;
@ -9,13 +20,6 @@ defineProps<{
rules: string; rules: string;
}>; }>;
}>(); }>();
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
];
</script> </script>
<template> <template>

View File

@ -1,7 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3'; import { Head, useForm } from '@inertiajs/vue3';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
import AppLayout from '@/layouts/AppLayout.vue';
defineOptions({
layout: (props: any) => ({
breadcrumbs: [
{
title: 'Dynamics',
href: route('dynamics.index'),
},
{
title: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
title: 'Invite User',
href: route('dynamics.invitations.create', props.dynamic.id),
},
],
}),
});
const props = defineProps<{ const props = defineProps<{
dynamic: { dynamic: {
@ -15,21 +33,6 @@ const form = useForm({
role: 'participant', role: 'participant',
}); });
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
{
name: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
name: 'Invite User',
href: route('dynamics.invitations.create', props.dynamic.id),
},
];
function submit() { function submit() {
form.post(route('dynamics.invitations.store', props.dynamic.id), { form.post(route('dynamics.invitations.store', props.dynamic.id), {
onSuccess: () => form.reset(), onSuccess: () => form.reset(),

View File

@ -1,7 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { Head, useForm, Link as InertiaLink } from '@inertiajs/vue3'; import { Head, useForm, Link as InertiaLink } from '@inertiajs/vue3';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
import AppLayout from '@/layouts/AppLayout.vue';
defineOptions({
layout: (props: any) => ({
breadcrumbs: [
{
title: 'Dynamics',
href: route('dynamics.index'),
},
{
title: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
title: 'Settings',
href: route('dynamics.edit', props.dynamic.id),
},
],
}),
});
const props = defineProps<{ const props = defineProps<{
dynamic: { dynamic: {
@ -16,21 +34,6 @@ const form = useForm({
rules: props.dynamic.rules, rules: props.dynamic.rules,
}); });
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
{
name: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
name: 'Settings',
href: route('dynamics.edit', props.dynamic.id),
},
];
function submit() { function submit() {
form.patch(route('dynamics.update', props.dynamic.id)); form.patch(route('dynamics.update', props.dynamic.id));
} }

View File

@ -43,18 +43,6 @@ const props = defineProps<{
update: boolean; update: boolean;
} }
}>(); }>();
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
{
name: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
];
</script> </script>
<template> <template>

View File

@ -1,30 +1,33 @@
<script setup lang="ts"> <script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3'; import { Head, useForm } from '@inertiajs/vue3';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
import AppLayout from '@/layouts/AppLayout.vue';
import CreateLedgerForm from '@/components/CreateLedgerForm.vue'; import CreateLedgerForm from '@/components/CreateLedgerForm.vue';
defineOptions({
layout: (props: any) => ({
breadcrumbs: [
{
title: 'Dynamics',
href: route('dynamics.index'),
},
{
title: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
title: 'Create Ledger',
href: route('dynamics.ledgers.create', props.dynamic.id),
},
],
}),
});
const props = defineProps<{ const props = defineProps<{
dynamic: { dynamic: {
id: number; id: number;
name: string; name: string;
}; };
}>(); }>();
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
{
name: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
name: 'Create Ledger',
href: route('dynamics.ledgers.create', props.dynamic.id),
},
];
</script> </script>
<template> <template>

View File

@ -1,7 +1,29 @@
<script setup lang="ts"> <script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3'; import { Head, useForm, Link } from '@inertiajs/vue3';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
import AppLayout from '@/layouts/AppLayout.vue';
defineOptions({
layout: (props: any) => ({
breadcrumbs: [
{
title: 'Dynamics',
href: route('dynamics.index'),
},
{
title: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
title: props.ledger.name,
href: route('dynamics.ledgers.show', [props.dynamic.id, props.ledger.id]),
},
{
title: 'Edit',
href: route('dynamics.ledgers.edit', [props.dynamic.id, props.ledger.id]),
},
],
}),
});
const props = defineProps<{ const props = defineProps<{
dynamic: { dynamic: {
@ -20,25 +42,6 @@ const form = useForm({
rules: props.ledger.rules, rules: props.ledger.rules,
}); });
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
{
name: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
name: props.ledger.name,
href: route('dynamics.ledgers.show', [props.dynamic.id, props.ledger.id]),
},
{
name: 'Edit',
href: route('dynamics.ledgers.edit', [props.dynamic.id, props.ledger.id]),
},
];
function submit() { function submit() {
form.put(route('dynamics.ledgers.update', [props.dynamic.id, props.ledger.id])); form.put(route('dynamics.ledgers.update', [props.dynamic.id, props.ledger.id]));
} }
@ -47,45 +50,43 @@ function submit() {
<template> <template>
<Head title="Edit Ledger" /> <Head title="Edit Ledger" />
<AppLayout :breadcrumbs="breadcrumbs"> <div class="c-ledger-edit">
<div class="c-ledger-edit"> <div class="c-ledger-edit__container">
<div class="c-ledger-edit__container"> <div class="c-ledger-edit__card">
<div class="c-ledger-edit__card"> <div class="c-ledger-edit__body">
<div class="c-ledger-edit__body"> <h3 class="c-ledger-edit__title">
<h3 class="c-ledger-edit__title"> Edit {{ ledger.name }}
Edit {{ ledger.name }} </h3>
</h3>
<form @submit.prevent="submit" class="c-ledger-edit__form"> <form @submit.prevent="submit" class="c-ledger-edit__form">
<div class="c-ledger-edit__field"> <div class="c-ledger-edit__field">
<label for="name" class="c-ledger-edit__label">Name</label> <label for="name" class="c-ledger-edit__label">Name</label>
<input v-model="form.name" id="name" type="text" class="c-ledger-edit__input" /> <input v-model="form.name" id="name" type="text" class="c-ledger-edit__input" />
</div> </div>
<div class="c-ledger-edit__field"> <div class="c-ledger-edit__field">
<label for="rules" class="c-ledger-edit__label">Rules</label> <label for="rules" class="c-ledger-edit__label">Rules</label>
<textarea v-model="form.rules" id="rules" rows="4" class="c-ledger-edit__textarea"></textarea> <textarea v-model="form.rules" id="rules" rows="4" class="c-ledger-edit__textarea"></textarea>
</div> </div>
<div class="c-ledger-edit__actions"> <div class="c-ledger-edit__actions">
<button type="submit" :disabled="form.processing" class="c-ledger-edit__submit-btn"> <button type="submit" :disabled="form.processing" class="c-ledger-edit__submit-btn">
Save Save
</button> </button>
<Link <Link
:href="route('dynamics.ledgers.close', [dynamic.id, ledger.id])" :href="route('dynamics.ledgers.close', [dynamic.id, ledger.id])"
method="put" method="put"
as="button" as="button"
class="c-ledger-edit__submit-btn c-ledger-edit__submit-btn--danger" class="c-ledger-edit__submit-btn c-ledger-edit__submit-btn--danger"
> >
Close Ledger Close Ledger
</Link> </Link>
</div> </div>
</form> </form>
</div>
</div> </div>
</div> </div>
</div> </div>
</AppLayout> </div>
</template> </template>
<style scoped> <style scoped>

View File

@ -43,6 +43,7 @@ const props = defineProps<{
score: number; score: number;
rules: string; rules: string;
alignment: string; alignment: string;
status: string;
media?: Array<{ id: number; url: string; mime_type: string }>; media?: Array<{ id: number; url: string; mime_type: string }>;
mutations: Array<{ mutations: Array<{
id: number; id: number;
@ -56,30 +57,15 @@ const props = defineProps<{
media?: Array<{ id: number; url: string; mime_type: string }>; media?: Array<{ id: number; url: string; mime_type: string }>;
}>; }>;
}; };
isOwner: boolean; can: {
update: boolean;
close: boolean;
};
messages: { messages: {
data: Array<any>; data: Array<any>;
next_page_url: string | null; next_page_url: string | null;
}; };
}>(); }>();
const breadcrumbs = [
{
name: 'Dynamics',
href: route('dynamics.index'),
},
{
name: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id),
},
{
name: props.ledger.name,
href: route('dynamics.ledgers.show', {
dynamic: props.dynamic.id,
ledger: props.ledger.id,
}),
},
];
// Lightbox Modal state // Lightbox Modal state
const activeLightboxUrl = ref<string | null>(null); const activeLightboxUrl = ref<string | null>(null);
@ -202,7 +188,7 @@ function isOwnerUser(userId: number): boolean {
{{ ledger.rules }} {{ ledger.rules }}
</p> </p>
</div> </div>
<div v-if="isOwner" class="flex flex-col gap-2"> <div v-if="can.update" class="flex flex-col gap-2">
<InertiaLink <InertiaLink
:href="route('dynamics.ledgers.predefined-mutations.index', [dynamic.id, ledger.id])" :href="route('dynamics.ledgers.predefined-mutations.index', [dynamic.id, ledger.id])"
class="c-ledger-show__manage-btn" class="c-ledger-show__manage-btn"
@ -284,7 +270,7 @@ function isOwnerUser(userId: number): boolean {
:ledger-id="ledger.id" :ledger-id="ledger.id"
:mutations="ledger.mutations" :mutations="ledger.mutations"
:participants="dynamic.participants" :participants="dynamic.participants"
:is-owner="isOwner" :can-update="can.update"
:ledger-alignment="ledger.alignment" :ledger-alignment="ledger.alignment"
@open-lightbox="openLightbox" @open-lightbox="openLightbox"
/> />