♻️ OAuth authenticate
This commit is contained in:
parent
0d02eca76e
commit
21d3d71936
@ -5024,6 +5024,7 @@ true posixrules
|
|||||||
</table>
|
</table>
|
||||||
<table id="292" parent="267" name="passport_notifications">
|
<table id="292" parent="267" name="passport_notifications">
|
||||||
<ObjectId>16445</ObjectId>
|
<ObjectId>16445</ObjectId>
|
||||||
|
<Outdated>1</Outdated>
|
||||||
<StateNumber>25751</StateNumber>
|
<StateNumber>25751</StateNumber>
|
||||||
<AccessMethodId>2</AccessMethodId>
|
<AccessMethodId>2</AccessMethodId>
|
||||||
<OwnerName>postgres</OwnerName>
|
<OwnerName>postgres</OwnerName>
|
||||||
|
69
.idea/workspace.xml
generated
69
.idea/workspace.xml
generated
@ -4,15 +4,8 @@
|
|||||||
<option name="autoReloadType" value="ALL" />
|
<option name="autoReloadType" value="ALL" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<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">
|
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":recycle: OAuth authenticate">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/web/src/views/auth/authorize.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/views/auth/authorize.vue" 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>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@ -47,41 +40,41 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"DefaultGoTemplateProperty": "Go File",
|
"DefaultGoTemplateProperty": "Go File",
|
||||||
"Go Build.Backend.executor": "Run",
|
"Go Build.Backend.executor": "Run",
|
||||||
"Go 构建.Backend.executor": "Run",
|
"Go 构建.Backend.executor": "Run",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.go.formatter.settings.were.checked": "true",
|
"RunOnceActivity.go.formatter.settings.were.checked": "true",
|
||||||
"RunOnceActivity.go.migrated.go.modules.settings": "true",
|
"RunOnceActivity.go.migrated.go.modules.settings": "true",
|
||||||
"RunOnceActivity.go.modules.automatic.dependencies.download": "true",
|
"RunOnceActivity.go.modules.automatic.dependencies.download": "true",
|
||||||
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
|
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
|
||||||
"git-widget-placeholder": "refactor/v2",
|
"git-widget-placeholder": "refactor/v2",
|
||||||
"go.import.settings.migrated": "true",
|
"go.import.settings.migrated": "true",
|
||||||
"go.sdk.automatically.set": "true",
|
"go.sdk.automatically.set": "true",
|
||||||
"last_opened_file_path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web",
|
"last_opened_file_path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/pkg/internal/server/api",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"run.code.analysis.last.selected.profile": "pProject Default",
|
"run.code.analysis.last.selected.profile": "pProject Default",
|
||||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||||
"ts.external.directory.path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib",
|
"ts.external.directory.path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
},
|
},
|
||||||
"keyToStringList": {
|
"keyToStringList": {
|
||||||
"DatabaseDriversLRU": [
|
"DatabaseDriversLRU": [
|
||||||
"postgresql"
|
"postgresql"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="CopyFile.RECENT_KEYS">
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="$PROJECT_DIR$/pkg/internal/server/api" />
|
||||||
<recent name="$PROJECT_DIR$/web" />
|
<recent name="$PROJECT_DIR$/web" />
|
||||||
<recent name="$PROJECT_DIR$/pkg/services" />
|
<recent name="$PROJECT_DIR$/pkg/services" />
|
||||||
<recent name="$PROJECT_DIR$/pkg/server/ui" />
|
<recent name="$PROJECT_DIR$/pkg/server/ui" />
|
||||||
<recent name="$PROJECT_DIR$/pkg/views/users" />
|
<recent name="$PROJECT_DIR$/pkg/views/users" />
|
||||||
<recent name="$PROJECT_DIR$/pkg/views" />
|
|
||||||
</key>
|
</key>
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
<recent name="$PROJECT_DIR$/pkg/internal/server/exts" />
|
<recent name="$PROJECT_DIR$/pkg/internal/server/exts" />
|
||||||
@ -150,8 +143,6 @@
|
|||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<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=":bug: Bug fixes in update avatar" />
|
||||||
<MESSAGE value=":sparkles: Firebase is back" />
|
<MESSAGE value=":sparkles: Firebase is back" />
|
||||||
<MESSAGE value=":sparkles: Apple push notification services" />
|
<MESSAGE value=":sparkles: Apple push notification services" />
|
||||||
@ -175,7 +166,9 @@
|
|||||||
<MESSAGE value=":ambulance: Fix query services too much 429" />
|
<MESSAGE value=":ambulance: Fix query services too much 429" />
|
||||||
<MESSAGE value=":ambulance: Fix nil map panic" />
|
<MESSAGE value=":ambulance: Fix nil map panic" />
|
||||||
<MESSAGE value=":recycle: Update the sign in web page to the latest API" />
|
<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>
|
||||||
<component name="VgoProject">
|
<component name="VgoProject">
|
||||||
<settings-migrated>true</settings-migrated>
|
<settings-migrated>true</settings-migrated>
|
||||||
|
@ -123,7 +123,7 @@ func editUserinfo(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func killSession(c *fiber.Ctx) error {
|
func killTicket(c *fiber.Ctx) error {
|
||||||
if err := exts.EnsureAuthenticated(c); err != nil {
|
if err := exts.EnsureAuthenticated(c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func MapAPIs(app *fiber.App) {
|
|||||||
me.Put("/", editUserinfo)
|
me.Put("/", editUserinfo)
|
||||||
me.Get("/events", getEvents)
|
me.Get("/events", getEvents)
|
||||||
me.Get("/tickets", getTickets)
|
me.Get("/tickets", getTickets)
|
||||||
me.Delete("/tickets/:ticketId", killSession)
|
me.Delete("/tickets/:ticketId", killTicket)
|
||||||
|
|
||||||
me.Post("/confirm", doRegisterConfirm)
|
me.Post("/confirm", doRegisterConfirm)
|
||||||
|
|
||||||
@ -51,12 +51,18 @@ func MapAPIs(app *fiber.App) {
|
|||||||
|
|
||||||
api.Post("/users", doRegister)
|
api.Post("/users", doRegister)
|
||||||
|
|
||||||
api.Post("/auth", doAuthenticate)
|
auth := api.Group("/auth").Name("Auth")
|
||||||
api.Post("/auth/mfa", doMultiFactorAuthenticate)
|
{
|
||||||
api.Post("/auth/token", getToken)
|
auth.Post("/", doAuthenticate)
|
||||||
|
auth.Post("/mfa", doMultiFactorAuthenticate)
|
||||||
|
auth.Post("/token", getToken)
|
||||||
|
|
||||||
api.Get("/auth/factors", getAvailableFactors)
|
auth.Get("/factors", getAvailableFactors)
|
||||||
api.Post("/auth/factors/:factorId", requestFactorToken)
|
auth.Post("/factors/:factorId", requestFactorToken)
|
||||||
|
|
||||||
|
auth.Get("/o/authorize", tryAuthorizeThirdClient)
|
||||||
|
auth.Post("/o/authorize", authorizeThirdClient)
|
||||||
|
}
|
||||||
|
|
||||||
realms := api.Group("/realms").Name("Realms API")
|
realms := api.Group("/realms").Name("Realms API")
|
||||||
{
|
{
|
||||||
|
128
pkg/internal/server/api/oauth_api.go
Executable file
128
pkg/internal/server/api/oauth_api.go
Executable 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")
|
||||||
|
}
|
||||||
|
}
|
@ -81,7 +81,7 @@ func NewOauthTicket(
|
|||||||
AccessToken: lo.ToPtr(uuid.NewString()),
|
AccessToken: lo.ToPtr(uuid.NewString()),
|
||||||
RefreshToken: lo.ToPtr(uuid.NewString()),
|
RefreshToken: lo.ToPtr(uuid.NewString()),
|
||||||
AvailableAt: lo.ToPtr(time.Now()),
|
AvailableAt: lo.ToPtr(time.Now()),
|
||||||
ExpiredAt: lo.ToPtr(time.Now()),
|
ExpiredAt: lo.ToPtr(time.Now().Add(7 * 24 * time.Hour)),
|
||||||
ClientID: &client.ID,
|
ClientID: &client.ID,
|
||||||
AccountID: user.ID,
|
AccountID: user.ID,
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-xs text-center opacity-80">
|
<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>
|
<p>Powered by <a class="underline" href="https://git.solsynth.dev/Hydrogen/Passport">Hydrogen.Passport</a></p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-menu eager :close-on-content-click="false">
|
<v-navigation-drawer :model-value="props.open" @update:model-value="val => emits('update:open', val)" location="right"
|
||||||
<template #activator="{ props }">
|
temporary order="0" width="400">
|
||||||
<v-btn v-bind="props" icon size="small" variant="text" :loading="loading">
|
<v-list-item prepend-icon="mdi-bell" title="Notifications" class="py-3"></v-list-item>
|
||||||
<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-divider color="black" class="mb-1" />
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<v-list v-if="notify.notifications.length <= 0" class="w-[380px]" density="compact">
|
<v-list v-if="notify.notifications.length <= 0" density="compact">
|
||||||
<v-list-item>
|
<v-list-item color="secondary" prepend-icon="mdi-check" title="All notifications read"
|
||||||
<v-alert class="text-sm" variant="tonal" type="info">You are done! There is no unread notifications for you.</v-alert>
|
subtitle="There is no more new things for you..." />
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
<v-list v-else class="w-[380px]" density="compact" lines="three">
|
<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 #title>{{ item.subject }}</template>
|
||||||
<template #subtitle>{{ item.content }}</template>
|
<template #subtitle>{{ item.content }}</template>
|
||||||
|
|
||||||
@ -26,11 +20,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex text-xs gap-1">
|
<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>
|
</div>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<!-- @vue-ignore -->
|
<!-- @vue-ignore -->
|
||||||
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
<v-snackbar v-model="error" :timeout="5000">Something went wrong... {{ error }}</v-snackbar>
|
||||||
@ -39,8 +34,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { request } from "@/scripts/request"
|
import { request } from "@/scripts/request"
|
||||||
import { getAtk } from "@/stores/userinfo"
|
import { getAtk } from "@/stores/userinfo"
|
||||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
import { computed, onMounted, onUnmounted, ref } from "vue"
|
||||||
import { useNotifications } from "@/stores/notifications";
|
import { useNotifications } from "@/stores/notifications"
|
||||||
|
|
||||||
|
const props = defineProps<{ open: boolean }>()
|
||||||
|
const emits = defineEmits(["update:open"])
|
||||||
|
|
||||||
const notify = useNotifications()
|
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>
|
<template>
|
||||||
<v-app-bar height="64" color="primary" scroll-behavior="elevate" flat>
|
<AppBar />
|
||||||
<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>
|
|
||||||
|
|
||||||
<v-main>
|
<v-main>
|
||||||
<router-view />
|
<router-view />
|
||||||
@ -25,8 +8,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUserinfo } from "@/stores/userinfo"
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
import NotificationList from "@/components/NotificationList.vue"
|
import AppBar from "@/components/navigation/AppBar.vue"
|
||||||
import UserMenu from "@/components/UserMenu.vue"
|
|
||||||
|
|
||||||
const id = useUserinfo()
|
const id = useUserinfo()
|
||||||
|
|
||||||
@ -34,12 +16,6 @@ id.readProfiles()
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.editor-fab {
|
|
||||||
position: fixed !important;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-filter {
|
.icon-filter {
|
||||||
filter: invert(100%) sepia(100%) saturate(14%) hue-rotate(212deg) brightness(104%) contrast(104%);
|
filter: invert(100%) sepia(100%) saturate(14%) hue-rotate(212deg) brightness(104%) contrast(104%);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container class="pt-6 px-6">
|
<AppBar>
|
||||||
<v-row>
|
<template #extension>
|
||||||
<v-col :cols="12" :xs="12" :sm="12" :md="4" :lg="3">
|
<v-tabs align-tabs="title" color="white">
|
||||||
<v-card title="Navigation">
|
<v-tab text="Dashboard" prepend-icon="mdi-view-dashboard" :to="{ name: 'dashboard' }" exact />
|
||||||
<v-list density="comfortable">
|
<v-tab text="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'personalize' }" exact />
|
||||||
<v-list-item title="Dashboard" prepend-icon="mdi-view-dashboard" :to="{ name: 'dashboard' }" exact />
|
<v-tab text="Security" prepend-icon="mdi-security" :to="{ name: 'security' }" exact />
|
||||||
<v-list-item title="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'personalize' }" />
|
</v-tabs>
|
||||||
<v-list-item title="Security" prepend-icon="mdi-security" :to="{ name: 'security' }" />
|
</template>
|
||||||
</v-list>
|
</AppBar>
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col :cols="12" :xs="12" :sm="12" :md="8" :lg="9">
|
<v-main>
|
||||||
|
<v-container class="pt-6 px-6 p-container">
|
||||||
<router-view />
|
<router-view />
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
|
<Copyright />
|
||||||
|
</v-main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import AppBar from "@/components/navigation/AppBar.vue"
|
||||||
|
import Copyright from "@/components/Copyright.vue"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.p-container {
|
||||||
|
max-width: 64rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,5 @@
|
|||||||
import { createRouter, createWebHistory } from "vue-router"
|
import { createRouter, createWebHistory } from "vue-router"
|
||||||
import { useUserinfo } from "@/stores/userinfo"
|
import { useUserinfo } from "@/stores/userinfo"
|
||||||
import MasterLayout from "@/layouts/master.vue"
|
|
||||||
import UserCenterLayout from "@/layouts/user-center.vue"
|
import UserCenterLayout from "@/layouts/user-center.vue"
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@ -10,10 +9,6 @@ const router = createRouter({
|
|||||||
path: "/",
|
path: "/",
|
||||||
redirect: { name: "dashboard" },
|
redirect: { name: "dashboard" },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
component: MasterLayout,
|
|
||||||
children: [
|
|
||||||
{
|
{
|
||||||
path: "/users",
|
path: "/users",
|
||||||
component: UserCenterLayout,
|
component: UserCenterLayout,
|
||||||
@ -38,8 +33,6 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
children: [
|
children: [
|
||||||
|
@ -39,7 +39,7 @@ export const useNotifications = defineStore("notifications", () => {
|
|||||||
async function connect() {
|
async function connect() {
|
||||||
if (!(checkLoggedIn())) return;
|
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}`);
|
socket = new WebSocket(uri + `?tk=${getAtk() as string}`);
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
<p class="opacity-80 text-xs">Permissions they requested</p>
|
<p class="opacity-80 text-xs">Permissions they requested</p>
|
||||||
<v-card variant="tonal" class="mt-1 mx-[-4px]">
|
<v-card variant="tonal" class="mt-1 mx-[-4px]">
|
||||||
<v-list density="compact">
|
<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>
|
<template #title>
|
||||||
<span class="capitalize">{{ getClaimDescription(claim)?.name }}</span>
|
<span class="capitalize">{{ getClaimDescription(claim)?.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
@ -106,8 +106,8 @@ const requestedClaims = computed(() => {
|
|||||||
|
|
||||||
const panel = ref("confirm")
|
const panel = ref("confirm")
|
||||||
|
|
||||||
async function preconnect() {
|
async function tryAuthorize() {
|
||||||
const res = await request(`/api/auth/o/connect${location.search}`, {
|
const res = await request(`/api/auth/o/authorize${location.search}`, {
|
||||||
headers: { Authorization: `Bearer ${getAtk()}` },
|
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -116,9 +116,9 @@ async function preconnect() {
|
|||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (data["session"]) {
|
if (data["ticket"]) {
|
||||||
panel.value = "callback"
|
panel.value = "callback"
|
||||||
callback(data["session"])
|
callback(data["ticket"])
|
||||||
} else {
|
} else {
|
||||||
document.title = `Solarpass | Connect to ${data["client"]?.name}`
|
document.title = `Solarpass | Connect to ${data["client"]?.name}`
|
||||||
metadata.value = data["client"]
|
metadata.value = data["client"]
|
||||||
@ -127,7 +127,7 @@ async function preconnect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preconnect()
|
tryAuthorize()
|
||||||
|
|
||||||
function decline() {
|
function decline() {
|
||||||
if (window.history.length > 0) {
|
if (window.history.length > 0) {
|
||||||
@ -140,7 +140,7 @@ function decline() {
|
|||||||
async function approve() {
|
async function approve() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const res = await request(
|
const res = await request(
|
||||||
"/api/auth/o/connect?" +
|
"/api/auth/o/authorize?" +
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
client_id: route.query["client_id"] as string,
|
client_id: route.query["client_id"] as string,
|
||||||
redirect_uri: encodeURIComponent(route.query["redirect_uri"] as string),
|
redirect_uri: encodeURIComponent(route.query["redirect_uri"] as string),
|
||||||
@ -159,17 +159,21 @@ async function approve() {
|
|||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
panel.value = "callback"
|
panel.value = "callback"
|
||||||
setTimeout(() => callback(data["session"]), 1850)
|
setTimeout(() => callback(data["ticket"]), 1850)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function callback(session: any) {
|
function callback(ticket: any) {
|
||||||
const url = `${route.query["redirect_uri"]}?code=${session["grant_token"]}&state=${route.query["state"]}`
|
const url = `${route.query["redirect_uri"]}?code=${ticket["grant_token"]}&state=${route.query["state"]}`
|
||||||
window.open(url, "_self")
|
window.open(url, "_self")
|
||||||
}
|
}
|
||||||
|
|
||||||
function getClaimDescription(key: string): ClaimType {
|
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>
|
</script>
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<v-expansion-panels>
|
<v-expansion-panels>
|
||||||
<v-expansion-panel eager title="Challenges">
|
<v-expansion-panel eager title="Tickets">
|
||||||
<template #text>
|
<template #text>
|
||||||
<v-card :loading="reverting.challenges" variant="outlined">
|
<v-card :loading="reverting.tickets" variant="outlined">
|
||||||
<v-data-table-server
|
<v-data-table-server
|
||||||
density="compact"
|
density="compact"
|
||||||
:headers="dataDefinitions.challenges"
|
:headers="dataDefinitions.tickets"
|
||||||
:items="challenges"
|
:items="tickets"
|
||||||
:items-length="pagination.challenges.total"
|
:items-length="pagination.tickets.total"
|
||||||
:loading="reverting.challenges"
|
:loading="reverting.tickets"
|
||||||
v-model:items-per-page="pagination.challenges.pageSize"
|
v-model:items-per-page="pagination.tickets.pageSize"
|
||||||
@update:options="readChallenges"
|
@update:options="readTickets"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
>
|
>
|
||||||
<template v-slot:item="{ item }: { item: any }">
|
<template v-slot:item="{ item }: { item: any }">
|
||||||
@ -28,40 +28,6 @@
|
|||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ new Date(item.created_at).toLocaleString() }}</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>
|
<td>
|
||||||
<v-tooltip text="Sign out">
|
<v-tooltip text="Sign out">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
@ -71,7 +37,7 @@
|
|||||||
size="x-small"
|
size="x-small"
|
||||||
color="error"
|
color="error"
|
||||||
icon="mdi-logout-variant"
|
icon="mdi-logout-variant"
|
||||||
@click="killSession(item)"
|
@click="killTicket(item)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
@ -124,25 +90,17 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { request } from "@/scripts/request"
|
import { request } from "@/scripts/request"
|
||||||
import { getAtk, useUserinfo } from "@/stores/userinfo"
|
import { getAtk } from "@/stores/userinfo"
|
||||||
import { reactive, ref } from "vue"
|
import { reactive, ref } from "vue"
|
||||||
|
|
||||||
const id = useUserinfo()
|
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
const dataDefinitions: { [id: string]: any[] } = {
|
const dataDefinitions: { [id: string]: any[] } = {
|
||||||
challenges: [
|
tickets: [
|
||||||
{ align: "start", key: "id", title: "ID" },
|
{ align: "start", key: "id", title: "ID" },
|
||||||
{ align: "start", key: "ip_address", title: "IP Address" },
|
{ align: "start", key: "ip_address", title: "IP Address" },
|
||||||
{ align: "start", key: "user_agent", title: "User Agent" },
|
{ align: "start", key: "user_agent", title: "User Agent" },
|
||||||
{ align: "start", key: "created_at", title: "Issued At" },
|
{ 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 },
|
{ align: "start", key: "actions", title: "Actions", sortable: false },
|
||||||
],
|
],
|
||||||
events: [
|
events: [
|
||||||
@ -155,52 +113,25 @@ const dataDefinitions: { [id: string]: any[] } = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
const challenges = ref<any>([])
|
const tickets = ref<any>([])
|
||||||
const sessions = ref<any>([])
|
|
||||||
const events = 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({
|
const pagination = reactive({
|
||||||
challenges: { page: 1, pageSize: 5, total: 0 },
|
tickets: { page: 1, pageSize: 5, total: 0 },
|
||||||
sessions: { page: 1, pageSize: 5, total: 0 },
|
|
||||||
events: { page: 1, pageSize: 5, total: 0 },
|
events: { page: 1, pageSize: 5, total: 0 },
|
||||||
})
|
})
|
||||||
|
|
||||||
async function readChallenges({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
|
async function readTickets({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
|
||||||
if (itemsPerPage) pagination.challenges.pageSize = itemsPerPage
|
if (itemsPerPage) pagination.tickets.pageSize = itemsPerPage
|
||||||
if (page) pagination.challenges.page = page
|
if (page) pagination.tickets.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
|
|
||||||
|
|
||||||
reverting.sessions = true
|
reverting.sessions = true
|
||||||
const res = await request(
|
const res = await request(
|
||||||
"/api/users/me/sessions?" +
|
"/api/users/me/tickets?" +
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
take: pagination.sessions.pageSize.toString(),
|
take: pagination.tickets.pageSize.toString(),
|
||||||
offset: ((pagination.sessions.page - 1) * pagination.sessions.pageSize).toString(),
|
offset: ((pagination.tickets.page - 1) * pagination.tickets.pageSize).toString(),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
headers: { Authorization: `Bearer ${getAtk()}` },
|
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||||
@ -210,8 +141,8 @@ async function readSessions({ page, itemsPerPage }: { page?: number; itemsPerPag
|
|||||||
error.value = await res.text()
|
error.value = await res.text()
|
||||||
} else {
|
} else {
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
sessions.value = data["data"]
|
tickets.value = data["data"]
|
||||||
pagination.sessions.total = data["count"]
|
pagination.tickets.total = data["count"]
|
||||||
}
|
}
|
||||||
reverting.sessions = false
|
reverting.sessions = false
|
||||||
}
|
}
|
||||||
@ -241,18 +172,18 @@ async function readEvents({ page, itemsPerPage }: { page?: number; itemsPerPage?
|
|||||||
reverting.events = false
|
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
|
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",
|
method: "DELETE",
|
||||||
headers: { Authorization: `Bearer ${getAtk()}` },
|
headers: { Authorization: `Bearer ${getAtk()}` },
|
||||||
})
|
})
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
error.value = await res.text()
|
error.value = await res.text()
|
||||||
} else {
|
} else {
|
||||||
await readSessions({})
|
await readTickets({})
|
||||||
error.value = null
|
error.value = null
|
||||||
}
|
}
|
||||||
reverting.sessions = false
|
reverting.sessions = false
|
||||||
|
Loading…
Reference in New Issue
Block a user