Adding an API to an AdonisJS Web App #2.8

Deleting/Revoking Access Tokens

In This Lesson

We'll add the ability for our users to revoke an access token by deleting it out of our database.

Created by
@tomgobich
Published

As promised, below you can find the completed alterations we've made to our web application in VueJS.

Organization Access Token Card

This is the card component that houses all our client-side access token logic including listing, creating, deleting, and copying our access tokens.

<script setup lang="ts">
import AccessTokenDto from '#dtos/access_token'
import TokenActions from '#enums/token_actions'
import { router } from '@inertiajs/vue3'
import axios from 'axios'
import { Clipboard, ClipboardCheck, Loader, Trash2 } from 'lucide-vue-next'
import { DateTime } from 'luxon'
import { ref } from 'vue'
import { useResourceActions } from '~/composables/resource_actions'

defineProps<{ tokens: AccessTokenDto[] }>()

let copyTimeout: NodeJS.Timeout | null = null
const isDialogShown = ref(false)
const processing = ref(false)
const isCopied = ref(false)
const token = ref<string | null>(null)
const permissionOptions = Object.values(TokenActions)
const form = ref({
  name: '',
  permissions: new Set(['read']),
})

const { destroy } = useResourceActions<AccessTokenDto>()({})

async function onSubmit() {
  processing.value = true

  const { data } = await axios.post('/settings/organization/access-tokens', {
    name: form.value.name,
    permissions: [...form.value.permissions],
  })

  token.value = data.accessToken.token

  processing.value = false

  router.reload({ only: ['accessTokens'] })
}

function onCopy() {
  if (!token.value) return
  if (copyTimeout) clearTimeout(copyTimeout)

  navigator.clipboard.writeText(token.value)
  isCopied.value = true

  copyTimeout = setTimeout(() => (isCopied.value = false), 1_000)
}

function onClose() {
  isDialogShown.value = false
  token.value = null
}
</script>

<template>
  <Card>
    <CardHeader>
      <CardTitle>Organization Access Tokens</CardTitle>
      <CardDescription>Manage tokens your organization can use to access our API.</CardDescription>
    </CardHeader>
    <CardContent>
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>Name</TableHead>
            <TableHead>Permissions</TableHead>
            <TableHead>Created</TableHead>
            <TableHead>Last Used</TableHead>
            <TableHead></TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          <TableRow v-for="item in tokens" :key="item.id">
            <TableCell>
              {{ item.name }}
            </TableCell>
            <TableCell class="capitalize">
              {{ item.abilities.join(', ') }}
            </TableCell>
            <TableCell>
              {{ DateTime.fromISO(item.createdAt).toLocaleString() }}
            </TableCell>
            <TableCell>
              <span v-if="item.lastUsedAt">
                {{ DateTime.fromISO(item.lastUsedAt).toLocaleString() }}
              </span>
            </TableCell>
            <TableCell>
              <Button variant="destructive" size="xs" @click="destroy.open(item)">
                <Trash2 class="w-3 h-3" />
              </Button>
            </TableCell>
          </TableRow>
        </TableBody>
      </Table>

      <div class="flex justify-end mt-4">
        <Button type="button" @click="isDialogShown = true"> Add Access Token </Button>
      </div>
    </CardContent>
  </Card>

  <ConfirmDestroyDialog
    v-model:open="destroy.isOpen"
    title="Delete Access Token?"
    :action-href="`/settings/organization/access-tokens/${destroy.resource?.id}`"
  >
    Are you sure you'd like to delete your <strong>{{ destroy.resource?.name }}</strong> access
    token? It will become immediately unusable and cannot be restored.
  </ConfirmDestroyDialog>

  <Dialog v-model:open="isDialogShown">
    <DialogContent v-if="token">
      <DialogHeader>
        <DialogTitle>Access Token Created</DialogTitle>
      </DialogHeader>

      <p>
        Please copy your access token below and store it somewhere safe. Once this dialog is closed,
        you will not be able to access your token again.
      </p>

      <div class="relative">
        <Input type="text" :model-value="token" />
        <Button
          variant="outline"
          size="icon"
          class="absolute right-1 top-1 !w-8 !h-8 z-20 shadow-md"
          @click="onCopy"
        >
          <ClipboardCheck v-if="isCopied" class="w-4 h-4 text-green-500" />
          <Clipboard v-else class="w-4 h-4" />
        </Button>
        <div
          class="absolute right-1 top-px w-32 h-[calc(100%-2px)] z-10 bg-gradient-to-r from-transparent to-white"
        ></div>
      </div>

      <DialogFooter>
        <Button type="button" @click="onClose"> Got my token! Close Dialog </Button>
      </DialogFooter>
    </DialogContent>
    <DialogContent v-else>
      <DialogHeader>
        <DialogTitle>Add Access Token</DialogTitle>
      </DialogHeader>

      <form id="accessTokenDialog" @submit.prevent="onSubmit">
        <FormInput
          label="Name"
          type="text"
          class="flex-1"
          v-model="form.name"
          :disabled="processing"
        />

        <div class="flex flex-col gap-3 my-3">
          <Label>Permissions</Label>

          <FormInput type="group">
            <div v-for="option in permissionOptions" :key="option" class="flex items-center gap-2">
              <Checkbox
                :checked="form.permissions.has(option)"
                :disable="processing"
                @update:checked="
                  $event ? form.permissions.add(option) : form.permissions.delete(option)
                "
              />
              <span class="capitalize">{{ option }}</span>
            </div>
          </FormInput>
        </div>
      </form>

      <DialogFooter>
        <Button type="submit" :disabled="processing" form="accessTokenDialog">
          <Loader v-if="processing" class="mr-2 h-4 w-4 animate-spin" />
          Create Access Token
        </Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>
</template>
Copied!
  • inertia
  • components
  • OrganizationAccessTokensCard.vue

Organization Settings Page

For the page, all we needed to do was rig up our OrgaizationAccessTokensCard component and pass along the access tokens to it.

<script setup lang="ts">
import { Abilities } from '#actions/abilities/get_abilities'
import AccessTokenDto from '#dtos/access_token'
import OrganizationDto from '#dtos/organization'
import OrganizationInviteDto from '#dtos/organization_invite'
import RoleDto from '#dtos/role'
import UserDto from '#dtos/user'

defineProps<{
  organization: OrganizationDto
  user: UserDto
  users: UserDto[]
  roles: RoleDto[]
  invites: OrganizationInviteDto[]
  accessTokens: AccessTokenDto[]
  can: Abilities
}>()
</script>

<template>
  <AppHead
    title="Organization Settings"
    :description="`Manage your ${organization.name} organization`"
  />

  <SettingsShell>
    <OrganizationEditCard v-if="can.organization.edit" :organization="organization" />
    <OrganizationUsersCard :user="user" :users="users" :roles="roles" :can="can" />
    <OrganizationUserInvitesCard :invites="invites" :roles="roles" :can="can" />
    <OrganizationAccessTokensCard :tokens="accessTokens" />
  </SettingsShell>
</template>
Copied!
  • inertia
  • pages
  • settings
  • organization.vue

Join the Discussion 0 comments

Create a free account to join in on the discussion
robot comment bubble

Be the first to comment!