✨ Admin panel & users, users' permissions management
This commit is contained in:
		
							
								
								
									
										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