✨ 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