Skip to content

Commit

Permalink
Merge pull request #26 from openfoodfoundation/feature/audit-trail-up…
Browse files Browse the repository at this point in the history
…date

Feature: Added audit trail to admin section
  • Loading branch information
ok200paul authored Aug 16, 2024
2 parents 7af83a6 + c07b01e commit c86dade
Show file tree
Hide file tree
Showing 12 changed files with 1,222 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ class ApiAdminAuditItemsController extends Controller
'team',
];

public static array $searchableFields = [];
public static array $searchableFields = [
'id',
];

/**
* GET /
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ class ApiMyTeamAuditItemsController extends Controller
'team',
];

public static array $searchableFields = [];
public static array $searchableFields = [
'id',
];

/**
* GET /
Expand Down
47 changes: 47 additions & 0 deletions app/Models/AuditItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Models;

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Expand All @@ -17,6 +18,10 @@ class AuditItem extends Model
'auditable_text',
'auditable_team_id',
];
protected $appends = [
'dashboard_url',
'admin_url',
];

public function auditable(): MorphTo
{
Expand All @@ -27,4 +32,46 @@ public function team(): BelongsTo
{
return $this->belongsTo(Team::class, 'auditable_team_id', 'id');
}

/**
* Get the audit trails dashboard url
*
* @return Attribute
*/
public function dashboardUrl(): Attribute
{
$url = '#';
switch ($this->auditable_type) {
case User::class:
case Team::class:
$url = '/my-team';
break;
}

return Attribute::make(
get: fn ($value, $attributes) => $url,
);
}

/**
* Get the audit trails dashboard url
*
* @return Attribute
*/
public function adminUrl(): Attribute
{
$url = '#';
switch ($this->auditable_type) {
case User::class:
$url = '/admin/user/' . $this->auditable_id;
break;
case Team::class:
$url = '/admin/team/' . $this->auditable_id;
break;
}

return Attribute::make(
get: fn ($value, $attributes) => $url,
);
}
}
2 changes: 1 addition & 1 deletion database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function run(): void
);

foreach ($userAndTeam['users'] as $user) {
$user = User::factory()->createQuietly(
$user = User::factory()->create(
[
'name' => $user['name'],
'email' => $user['email'],
Expand Down
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion resources/js/Components/Admin/AdminTopNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,15 @@ function highlightMatchingText(text) {

<template>


<h2>
Admin Dashboard
</h2>


<div class="flex justify-between flex-wrap">
<div class="w-full md:mt-4 md:w-2/3 md:flex-grow lg:flex lg:justify-start lg:items-start lg:gap-x-4 grid grid-cols-3">
<div
class="w-full md:mt-4 md:w-2/3 md:flex-grow lg:flex lg:justify-start lg:items-start lg:gap-x-4 grid grid-cols-3">
<Link :href="route('admin.home')">
Admin Home
</Link>
Expand Down
104 changes: 68 additions & 36 deletions resources/js/Components/AuditItemsComponent.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,78 @@
<script setup>
import moment from "moment";
import Swal from "sweetalert2";
import {onMounted, ref} from "vue";
import {Link} from "@inertiajs/vue3";
const $props = defineProps(
{
isAdmin: {
type: Boolean,
default: false,
required: false
}
}
)
const auditItems = ref({});
function getData() {
let endpoint = '/my-team-audit-items?cache=false&orderBy=id,desc';
if ($props.isAdmin) {
endpoint = '/admin/audit-items?cache=false&relations=team&orderBy=id,desc';
}
axios.get(endpoint).then(response => {
auditItems.value = response.data.data;
}).catch(error => {
Swal.fire({
icon: 'error',
title: 'Oops!',
text: error.response.data.message
});
});
}
onMounted(() => {
getData();
});
const $props = defineProps(['auditItems', 'isAdmin'])
</script>

<template>

<div v-for="auditItem in $props.auditItems.data"
class="flex justify-between items-center border-b border-gray-200 p-4">
<div class="">
<span :class="{
'text-green-500': auditItem.auditable_text.includes('created'),
'text-orange-500': auditItem.auditable_text.includes('updated'),
'text-red-500': auditItem.auditable_text.includes('deleted')
}"
class="capitalize font-bold"
>
{{ auditItem.auditable_text }}
</span>

<span class="ml-6">
{{ auditItem.auditable_type.substring(auditItem.auditable_type.lastIndexOf("\\") + 1) }}
</span>


<span class="text-sm text-gray-600 ml-2">
# {{ auditItem.auditable_id }}
</span>

<a v-if="isAdmin && auditItem.team?.id" :href="'/admin/team/' + auditItem.team.id">
<span class="text-sm text-gray-600 hover:underline hover:text-blue-500">
{{ auditItem.team.name }}
</span>
</a>
</div>

<div class="">


{{ moment(auditItem.updated_at).format("dddd, MMMM Do YYYY [at] h:mm:ss a") }}
</div>
</div>
<div class="card">
<div class="card-header">
Audit Trail
</div>
<div v-for="auditItem in auditItems.data">
<Link class="hover:no-underline" :href="(isAdmin)? auditItem.admin_url : auditItem.dashboard_url">
<div
class="flex justify-between items-center border-b border-gray-200 p-4">
<div>
<div>
{{ auditItem.auditable_text }}
</div>

<div class="text-xs text-gray-500 italic">
{{ moment(auditItem.created_at).format("dddd, MMMM Do YYYY [at] h:mm:ss a") }}
</div>
</div>

<div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</div>
</div>
</Link>
</div>
</div>

</template>
26 changes: 14 additions & 12 deletions resources/js/Layouts/AuthenticatedLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const showingNavigationDropdown = ref(false);
<!-- Primary Navigation Menu -->
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<div class="flex items-center">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<Link :href="route('dashboard')">
Expand All @@ -33,16 +33,13 @@ const showingNavigationDropdown = ref(false);
Dashboard
</NavLink>

<NavLink :href="route('audit-trail')" :active="route().current('audit-trail')">
Audit Trail
</NavLink>

<a href="/api-documentation"
target="_blank"
class="inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-light leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out">
Api Docs
</a>
</div>

</div>

<div class="hidden sm:flex sm:items-center sm:ms-6">
Expand All @@ -51,11 +48,12 @@ const showingNavigationDropdown = ref(false);
<Dropdown align="right" width="48">
<template #trigger>
<span class="inline-flex rounded-md">

<button
type="button"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
>
{{ $page.props.auth.user.name }}
{{ $page.props.auth.user.name }} - {{ $page.props.auth.currentTeam.name}}

<svg
class="ms-2 -me-0.5 h-4 w-4"
Expand All @@ -70,7 +68,9 @@ const showingNavigationDropdown = ref(false);
/>
</svg>
</button>

</span>

</template>

<template #content>
Expand All @@ -80,10 +80,15 @@ const showingNavigationDropdown = ref(false);
>
Admin Section
</DropdownLink>
<DropdownLink :href="route('profile.edit')"> Profile </DropdownLink>
<DropdownLink :href="route('profile.edit')">
Profile
</DropdownLink>
<DropdownLink :href="route('my-team')">
My Team
</DropdownLink>
<DropdownLink :href="route('audit-trail')">
Audit Trail
</DropdownLink>
<DropdownLink :href="route('logout')" method="post" as="button">
Log Out
</DropdownLink>
Expand Down Expand Up @@ -146,6 +151,7 @@ const showingNavigationDropdown = ref(false);
{{ $page.props.auth.user.name }}
</div>
<div class=" text-sm text-gray-500">{{ $page.props.auth.user.email }}</div>

</div>

<div class="mt-3 space-y-1">
Expand All @@ -160,12 +166,8 @@ const showingNavigationDropdown = ref(false);

<!-- Page Heading -->
<header class="bg-white shadow" v-if="$slots.header">
<div class="container mx-auto py-6 px-4 sm:px-6 lg:px-8 flex justify-between items-center">
<div class="container mx-auto py-6 px-4 ">
<slot name="header" />

<div class="opacity-50">
Logged into: {{ $page.props.auth.currentTeam.name}}
</div>
</div>
</header>

Expand Down
2 changes: 2 additions & 0 deletions resources/js/Pages/Admin/AdminHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head, Link } from '@inertiajs/vue3';
import AdminTopNavigation from "@/Components/Admin/AdminTopNavigation.vue";
import SystemStatisticsComponent from "@/Components/Admin/SystemStatistics/SystemStatisticsComponent.vue";
import AuditItemsComponent from "@/Components/AuditItemsComponent.vue";
</script>

<template>
Expand All @@ -16,5 +17,6 @@ import SystemStatisticsComponent from "@/Components/Admin/SystemStatistics/Syste
</template>

<system-statistics-component></system-statistics-component>
<AuditItemsComponent :is-admin="true"></AuditItemsComponent>
</AuthenticatedLayout>
</template>
20 changes: 12 additions & 8 deletions resources/js/Pages/Admin/Users/User.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,9 @@ function textFormat(ability) {
</template>

<div class="card">
<div class="flex items-start font-bold">
<div>#{{ $props.id }}</div>
<div class="pl-2 text-2xl">{{ user.name }}</div>
</div>

<h2>{{ user.name }}</h2>

</div>

<div class="card">
Expand All @@ -104,10 +103,13 @@ function textFormat(ability) {
</div>

<div v-if="userTeams.data && userTeams.data.length > 0">
<Link :href="route('admin.team', userTeam.team_id)" v-for="userTeam in userTeams.data" class="hover:no-underline hover:opacity-75">
<Link :href="route('admin.team', userTeam.team_id)" v-for="userTeam in userTeams.data"
class="hover:no-underline hover:opacity-75">
<div :class="{'border-b p-2': userTeams.data.length > 1}">
<div v-if="userTeam.team">
<div v-if="userTeam.team_id === user.current_team_id" class="text-xs text-red-500">*Current team</div>
<div v-if="userTeam.team_id === user.current_team_id" class="text-xs text-red-500">*Current
team
</div>
<div class="">{{ userTeam.team.name }}</div>
</div>
</div>
Expand Down Expand Up @@ -163,13 +165,15 @@ function textFormat(ability) {
<div class="grid grid-cols-1 md:grid-cols-4">
<div v-for="ability in personalAccessTokenAbilities">
<label :for="ability" class="cursor-pointer">
<input type="checkbox" :id="ability" class="mr-4" :value="ability" v-model="newPAT.token_abilities"> {{ textFormat(ability) }}
<input type="checkbox" :id="ability" class="mr-4" :value="ability"
v-model="newPAT.token_abilities"> {{ textFormat(ability) }}
</label>
</div>
</div>

<div class="flex items-center justify-end mt-4">
<PrimaryButton @click.prevent="createPAT()" class="ms-4" :class="{ 'opacity-25': !newPAT.name }" :desabled="!newPAT.name">
<PrimaryButton @click.prevent="createPAT()" class="ms-4" :class="{ 'opacity-25': !newPAT.name }"
:desabled="!newPAT.name">
Submit
</PrimaryButton>
</div>
Expand Down
Loading

0 comments on commit c86dade

Please sign in to comment.