♻️ 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>
<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>

View File

@ -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[{
&quot;keyToString&quot;: { "keyToString": {
&quot;DefaultGoTemplateProperty&quot;: &quot;Go File&quot;, "DefaultGoTemplateProperty": "Go File",
&quot;Go Build.Backend.executor&quot;: &quot;Run&quot;, "Go Build.Backend.executor": "Run",
&quot;Go 构建.Backend.executor&quot;: &quot;Run&quot;, "Go 构建.Backend.executor": "Run",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.go.formatter.settings.were.checked&quot;: &quot;true&quot;, "RunOnceActivity.go.formatter.settings.were.checked": "true",
&quot;RunOnceActivity.go.migrated.go.modules.settings&quot;: &quot;true&quot;, "RunOnceActivity.go.migrated.go.modules.settings": "true",
&quot;RunOnceActivity.go.modules.automatic.dependencies.download&quot;: &quot;true&quot;, "RunOnceActivity.go.modules.automatic.dependencies.download": "true",
&quot;RunOnceActivity.go.modules.go.list.on.any.changes.was.set&quot;: &quot;true&quot;, "RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
&quot;git-widget-placeholder&quot;: &quot;refactor/v2&quot;, "git-widget-placeholder": "refactor/v2",
&quot;go.import.settings.migrated&quot;: &quot;true&quot;, "go.import.settings.migrated": "true",
&quot;go.sdk.automatically.set&quot;: &quot;true&quot;, "go.sdk.automatically.set": "true",
&quot;last_opened_file_path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web&quot;, "last_opened_file_path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/pkg/internal/server/api",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "nodejs_package_manager_path": "npm",
&quot;run.code.analysis.last.selected.profile&quot;: &quot;pProject Default&quot;, "run.code.analysis.last.selected.profile": "pProject Default",
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;, "settings.editor.selected.configurable": "preferences.pluginManager",
&quot;ts.external.directory.path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib&quot;, "ts.external.directory.path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib",
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot; "vue.rearranger.settings.migration": "true"
}, },
&quot;keyToStringList&quot;: { "keyToStringList": {
&quot;DatabaseDriversLRU&quot;: [ "DatabaseDriversLRU": [
&quot;postgresql&quot; "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>

View File

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

View File

@ -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")
{ {

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()), 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,
} }

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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