⏪️ 重现旧 UI #4
| @@ -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> | ||||
|   <AppBar> | ||||
|     <template #extension> | ||||
|       <v-tabs align-tabs="title" color="white"> | ||||
|         <v-tab text="Dashboard" prepend-icon="mdi-view-dashboard" :to="{ name: 'dashboard' }" exact /> | ||||
|         <v-tab text="Personalize" prepend-icon="mdi-card-bulleted-outline" :to="{ name: 'personalize' }" exact /> | ||||
|         <v-tab text="Security" prepend-icon="mdi-security" :to="{ name: 'security' }" exact /> | ||||
|       </v-tabs> | ||||
|     </template> | ||||
|   </AppBar> | ||||
|  | ||||
|       <v-col :cols="12" :xs="12" :sm="12" :md="8" :lg="9"> | ||||
|         <router-view /> | ||||
|       </v-col> | ||||
|     </v-row> | ||||
|   </v-container> | ||||
|   <v-main> | ||||
|     <v-container class="pt-6 px-6 p-container"> | ||||
|       <router-view /> | ||||
|     </v-container> | ||||
|  | ||||
|     <Copyright /> | ||||
|   </v-main> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| </script> | ||||
| import AppBar from "@/components/navigation/AppBar.vue" | ||||
| import Copyright from "@/components/Copyright.vue" | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .p-container { | ||||
|   max-width: 64rem; | ||||
| } | ||||
| </style> | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { createRouter, createWebHistory } from "vue-router" | ||||
| import { useUserinfo } from "@/stores/userinfo" | ||||
| import MasterLayout from "@/layouts/master.vue" | ||||
| import UserCenterLayout from "@/layouts/user-center.vue" | ||||
|  | ||||
| const router = createRouter({ | ||||
| @@ -11,32 +10,26 @@ const router = createRouter({ | ||||
|       redirect: { name: "dashboard" }, | ||||
|     }, | ||||
|     { | ||||
|       path: "/", | ||||
|       component: MasterLayout, | ||||
|       path: "/users", | ||||
|       component: UserCenterLayout, | ||||
|       children: [ | ||||
|         { | ||||
|           path: "/users", | ||||
|           component: UserCenterLayout, | ||||
|           children: [ | ||||
|             { | ||||
|               path: "/me", | ||||
|               name: "dashboard", | ||||
|               component: () => import("@/views/dashboard.vue"), | ||||
|               meta: { title: "Your account" }, | ||||
|             }, | ||||
|             { | ||||
|               path: "/me/personalize", | ||||
|               name: "personalize", | ||||
|               component: () => import("@/views/personalize.vue"), | ||||
|               meta: { title: "Your personality" }, | ||||
|             }, | ||||
|             { | ||||
|               path: "/me/security", | ||||
|               name: "security", | ||||
|               component: () => import("@/views/security.vue"), | ||||
|               meta: { title: "Your security" }, | ||||
|             }, | ||||
|           ], | ||||
|           path: "/me", | ||||
|           name: "dashboard", | ||||
|           component: () => import("@/views/dashboard.vue"), | ||||
|           meta: { title: "Your account" }, | ||||
|         }, | ||||
|         { | ||||
|           path: "/me/personalize", | ||||
|           name: "personalize", | ||||
|           component: () => import("@/views/personalize.vue"), | ||||
|           meta: { title: "Your personality" }, | ||||
|         }, | ||||
|         { | ||||
|           path: "/me/security", | ||||
|           name: "security", | ||||
|           component: () => import("@/views/security.vue"), | ||||
|           meta: { title: "Your security" }, | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   | ||||
| @@ -39,7 +39,7 @@ export const useNotifications = defineStore("notifications", () => { | ||||
|   async function connect() { | ||||
|     if (!(checkLoggedIn())) return; | ||||
|  | ||||
|     const uri = `ws://${window.location.host}/api/notifications/listen`; | ||||
|     const uri = `ws://${window.location.host}/api/ws`; | ||||
|  | ||||
|     socket = new WebSocket(uri + `?tk=${getAtk() as string}`); | ||||
|  | ||||
|   | ||||
| @@ -31,7 +31,7 @@ | ||||
|                   <p class="opacity-80 text-xs">Permissions they requested</p> | ||||
|                   <v-card variant="tonal" class="mt-1 mx-[-4px]"> | ||||
|                     <v-list density="compact"> | ||||
|                       <v-list-item v-for="claim in requestedClaims" lines="two"> | ||||
|                       <v-list-item v-for="(claim, key) in requestedClaims" :key="key" lines="two"> | ||||
|                         <template #title> | ||||
|                           <span class="capitalize">{{ getClaimDescription(claim)?.name }}</span> | ||||
|                         </template> | ||||
| @@ -106,8 +106,8 @@ const requestedClaims = computed(() => { | ||||
|  | ||||
| const panel = ref("confirm") | ||||
|  | ||||
| async function preconnect() { | ||||
|   const res = await request(`/api/auth/o/connect${location.search}`, { | ||||
| async function tryAuthorize() { | ||||
|   const res = await request(`/api/auth/o/authorize${location.search}`, { | ||||
|     headers: { Authorization: `Bearer ${getAtk()}` }, | ||||
|   }) | ||||
|  | ||||
| @@ -116,9 +116,9 @@ async function preconnect() { | ||||
|   } else { | ||||
|     const data = await res.json() | ||||
|  | ||||
|     if (data["session"]) { | ||||
|     if (data["ticket"]) { | ||||
|       panel.value = "callback" | ||||
|       callback(data["session"]) | ||||
|       callback(data["ticket"]) | ||||
|     } else { | ||||
|       document.title = `Solarpass | Connect to ${data["client"]?.name}` | ||||
|       metadata.value = data["client"] | ||||
| @@ -127,7 +127,7 @@ async function preconnect() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| preconnect() | ||||
| tryAuthorize() | ||||
|  | ||||
| function decline() { | ||||
|   if (window.history.length > 0) { | ||||
| @@ -140,7 +140,7 @@ function decline() { | ||||
| async function approve() { | ||||
|   loading.value = true | ||||
|   const res = await request( | ||||
|     "/api/auth/o/connect?" + | ||||
|     "/api/auth/o/authorize?" + | ||||
|     new URLSearchParams({ | ||||
|       client_id: route.query["client_id"] as string, | ||||
|       redirect_uri: encodeURIComponent(route.query["redirect_uri"] as string), | ||||
| @@ -159,17 +159,21 @@ async function approve() { | ||||
|   } else { | ||||
|     const data = await res.json() | ||||
|     panel.value = "callback" | ||||
|     setTimeout(() => callback(data["session"]), 1850) | ||||
|     setTimeout(() => callback(data["ticket"]), 1850) | ||||
|   } | ||||
| } | ||||
|  | ||||
| function callback(session: any) { | ||||
|   const url = `${route.query["redirect_uri"]}?code=${session["grant_token"]}&state=${route.query["state"]}` | ||||
| function callback(ticket: any) { | ||||
|   const url = `${route.query["redirect_uri"]}?code=${ticket["grant_token"]}&state=${route.query["state"]}` | ||||
|   window.open(url, "_self") | ||||
| } | ||||
|  | ||||
| function getClaimDescription(key: string): ClaimType { | ||||
|   return claims.hasOwnProperty(key) ? claims[key] : { icon: "mdi-asterisk", name: key, description: "Unknown claim..." } | ||||
|   return Object.prototype.hasOwnProperty.call(claims, key) ? claims[key] : { | ||||
|     icon: "mdi-asterisk", | ||||
|     name: key, | ||||
|     description: "Unknown claim...", | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <v-expansion-panels> | ||||
|       <v-expansion-panel eager title="Challenges"> | ||||
|       <v-expansion-panel eager title="Tickets"> | ||||
|         <template #text> | ||||
|           <v-card :loading="reverting.challenges" variant="outlined"> | ||||
|           <v-card :loading="reverting.tickets" variant="outlined"> | ||||
|             <v-data-table-server | ||||
|               density="compact" | ||||
|               :headers="dataDefinitions.challenges" | ||||
|               :items="challenges" | ||||
|               :items-length="pagination.challenges.total" | ||||
|               :loading="reverting.challenges" | ||||
|               v-model:items-per-page="pagination.challenges.pageSize" | ||||
|               @update:options="readChallenges" | ||||
|               :headers="dataDefinitions.tickets" | ||||
|               :items="tickets" | ||||
|               :items-length="pagination.tickets.total" | ||||
|               :loading="reverting.tickets" | ||||
|               v-model:items-per-page="pagination.tickets.pageSize" | ||||
|               @update:options="readTickets" | ||||
|               item-value="id" | ||||
|             > | ||||
|               <template v-slot:item="{ item }: { item: any }"> | ||||
| @@ -28,40 +28,6 @@ | ||||
|                     </v-tooltip> | ||||
|                   </td> | ||||
|                   <td>{{ new Date(item.created_at).toLocaleString() }}</td> | ||||
|                 </tr> | ||||
|               </template> | ||||
|             </v-data-table-server> | ||||
|           </v-card> | ||||
|         </template> | ||||
|       </v-expansion-panel> | ||||
|  | ||||
|       <v-expansion-panel eager title="Sessions"> | ||||
|         <template #text> | ||||
|           <v-card :loading="reverting.sessions" variant="outlined"> | ||||
|             <v-data-table-server | ||||
|               density="compact" | ||||
|               :headers="dataDefinitions.sessions" | ||||
|               :items="sessions" | ||||
|               :items-length="pagination.sessions.total" | ||||
|               :loading="reverting.sessions" | ||||
|               v-model:items-per-page="pagination.sessions.pageSize" | ||||
|               @update:options="readSessions" | ||||
|               item-value="id" | ||||
|             > | ||||
|               <template v-slot:item="{ item }: { item: any }"> | ||||
|                 <tr> | ||||
|                   <td>{{ item.id }}</td> | ||||
|                   <td> | ||||
|                     <v-chip v-for="value in item.audiences" size="x-small" color="warning" class="capitalize"> | ||||
|                       {{ value }} | ||||
|                     </v-chip> | ||||
|                   </td> | ||||
|                   <td> | ||||
|                     <v-chip v-for="value in item.claims" size="x-small" color="info" class="font-mono"> | ||||
|                       {{ value }} | ||||
|                     </v-chip> | ||||
|                   </td> | ||||
|                   <td>{{ new Date(item.created_at).toLocaleString() }}</td> | ||||
|                   <td> | ||||
|                     <v-tooltip text="Sign out"> | ||||
|                       <template #activator="{ props }"> | ||||
| @@ -71,7 +37,7 @@ | ||||
|                           size="x-small" | ||||
|                           color="error" | ||||
|                           icon="mdi-logout-variant" | ||||
|                           @click="killSession(item)" | ||||
|                           @click="killTicket(item)" | ||||
|                         /> | ||||
|                       </template> | ||||
|                     </v-tooltip> | ||||
| @@ -124,25 +90,17 @@ | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { request } from "@/scripts/request" | ||||
| import { getAtk, useUserinfo } from "@/stores/userinfo" | ||||
| import { getAtk } from "@/stores/userinfo" | ||||
| import { reactive, ref } from "vue" | ||||
|  | ||||
| const id = useUserinfo() | ||||
|  | ||||
| const error = ref<string | null>(null) | ||||
|  | ||||
| const dataDefinitions: { [id: string]: any[] } = { | ||||
|   challenges: [ | ||||
|   tickets: [ | ||||
|     { align: "start", key: "id", title: "ID" }, | ||||
|     { align: "start", key: "ip_address", title: "IP Address" }, | ||||
|     { align: "start", key: "user_agent", title: "User Agent" }, | ||||
|     { align: "start", key: "created_at", title: "Issued At" }, | ||||
|   ], | ||||
|   sessions: [ | ||||
|     { align: "start", key: "id", title: "ID" }, | ||||
|     { align: "start", key: "audiences", title: "Audiences" }, | ||||
|     { align: "start", key: "claims", title: "Claims" }, | ||||
|     { align: "start", key: "created_at", title: "Issued At" }, | ||||
|     { align: "start", key: "actions", title: "Actions", sortable: false }, | ||||
|   ], | ||||
|   events: [ | ||||
| @@ -155,52 +113,25 @@ const dataDefinitions: { [id: string]: any[] } = { | ||||
|   ], | ||||
| } | ||||
|  | ||||
| const challenges = ref<any>([]) | ||||
| const sessions = ref<any>([]) | ||||
| const tickets = ref<any>([]) | ||||
| const events = ref<any>([]) | ||||
|  | ||||
| const reverting = reactive({ challenges: false, sessions: false, events: false }) | ||||
| const reverting = reactive({ tickets: false, sessions: false, events: false }) | ||||
| const pagination = reactive({ | ||||
|   challenges: { page: 1, pageSize: 5, total: 0 }, | ||||
|   sessions: { page: 1, pageSize: 5, total: 0 }, | ||||
|   tickets: { page: 1, pageSize: 5, total: 0 }, | ||||
|   events: { page: 1, pageSize: 5, total: 0 }, | ||||
| }) | ||||
|  | ||||
| async function readChallenges({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) { | ||||
|   if (itemsPerPage) pagination.challenges.pageSize = itemsPerPage | ||||
|   if (page) pagination.challenges.page = page | ||||
|  | ||||
|   reverting.challenges = true | ||||
|   const res = await request( | ||||
|     "/api/users/me/challenges?" + | ||||
|       new URLSearchParams({ | ||||
|         take: pagination.challenges.pageSize.toString(), | ||||
|         offset: ((pagination.challenges.page - 1) * pagination.challenges.pageSize).toString(), | ||||
|       }), | ||||
|     { | ||||
|       headers: { Authorization: `Bearer ${getAtk()}` }, | ||||
|     }, | ||||
|   ) | ||||
|   if (res.status !== 200) { | ||||
|     error.value = await res.text() | ||||
|   } else { | ||||
|     const data = await res.json() | ||||
|     challenges.value = data["data"] | ||||
|     pagination.challenges.total = data["count"] | ||||
|   } | ||||
|   reverting.challenges = false | ||||
| } | ||||
|  | ||||
| async function readSessions({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) { | ||||
|   if (itemsPerPage) pagination.sessions.pageSize = itemsPerPage | ||||
|   if (page) pagination.sessions.page = page | ||||
| async function readTickets({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) { | ||||
|   if (itemsPerPage) pagination.tickets.pageSize = itemsPerPage | ||||
|   if (page) pagination.tickets.page = page | ||||
|  | ||||
|   reverting.sessions = true | ||||
|   const res = await request( | ||||
|     "/api/users/me/sessions?" + | ||||
|     "/api/users/me/tickets?" + | ||||
|       new URLSearchParams({ | ||||
|         take: pagination.sessions.pageSize.toString(), | ||||
|         offset: ((pagination.sessions.page - 1) * pagination.sessions.pageSize).toString(), | ||||
|         take: pagination.tickets.pageSize.toString(), | ||||
|         offset: ((pagination.tickets.page - 1) * pagination.tickets.pageSize).toString(), | ||||
|       }), | ||||
|     { | ||||
|       headers: { Authorization: `Bearer ${getAtk()}` }, | ||||
| @@ -210,8 +141,8 @@ async function readSessions({ page, itemsPerPage }: { page?: number; itemsPerPag | ||||
|     error.value = await res.text() | ||||
|   } else { | ||||
|     const data = await res.json() | ||||
|     sessions.value = data["data"] | ||||
|     pagination.sessions.total = data["count"] | ||||
|     tickets.value = data["data"] | ||||
|     pagination.tickets.total = data["count"] | ||||
|   } | ||||
|   reverting.sessions = false | ||||
| } | ||||
| @@ -241,18 +172,18 @@ async function readEvents({ page, itemsPerPage }: { page?: number; itemsPerPage? | ||||
|   reverting.events = false | ||||
| } | ||||
|  | ||||
| Promise.all([readChallenges({}), readSessions({}), readEvents({})]) | ||||
| Promise.all([readTickets({}), readEvents({})]) | ||||
|  | ||||
| async function killSession(item: any) { | ||||
| async function killTicket(item: any) { | ||||
|   reverting.sessions = true | ||||
|   const res = await request(`/api/users/me/sessions/${item.id}`, { | ||||
|   const res = await request(`/api/users/me/tickets/${item.id}`, { | ||||
|     method: "DELETE", | ||||
|     headers: { Authorization: `Bearer ${getAtk()}` }, | ||||
|   }) | ||||
|   if (res.status !== 200) { | ||||
|     error.value = await res.text() | ||||
|   } else { | ||||
|     await readSessions({}) | ||||
|     await readTickets({}) | ||||
|     error.value = null | ||||
|   } | ||||
|   reverting.sessions = false | ||||
|   | ||||
		Reference in New Issue
	
	Block a user