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