refactor(stage-web,ui-server-auth): rollback sign in design

This commit is contained in:
Neko Ayaka
2026-04-07 18:18:55 +08:00
parent d989e2c8b7
commit ffb7c2e05d
4 changed files with 159 additions and 91 deletions
+24 -8
View File
@@ -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>
+37 -34
View File
@@ -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>
+31 -15
View File
@@ -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>