Skip to content

Commit

Permalink
Create API access token list and token page
Browse files Browse the repository at this point in the history
  • Loading branch information
ok200manami committed Aug 19, 2024
1 parent 78f873c commit 43c357d
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@

use App\Enums\ApiResponse;
use App\Enums\PersonalAccessTokenAbility;
use App\Exceptions\DisallowedApiFieldException;
use App\Http\Controllers\Api\HandlesAPIRequests;
use App\Http\Controllers\Controller;
use App\Models\PersonalAccessToken;
use App\Models\User;
use Exception;
use Illuminate\Http\JsonResponse;
Expand All @@ -23,19 +25,26 @@ class ApiAdminUserPersonalAccessTokensController extends Controller
/**
* Set the related data the GET request is allowed to ask for
*/
public array $availableRelations = [];
public array $availableRelations = [
'user',
];

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

/**
* GET /
*
* @return JsonResponse
*
* @throws DisallowedApiFieldException
*/
public function index(): JsonResponse
{
$this->responseCode = 403;
$this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value;
$this->query = PersonalAccessToken::with($this->associatedData)->select(['id', 'tokenable_id', 'name', 'last_used_at', 'expires_at']);
$this->query = $this->updateReadQueryBasedOnUrl();
$this->data = $this->query->paginate($this->limit);

return $this->respond();
}
Expand Down Expand Up @@ -114,11 +123,14 @@ public function store(): JsonResponse
* @param string $id
*
* @return JsonResponse
*
* @throws DisallowedApiFieldException
*/
public function show(string $id)
{
$this->responseCode = 403;
$this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value;
$this->query = PersonalAccessToken::with($this->associatedData)->select(['id', 'tokenable_type', 'tokenable_id', 'name', 'abilities', 'last_used_at', 'expires_at']);
$this->query = $this->updateReadQueryBasedOnUrl();
$this->data = $this->query->find($id);

return $this->respond();
}
Expand Down Expand Up @@ -147,8 +159,30 @@ public function update(string $id)
*/
public function destroy(string $id)
{
$this->responseCode = 403;
$this->message = ApiResponse::RESPONSE_METHOD_NOT_ALLOWED->value;
try {

$model = PersonalAccessToken::find($id);

if (!$model) {

$this->responseCode = 404;
$this->message = ApiResponse::RESPONSE_NOT_FOUND->value;

}
else {

$model->delete();
$this->message = ApiResponse::RESPONSE_DELETED->value;

}

}
catch (Exception $e) {

$this->responseCode = 500;
$this->message = ApiResponse::RESPONSE_ERROR->value . ':' . $e->getMessage();

}

return $this->respond();
}
Expand Down
6 changes: 6 additions & 0 deletions app/Models/PersonalAccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;

class PersonalAccessToken extends SanctumPersonalAccessToken
Expand All @@ -17,4 +18,9 @@ class PersonalAccessToken extends SanctumPersonalAccessToken
'abilities',
'team_id',
];

public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'tokenable_id', 'id');
}
}
19 changes: 8 additions & 11 deletions package-lock.json

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

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@
"@vitejs/plugin-vue": "^5.0.0",
"autoprefixer": "^10.4.12",
"axios": "^1.6.4",
"dayjs": "^1.11.12",
"laravel-vite-plugin": "^1.0",
"postcss": "^8.4.31",
"sweetalert2": "^11.12.4",
"tailwindcss": "^3.2.1",
"vite": "^5.0",
"vue": "^3.4.0"
},
"dependencies": {
"moment": "^2.30.1"
}
}
2 changes: 1 addition & 1 deletion resources/js/Components/Admin/AdminTopNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ function highlightMatchingText(text) {
Redemptions
</Link>

<Link :href="route('admin.teams')">
<Link :href="route('admin.api-access-tokens')">
API Access Tokens
</Link>
</div>
Expand Down
4 changes: 2 additions & 2 deletions resources/js/Components/AuditItemsComponent.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup>
import moment from "moment";
import dayjs from "dayjs";
import Swal from "sweetalert2";
import {onMounted, ref} from "vue";
import {Link} from "@inertiajs/vue3";
Expand Down Expand Up @@ -61,7 +61,7 @@ onMounted(() => {
</div>

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

Expand Down
121 changes: 121 additions & 0 deletions resources/js/Pages/Admin/APIAccessTokens/APIAccessToken.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import {Head, Link, usePage} from '@inertiajs/vue3';
import AdminTopNavigation from "@/Components/Admin/AdminTopNavigation.vue";
import {onMounted, ref} from "vue";
import Swal from "sweetalert2";
import dayjs from "dayjs";
import relativeTime from 'dayjs/plugin/relativeTime'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import PrimaryButton from "@/Components/PrimaryButton.vue";
const $props = defineProps({
id: {
required: true,
type: Number,
}
});
const apiAccessToken = ref({})
onMounted(() => {
getApiAccessToken()
})
function dateFormat(dateTime) {
dayjs.extend(relativeTime)
dayjs.extend(localizedFormat)
return dayjs(dateTime).fromNow() + ' (' + dayjs(dateTime).format('LLL') + ')'
}
function getApiAccessToken() {
axios.get('/admin/user-personal-access-tokens/' + $props.id + '?cached=false&relations=user').then(response => {
apiAccessToken.value = response.data.data
}).catch(error => {
console.log(error)
})
}
function revokeApiAccessToken() {
Swal.fire({
title: "Are you sure you want to delete this token?",
text: "This action cannot be undone, and the user will no longer be able to use this token. Please confirm if you wish to proceed.",
icon: "warning",
confirmButtonColor: "#3085d6",
confirmButtonText: "Revoke this token",
showCancelButton: true,
}).then((result) => {
if (result.isConfirmed) {
axios.delete('/admin/user-personal-access-tokens/' + $props.id).then(response => {
window.location.href = route('admin.api-access-tokens')
}).catch(error => {
console.log(error)
})
}
});
}
function textFormat(ability) {
return ability.replaceAll('-', ' ')
}
</script>

<template>
<Head title="API Access Token"/>

<AuthenticatedLayout>
<template #header>
<AdminTopNavigation></AdminTopNavigation>
</template>

<div class="card">

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

</div>

<div class="card">
<div class="card-header">
API Access Token details
</div>

<div class="text-xs">#{{ apiAccessToken.id }}</div>
<div class="">{{ apiAccessToken.name }}</div>
<div v-if="apiAccessToken.last_used_at" class="mt-2">Last used at: {{ dateFormat(apiAccessToken.last_used_at) }}</div>
</div>

<div class="card">
<div class="card-header">
Issued to user
</div>

<div v-if="apiAccessToken.user">
<Link :href="route('admin.user', apiAccessToken.tokenable_id)">
{{ apiAccessToken.user.name }}
</Link>
</div>
</div>

<div class="card">
<div class="card-header">
API Access Token abilities
</div>

<div v-if="apiAccessToken.abilities && apiAccessToken.abilities.length">
<div v-for="ability in apiAccessToken.abilities" class="py-1">
<div class="list-item ml-8">
{{ textFormat(ability) }}
</div>
</div>
</div>
</div>

<div class="card">
<PrimaryButton @click="revokeApiAccessToken()">
Revoke this token
</PrimaryButton>
</div>
</AuthenticatedLayout>
</template>
69 changes: 69 additions & 0 deletions resources/js/Pages/Admin/APIAccessTokens/APIAccessTokens.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import {Head, Link} from '@inertiajs/vue3';
import AdminTopNavigation from "@/Components/Admin/AdminTopNavigation.vue";
import {onMounted, ref} from "vue";
import PaginatorComponent from "@/Components/Admin/PaginatorComponent.vue";
const apiAccessTokens = ref({})
onMounted(() => {
getApiAccessTokens()
})
function getApiAccessTokens(page = 1) {
axios.get('/admin/user-personal-access-tokens?cached=false&page=' + page + '&relations=user&orderBy=id,desc').then(response => {
apiAccessTokens.value = response.data.data
}).catch(error => {
console.log(error)
})
}
</script>

<template>
<Head title="API Access Tokens"/>

<AuthenticatedLayout>
<template #header>
<AdminTopNavigation></AdminTopNavigation>
</template>


<div class=" card">
<div v-if="apiAccessTokens.data && apiAccessTokens.data.length">
<Link :href="route('admin.api-access-token', token.id)" v-for="token in apiAccessTokens.data" class="hover:no-underline hover:opacity-75">
<div class="border-b flex justify-between items-center py-2 sm:p-2">
<div>

<div class="font-bold">
<span class="text-xs opacity-25">
#{{ token.id }}
</span>
{{ token.name }}
</div>
<div v-if="token.user" class="text-sm">
Issued to: {{ token.user.name }}
</div>
</div>
<div class="text-2xl">
<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 class="flex justify-end items-center mt-4">
<div class="w-full lg:w-1/3">
<PaginatorComponent
@setDataPage="getApiAccessTokens"
:pagination-data="apiAccessTokens"></PaginatorComponent>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
Loading

0 comments on commit 43c357d

Please sign in to comment.