✨ Dev portal bot keys
This commit is contained in:
		@@ -12,6 +12,7 @@
 | 
			
		||||
            v-model:items-per-page="pagination.tickets.pageSize"
 | 
			
		||||
            @update:options="readTickets"
 | 
			
		||||
            item-value="id"
 | 
			
		||||
            class="overflow-y-auto text-no-wrap"
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:item="{ item }: { item: any }">
 | 
			
		||||
              <tr>
 | 
			
		||||
@@ -60,6 +61,7 @@
 | 
			
		||||
            v-model:items-per-page="pagination.events.pageSize"
 | 
			
		||||
            @update:options="readEvents"
 | 
			
		||||
            item-value="id"
 | 
			
		||||
            class="overflow-y-auto text-no-wrap"
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:item="{ item }: { item: any }">
 | 
			
		||||
              <tr>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										83
									
								
								components/dev/BotTokenCreate.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								components/dev/BotTokenCreate.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-dialog max-width="640">
 | 
			
		||||
    <template v-slot:activator="{ props }">
 | 
			
		||||
      <slot name="activator" v-bind="{ props }" />
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
    <template v-slot:default="{ isActive }">
 | 
			
		||||
      <v-card title="Create Bot Key" :subtitle="`for bot @${props.item.name}`">
 | 
			
		||||
        <v-form @submit.prevent="(evt) => { submit(evt).then(() => isActive.value = false) }">
 | 
			
		||||
          <v-card-text class="pt-0 px-5">
 | 
			
		||||
            <v-expand-transition>
 | 
			
		||||
              <v-alert v-if="error" variant="tonal" type="error" class="text-xs mb-5">
 | 
			
		||||
                {{ t("errorOccurred", [error]) }}
 | 
			
		||||
              </v-alert>
 | 
			
		||||
            </v-expand-transition>
 | 
			
		||||
 | 
			
		||||
            <v-row>
 | 
			
		||||
              <v-col cols="12" md="6">
 | 
			
		||||
                <v-text-field label="Name" name="name" variant="outlined" hide-details />
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="12" md="6">
 | 
			
		||||
                <v-textarea auto-grow rows="1" label="Description" name="description" variant="outlined" hide-details />
 | 
			
		||||
              </v-col>
 | 
			
		||||
              <v-col cols="12">
 | 
			
		||||
                <v-text-field type="number" label="Lifecycle" name="lifecycle" variant="outlined"
 | 
			
		||||
                              hint="How long will this key last (in seconds)" clearable persistent-hint />
 | 
			
		||||
              </v-col>
 | 
			
		||||
            </v-row>
 | 
			
		||||
          </v-card-text>
 | 
			
		||||
 | 
			
		||||
          <v-card-actions>
 | 
			
		||||
            <v-spacer />
 | 
			
		||||
 | 
			
		||||
            <v-btn
 | 
			
		||||
              text="Cancel"
 | 
			
		||||
              color="grey"
 | 
			
		||||
              @click="isActive.value = false"
 | 
			
		||||
            />
 | 
			
		||||
            <v-btn
 | 
			
		||||
              text="Create"
 | 
			
		||||
              type="submit"
 | 
			
		||||
            />
 | 
			
		||||
          </v-card-actions>
 | 
			
		||||
        </v-form>
 | 
			
		||||
      </v-card>
 | 
			
		||||
    </template>
 | 
			
		||||
  </v-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
const props = defineProps<{ item: any }>()
 | 
			
		||||
const emits = defineEmits(["completed"])
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n()
 | 
			
		||||
 | 
			
		||||
const error = ref<null | string>(null)
 | 
			
		||||
 | 
			
		||||
const submitting = ref(false)
 | 
			
		||||
 | 
			
		||||
async function submit(evt: SubmitEvent) {
 | 
			
		||||
  const data: any = Object.fromEntries(new FormData(evt.target as HTMLFormElement).entries())
 | 
			
		||||
  if (!data.name) return
 | 
			
		||||
 | 
			
		||||
  data.lifecycle = parseInt(data.lifecycle)
 | 
			
		||||
  if (Number.isNaN(data.lifecycle)) delete data.lifecycle
 | 
			
		||||
 | 
			
		||||
  submitting.value = true
 | 
			
		||||
 | 
			
		||||
  const res = await solarFetch(`/cgi/id/dev/bots/${props.item.id}/keys`, {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: { "Content-Type": "application/json" },
 | 
			
		||||
    body: JSON.stringify(data),
 | 
			
		||||
  })
 | 
			
		||||
  if (res.status != 200) {
 | 
			
		||||
    error.value = await res.text()
 | 
			
		||||
    throw new Error(error.value)
 | 
			
		||||
  } else {
 | 
			
		||||
    emits("completed")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  submitting.value = false
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										157
									
								
								components/dev/BotTokenDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								components/dev/BotTokenDialog.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-dialog max-width="640">
 | 
			
		||||
    <template v-slot:activator="{ props }">
 | 
			
		||||
      <slot name="activator" v-bind="{ props }" />
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
    <v-card title="Bot Keys" :subtitle="`of bot @${props.item.name}`">
 | 
			
		||||
      <v-card-text class="pb-0 pt-0">
 | 
			
		||||
        <v-card variant="outlined">
 | 
			
		||||
          <v-data-table-server
 | 
			
		||||
            density="default"
 | 
			
		||||
            :headers="dataDefinitions.keys"
 | 
			
		||||
            :items="keys"
 | 
			
		||||
            :items-length="pagination.keys.total"
 | 
			
		||||
            :loading="reverting.keys"
 | 
			
		||||
            v-model:items-per-page="pagination.keys.pageSize"
 | 
			
		||||
            @update:options="readKeys"
 | 
			
		||||
            item-value="id"
 | 
			
		||||
            class="overflow-y-auto text-no-wrap"
 | 
			
		||||
          >
 | 
			
		||||
            <template v-slot:item="{ item }: { item: any }">
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>{{ item.id }}</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <p>{{ item.name }}</p>
 | 
			
		||||
                  <p class="text-xs">{{ item.description }}</p>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{{ new Date(item.created_at).toLocaleString() }}</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <dev-bot-token-grant :item="item">
 | 
			
		||||
                    <template #activator="{ props }">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        v-bind="props"
 | 
			
		||||
                        variant="text"
 | 
			
		||||
                        size="x-small"
 | 
			
		||||
                        color="info"
 | 
			
		||||
                        icon="mdi-key-variant"
 | 
			
		||||
                      />
 | 
			
		||||
                    </template>
 | 
			
		||||
                  </dev-bot-token-grant>
 | 
			
		||||
 | 
			
		||||
                  <v-dialog max-width="480">
 | 
			
		||||
                    <template #activator="{ props }">
 | 
			
		||||
                      <v-btn
 | 
			
		||||
                        v-bind="props"
 | 
			
		||||
                        variant="text"
 | 
			
		||||
                        size="x-small"
 | 
			
		||||
                        color="error"
 | 
			
		||||
                        icon="mdi-delete"
 | 
			
		||||
                        :disabled="submitting"
 | 
			
		||||
                      />
 | 
			
		||||
                    </template>
 | 
			
		||||
 | 
			
		||||
                    <template v-slot:default="{ isActive }">
 | 
			
		||||
                      <v-card :title="`Delete token ${item.name}?`">
 | 
			
		||||
                        <v-card-text>
 | 
			
		||||
                          This action will delete the token and invalid it immediately.
 | 
			
		||||
                        </v-card-text>
 | 
			
		||||
 | 
			
		||||
                        <v-card-actions>
 | 
			
		||||
                          <v-spacer></v-spacer>
 | 
			
		||||
 | 
			
		||||
                          <v-btn
 | 
			
		||||
                            text="Cancel"
 | 
			
		||||
                            color="grey"
 | 
			
		||||
                            @click="isActive.value = false"
 | 
			
		||||
                          ></v-btn>
 | 
			
		||||
 | 
			
		||||
                          <v-btn
 | 
			
		||||
                            text="Delete"
 | 
			
		||||
                            color="error"
 | 
			
		||||
                            @click="() => { revokeKey(item); isActive.value = false }"
 | 
			
		||||
                          />
 | 
			
		||||
                        </v-card-actions>
 | 
			
		||||
                      </v-card>
 | 
			
		||||
                    </template>
 | 
			
		||||
                  </v-dialog>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </template>
 | 
			
		||||
          </v-data-table-server>
 | 
			
		||||
        </v-card>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
 | 
			
		||||
      <div class="flex justify-end px-5.5 py-5">
 | 
			
		||||
        <dev-bot-token-create :item="props.item" @completed="readKeys({})">
 | 
			
		||||
          <template #activator="{ props }">
 | 
			
		||||
            <v-btn variant="flat" text="Create" append-icon="mdi-plus" v-bind="props" />
 | 
			
		||||
          </template>
 | 
			
		||||
        </dev-bot-token-create>
 | 
			
		||||
      </div>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </v-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { solarFetch } from "~/utils/request"
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ item: any }>()
 | 
			
		||||
 | 
			
		||||
const keys = ref<any[]>([])
 | 
			
		||||
 | 
			
		||||
const error = ref<null | string>(null)
 | 
			
		||||
 | 
			
		||||
const dataDefinitions: { [id: string]: any[] } = {
 | 
			
		||||
  keys: [
 | 
			
		||||
    { align: "start", key: "id", title: "ID" },
 | 
			
		||||
    { align: "start", key: "name", title: "Name" },
 | 
			
		||||
    { align: "start", key: "created_at", title: "Created At" },
 | 
			
		||||
    { align: "start", key: "actions", title: "Actions", sortable: false },
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const reverting = reactive({ keys: false })
 | 
			
		||||
const pagination = reactive({
 | 
			
		||||
  keys: { page: 1, pageSize: 5, total: 0 },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
async function readKeys({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
 | 
			
		||||
  if (itemsPerPage) pagination.keys.pageSize = itemsPerPage
 | 
			
		||||
  if (page) pagination.keys.page = page
 | 
			
		||||
 | 
			
		||||
  reverting.keys = true
 | 
			
		||||
  const res = await solarFetch(
 | 
			
		||||
    `/cgi/id/dev/bots/${props.item.id}/keys?` +
 | 
			
		||||
    new URLSearchParams({
 | 
			
		||||
      take: pagination.keys.pageSize.toString(),
 | 
			
		||||
      offset: ((pagination.keys.page - 1) * pagination.keys.pageSize).toString(),
 | 
			
		||||
    }),
 | 
			
		||||
  )
 | 
			
		||||
  if (res.status !== 200) {
 | 
			
		||||
    error.value = await res.text()
 | 
			
		||||
  } else {
 | 
			
		||||
    const data = await res.json()
 | 
			
		||||
    keys.value = data["data"]
 | 
			
		||||
    pagination.keys.total = data["count"]
 | 
			
		||||
  }
 | 
			
		||||
  reverting.keys = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => readKeys({}))
 | 
			
		||||
 | 
			
		||||
async function revokeKey(item: any) {
 | 
			
		||||
  submitting.value = true
 | 
			
		||||
  const res = await solarFetch(`/cgi/id/dev/bots/${item.account_id}/keys/${item.id}`, {
 | 
			
		||||
    method: "DELETE",
 | 
			
		||||
  })
 | 
			
		||||
  if (res.status !== 200) {
 | 
			
		||||
    error.value = await res.text()
 | 
			
		||||
  } else {
 | 
			
		||||
    await readKeys({ page: 1 })
 | 
			
		||||
  }
 | 
			
		||||
  submitting.value = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const submitting = ref(false)
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										103
									
								
								components/dev/BotTokenGrant.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								components/dev/BotTokenGrant.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-dialog max-width="640">
 | 
			
		||||
    <template v-slot:activator="{ props }">
 | 
			
		||||
      <slot name="activator" v-bind="{ props }" />
 | 
			
		||||
    </template>
 | 
			
		||||
 | 
			
		||||
    <v-card title="Bot Key" :subtitle="`#${props.item.id.toString().padStart(8, '0')}`">
 | 
			
		||||
      <v-card-text>
 | 
			
		||||
        <v-row>
 | 
			
		||||
          <v-col cols="6">
 | 
			
		||||
            <div class="flex justify-between items-center">
 | 
			
		||||
              <span>Granted</span>
 | 
			
		||||
              <v-icon :icon="getIcon(props.item.ticket.last_grant_at != null)" size="16" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </v-col>
 | 
			
		||||
          <v-col cols="6">
 | 
			
		||||
            <div class="flex justify-between items-center">
 | 
			
		||||
              <span>Lifecycle</span>
 | 
			
		||||
              <span class="font-mono">{{ props.item.lifecycle ?? "-" }}</span>
 | 
			
		||||
            </div>
 | 
			
		||||
          </v-col>
 | 
			
		||||
        </v-row>
 | 
			
		||||
 | 
			
		||||
        <v-expand-transition>
 | 
			
		||||
          <v-alert v-if="error" variant="tonal" type="error" class="text-xs mt-5">
 | 
			
		||||
            {{ t("errorOccurred", [error]) }}
 | 
			
		||||
          </v-alert>
 | 
			
		||||
        </v-expand-transition>
 | 
			
		||||
 | 
			
		||||
        <v-expand-transition>
 | 
			
		||||
          <div v-if="token" class="flex flex-col gap-2 mt-5">
 | 
			
		||||
            <div>
 | 
			
		||||
              <p class="mb-0.25">Access Token</p>
 | 
			
		||||
              <v-code class="font-mono px-3 mx-[-4px] overflow-y-auto text-no-wrap">
 | 
			
		||||
                {{ token.access_token }}
 | 
			
		||||
              </v-code>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div>
 | 
			
		||||
              <p class="mb-0.25">Refresh Token</p>
 | 
			
		||||
              <v-code class="font-mono px-3 mx-[-4px] overflow-y-auto text-no-wrap">
 | 
			
		||||
                {{ token.refresh_token }}
 | 
			
		||||
              </v-code>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </v-expand-transition>
 | 
			
		||||
      </v-card-text>
 | 
			
		||||
 | 
			
		||||
      <div class="flex justify-end px-5.5 py-5">
 | 
			
		||||
        <v-btn variant="tonal" text="Roll / Grant" append-icon="mdi-refresh" :loading="submitting" @click="getToken" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </v-card>
 | 
			
		||||
  </v-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
const { t } = useI18n()
 | 
			
		||||
const props = defineProps<{ item: any }>()
 | 
			
		||||
 | 
			
		||||
const error = ref<null | string>(null)
 | 
			
		||||
 | 
			
		||||
const token = ref<null | { access_token: string, refresh_token: string }>(null)
 | 
			
		||||
 | 
			
		||||
const submitting = ref(false)
 | 
			
		||||
 | 
			
		||||
function getIcon(value: boolean): string {
 | 
			
		||||
  if (value) return "mdi-check"
 | 
			
		||||
  else return "mdi-close"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getToken() {
 | 
			
		||||
  submitting.value = true
 | 
			
		||||
 | 
			
		||||
  let code = props.item.ticket.grant_token
 | 
			
		||||
  if (props.item.ticket.last_grant_at != null) {
 | 
			
		||||
    const res = await solarFetch(`/cgi/id/dev/bots/${props.item.account_id}/keys/${props.item.id}/roll`, {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
    })
 | 
			
		||||
    if (res.status != 200) {
 | 
			
		||||
      error.value = await res.text()
 | 
			
		||||
      submitting.value = false
 | 
			
		||||
      return
 | 
			
		||||
    } else {
 | 
			
		||||
      code = (await res.json()).ticket.grant_token
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const res = await solarFetch("/cgi/id/auth/token", {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: { "Content-Type": "application/json" },
 | 
			
		||||
    body: JSON.stringify({
 | 
			
		||||
      grant_type: "grant_token",
 | 
			
		||||
      code: code,
 | 
			
		||||
    }),
 | 
			
		||||
  })
 | 
			
		||||
  if (res.status != 200) {
 | 
			
		||||
    error.value = await res.text()
 | 
			
		||||
  } else {
 | 
			
		||||
    token.value = await res.json()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  submitting.value = false
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
@@ -37,6 +37,18 @@
 | 
			
		||||
              <td>{{ item.name }}</td>
 | 
			
		||||
              <td>{{ new Date(item.created_at).toLocaleString() }}</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <dev-bot-token-dialog :item="item">
 | 
			
		||||
                  <template #activator="{ props }">
 | 
			
		||||
                    <v-btn
 | 
			
		||||
                      v-bind="props"
 | 
			
		||||
                      variant="text"
 | 
			
		||||
                      size="x-small"
 | 
			
		||||
                      color="info"
 | 
			
		||||
                      icon="mdi-key"
 | 
			
		||||
                    />
 | 
			
		||||
                  </template>
 | 
			
		||||
                </dev-bot-token-dialog>
 | 
			
		||||
 | 
			
		||||
                <v-dialog max-width="480">
 | 
			
		||||
                  <template #activator="{ props }">
 | 
			
		||||
                    <v-btn
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user