♻️ OAuth authenticate

This commit is contained in:
LittleSheep 2024-06-26 14:47:34 +08:00
parent 0d02eca76e
commit 21d3d71936
15 changed files with 328 additions and 240 deletions

View File

@ -5024,6 +5024,7 @@ true posixrules
</table>
<table id="292" parent="267" name="passport_notifications">
<ObjectId>16445</ObjectId>
<Outdated>1</Outdated>
<StateNumber>25751</StateNumber>
<AccessMethodId>2</AccessMethodId>
<OwnerName>postgres</OwnerName>

69
.idea/workspace.xml generated
View File

@ -4,15 +4,8 @@
<option name="autoReloadType" value="ALL" />
</component>
<component name="ChangeListManager">
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":recycle: Update the sign in web page to the latest API">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/database/migrator.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/database/migrator.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/models/accounts.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/models/accounts.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/models/profiles.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/models/profiles.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/api/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/api/index.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/api/page_api.go" beforeDir="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/personal-page.vue" beforeDir="false" />
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":recycle: OAuth authenticate">
<change beforePath="$PROJECT_DIR$/web/src/views/auth/authorize.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/views/auth/authorize.vue" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -47,41 +40,41 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;DefaultGoTemplateProperty&quot;: &quot;Go File&quot;,
&quot;Go Build.Backend.executor&quot;: &quot;Run&quot;,
&quot;Go 构建.Backend.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.formatter.settings.were.checked&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.migrated.go.modules.settings&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.modules.automatic.dependencies.download&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.modules.go.list.on.any.changes.was.set&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;refactor/v2&quot;,
&quot;go.import.settings.migrated&quot;: &quot;true&quot;,
&quot;go.sdk.automatically.set&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;run.code.analysis.last.selected.profile&quot;: &quot;pProject Default&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
&quot;ts.external.directory.path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"DefaultGoTemplateProperty": "Go File",
"Go Build.Backend.executor": "Run",
"Go 构建.Backend.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.go.formatter.settings.were.checked": "true",
"RunOnceActivity.go.migrated.go.modules.settings": "true",
"RunOnceActivity.go.modules.automatic.dependencies.download": "true",
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
"git-widget-placeholder": "refactor/v2",
"go.import.settings.migrated": "true",
"go.sdk.automatically.set": "true",
"last_opened_file_path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/pkg/internal/server/api",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"run.code.analysis.last.selected.profile": "pProject Default",
"settings.editor.selected.configurable": "preferences.pluginManager",
"ts.external.directory.path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib",
"vue.rearranger.settings.migration": "true"
},
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;postgresql&quot;
"keyToStringList": {
"DatabaseDriversLRU": [
"postgresql"
]
}
}</component>
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/pkg/internal/server/api" />
<recent name="$PROJECT_DIR$/web" />
<recent name="$PROJECT_DIR$/pkg/services" />
<recent name="$PROJECT_DIR$/pkg/server/ui" />
<recent name="$PROJECT_DIR$/pkg/views/users" />
<recent name="$PROJECT_DIR$/pkg/views" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/pkg/internal/server/exts" />
@ -150,8 +143,6 @@
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value=":sparkles: Check permissions GRPC method" />
<MESSAGE value=":recycle: Use paperclip to store avatar and more" />
<MESSAGE value=":bug: Bug fixes in update avatar" />
<MESSAGE value=":sparkles: Firebase is back" />
<MESSAGE value=":sparkles: Apple push notification services" />
@ -175,7 +166,9 @@
<MESSAGE value=":ambulance: Fix query services too much 429" />
<MESSAGE value=":ambulance: Fix nil map panic" />
<MESSAGE value=":recycle: Update the sign in web page to the latest API" />
<option name="LAST_COMMIT_MESSAGE" value=":recycle: Update the sign in web page to the latest API" />
<MESSAGE value=":wastebasket: Remove the personal page" />
<MESSAGE value=":recycle: OAuth authenticate" />
<option name="LAST_COMMIT_MESSAGE" value=":recycle: OAuth authenticate" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>

View File

@ -123,7 +123,7 @@ func editUserinfo(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
func killSession(c *fiber.Ctx) error {
func killTicket(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}

View File

@ -29,7 +29,7 @@ func MapAPIs(app *fiber.App) {
me.Put("/", editUserinfo)
me.Get("/events", getEvents)
me.Get("/tickets", getTickets)
me.Delete("/tickets/:ticketId", killSession)
me.Delete("/tickets/:ticketId", killTicket)
me.Post("/confirm", doRegisterConfirm)
@ -51,12 +51,18 @@ func MapAPIs(app *fiber.App) {
api.Post("/users", doRegister)
api.Post("/auth", doAuthenticate)
api.Post("/auth/mfa", doMultiFactorAuthenticate)
api.Post("/auth/token", getToken)
auth := api.Group("/auth").Name("Auth")
{
auth.Post("/", doAuthenticate)
auth.Post("/mfa", doMultiFactorAuthenticate)
auth.Post("/token", getToken)
api.Get("/auth/factors", getAvailableFactors)
api.Post("/auth/factors/:factorId", requestFactorToken)
auth.Get("/factors", getAvailableFactors)
auth.Post("/factors/:factorId", requestFactorToken)
auth.Get("/o/authorize", tryAuthorizeThirdClient)
auth.Post("/o/authorize", authorizeThirdClient)
}
realms := api.Group("/realms").Name("Realms API")
{

View File

@ -0,0 +1,128 @@
package api
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"strings"
"time"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func tryAuthorizeThirdClient(c *fiber.Ctx) error {
id := c.Query("client_id")
redirect := c.Query("redirect_uri")
if len(id) <= 0 || len(redirect) <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "invalid request, missing query parameters")
}
var client models.ThirdClient
if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else if !client.IsDraft && !lo.Contains(client.Callbacks, strings.Split(redirect, "?")[0]) {
return fiber.NewError(fiber.StatusBadRequest, "invalid callback url")
}
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var ticket models.AuthTicket
if err := database.C.Where(&models.AuthTicket{
AccountID: user.ID,
ClientID: &client.ID,
}).Where("last_grant_at IS NULL").First(&ticket).Error; err == nil {
if ticket.ExpiredAt != nil && ticket.ExpiredAt.Unix() < time.Now().Unix() {
return c.JSON(fiber.Map{
"client": client,
"ticket": nil,
})
} else {
ticket, err = services.RegenSession(ticket)
}
return c.JSON(fiber.Map{
"client": client,
"ticket": ticket,
})
}
return c.JSON(fiber.Map{
"client": client,
"ticket": nil,
})
}
func authorizeThirdClient(c *fiber.Ctx) error {
id := c.Query("client_id")
response := c.Query("response_type")
redirect := c.Query("redirect_uri")
scope := c.Query("scope")
if len(scope) <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "invalid request params")
}
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var client models.ThirdClient
if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
switch response {
case "code":
// OAuth Authorization Mode
ticket, err := services.NewOauthTicket(
user,
client,
strings.Split(scope, " "),
[]string{"passport", client.Alias},
c.IP(),
c.Get(fiber.HeaderUserAgent),
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
return c.JSON(fiber.Map{
"ticket": ticket,
"redirect_uri": redirect,
})
}
case "token":
// OAuth Implicit Mode
ticket, err := services.NewOauthTicket(
user,
client,
strings.Split(scope, " "),
[]string{"passport", client.Alias},
c.IP(),
c.Get(fiber.HeaderUserAgent),
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else if access, refresh, err := services.GetToken(ticket); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent))
return c.JSON(fiber.Map{
"access_token": access,
"refresh_token": refresh,
"redirect_uri": redirect,
"ticket": ticket,
})
}
default:
return fiber.NewError(fiber.StatusBadRequest, "unsupported response type")
}
}

View File

@ -81,7 +81,7 @@ func NewOauthTicket(
AccessToken: lo.ToPtr(uuid.NewString()),
RefreshToken: lo.ToPtr(uuid.NewString()),
AvailableAt: lo.ToPtr(time.Now()),
ExpiredAt: lo.ToPtr(time.Now()),
ExpiredAt: lo.ToPtr(time.Now().Add(7 * 24 * time.Hour)),
ClientID: &client.ID,
AccountID: user.ID,
}

View File

@ -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>

View File

@ -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()

View 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>

View File

@ -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%);
}

View File

@ -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>

View File

@ -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" },
},
],
},

View File

@ -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}`);

View File

@ -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>

View File

@ -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