✨ Dev portal bot keys
This commit is contained in:
		| @@ -12,6 +12,7 @@ | |||||||
|             v-model:items-per-page="pagination.tickets.pageSize" |             v-model:items-per-page="pagination.tickets.pageSize" | ||||||
|             @update:options="readTickets" |             @update:options="readTickets" | ||||||
|             item-value="id" |             item-value="id" | ||||||
|  |             class="overflow-y-auto text-no-wrap" | ||||||
|           > |           > | ||||||
|             <template v-slot:item="{ item }: { item: any }"> |             <template v-slot:item="{ item }: { item: any }"> | ||||||
|               <tr> |               <tr> | ||||||
| @@ -60,6 +61,7 @@ | |||||||
|             v-model:items-per-page="pagination.events.pageSize" |             v-model:items-per-page="pagination.events.pageSize" | ||||||
|             @update:options="readEvents" |             @update:options="readEvents" | ||||||
|             item-value="id" |             item-value="id" | ||||||
|  |             class="overflow-y-auto text-no-wrap" | ||||||
|           > |           > | ||||||
|             <template v-slot:item="{ item }: { item: any }"> |             <template v-slot:item="{ item }: { item: any }"> | ||||||
|               <tr> |               <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>{{ item.name }}</td> | ||||||
|               <td>{{ new Date(item.created_at).toLocaleString() }}</td> |               <td>{{ new Date(item.created_at).toLocaleString() }}</td> | ||||||
|               <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"> |                 <v-dialog max-width="480"> | ||||||
|                   <template #activator="{ props }"> |                   <template #activator="{ props }"> | ||||||
|                     <v-btn |                     <v-btn | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user