feat(auth): email login & profile (#1745)
Co-authored-by: Liet Blue <127093491+lietblue@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { applyOIDCTokens, fetchSession } from '@proj-airi/stage-ui/libs/auth'
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { applyOIDCTokens, fetchSession, triggerSignIn } from '@proj-airi/stage-ui/libs/auth'
|
||||
import { consumeFlowState, exchangeCodeForTokens } from '@proj-airi/stage-ui/libs/auth-oidc'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
@@ -39,12 +40,12 @@ onMounted(async () => {
|
||||
router.replace('/')
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err instanceof Error ? err.message : t('server.auth.webCallback.message.tokenExchangeFailed')
|
||||
error.value = errorMessageFrom(err) ?? t('server.auth.webCallback.message.tokenExchangeFailed')
|
||||
}
|
||||
})
|
||||
|
||||
function handleTryAgain() {
|
||||
router.replace('/auth/sign-in')
|
||||
async function handleTryAgain() {
|
||||
await triggerSignIn()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { OAuthProvider } from '@proj-airi/stage-ui/libs/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 { Button } from '@proj-airi/ui'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { isDesktop } = useBreakpoints()
|
||||
|
||||
const loading = ref<Record<OAuthProvider, boolean>>({
|
||||
google: false,
|
||||
github: false,
|
||||
})
|
||||
|
||||
async function handleSignIn(provider: OAuthProvider) {
|
||||
loading.value[provider] = true
|
||||
try {
|
||||
await signInOIDC({
|
||||
clientId: OIDC_CLIENT_ID,
|
||||
redirectUri: OIDC_REDIRECT_URI,
|
||||
provider,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('server.auth.signIn.error.unknown'))
|
||||
}
|
||||
finally {
|
||||
loading.value[provider] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Check URL for error from failed OAuth callback
|
||||
const url = new URL(window.location.href)
|
||||
const error = url.searchParams.get('error')
|
||||
if (error) {
|
||||
toast.error(error === 'auth_failed' ? t('server.auth.signIn.error.authFailed') : error)
|
||||
url.searchParams.delete('error')
|
||||
window.history.replaceState(null, '', url.pathname)
|
||||
}
|
||||
|
||||
fetchSession()
|
||||
.then((authenticated) => {
|
||||
if (authenticated || !isDesktop.value) {
|
||||
router.replace('/')
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
|
||||
watch(isDesktop, (val) => {
|
||||
if (!val) {
|
||||
router.replace('/')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isDesktop" class="min-h-screen flex flex-col items-center justify-center font-cuteen">
|
||||
<div class="mb-8 text-3xl font-bold">
|
||||
{{ t('server.auth.signIn.title') }}
|
||||
</div>
|
||||
<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">
|
||||
{{ t('server.auth.signIn.footer.prefix') }}
|
||||
<a href="https://airi.moeru.ai/docs/en/about/terms" class="underline">{{ t('server.auth.signIn.footer.terms') }}</a>
|
||||
{{ t('server.auth.signIn.footer.and') }}
|
||||
<a href="https://airi.moeru.ai/docs/en/about/privacy" class="underline">{{ t('server.auth.signIn.footer.privacy') }}</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">
|
||||
<img src="../../assets/logo.svg" class="h-24 w-24 rounded-3xl shadow-lg">
|
||||
<div class="text-3xl font-bold">
|
||||
AIRI
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoginDrawer :open="true" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -154,6 +154,18 @@ export default defineConfig({
|
||||
Unocss(),
|
||||
|
||||
// https://github.com/antfu/vite-plugin-pwa
|
||||
// NOTICE:
|
||||
// The plugin must stay registered in dev — `src/modules/pwa.ts` imports
|
||||
// the `virtual:pwa-register` module the plugin synthesises, and dropping
|
||||
// the plugin breaks Vite's import-analysis with a "Failed to resolve
|
||||
// import" error.
|
||||
// SW generation in dev is already disabled by `devOptions.enabled:
|
||||
// false` (the plugin's own default). So new SWs do NOT register from
|
||||
// `pnpm dev` alone — but a previously-registered SW (e.g. from an
|
||||
// earlier `vite preview` / `vite build`) lives on per-origin in the
|
||||
// browser and keeps intercepting fetches even in dev. To recover from
|
||||
// that state, unregister via DevTools → Application → Storage → Clear
|
||||
// site data.
|
||||
...(env.TARGET_HUGGINGFACE_SPACE
|
||||
? []
|
||||
: [VitePWA({
|
||||
|
||||
Reference in New Issue
Block a user