♻️ OAuth authenticate
This commit is contained in:
		@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="text-xs text-center opacity-80">
 | 
			
		||||
    <p>Copyright © {{ new Date().getFullYear() }} Solsynth</p>
 | 
			
		||||
    <p>Copyright © {{ new Date().getFullYear() }} Solsynth LLC</p>
 | 
			
		||||
    <p>Powered by <a class="underline" href="https://git.solsynth.dev/Hydrogen/Passport">Hydrogen.Passport</a></p>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,17 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-menu eager :close-on-content-click="false">
 | 
			
		||||
    <template #activator="{ props }">
 | 
			
		||||
      <v-btn v-bind="props" icon size="small" variant="text" :loading="loading">
 | 
			
		||||
        <v-badge v-if="notify.total > 0" color="error" :content="notify.total">
 | 
			
		||||
          <v-icon icon="mdi-bell" />
 | 
			
		||||
        </v-badge>
 | 
			
		||||
  <v-navigation-drawer :model-value="props.open" @update:model-value="val => emits('update:open', val)" location="right"
 | 
			
		||||
                       temporary order="0" width="400">
 | 
			
		||||
    <v-list-item prepend-icon="mdi-bell" title="Notifications" class="py-3"></v-list-item>
 | 
			
		||||
 | 
			
		||||
        <v-icon v-else icon="mdi-bell" />
 | 
			
		||||
      </v-btn>
 | 
			
		||||
    </template>
 | 
			
		||||
    <v-divider color="black" class="mb-1" />
 | 
			
		||||
 | 
			
		||||
    <v-list v-if="notify.notifications.length <= 0" class="w-[380px]" density="compact">
 | 
			
		||||
      <v-list-item>
 | 
			
		||||
        <v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for you.</v-alert>
 | 
			
		||||
      </v-list-item>
 | 
			
		||||
    <v-list v-if="notify.notifications.length <= 0" density="compact">
 | 
			
		||||
      <v-list-item color="secondary" prepend-icon="mdi-check" title="All notifications read"
 | 
			
		||||
                   subtitle="There is no more new things for you..." />
 | 
			
		||||
    </v-list>
 | 
			
		||||
 | 
			
		||||
    <v-list v-else class="w-[380px]" density="compact" lines="three">
 | 
			
		||||
      <v-list-item v-for="(item, idx) in notify.notifications">
 | 
			
		||||
      <v-list-item v-for="(item, idx) in notify.notifications" :key="idx">
 | 
			
		||||
        <template #title>{{ item.subject }}</template>
 | 
			
		||||
        <template #subtitle>{{ item.content }}</template>
 | 
			
		||||
 | 
			
		||||
@@ -26,11 +20,12 @@
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <div class="flex text-xs gap-1">
 | 
			
		||||
          <a v-for="link in item.links" class="mt-1 underline" target="_blank" :href="link.url">{{ link.label }}</a>
 | 
			
		||||
          <a v-for="(link, idx) in item.links" :key="idx" class="mt-1 underline" target="_blank"
 | 
			
		||||
             :href="link.url">{{ link.label }}</a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </v-list-item>
 | 
			
		||||
    </v-list>
 | 
			
		||||
  </v-menu>
 | 
			
		||||
  </v-navigation-drawer>
 | 
			
		||||
 | 
			
		||||
  <!-- @vue-ignore -->
 | 
			
		||||
  <v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
 | 
			
		||||
@@ -39,8 +34,11 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { request } from "@/scripts/request"
 | 
			
		||||
import { getAtk } from "@/stores/userinfo"
 | 
			
		||||
import { computed, onMounted, onUnmounted, ref } from "vue";
 | 
			
		||||
import { useNotifications } from "@/stores/notifications";
 | 
			
		||||
import { computed, onMounted, onUnmounted, ref } from "vue"
 | 
			
		||||
import { useNotifications } from "@/stores/notifications"
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{ open: boolean }>()
 | 
			
		||||
const emits = defineEmits(["update:open"])
 | 
			
		||||
 | 
			
		||||
const notify = useNotifications()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								web/src/components/navigation/AppBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								web/src/components/navigation/AppBar.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-app-bar height="64" color="primary" scroll-behavior="elevate" flat>
 | 
			
		||||
    <div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
 | 
			
		||||
      <router-link :to="{ name: 'dashboard' }" class="flex gap-1">
 | 
			
		||||
        <img src="/favicon.png" alt="logo" width="27" height="24" class="icon-filter" />
 | 
			
		||||
        <h2 class="ml-2 text-lg font-500">Solarpass</h2>
 | 
			
		||||
      </router-link>
 | 
			
		||||
 | 
			
		||||
      <v-spacer />
 | 
			
		||||
 | 
			
		||||
      <div class="me-2">
 | 
			
		||||
        <v-btn icon size="small" variant="text" @click="openNotify = !openNotify">
 | 
			
		||||
          <v-badge v-if="notify.total > 0" color="error" :content="notify.total">
 | 
			
		||||
            <v-icon icon="mdi-bell" />
 | 
			
		||||
          </v-badge>
 | 
			
		||||
 | 
			
		||||
          <v-icon v-else icon="mdi-bell" />
 | 
			
		||||
        </v-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <user-menu />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <template #extension>
 | 
			
		||||
      <slot name="extension" />
 | 
			
		||||
    </template>
 | 
			
		||||
  </v-app-bar>
 | 
			
		||||
 | 
			
		||||
  <NotificationList v-model:open="openNotify" />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import NotificationList from "@/components/NotificationList.vue"
 | 
			
		||||
import UserMenu from "@/components/UserMenu.vue"
 | 
			
		||||
import { useNotifications } from "@/stores/notifications"
 | 
			
		||||
import { ref } from "vue"
 | 
			
		||||
 | 
			
		||||
const notify = useNotifications()
 | 
			
		||||
 | 
			
		||||
const openNotify = ref(false)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.icon-filter {
 | 
			
		||||
  filter: invert(100%) sepia(100%) saturate(14%) hue-rotate(212deg) brightness(104%) contrast(104%);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,22 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-app-bar height="64" color="primary" scroll-behavior="elevate" flat>
 | 
			
		||||
    <div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
 | 
			
		||||
      <router-link :to="{ name: 'dashboard' }" class="flex gap-1">
 | 
			
		||||
        <img src="/favicon.png" width="27" height="24" class="icon-filter" />
 | 
			
		||||
        <h2 class="ml-2 text-lg font-500">Solarpass</h2>
 | 
			
		||||
      </router-link>
 | 
			
		||||
 | 
			
		||||
      <v-spacer />
 | 
			
		||||
 | 
			
		||||
      <div class="me-2">
 | 
			
		||||
        <notification-list />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div>
 | 
			
		||||
        <user-menu />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </v-app-bar>
 | 
			
		||||
  <AppBar />
 | 
			
		||||
 | 
			
		||||
  <v-main>
 | 
			
		||||
    <router-view />
 | 
			
		||||
@@ -25,8 +8,7 @@
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useUserinfo } from "@/stores/userinfo"
 | 
			
		||||
import NotificationList from "@/components/NotificationList.vue"
 | 
			
		||||
import UserMenu from "@/components/UserMenu.vue"
 | 
			
		||||
import AppBar from "@/components/navigation/AppBar.vue"
 | 
			
		||||
 | 
			
		||||
const id = useUserinfo()
 | 
			
		||||
 | 
			
		||||
@@ -34,12 +16,6 @@ id.readProfiles()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.editor-fab {
 | 
			
		||||
  position: fixed !important;
 | 
			
		||||
  bottom: 16px;
 | 
			
		||||
  right: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon-filter {
 | 
			
		||||
  filter: invert(100%) sepia(100%) saturate(14%) hue-rotate(212deg) brightness(104%) contrast(104%);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,30 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <v-container class="pt-6 px-6">
 | 
			
		||||
    <v-row>
 | 
			
		||||
      <v-col :cols="12" :xs="12" :sm="12" :md="4" :lg="3">
 | 
			
		||||
        <v-card title="Navigation">
 | 
			
		||||
          <v-list density="comfortable">
 | 
			
		||||
            <v-list-item title="Dashboard" prepend-icon="mdi-view-dashboard" :to="{ name: 'dashboard' }" exact />
 | 
			
		||||
            <v-list-item title="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'personalize' }" />
 | 
			
		||||
            <v-list-item title="Security" prepend-icon="mdi-security" :to="{ name: 'security' }" />
 | 
			
		||||
          </v-list>
 | 
			
		||||
        </v-card>
 | 
			
		||||
      </v-col>
 | 
			
		||||
  <AppBar>
 | 
			
		||||
    <template #extension>
 | 
			
		||||
      <v-tabs align-tabs="title" color="white">
 | 
			
		||||
        <v-tab text="Dashboard" prepend-icon="mdi-view-dashboard" :to="{ name: 'dashboard' }" exact />
 | 
			
		||||
        <v-tab text="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'personalize' }" exact />
 | 
			
		||||
        <v-tab text="Security" prepend-icon="mdi-security" :to="{ name: 'security' }" exact />
 | 
			
		||||
      </v-tabs>
 | 
			
		||||
    </template>
 | 
			
		||||
  </AppBar>
 | 
			
		||||
 | 
			
		||||
      <v-col :cols="12" :xs="12" :sm="12" :md="8" :lg="9">
 | 
			
		||||
        <router-view />
 | 
			
		||||
      </v-col>
 | 
			
		||||
    </v-row>
 | 
			
		||||
  </v-container>
 | 
			
		||||
  <v-main>
 | 
			
		||||
    <v-container class="pt-6 px-6 p-container">
 | 
			
		||||
      <router-view />
 | 
			
		||||
    </v-container>
 | 
			
		||||
 | 
			
		||||
    <Copyright />
 | 
			
		||||
  </v-main>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
</script>
 | 
			
		||||
import AppBar from "@/components/navigation/AppBar.vue"
 | 
			
		||||
import Copyright from "@/components/Copyright.vue"
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.p-container {
 | 
			
		||||
  max-width: 64rem;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import { createRouter, createWebHistory } from "vue-router"
 | 
			
		||||
import { useUserinfo } from "@/stores/userinfo"
 | 
			
		||||
import MasterLayout from "@/layouts/master.vue"
 | 
			
		||||
import UserCenterLayout from "@/layouts/user-center.vue"
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
@@ -11,32 +10,26 @@ const router = createRouter({
 | 
			
		||||
      redirect: { name: "dashboard" },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: "/",
 | 
			
		||||
      component: MasterLayout,
 | 
			
		||||
      path: "/users",
 | 
			
		||||
      component: UserCenterLayout,
 | 
			
		||||
      children: [
 | 
			
		||||
        {
 | 
			
		||||
          path: "/users",
 | 
			
		||||
          component: UserCenterLayout,
 | 
			
		||||
          children: [
 | 
			
		||||
            {
 | 
			
		||||
              path: "/me",
 | 
			
		||||
              name: "dashboard",
 | 
			
		||||
              component: () => import("@/views/dashboard.vue"),
 | 
			
		||||
              meta: { title: "Your account" },
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: "/me/personalize",
 | 
			
		||||
              name: "personalize",
 | 
			
		||||
              component: () => import("@/views/personalize.vue"),
 | 
			
		||||
              meta: { title: "Your personality" },
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              path: "/me/security",
 | 
			
		||||
              name: "security",
 | 
			
		||||
              component: () => import("@/views/security.vue"),
 | 
			
		||||
              meta: { title: "Your security" },
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          path: "/me",
 | 
			
		||||
          name: "dashboard",
 | 
			
		||||
          component: () => import("@/views/dashboard.vue"),
 | 
			
		||||
          meta: { title: "Your account" },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: "/me/personalize",
 | 
			
		||||
          name: "personalize",
 | 
			
		||||
          component: () => import("@/views/personalize.vue"),
 | 
			
		||||
          meta: { title: "Your personality" },
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          path: "/me/security",
 | 
			
		||||
          name: "security",
 | 
			
		||||
          component: () => import("@/views/security.vue"),
 | 
			
		||||
          meta: { title: "Your security" },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ export const useNotifications = defineStore("notifications", () => {
 | 
			
		||||
  async function connect() {
 | 
			
		||||
    if (!(checkLoggedIn())) return;
 | 
			
		||||
 | 
			
		||||
    const uri = `ws://${window.location.host}/api/notifications/listen`;
 | 
			
		||||
    const uri = `ws://${window.location.host}/api/ws`;
 | 
			
		||||
 | 
			
		||||
    socket = new WebSocket(uri + `?tk=${getAtk() as string}`);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@
 | 
			
		||||
                  <p class="opacity-80 text-xs">Permissions they requested</p>
 | 
			
		||||
                  <v-card variant="tonal" class="mt-1 mx-[-4px]">
 | 
			
		||||
                    <v-list density="compact">
 | 
			
		||||
                      <v-list-item v-for="claim in requestedClaims" lines="two">
 | 
			
		||||
                      <v-list-item v-for="(claim, key) in requestedClaims" :key="key" lines="two">
 | 
			
		||||
                        <template #title>
 | 
			
		||||
                          <span class="capitalize">{{ getClaimDescription(claim)?.name }}</span>
 | 
			
		||||
                        </template>
 | 
			
		||||
@@ -106,8 +106,8 @@ const requestedClaims = computed(() => {
 | 
			
		||||
 | 
			
		||||
const panel = ref("confirm")
 | 
			
		||||
 | 
			
		||||
async function preconnect() {
 | 
			
		||||
  const res = await request(`/api/auth/o/connect${location.search}`, {
 | 
			
		||||
async function tryAuthorize() {
 | 
			
		||||
  const res = await request(`/api/auth/o/authorize${location.search}`, {
 | 
			
		||||
    headers: { Authorization: `Bearer ${getAtk()}` },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
@@ -116,9 +116,9 @@ async function preconnect() {
 | 
			
		||||
  } else {
 | 
			
		||||
    const data = await res.json()
 | 
			
		||||
 | 
			
		||||
    if (data["session"]) {
 | 
			
		||||
    if (data["ticket"]) {
 | 
			
		||||
      panel.value = "callback"
 | 
			
		||||
      callback(data["session"])
 | 
			
		||||
      callback(data["ticket"])
 | 
			
		||||
    } else {
 | 
			
		||||
      document.title = `Solarpass | Connect to ${data["client"]?.name}`
 | 
			
		||||
      metadata.value = data["client"]
 | 
			
		||||
@@ -127,7 +127,7 @@ async function preconnect() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
preconnect()
 | 
			
		||||
tryAuthorize()
 | 
			
		||||
 | 
			
		||||
function decline() {
 | 
			
		||||
  if (window.history.length > 0) {
 | 
			
		||||
@@ -140,7 +140,7 @@ function decline() {
 | 
			
		||||
async function approve() {
 | 
			
		||||
  loading.value = true
 | 
			
		||||
  const res = await request(
 | 
			
		||||
    "/api/auth/o/connect?" +
 | 
			
		||||
    "/api/auth/o/authorize?" +
 | 
			
		||||
    new URLSearchParams({
 | 
			
		||||
      client_id: route.query["client_id"] as string,
 | 
			
		||||
      redirect_uri: encodeURIComponent(route.query["redirect_uri"] as string),
 | 
			
		||||
@@ -159,17 +159,21 @@ async function approve() {
 | 
			
		||||
  } else {
 | 
			
		||||
    const data = await res.json()
 | 
			
		||||
    panel.value = "callback"
 | 
			
		||||
    setTimeout(() => callback(data["session"]), 1850)
 | 
			
		||||
    setTimeout(() => callback(data["ticket"]), 1850)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function callback(session: any) {
 | 
			
		||||
  const url = `${route.query["redirect_uri"]}?code=${session["grant_token"]}&state=${route.query["state"]}`
 | 
			
		||||
function callback(ticket: any) {
 | 
			
		||||
  const url = `${route.query["redirect_uri"]}?code=${ticket["grant_token"]}&state=${route.query["state"]}`
 | 
			
		||||
  window.open(url, "_self")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getClaimDescription(key: string): ClaimType {
 | 
			
		||||
  return claims.hasOwnProperty(key) ? claims[key] : { icon: "mdi-asterisk", name: key, description: "Unknown claim..." }
 | 
			
		||||
  return Object.prototype.hasOwnProperty.call(claims, key) ? claims[key] : {
 | 
			
		||||
    icon: "mdi-asterisk",
 | 
			
		||||
    name: key,
 | 
			
		||||
    description: "Unknown claim...",
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <v-expansion-panels>
 | 
			
		||||
      <v-expansion-panel eager title="Challenges">
 | 
			
		||||
      <v-expansion-panel eager title="Tickets">
 | 
			
		||||
        <template #text>
 | 
			
		||||
          <v-card :loading="reverting.challenges" variant="outlined">
 | 
			
		||||
          <v-card :loading="reverting.tickets" variant="outlined">
 | 
			
		||||
            <v-data-table-server
 | 
			
		||||
              density="compact"
 | 
			
		||||
              :headers="dataDefinitions.challenges"
 | 
			
		||||
              :items="challenges"
 | 
			
		||||
              :items-length="pagination.challenges.total"
 | 
			
		||||
              :loading="reverting.challenges"
 | 
			
		||||
              v-model:items-per-page="pagination.challenges.pageSize"
 | 
			
		||||
              @update:options="readChallenges"
 | 
			
		||||
              :headers="dataDefinitions.tickets"
 | 
			
		||||
              :items="tickets"
 | 
			
		||||
              :items-length="pagination.tickets.total"
 | 
			
		||||
              :loading="reverting.tickets"
 | 
			
		||||
              v-model:items-per-page="pagination.tickets.pageSize"
 | 
			
		||||
              @update:options="readTickets"
 | 
			
		||||
              item-value="id"
 | 
			
		||||
            >
 | 
			
		||||
              <template v-slot:item="{ item }: { item: any }">
 | 
			
		||||
@@ -28,40 +28,6 @@
 | 
			
		||||
                    </v-tooltip>
 | 
			
		||||
                  </td>
 | 
			
		||||
                  <td>{{ new Date(item.created_at).toLocaleString() }}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </template>
 | 
			
		||||
            </v-data-table-server>
 | 
			
		||||
          </v-card>
 | 
			
		||||
        </template>
 | 
			
		||||
      </v-expansion-panel>
 | 
			
		||||
 | 
			
		||||
      <v-expansion-panel eager title="Sessions">
 | 
			
		||||
        <template #text>
 | 
			
		||||
          <v-card :loading="reverting.sessions" variant="outlined">
 | 
			
		||||
            <v-data-table-server
 | 
			
		||||
              density="compact"
 | 
			
		||||
              :headers="dataDefinitions.sessions"
 | 
			
		||||
              :items="sessions"
 | 
			
		||||
              :items-length="pagination.sessions.total"
 | 
			
		||||
              :loading="reverting.sessions"
 | 
			
		||||
              v-model:items-per-page="pagination.sessions.pageSize"
 | 
			
		||||
              @update:options="readSessions"
 | 
			
		||||
              item-value="id"
 | 
			
		||||
            >
 | 
			
		||||
              <template v-slot:item="{ item }: { item: any }">
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td>{{ item.id }}</td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    <v-chip v-for="value in item.audiences" size="x-small" color="warning" class="capitalize">
 | 
			
		||||
                      {{ value }}
 | 
			
		||||
                    </v-chip>
 | 
			
		||||
                  </td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    <v-chip v-for="value in item.claims" size="x-small" color="info" class="font-mono">
 | 
			
		||||
                      {{ value }}
 | 
			
		||||
                    </v-chip>
 | 
			
		||||
                  </td>
 | 
			
		||||
                  <td>{{ new Date(item.created_at).toLocaleString() }}</td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    <v-tooltip text="Sign out">
 | 
			
		||||
                      <template #activator="{ props }">
 | 
			
		||||
@@ -71,7 +37,7 @@
 | 
			
		||||
                          size="x-small"
 | 
			
		||||
                          color="error"
 | 
			
		||||
                          icon="mdi-logout-variant"
 | 
			
		||||
                          @click="killSession(item)"
 | 
			
		||||
                          @click="killTicket(item)"
 | 
			
		||||
                        />
 | 
			
		||||
                      </template>
 | 
			
		||||
                    </v-tooltip>
 | 
			
		||||
@@ -124,25 +90,17 @@
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { request } from "@/scripts/request"
 | 
			
		||||
import { getAtk, useUserinfo } from "@/stores/userinfo"
 | 
			
		||||
import { getAtk } from "@/stores/userinfo"
 | 
			
		||||
import { reactive, ref } from "vue"
 | 
			
		||||
 | 
			
		||||
const id = useUserinfo()
 | 
			
		||||
 | 
			
		||||
const error = ref<string | null>(null)
 | 
			
		||||
 | 
			
		||||
const dataDefinitions: { [id: string]: any[] } = {
 | 
			
		||||
  challenges: [
 | 
			
		||||
  tickets: [
 | 
			
		||||
    { align: "start", key: "id", title: "ID" },
 | 
			
		||||
    { align: "start", key: "ip_address", title: "IP Address" },
 | 
			
		||||
    { align: "start", key: "user_agent", title: "User Agent" },
 | 
			
		||||
    { align: "start", key: "created_at", title: "Issued At" },
 | 
			
		||||
  ],
 | 
			
		||||
  sessions: [
 | 
			
		||||
    { align: "start", key: "id", title: "ID" },
 | 
			
		||||
    { align: "start", key: "audiences", title: "Audiences" },
 | 
			
		||||
    { align: "start", key: "claims", title: "Claims" },
 | 
			
		||||
    { align: "start", key: "created_at", title: "Issued At" },
 | 
			
		||||
    { align: "start", key: "actions", title: "Actions", sortable: false },
 | 
			
		||||
  ],
 | 
			
		||||
  events: [
 | 
			
		||||
@@ -155,52 +113,25 @@ const dataDefinitions: { [id: string]: any[] } = {
 | 
			
		||||
  ],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const challenges = ref<any>([])
 | 
			
		||||
const sessions = ref<any>([])
 | 
			
		||||
const tickets = ref<any>([])
 | 
			
		||||
const events = ref<any>([])
 | 
			
		||||
 | 
			
		||||
const reverting = reactive({ challenges: false, sessions: false, events: false })
 | 
			
		||||
const reverting = reactive({ tickets: false, sessions: false, events: false })
 | 
			
		||||
const pagination = reactive({
 | 
			
		||||
  challenges: { page: 1, pageSize: 5, total: 0 },
 | 
			
		||||
  sessions: { page: 1, pageSize: 5, total: 0 },
 | 
			
		||||
  tickets: { page: 1, pageSize: 5, total: 0 },
 | 
			
		||||
  events: { page: 1, pageSize: 5, total: 0 },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
async function readChallenges({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
 | 
			
		||||
  if (itemsPerPage) pagination.challenges.pageSize = itemsPerPage
 | 
			
		||||
  if (page) pagination.challenges.page = page
 | 
			
		||||
 | 
			
		||||
  reverting.challenges = true
 | 
			
		||||
  const res = await request(
 | 
			
		||||
    "/api/users/me/challenges?" +
 | 
			
		||||
      new URLSearchParams({
 | 
			
		||||
        take: pagination.challenges.pageSize.toString(),
 | 
			
		||||
        offset: ((pagination.challenges.page - 1) * pagination.challenges.pageSize).toString(),
 | 
			
		||||
      }),
 | 
			
		||||
    {
 | 
			
		||||
      headers: { Authorization: `Bearer ${getAtk()}` },
 | 
			
		||||
    },
 | 
			
		||||
  )
 | 
			
		||||
  if (res.status !== 200) {
 | 
			
		||||
    error.value = await res.text()
 | 
			
		||||
  } else {
 | 
			
		||||
    const data = await res.json()
 | 
			
		||||
    challenges.value = data["data"]
 | 
			
		||||
    pagination.challenges.total = data["count"]
 | 
			
		||||
  }
 | 
			
		||||
  reverting.challenges = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function readSessions({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
 | 
			
		||||
  if (itemsPerPage) pagination.sessions.pageSize = itemsPerPage
 | 
			
		||||
  if (page) pagination.sessions.page = page
 | 
			
		||||
async function readTickets({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
 | 
			
		||||
  if (itemsPerPage) pagination.tickets.pageSize = itemsPerPage
 | 
			
		||||
  if (page) pagination.tickets.page = page
 | 
			
		||||
 | 
			
		||||
  reverting.sessions = true
 | 
			
		||||
  const res = await request(
 | 
			
		||||
    "/api/users/me/sessions?" +
 | 
			
		||||
    "/api/users/me/tickets?" +
 | 
			
		||||
      new URLSearchParams({
 | 
			
		||||
        take: pagination.sessions.pageSize.toString(),
 | 
			
		||||
        offset: ((pagination.sessions.page - 1) * pagination.sessions.pageSize).toString(),
 | 
			
		||||
        take: pagination.tickets.pageSize.toString(),
 | 
			
		||||
        offset: ((pagination.tickets.page - 1) * pagination.tickets.pageSize).toString(),
 | 
			
		||||
      }),
 | 
			
		||||
    {
 | 
			
		||||
      headers: { Authorization: `Bearer ${getAtk()}` },
 | 
			
		||||
@@ -210,8 +141,8 @@ async function readSessions({ page, itemsPerPage }: { page?: number; itemsPerPag
 | 
			
		||||
    error.value = await res.text()
 | 
			
		||||
  } else {
 | 
			
		||||
    const data = await res.json()
 | 
			
		||||
    sessions.value = data["data"]
 | 
			
		||||
    pagination.sessions.total = data["count"]
 | 
			
		||||
    tickets.value = data["data"]
 | 
			
		||||
    pagination.tickets.total = data["count"]
 | 
			
		||||
  }
 | 
			
		||||
  reverting.sessions = false
 | 
			
		||||
}
 | 
			
		||||
@@ -241,18 +172,18 @@ async function readEvents({ page, itemsPerPage }: { page?: number; itemsPerPage?
 | 
			
		||||
  reverting.events = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Promise.all([readChallenges({}), readSessions({}), readEvents({})])
 | 
			
		||||
Promise.all([readTickets({}), readEvents({})])
 | 
			
		||||
 | 
			
		||||
async function killSession(item: any) {
 | 
			
		||||
async function killTicket(item: any) {
 | 
			
		||||
  reverting.sessions = true
 | 
			
		||||
  const res = await request(`/api/users/me/sessions/${item.id}`, {
 | 
			
		||||
  const res = await request(`/api/users/me/tickets/${item.id}`, {
 | 
			
		||||
    method: "DELETE",
 | 
			
		||||
    headers: { Authorization: `Bearer ${getAtk()}` },
 | 
			
		||||
  })
 | 
			
		||||
  if (res.status !== 200) {
 | 
			
		||||
    error.value = await res.text()
 | 
			
		||||
  } else {
 | 
			
		||||
    await readSessions({})
 | 
			
		||||
    await readTickets({})
 | 
			
		||||
    error.value = null
 | 
			
		||||
  }
 | 
			
		||||
  reverting.sessions = false
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user