refactor(stage-web,ui-server-auth): rollback sign in design
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { applyOIDCTokens, fetchSession } from '@proj-airi/stage-ui/libs/auth'
|
||||
import { consumeFlowState, exchangeCodeForTokens } from '@proj-airi/stage-ui/libs/auth-oidc'
|
||||
import { Button, Callout } from '@proj-airi/ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
@@ -39,22 +40,37 @@ onMounted(async () => {
|
||||
error.value = err instanceof Error ? err.message : 'Token exchange failed'
|
||||
}
|
||||
})
|
||||
|
||||
function handleTryAgain() {
|
||||
router.replace('/auth/sign-in')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['min-h-screen', 'flex flex-col items-center justify-center']">
|
||||
<div v-if="error" :class="['max-w-md', 'text-center']">
|
||||
<div :class="['text-lg font-semibold', 'text-red-600 dark:text-red-400']">
|
||||
Authentication failed
|
||||
<div v-if="error" :class="['max-w-md', 'flex flex-col items-center']">
|
||||
<div class="mb-8 text-3xl font-bold">
|
||||
Sign in
|
||||
</div>
|
||||
<div :class="['mt-2', 'text-sm text-gray-500']">
|
||||
{{ error }}
|
||||
</div>
|
||||
<a href="/auth/sign-in" :class="['mt-4 inline-block', 'text-sm underline']">
|
||||
<Callout theme="orange" label="We encountered an error while signing you in">
|
||||
<div :class="['mt-1', 'text-sm']">
|
||||
{{ error }}
|
||||
</div>
|
||||
</Callout>
|
||||
<Button :class="['mt-3 inline-flex']" @click="handleTryAgain">
|
||||
Try again
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<div v-else :class="['text-center']">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
'mx-auto mb-3',
|
||||
'h-10 w-10',
|
||||
'i-svg-spinners:ring-resize',
|
||||
'text-primary-500',
|
||||
]"
|
||||
/>
|
||||
<div :class="['text-lg']">
|
||||
Signing in...
|
||||
</div>
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { OAuthProvider } from '@proj-airi/stage-ui/libs/auth'
|
||||
|
||||
import { defaultSignInProviders, LoginDrawer, SignInPanel } from '@proj-airi/stage-ui/components/auth'
|
||||
import { LoginDrawer } from '@proj-airi/stage-ui/components/auth'
|
||||
import { useBreakpoints } from '@proj-airi/stage-ui/composables'
|
||||
import { fetchSession, signInOIDC } from '@proj-airi/stage-ui/libs/auth'
|
||||
import { OIDC_CLIENT_ID, OIDC_REDIRECT_URI } from '@proj-airi/stage-ui/libs/auth-config'
|
||||
import { onMounted, shallowRef, watch } from 'vue'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const { isDesktop } = useBreakpoints()
|
||||
|
||||
const pendingProvider = shallowRef<OAuthProvider | null>(null)
|
||||
const errorMessage = shallowRef<string | null>(null)
|
||||
const loading = ref<Record<OAuthProvider, boolean>>({
|
||||
google: false,
|
||||
github: false,
|
||||
})
|
||||
|
||||
async function handleSignIn(provider: OAuthProvider) {
|
||||
errorMessage.value = null
|
||||
pendingProvider.value = provider
|
||||
|
||||
loading.value[provider] = true
|
||||
try {
|
||||
await signInOIDC({
|
||||
clientId: OIDC_CLIENT_ID,
|
||||
@@ -27,10 +29,10 @@ async function handleSignIn(provider: OAuthProvider) {
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : 'An unknown error occurred'
|
||||
toast.error(error instanceof Error ? error.message : 'An unknown error occurred')
|
||||
}
|
||||
finally {
|
||||
pendingProvider.value = null
|
||||
loading.value[provider] = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +41,7 @@ onMounted(() => {
|
||||
const url = new URL(window.location.href)
|
||||
const error = url.searchParams.get('error')
|
||||
if (error) {
|
||||
errorMessage.value = error === 'auth_failed' ? 'Authentication failed. Please try again.' : error
|
||||
toast.error(error === 'auth_failed' ? 'Authentication failed. Please try again.' : error)
|
||||
url.searchParams.delete('error')
|
||||
window.history.replaceState(null, '', url.pathname)
|
||||
}
|
||||
@@ -61,31 +63,32 @@ watch(isDesktop, (val) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main
|
||||
v-if="isDesktop"
|
||||
:class="[
|
||||
'relative min-h-screen overflow-hidden bg-[radial-gradient(circle_at_top,_rgba(115,190,255,0.18),_transparent_45%),linear-gradient(180deg,_rgba(255,255,255,0.98),_rgba(243,244,246,0.96))] px-6 py-10',
|
||||
'dark:bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.16),_transparent_38%),linear-gradient(180deg,_rgba(3,7,18,0.98),_rgba(10,15,28,0.98))]',
|
||||
'flex items-center justify-center',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'pointer-events-none absolute left-1/2 top-0 h-72 w-72 -translate-x-1/2 rounded-full bg-primary-300/20 blur-3xl',
|
||||
'dark:bg-primary-500/10',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div :class="['relative w-full max-w-md']">
|
||||
<SignInPanel
|
||||
:providers="defaultSignInProviders"
|
||||
:pending-provider="pendingProvider"
|
||||
:error="errorMessage"
|
||||
subtitle="Choose a provider to sign in and return to AIRI."
|
||||
@select="handleSignIn"
|
||||
/>
|
||||
<div v-if="isDesktop" class="min-h-screen flex flex-col items-center justify-center">
|
||||
<div class="mb-8 text-3xl font-bold">
|
||||
Sign in
|
||||
</div>
|
||||
</main>
|
||||
<div class="max-w-xs w-full flex flex-col gap-3">
|
||||
<Button
|
||||
:class="['w-full', 'py-2', 'flex', 'items-center', 'justify-center']"
|
||||
icon="i-simple-icons-google"
|
||||
:loading="loading.google"
|
||||
@click="handleSignIn('google')"
|
||||
>
|
||||
<span>Google</span>
|
||||
</Button>
|
||||
<Button
|
||||
:class="['w-full', 'py-2', 'flex', 'items-center', 'justify-center']"
|
||||
icon="i-simple-icons-github"
|
||||
:loading="loading.github"
|
||||
@click="handleSignIn('github')"
|
||||
>
|
||||
<span>GitHub</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mt-8 text-xs text-gray-400">
|
||||
By continuing, you agree to our <a href="https://airi.moeru.ai/docs/en/about/terms" class="underline">Terms</a> and <a href="https://airi.moeru.ai/docs/en/about/privacy" class="underline">Privacy Policy</a>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="min-h-screen flex flex-col items-center justify-center bg-neutral-100 dark:bg-neutral-950">
|
||||
<div class="mb-12 flex flex-col items-center gap-4">
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { BackgroundGradientOverlay } from '@proj-airi/stage-ui/components'
|
||||
import { onMounted, shallowRef } from 'vue'
|
||||
|
||||
import AuthNotice from '../components/auth-notice.vue'
|
||||
|
||||
import { parseElectronCallbackQuery } from '../composables/electron-callback.shared'
|
||||
|
||||
type CallbackStatus = 'loading' | 'success' | 'fallback' | 'error'
|
||||
@@ -129,46 +126,82 @@ onMounted(() => {
|
||||
<template>
|
||||
<main
|
||||
:class="[
|
||||
'relative min-h-screen overflow-hidden px-4 py-10 sm:px-6',
|
||||
'flex items-center justify-center',
|
||||
'bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.95),rgba(245,247,255,0.72)_42%,rgba(238,243,255,0.42)_100%)]',
|
||||
'dark:bg-[radial-gradient(circle_at_top,rgba(27,31,45,0.96),rgba(12,15,24,0.92)_46%,rgba(5,7,12,1)_100%)]',
|
||||
'min-h-screen flex items-center justify-center px-6 py-10',
|
||||
]"
|
||||
>
|
||||
<BackgroundGradientOverlay color="color-mix(in srgb, rgb(83 122 255 / 28%) 55%, transparent)" />
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
:class="[
|
||||
'pointer-events-none absolute left-1/2 top-[16%] size-[22rem] -translate-x-1/2 rounded-full blur-3xl',
|
||||
'bg-[radial-gradient(circle,rgba(118,156,255,0.28),transparent_68%)]',
|
||||
'dark:bg-[radial-gradient(circle,rgba(118,156,255,0.16),transparent_72%)]',
|
||||
'max-w-xl w-full rounded-xl border border-neutral-200 bg-white p-6',
|
||||
'dark:border-neutral-800 dark:bg-neutral-900',
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="relative z-1 max-w-3xl w-full flex flex-col items-center gap-5">
|
||||
<AuthNotice
|
||||
:description="viewModel.description"
|
||||
:detail="viewModel.detail"
|
||||
:primary-action-disabled="viewModel.primaryActionDisabled"
|
||||
:primary-action-label="viewModel.primaryActionLabel"
|
||||
:secondary-action-label="viewModel.secondaryActionLabel"
|
||||
:status="viewModel.status"
|
||||
:title="viewModel.title"
|
||||
@primary-action="openRelayUrl"
|
||||
@secondary-action="copyRelayUrl"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'text-2xl font-bold',
|
||||
]"
|
||||
>
|
||||
<a
|
||||
v-if="viewModel.relayUrl && viewModel.status === 'fallback'"
|
||||
{{ viewModel.title }}
|
||||
</div>
|
||||
|
||||
<p
|
||||
:class="[
|
||||
'mt-3 text-sm text-neutral-600 dark:text-neutral-300',
|
||||
]"
|
||||
>
|
||||
{{ viewModel.description }}
|
||||
</p>
|
||||
|
||||
<p
|
||||
v-if="viewModel.detail"
|
||||
:class="[
|
||||
'mt-3 break-all text-xs text-neutral-500 dark:text-neutral-400',
|
||||
]"
|
||||
>
|
||||
{{ viewModel.detail }}
|
||||
</p>
|
||||
|
||||
<div
|
||||
:class="[
|
||||
'mt-6 flex flex-wrap items-center gap-2',
|
||||
]"
|
||||
>
|
||||
<button
|
||||
v-if="viewModel.primaryActionLabel"
|
||||
type="button"
|
||||
:disabled="viewModel.primaryActionDisabled"
|
||||
:class="[
|
||||
'break-all text-center text-xs leading-6 text-neutral-500 underline decoration-dotted underline-offset-4',
|
||||
'hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200',
|
||||
'rounded-md border border-neutral-300 px-3 py-2 text-sm',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'dark:border-neutral-700',
|
||||
]"
|
||||
:href="viewModel.relayUrl"
|
||||
@click="openRelayUrl"
|
||||
>
|
||||
{{ viewModel.relayUrl }}
|
||||
</a>
|
||||
</AuthNotice>
|
||||
{{ viewModel.primaryActionLabel }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="viewModel.secondaryActionLabel"
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-md border border-neutral-300 px-3 py-2 text-sm',
|
||||
'dark:border-neutral-700',
|
||||
]"
|
||||
@click="copyRelayUrl"
|
||||
>
|
||||
{{ viewModel.secondaryActionLabel }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<a
|
||||
v-if="viewModel.relayUrl && viewModel.status === 'fallback'"
|
||||
:class="[
|
||||
'mt-4 block break-all text-xs text-neutral-500 underline decoration-dotted underline-offset-4',
|
||||
'hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200',
|
||||
]"
|
||||
:href="viewModel.relayUrl"
|
||||
>
|
||||
{{ viewModel.relayUrl }}
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { OAuthProvider } from '@proj-airi/stage-ui/libs/auth'
|
||||
|
||||
import { defaultSignInProviders, SignInPanel } from '@proj-airi/stage-ui/components/auth'
|
||||
import { defaultSignInProviders } from '@proj-airi/stage-ui/components/auth'
|
||||
import { SERVER_URL } from '@proj-airi/stage-ui/libs/server'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
@@ -62,26 +63,41 @@ async function handleProviderSelect(provider: OAuthProvider) {
|
||||
<template>
|
||||
<main
|
||||
:class="[
|
||||
'relative min-h-screen overflow-hidden bg-[radial-gradient(circle_at_top,_rgba(115,190,255,0.18),_transparent_45%),linear-gradient(180deg,_rgba(255,255,255,0.98),_rgba(243,244,246,0.96))] px-6 py-10',
|
||||
'dark:bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.16),_transparent_38%),linear-gradient(180deg,_rgba(3,7,18,0.98),_rgba(10,15,28,0.98))]',
|
||||
'flex items-center justify-center',
|
||||
'min-h-screen flex flex-col items-center justify-center px-6 py-10',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'pointer-events-none absolute left-1/2 top-0 h-72 w-72 -translate-x-1/2 rounded-full bg-primary-300/20 blur-3xl',
|
||||
'dark:bg-primary-500/10',
|
||||
'mb-8 text-3xl font-bold',
|
||||
]"
|
||||
/>
|
||||
>
|
||||
Sign in
|
||||
</div>
|
||||
|
||||
<div :class="['relative w-full max-w-md']">
|
||||
<SignInPanel
|
||||
:providers="defaultSignInProviders"
|
||||
:pending-provider="pendingProvider"
|
||||
:error="errorMessage"
|
||||
subtitle="Pick a provider to resume the AIRI authorization flow."
|
||||
@select="handleProviderSelect"
|
||||
/>
|
||||
<div
|
||||
:class="[
|
||||
'max-w-xs w-full flex flex-col gap-3',
|
||||
]"
|
||||
>
|
||||
<Button
|
||||
v-for="provider in defaultSignInProviders"
|
||||
:key="provider.id"
|
||||
:class="['w-full', 'py-2', 'flex', 'items-center', 'justify-center']"
|
||||
:icon="provider.id === 'google' ? 'i-simple-icons-google' : provider.id === 'github' ? 'i-simple-icons-github' : undefined"
|
||||
:loading="pendingProvider === provider.id"
|
||||
@click="handleProviderSelect(provider.id)"
|
||||
>
|
||||
<span>{{ provider.name }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="errorMessage"
|
||||
:class="[
|
||||
'mt-4 max-w-xs w-full text-center text-sm text-red-500',
|
||||
]"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user