✨ Admin panel & users, users' permissions management
This commit is contained in:
		
							
								
								
									
										19
									
								
								.idea/workspace.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								.idea/workspace.xml
									
									
									
										generated
									
									
									
								
							| @@ -4,12 +4,17 @@ | ||||
|     <option name="autoReloadType" value="ALL" /> | ||||
|   </component> | ||||
|   <component name="ChangeListManager"> | ||||
|     <list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Admin notify one user"> | ||||
|       <change afterPath="$PROJECT_DIR$/pkg/internal/server/admin/factors_api.go" afterDir="false" /> | ||||
|     <list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Admin check users' auth factor"> | ||||
|       <change afterPath="$PROJECT_DIR$/web/src/components/admin/UserAssignPermsPanel.vue" afterDir="false" /> | ||||
|       <change afterPath="$PROJECT_DIR$/web/src/components/admin/UserDetailPanel.vue" afterDir="false" /> | ||||
|       <change afterPath="$PROJECT_DIR$/web/src/layouts/administrator.vue" afterDir="false" /> | ||||
|       <change afterPath="$PROJECT_DIR$/web/src/views/admin/dashboard.vue" afterDir="false" /> | ||||
|       <change afterPath="$PROJECT_DIR$/web/src/views/admin/users.vue" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/pkg/internal/server/admin/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/admin/index.go" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/pkg/internal/server/admin/user_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/admin/users_api.go" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/settings.toml" beforeDir="false" afterPath="$PROJECT_DIR$/settings.toml" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/web/src/components/navigation/AppBar.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/components/navigation/AppBar.vue" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/web/src/layouts/user-center.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/layouts/user-center.vue" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/web/src/router/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/router/index.ts" afterDir="false" /> | ||||
|       <change beforePath="$PROJECT_DIR$/web/src/views/security.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/views/security.vue" afterDir="false" /> | ||||
|     </list> | ||||
|     <option name="SHOW_DIALOG" value="false" /> | ||||
|     <option name="HIGHLIGHT_CONFLICTS" value="true" /> | ||||
| @@ -155,7 +160,6 @@ | ||||
|     </option> | ||||
|   </component> | ||||
|   <component name="VcsManagerConfiguration"> | ||||
|     <MESSAGE value=":sparkles: Last seen at" /> | ||||
|     <MESSAGE value=":sparkles: Edit, delete current status" /> | ||||
|     <MESSAGE value=":bug: Fix clear status affected the statutes cleared before" /> | ||||
|     <MESSAGE value=":sparkles: Get self-current status API" /> | ||||
| @@ -180,7 +184,8 @@ | ||||
|     <MESSAGE value=":zap: Optimized audit, event logging system
:sparkles: Audit logs
:sparkles: Admin edit user permissions" /> | ||||
|     <MESSAGE value=":sparkles: Admin force confirm account" /> | ||||
|     <MESSAGE value=":sparkles: Admin notify one user" /> | ||||
|     <option name="LAST_COMMIT_MESSAGE" value=":sparkles: Admin notify one user" /> | ||||
|     <MESSAGE value=":sparkles: Admin check users' auth factor" /> | ||||
|     <option name="LAST_COMMIT_MESSAGE" value=":sparkles: Admin check users' auth factor" /> | ||||
|   </component> | ||||
|   <component name="VgoProject"> | ||||
|     <settings-migrated>true</settings-migrated> | ||||
|   | ||||
							
								
								
									
										164
									
								
								web/src/components/admin/UserAssignPermsPanel.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								web/src/components/admin/UserAssignPermsPanel.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| <template> | ||||
|   <v-dialog class="max-w-[720px]" :model-value="data != null" @update:model-value="(val) => !val && emits('close')"> | ||||
|     <template v-slot:default="{ isActive }"> | ||||
|       <v-card title="Assign permissions" :subtitle="`To user @${props.data?.name}`" :loading="submitting"> | ||||
|         <v-card-text> | ||||
|           <v-sheet elevation="2" rounded="lg"> | ||||
|             <v-table density="comfortable"> | ||||
|               <thead> | ||||
|               <tr> | ||||
|                 <th class="text-left"> | ||||
|                   Key | ||||
|                 </th> | ||||
|                 <th class="text-left"> | ||||
|                   Value | ||||
|                 </th> | ||||
|               </tr> | ||||
|               </thead> | ||||
|               <tbody> | ||||
|               <tr | ||||
|                 v-for="[key, val] in Object.entries(perms)" | ||||
|                 :key="key" | ||||
|               > | ||||
|                 <td class="w-1/2"> | ||||
|                   <div> | ||||
|                     <p>{{ key }}</p> | ||||
|                     <div class="flex mx-[-8px]"> | ||||
|                       <v-btn color="error" text="Delete" variant="plain" size="x-small" | ||||
|                              @click="() => deleteNode(key)" /> | ||||
|                       <v-btn class="ms-[-8px]" color="info" text="Change" variant="plain" size="x-small" | ||||
|                              @click="() => changeNodeType(key)" /> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </td> | ||||
|                 <td class="w-1/2"> | ||||
|                   <div class="w-full flex items-center"> | ||||
|                     <v-checkbox v-if="typeof val === 'boolean'" class="my-1" density="comfortable" | ||||
|                                 :hide-details="true" | ||||
|                                 v-model="perms[key]" /> | ||||
|                     <v-number-input v-else-if="typeof val === 'number'" | ||||
|                                     controlVariant="default" | ||||
|                                     :reverse="false" | ||||
|                                     :hideInput="false" | ||||
|                                     :inset="false" | ||||
|                                     class="font-mono my-2" | ||||
|                                     density="compact" :hide-details="true" | ||||
|                                     v-model="perms[key]" /> | ||||
|                     <v-text-field v-else class="font-mono my-2" density="compact" :hide-details="true" | ||||
|                                   v-model="perms[key]" /> | ||||
|                   </div> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td> | ||||
|                   <v-text-field class="my-3.5" label="Key" density="compact" variant="solo-filled" | ||||
|                                 v-model="pendingNodeKey" | ||||
|                                 :hide-details="true" /> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                   <div class="w-full flex justify-center"> | ||||
|                     <v-btn prepend-icon="mdi-plus-circle" text="Add one" block rounded="md" @click="addNode" /> | ||||
|                   </div> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               </tbody> | ||||
|             </v-table> | ||||
|           </v-sheet> | ||||
|         </v-card-text> | ||||
|  | ||||
|         <v-card-actions> | ||||
|           <v-spacer></v-spacer> | ||||
|  | ||||
|           <v-btn | ||||
|             :disabled="submitting" | ||||
|             text="Cancel" | ||||
|             color="grey" | ||||
|             @click="isActive.value = false" | ||||
|           ></v-btn> | ||||
|           <v-btn | ||||
|             :disabled="submitting" | ||||
|             text="Apply Changes" | ||||
|             @click="saveNode" | ||||
|           ></v-btn> | ||||
|         </v-card-actions> | ||||
|       </v-card> | ||||
|     </template> | ||||
|   </v-dialog> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref, watch } from "vue" | ||||
| import { request } from "@/scripts/request" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
|  | ||||
| const perms = ref<any>({}) | ||||
|  | ||||
| const pendingNodeKey = ref("") | ||||
|  | ||||
| const props = defineProps<{ data: any }>() | ||||
| const emits = defineEmits(["close", "success", "error"]) | ||||
|  | ||||
| watch(props, (v) => { | ||||
|   if (v.data != null) { | ||||
|     perms.value = v.data["perm_nodes"] | ||||
|   } | ||||
| }, { immediate: true, deep: true }) | ||||
|  | ||||
| function addNode() { | ||||
|   if (pendingNodeKey.value) { | ||||
|     perms.value[pendingNodeKey.value] = false | ||||
|     pendingNodeKey.value = "" | ||||
|   } | ||||
| } | ||||
|  | ||||
| function deleteNode(key: string) { | ||||
|   delete perms.value[key] | ||||
| } | ||||
|  | ||||
| function changeNodeType(key: string) { | ||||
|   const typelist = [ | ||||
|     "boolean", | ||||
|     "number", | ||||
|     "string", | ||||
|   ] | ||||
|   const idx = typelist.indexOf(typeof perms.value[key]) | ||||
|   if (idx == -1 || idx == typelist.length - 1) { | ||||
|     perms.value[key] = false | ||||
|     return | ||||
|   } | ||||
|   switch (typelist[idx + 1]) { | ||||
|     case "boolean": | ||||
|       perms.value[key] = false | ||||
|       break | ||||
|     case "number": | ||||
|       perms.value[key] = 0 | ||||
|       break | ||||
|     default: | ||||
|       perms.value[key] = "" | ||||
|       break | ||||
|   } | ||||
| } | ||||
|  | ||||
| const submitting = ref(false) | ||||
|  | ||||
| async function saveNode() { | ||||
|   submitting.value = true | ||||
|   const res = await request(`/api/admin/users/${props.data.id}/permissions`, { | ||||
|     method: 'PUT', | ||||
|     headers: { | ||||
|       "Content-Type": "application/json", | ||||
|       "Authorization": `Bearer ${getAtk()}`, | ||||
|     }, | ||||
|     body: JSON.stringify({ | ||||
|       'perm_nodes': perms.value, | ||||
|     }), | ||||
|   }) | ||||
|   if (res.status !== 200) { | ||||
|     emits("error", await res.text()) | ||||
|   } else { | ||||
|     emits("success") | ||||
|     emits("close") | ||||
|   } | ||||
|   submitting.value = false | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										46
									
								
								web/src/components/admin/UserDetailPanel.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								web/src/components/admin/UserDetailPanel.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| <template> | ||||
|   <v-dialog class="max-w-[720px]" :model-value="data != null" @update:model-value="(val) => !val && emits('close')"> | ||||
|     <template v-slot:default="{ isActive }"> | ||||
|       <v-card :title="`User @${props.data?.name}`"> | ||||
|         <v-card-text> | ||||
|           <v-row> | ||||
|             <v-col cols="12" md="6"> | ||||
|               <h4 class="field-title">Name</h4> | ||||
|               <p>{{ props.data?.name }}</p> | ||||
|             </v-col> | ||||
|             <v-col cols="12" md="6"> | ||||
|               <h4 class="field-title">Nick</h4> | ||||
|               <p>{{ props.data?.nick }}</p> | ||||
|             </v-col> | ||||
|             <v-col cols="12"> | ||||
|               <h4 class="field-title">Entire Payload</h4> | ||||
|               <v-code class="font-mono overflow-x-scroll max-h-[360px]"> | ||||
|                 <pre>{{ JSON.stringify(props.data, null, 4) }}</pre> | ||||
|               </v-code> | ||||
|             </v-col> | ||||
|           </v-row> | ||||
|         </v-card-text> | ||||
|  | ||||
|         <v-card-actions> | ||||
|           <v-spacer></v-spacer> | ||||
|  | ||||
|           <v-btn | ||||
|             text="Close" | ||||
|             @click="isActive.value = false" | ||||
|           ></v-btn> | ||||
|         </v-card-actions> | ||||
|       </v-card> | ||||
|     </template> | ||||
|   </v-dialog> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| const props = defineProps<{ data: any }>() | ||||
| const emits = defineEmits(["close"]) | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .field-title { | ||||
|   font-weight: bold; | ||||
| } | ||||
| </style> | ||||
| @@ -3,7 +3,7 @@ | ||||
|     <div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center"> | ||||
|       <router-link :to="{ name: 'dashboard' }" class="flex gap-1 ms-0.5"> | ||||
|         <img src="/favicon.png" alt="logo" width="27" height="24" class="icon-filter" /> | ||||
|         <h2 class="ml-2 text-lg font-500">Solarpass</h2> | ||||
|         <h2 class="ml-2 text-lg font-500">{{ props.title ?? "Solarpass" }}</h2> | ||||
|       </router-link> | ||||
|  | ||||
|       <v-spacer /> | ||||
| @@ -23,7 +23,7 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <template #extension> | ||||
|     <template v-if="slots.extension" #extension> | ||||
|       <slot name="extension" /> | ||||
|     </template> | ||||
|   </v-app-bar> | ||||
| @@ -35,10 +35,13 @@ | ||||
| import NotificationList from "@/components/NotificationList.vue" | ||||
| import UserMenu from "@/components/UserMenu.vue" | ||||
| import { useNotifications } from "@/stores/notifications" | ||||
| import { ref } from "vue" | ||||
| import { ref, useSlots } from "vue" | ||||
|  | ||||
| const notify = useNotifications() | ||||
|  | ||||
| const slots = useSlots() | ||||
| const props = defineProps<{ title?: String }>() | ||||
|  | ||||
| const openNotify = ref(false) | ||||
| </script> | ||||
|  | ||||
|   | ||||
							
								
								
									
										30
									
								
								web/src/layouts/administrator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								web/src/layouts/administrator.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <template> | ||||
|   <app-bar title="Solarpass Administration" /> | ||||
|  | ||||
|   <v-main> | ||||
|     <router-view /> | ||||
|   </v-main> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { useUserinfo } from "@/stores/userinfo" | ||||
| import { useRouter } from "vue-router" | ||||
| import { onMounted } from "vue" | ||||
| import AppBar from "@/components/navigation/AppBar.vue" | ||||
|  | ||||
| const id = useUserinfo() | ||||
| const router = useRouter() | ||||
|  | ||||
| onMounted(async () => { | ||||
|   await id.readProfiles() | ||||
|   if (!id.userinfo.data.perm_nodes["AdminView"]) { | ||||
|     await router.push({ name: "dashboard" }) | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .icon-filter { | ||||
|   filter: invert(100%) sepia(100%) saturate(14%) hue-rotate(212deg) brightness(104%) contrast(104%); | ||||
| } | ||||
| </style> | ||||
| @@ -25,6 +25,6 @@ import Copyright from "@/components/Copyright.vue" | ||||
|  | ||||
| <style scoped> | ||||
| .p-container { | ||||
|   max-width: 40rem; | ||||
|   max-width: 64rem; | ||||
| } | ||||
| </style> | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { createRouter, createWebHistory } from "vue-router" | ||||
| import { useUserinfo } from "@/stores/userinfo" | ||||
| import UserCenterLayout from "@/layouts/user-center.vue" | ||||
| import AdministratorLayout from "@/layouts/administrator.vue" | ||||
|  | ||||
| const router = createRouter({ | ||||
|   history: createWebHistory(import.meta.env.BASE_URL), | ||||
| @@ -74,6 +75,22 @@ const router = createRouter({ | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|     { | ||||
|       path: "/admin", | ||||
|       component: AdministratorLayout, | ||||
|       children: [ | ||||
|         { | ||||
|           path: "", | ||||
|           name: "admin.dashboard", | ||||
|           component: () => import("@/views/admin/dashboard.vue"), | ||||
|         }, | ||||
|         { | ||||
|           path: "users", | ||||
|           name: "admin.users", | ||||
|           component: () => import("@/views/admin/users.vue"), | ||||
|         }, | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
| }) | ||||
|  | ||||
|   | ||||
							
								
								
									
										51
									
								
								web/src/views/admin/dashboard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								web/src/views/admin/dashboard.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| <template> | ||||
|   <div class="w-full h-full flex justify-center items-center"> | ||||
|     <v-empty-state | ||||
|       headline="Administration" | ||||
|       icon="mdi-cog" | ||||
|       title="What would you like to do today?" | ||||
|     > | ||||
|       <v-container> | ||||
|         <v-row> | ||||
|           <v-col cols="12" md="6"> | ||||
|             <v-card | ||||
|               :to="{ name: 'admin.users' }" | ||||
|               prepend-icon="mdi-account-group" | ||||
|               text="Manage to help users do something they can't" | ||||
|               title="Users" | ||||
|             ></v-card> | ||||
|           </v-col> | ||||
|  | ||||
|           <v-col cols="12" md="6"> | ||||
|             <v-card | ||||
|               disabled | ||||
|               prepend-icon="mdi-comment-quote" | ||||
|               text="Manage the content on the platform" | ||||
|               title="Posts & Articles" | ||||
|             ></v-card> | ||||
|           </v-col> | ||||
|  | ||||
|           <v-col cols="12" md="6"> | ||||
|             <v-card | ||||
|               disabled | ||||
|               prepend-icon="mdi-file-cabinet" | ||||
|               text="Manage attachments on the platform" | ||||
|               title="Attachments" | ||||
|             ></v-card> | ||||
|           </v-col> | ||||
|  | ||||
|           <v-col cols="12" md="6"> | ||||
|             <v-card | ||||
|               disabled | ||||
|               prepend-icon="mdi-ticket" | ||||
|               text="Solve the tickets issued by users" | ||||
|               title="Tickets" | ||||
|             ></v-card> | ||||
|           </v-col> | ||||
|         </v-row> | ||||
|       </v-container> | ||||
|     </v-empty-state> | ||||
|   </div> | ||||
| </template> | ||||
| <script setup lang="ts"> | ||||
| </script> | ||||
							
								
								
									
										123
									
								
								web/src/views/admin/users.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								web/src/views/admin/users.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-data-table-server | ||||
|       fixed-header | ||||
|       class="h-full" | ||||
|       density="compact" | ||||
|       :headers="dataDefinitions.users" | ||||
|       :items="users" | ||||
|       :items-length="pagination.total" | ||||
|       :loading="reverting" | ||||
|       v-model:items-per-page="pagination.pageSize" | ||||
|       @update:options="readUsers" | ||||
|       item-value="id" | ||||
|     > | ||||
|       <template v-slot:top> | ||||
|         <v-toolbar color="secondary"> | ||||
|           <div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center"> | ||||
|             <v-btn class="me-2" icon="mdi-account-group" density="compact" :to="{ name: 'admin.dashboard' }" exact /> | ||||
|             <h3 class="ml-2 text-lg font-500">Users</h3> | ||||
|           </div> | ||||
|         </v-toolbar> | ||||
|       </template> | ||||
|  | ||||
|       <template v-slot:item="{ item }: { item: any }"> | ||||
|         <tr> | ||||
|           <td>{{ item.id }}</td> | ||||
|           <td>{{ item.name }}</td> | ||||
|           <td>{{ item.nick }}</td> | ||||
|           <td>{{ new Date(item.created_at).toLocaleString() }}</td> | ||||
|           <td> | ||||
|             <v-tooltip text="Details"> | ||||
|               <template #activator="{ props }"> | ||||
|                 <v-btn | ||||
|                   v-bind="props" | ||||
|                   variant="text" | ||||
|                   size="x-small" | ||||
|                   color="info" | ||||
|                   icon="mdi-dots-horizontal" | ||||
|                   @click="viewingUser = item" | ||||
|                 /> | ||||
|               </template> | ||||
|             </v-tooltip> | ||||
|             <v-tooltip text="Assign Permissions"> | ||||
|               <template #activator="{ props }"> | ||||
|                 <v-btn | ||||
|                   v-bind="props" | ||||
|                   variant="text" | ||||
|                   size="x-small" | ||||
|                   color="teal" | ||||
|                   icon="mdi-code-block-braces" | ||||
|                   @click="assigningPermUser = item" | ||||
|                 /> | ||||
|               </template> | ||||
|             </v-tooltip> | ||||
|           </td> | ||||
|         </tr> | ||||
|       </template> | ||||
|     </v-data-table-server> | ||||
|  | ||||
|     <user-detail-panel :data="viewingUser" @close="viewingUser = null" /> | ||||
|     <user-assign-perms-panel :data="assigningPermUser" @close="assigningPermUser = null" | ||||
|                              @success="readUsers(pagination)" | ||||
|                              @error="val => error = val" /> | ||||
|  | ||||
|     <v-snackbar :timeout="3000" :model-value="error != null" @update:model-value="_ => error = null"> | ||||
|       {{ error }} | ||||
|     </v-snackbar> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { onMounted, reactive, ref } from "vue" | ||||
| import { request } from "@/scripts/request" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import UserDetailPanel from "@/components/admin/UserDetailPanel.vue" | ||||
| import UserAssignPermsPanel from "@/components/admin/UserAssignPermsPanel.vue" | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
|  | ||||
| const users = ref<any[]>([]) | ||||
|  | ||||
| const viewingUser = ref<any>(null) | ||||
| const assigningPermUser = ref<any>(null) | ||||
|  | ||||
| const dataDefinitions: { [id: string]: any[] } = { | ||||
|   users: [ | ||||
|     { align: "start", key: "id", title: "ID" }, | ||||
|     { align: "start", key: "name", title: "Name" }, | ||||
|     { align: "start", key: "nick", title: "Nick" }, | ||||
|     { align: "start", key: "created_at", title: "Created At" }, | ||||
|     { align: "start", key: "actions", title: "Actions", sortable: false }, | ||||
|   ], | ||||
| } | ||||
|  | ||||
| const reverting = ref(true) | ||||
| const pagination = reactive({ | ||||
|   page: 1, pageSize: 5, total: 0, | ||||
| }) | ||||
|  | ||||
| async function readUsers({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) { | ||||
|   reverting.value = true | ||||
|   const res = await request( | ||||
|     "/api/admin/users?" + | ||||
|     new URLSearchParams({ | ||||
|       take: pagination.pageSize.toString(), | ||||
|       offset: ((pagination.page - 1) * pagination.pageSize).toString(), | ||||
|     }), | ||||
|     { | ||||
|       headers: { Authorization: `Bearer ${getAtk()}` }, | ||||
|     }, | ||||
|   ) | ||||
|   if (res.status !== 200) { | ||||
|     error.value = await res.text() | ||||
|   } else { | ||||
|     const data = await res.json() | ||||
|     users.value = data["data"] | ||||
|     pagination.total = data["count"] | ||||
|   } | ||||
|   reverting.value = false | ||||
| } | ||||
|  | ||||
| onMounted(() => readUsers({})) | ||||
| </script> | ||||
| @@ -29,7 +29,7 @@ | ||||
|                   </td> | ||||
|                   <td>{{ new Date(item.created_at).toLocaleString() }}</td> | ||||
|                   <td> | ||||
|                     <v-tooltip text="Sign out"> | ||||
|                     <v-tooltip text="Sign Out"> | ||||
|                       <template #activator="{ props }"> | ||||
|                         <v-btn | ||||
|                           v-bind="props" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user