💄 Rework of the authorize page
This commit is contained in:
21
app/app.vue
21
app/app.vue
@@ -2,9 +2,10 @@
|
||||
<naive-config>
|
||||
<n-dialog-provider>
|
||||
<n-notification-provider>
|
||||
<naive-notification />
|
||||
<n-message-provider>
|
||||
<n-loading-bar-provider>
|
||||
<nuxt-loading-indicator />
|
||||
<naive-loading-bar navigation />
|
||||
<nuxt-layout>
|
||||
<nuxt-page />
|
||||
</nuxt-layout>
|
||||
@@ -17,22 +18,4 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import "@fontsource-variable/nunito"
|
||||
import { usePreferredColorScheme } from "@vueuse/core"
|
||||
|
||||
const { colorModePreference } = useNaiveColorMode()
|
||||
const colorScheme = usePreferredColorScheme()
|
||||
|
||||
colorModePreference.set("system")
|
||||
|
||||
onMounted(() => {
|
||||
switch (colorScheme.value) {
|
||||
case "dark":
|
||||
colorModePreference.set("dark")
|
||||
case "light":
|
||||
colorModePreference.set("light")
|
||||
default:
|
||||
colorModePreference.set("system")
|
||||
}
|
||||
colorModePreference.sync()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -36,6 +36,12 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.h-screen-no-header {
|
||||
.h-layout {
|
||||
/* margin of the navbar + actual navbar */
|
||||
height: calc(100vh - 64px*2);
|
||||
}
|
||||
|
||||
.min-h-layout {
|
||||
/* margin of the navbar + actual navbar */
|
||||
min-height: calc(100vh - 64px*2);
|
||||
}
|
||||
6
app/components.d.ts
vendored
6
app/components.d.ts
vendored
@@ -21,6 +21,7 @@ declare module 'vue' {
|
||||
NCarousel: typeof import('naive-ui')['NCarousel']
|
||||
NCarouselItem: typeof import('naive-ui')['NCarouselItem']
|
||||
NChip: typeof import('naive-ui')['NChip']
|
||||
NCode: typeof import('naive-ui')['NCode']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDialog: typeof import('naive-ui')['NDialog']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
@@ -32,6 +33,8 @@ declare module 'vue' {
|
||||
NImg: typeof import('naive-ui')['NImg']
|
||||
NInfiniteScroll: typeof import('naive-ui')['NInfiniteScroll']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
@@ -62,6 +65,7 @@ declare global {
|
||||
const NCarousel: typeof import('naive-ui')['NCarousel']
|
||||
const NCarouselItem: typeof import('naive-ui')['NCarouselItem']
|
||||
const NChip: typeof import('naive-ui')['NChip']
|
||||
const NCode: typeof import('naive-ui')['NCode']
|
||||
const NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
const NDialog: typeof import('naive-ui')['NDialog']
|
||||
const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
@@ -73,6 +77,8 @@ declare global {
|
||||
const NImg: typeof import('naive-ui')['NImg']
|
||||
const NInfiniteScroll: typeof import('naive-ui')['NInfiniteScroll']
|
||||
const NInput: typeof import('naive-ui')['NInput']
|
||||
const NList: typeof import('naive-ui')['NList']
|
||||
const NListItem: typeof import('naive-ui')['NListItem']
|
||||
const NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
const NMenu: typeof import('naive-ui')['NMenu']
|
||||
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
|
||||
@@ -3,6 +3,7 @@ import { keysToCamel, keysToSnake } from "~/utils/transformKeys"
|
||||
|
||||
export const useSolarNetwork = () => {
|
||||
const apiBase = useSolarNetworkUrl()
|
||||
const devToken = useRuntimeConfig().public.devToken
|
||||
|
||||
// Forward cookies from the incoming request
|
||||
const headers: HeadersInit = import.meta.server
|
||||
@@ -18,6 +19,11 @@ export const useSolarNetwork = () => {
|
||||
const side = import.meta.server ? "SERVER" : "CLIENT"
|
||||
console.log(`[useSolarNetwork] onRequest for ${request} on ${side}`)
|
||||
|
||||
if (devToken) {
|
||||
options.headers = new Headers(options.headers)
|
||||
options.headers.set("Authorization", `Bearer ${devToken}`)
|
||||
}
|
||||
|
||||
// Transform request data from camelCase to snake_case
|
||||
if (options.body && typeof options.body === "object") {
|
||||
options.body = keysToSnake(options.body)
|
||||
|
||||
@@ -1,110 +1,17 @@
|
||||
<template>
|
||||
<v-container class="d-flex align-center justify-center fill-height">
|
||||
<v-card max-width="1000" rounded="lg" width="100%">
|
||||
<div v-if="isLoading" class="d-flex justify-center mb-4">
|
||||
<v-progress-linear indeterminate color="primary" height="4" />
|
||||
</div>
|
||||
<div class="pa-8">
|
||||
<div class="mb-4">
|
||||
<img :src="IconLight" alt="CloudyLamb" height="60" width="60" />
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col cols="12" lg="6" class="d-flex align-start justify-start">
|
||||
<div class="md:text-left h-auto">
|
||||
<h2 class="text-2xl font-bold mb-1">Authorize Application</h2>
|
||||
<p class="text-lg">Grant access to your Solar Network account</p>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="6" class="d-flex align-center justify-stretch">
|
||||
<div class="w-full d-flex flex-column md:text-right">
|
||||
<div v-if="error" class="mb-4">
|
||||
<v-alert
|
||||
type="error"
|
||||
closable
|
||||
@update:model-value="error = null"
|
||||
>
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<!-- App Info Section -->
|
||||
<div v-if="clientInfo" class="mb-6">
|
||||
<div class="d-flex align-center mb-4 text-left">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold">
|
||||
{{ clientInfo.clientName || "Unknown Application" }}
|
||||
</h3>
|
||||
<p class="text-base">
|
||||
{{
|
||||
isNewApp
|
||||
? "wants to access your Solar Network account"
|
||||
: "wants to access your account"
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requested Permissions -->
|
||||
<v-card variant="outlined" class="pa-4 mb-4 text-left">
|
||||
<h4 class="font-medium mb-2">
|
||||
This will allow
|
||||
{{ clientInfo.clientName || "the app" }} to
|
||||
</h4>
|
||||
<ul class="space-y-1">
|
||||
<li
|
||||
v-for="scope in requestedScopes"
|
||||
:key="scope"
|
||||
class="d-flex align-start"
|
||||
>
|
||||
<v-icon class="mt-1 mr-2" color="success" size="18"
|
||||
>mdi-check</v-icon
|
||||
>
|
||||
<span>{{ scope }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</v-card>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="d-flex gap-3 mt-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:loading="isAuthorizing"
|
||||
class="grow"
|
||||
size="large"
|
||||
@click="handleAuthorize"
|
||||
>
|
||||
Authorize
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
:disabled="isAuthorizing"
|
||||
class="grow"
|
||||
size="large"
|
||||
@click="handleDeny"
|
||||
>
|
||||
Deny
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card>
|
||||
<footer-compact />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { useSolarNetwork } from "~/composables/useSolarNetwork"
|
||||
import { CheckIcon } from "lucide-vue-next"
|
||||
|
||||
import IconLight from "~/assets/images/cloudy-lamb.png"
|
||||
import type { SnCloudFile } from "~/types/api/post"
|
||||
|
||||
const route = useRoute()
|
||||
const api = useSolarNetwork()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
useHead({
|
||||
title: "Authorize Application"
|
||||
})
|
||||
@@ -112,11 +19,11 @@ useHead({
|
||||
// State
|
||||
const isLoading = ref(true)
|
||||
const isAuthorizing = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const clientInfo = ref<{
|
||||
clientName?: string
|
||||
homeUri?: string
|
||||
picture?: { url: string }
|
||||
picture?: SnCloudFile
|
||||
background?: SnCloudFile
|
||||
scopes?: string[]
|
||||
} | null>(null)
|
||||
const isNewApp = ref(false)
|
||||
@@ -133,9 +40,7 @@ async function fetchClientInfo() {
|
||||
clientInfo.value = await api(`/id/auth/open/authorize?${queryString}`)
|
||||
checkIfNewApp()
|
||||
} catch (err) {
|
||||
error.value =
|
||||
(err instanceof Error ? err.message : String(err)) ||
|
||||
"An error occurred while loading the authorization request"
|
||||
message.error(err instanceof Error ? err.message : String(err))
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
@@ -165,9 +70,7 @@ async function handleAuthorize(authorize = true) {
|
||||
window.location.href = data.redirectUri
|
||||
}
|
||||
} catch (err) {
|
||||
error.value =
|
||||
(err instanceof Error ? err.message : String(err)) ||
|
||||
"An error occurred during authorization"
|
||||
message.error(err instanceof Error ? err.message : String(err))
|
||||
} finally {
|
||||
isAuthorizing.value = false
|
||||
}
|
||||
@@ -185,8 +88,135 @@ onMounted(() => {
|
||||
definePageMeta({
|
||||
middleware: "auth"
|
||||
})
|
||||
|
||||
const apiBase = useSolarNetworkUrl()
|
||||
const clientAvatar = computed(() =>
|
||||
clientInfo.value?.picture
|
||||
? `${apiBase}/drive/files/${clientInfo.value.picture.id}`
|
||||
: undefined
|
||||
)
|
||||
const clientBackground = computed(() =>
|
||||
clientInfo.value?.background
|
||||
? `${apiBase}/drive/files/${clientInfo.value.background.id}?original=true`
|
||||
: undefined
|
||||
)
|
||||
|
||||
const pageStyle = computed(() => {
|
||||
if (!clientBackground.value) return {}
|
||||
return {
|
||||
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url('${clientBackground.value}')`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any custom styles here */
|
||||
</style>
|
||||
<template>
|
||||
<div class="fixed inset-0 transition-all duration-500" :style="pageStyle" />
|
||||
<div class="relative flex items-center justify-center min-h-layout px-4">
|
||||
<n-card
|
||||
:class="[
|
||||
'w-full',
|
||||
'max-w-[1000px]',
|
||||
{ 'backdrop-blur-2xl': clientBackground },
|
||||
{ 'shadow-xl': clientBackground }
|
||||
]"
|
||||
size="large"
|
||||
:style="
|
||||
clientBackground ? 'background-color: rgba(255, 255, 255, 0.1)' : ''
|
||||
"
|
||||
:content-style="clientBackground ? 'background-color: transparent' : ''"
|
||||
>
|
||||
<div v-if="isLoading" class="flex justify-center p-8">
|
||||
<n-spin size="large" />
|
||||
</div>
|
||||
<div v-else class="p-4 md:p-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left Column: Title -->
|
||||
<div class="flex flex-col items-start justify-start">
|
||||
<div class="mb-4">
|
||||
<img :src="IconLight" alt="CloudyLamb" height="60" width="60" />
|
||||
</div>
|
||||
<div class="text-left h-auto">
|
||||
<h2 class="text-2xl font-bold mb-1">Authorize Application</h2>
|
||||
<p class="text-lg">Grant access to your Solar Network account</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Content -->
|
||||
<div class="flex flex-col items-end justify-stretch">
|
||||
<div class="w-full flex flex-col md:text-right">
|
||||
<div class="mb-3 h-[60px] px-[4px] pt-[8px]">
|
||||
<n-avatar :src="clientAvatar" :size="52" />
|
||||
</div>
|
||||
<!-- App Info Section -->
|
||||
<div v-if="clientInfo" class="mb-6">
|
||||
<div class="flex flex-col items-end mb-4">
|
||||
<h3 class="text-xl font-semibold">
|
||||
{{ clientInfo.clientName || "Unknown Application" }}
|
||||
</h3>
|
||||
<p class="text-base">
|
||||
{{
|
||||
isNewApp
|
||||
? "Wants to access your Solar Network account"
|
||||
: "Wants to access your account"
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Requested Permissions -->
|
||||
<n-card embedded class="mb-4 text-left">
|
||||
<h4 class="font-medium mb-2">
|
||||
This will allow
|
||||
{{ clientInfo.clientName || "the app" }} to
|
||||
</h4>
|
||||
<n-list style="background-color: transparent" size="small">
|
||||
<n-list-item
|
||||
v-for="scope in requestedScopes"
|
||||
:key="scope"
|
||||
class="bg-transparent"
|
||||
style="padding: 0"
|
||||
>
|
||||
<template #prefix>
|
||||
<n-icon
|
||||
class="mt-1 mr-2"
|
||||
color="#18a058"
|
||||
:size="16"
|
||||
:component="CheckIcon"
|
||||
/>
|
||||
</template>
|
||||
<span>{{ scope }}</span>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-card>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex gap-3 mt-4">
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="isAuthorizing"
|
||||
class="grow"
|
||||
size="large"
|
||||
@click="handleAuthorize(true)"
|
||||
>
|
||||
Authorize
|
||||
</n-button>
|
||||
<n-button
|
||||
secondary
|
||||
:disabled="isAuthorizing"
|
||||
class="grow"
|
||||
size="large"
|
||||
@click="handleDeny"
|
||||
>
|
||||
Deny
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center h-screen-no-header px-4">
|
||||
<div class="flex items-center justify-center h-layout px-4">
|
||||
<n-card class="w-full max-w-[1000px]" size="large">
|
||||
<div class="p-4 md:p-8">
|
||||
<div class="mb-4">
|
||||
|
||||
@@ -191,9 +191,9 @@ async function exchangeToken() {
|
||||
|
||||
await userStore.fetchUser()
|
||||
|
||||
const redirectUri = route.query.redirect_uri as string
|
||||
const redirectUri = route.query.redirect as string
|
||||
if (redirectUri) {
|
||||
window.location.href = redirectUri
|
||||
window.location.href = decodeURIComponent(redirectUri)
|
||||
} else {
|
||||
await router.push("/")
|
||||
}
|
||||
@@ -258,7 +258,7 @@ function getFactorIcon(factorType: number) {
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-center h-screen-no-header px-4">
|
||||
<div class="flex flex-col gap-3 items-center justify-center h-layout px-4">
|
||||
<n-card class="w-full max-w-[1000px]" size="large">
|
||||
<div class="p-4 md:p-8">
|
||||
<div class="mb-4">
|
||||
@@ -450,5 +450,21 @@ function getFactorIcon(factorType: number) {
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<n-alert
|
||||
v-if="route.query.redirect"
|
||||
class="w-full max-w-[1000px]"
|
||||
type="info"
|
||||
title="Login before you continue"
|
||||
size="large"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p>
|
||||
You're requesting a page that requires authorization to access, please
|
||||
login with your Solarpass and then we will redirect you to:
|
||||
</p>
|
||||
<n-code class="text-xs">{{ route.query.redirect }}</n-code>
|
||||
</div>
|
||||
</n-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,9 @@ export const useUserStore = defineStore("user", () => {
|
||||
// Actions
|
||||
async function fetchUser(reload = true): Promise<void> {
|
||||
if (currentFetchPromise.value) {
|
||||
console.log("[UserStore] Fetch already in progress. Waiting for existing fetch.")
|
||||
console.log(
|
||||
"[UserStore] Fetch already in progress. Waiting for existing fetch."
|
||||
)
|
||||
return currentFetchPromise.value
|
||||
}
|
||||
if (!reload && user.value) {
|
||||
@@ -37,10 +39,18 @@ export const useUserStore = defineStore("user", () => {
|
||||
console.log(`[UserStore] Logged in as @${user.value.name}`)
|
||||
} catch (e: unknown) {
|
||||
// Check for 401 Unauthorized error
|
||||
const is401Error = (e instanceof FetchError && e.statusCode === 401) ||
|
||||
(e && typeof e === 'object' && 'status' in e && (e as { status: number }).status === 401) ||
|
||||
(e && typeof e === 'object' && 'statusCode' in e && (e as { statusCode: number }).statusCode === 401) ||
|
||||
(e instanceof Error && (e.message?.includes('401') || e.message?.includes('Unauthorized')))
|
||||
const is401Error =
|
||||
(e instanceof FetchError && e.statusCode === 401) ||
|
||||
(e &&
|
||||
typeof e === "object" &&
|
||||
"status" in e &&
|
||||
(e as { status: number }).status === 401) ||
|
||||
(e &&
|
||||
typeof e === "object" &&
|
||||
"statusCode" in e &&
|
||||
(e as { statusCode: number }).statusCode === 401) ||
|
||||
(e instanceof Error &&
|
||||
(e.message?.includes("401") || e.message?.includes("Unauthorized")))
|
||||
|
||||
if (is401Error) {
|
||||
error.value = "Unauthorized"
|
||||
@@ -50,6 +60,8 @@ export const useUserStore = defineStore("user", () => {
|
||||
user.value = null // Clear user data on error
|
||||
console.error("Failed to fetch user... ", e)
|
||||
}
|
||||
|
||||
console.log(`[UserStore] Logged as @${user.value!.name}`)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
currentFetchPromise.value = null
|
||||
|
||||
@@ -58,7 +58,8 @@ export default defineNuxtConfig({
|
||||
public: {
|
||||
development: process.env.NODE_ENV == "development",
|
||||
apiBase: process.env.NUXT_PUBLIC_API_BASE || "https://api.solian.app",
|
||||
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || "https://solian.app"
|
||||
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || "https://solian.app",
|
||||
devToken: process.env.NUXT_PUBLIC_DEV_TOKEN || ""
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
@@ -82,6 +83,8 @@ export default defineNuxtConfig({
|
||||
]
|
||||
},
|
||||
naiveui: {
|
||||
colorModePreference: "system",
|
||||
colorModePreferenceCookieName: "fi-ColorMode",
|
||||
themeConfig: {
|
||||
...generateTailwindColorThemes(),
|
||||
shared: {
|
||||
|
||||
Reference in New Issue
Block a user