feat(auth): email login & profile (#1745)

Co-authored-by: Liet Blue <127093491+lietblue@users.noreply.github.com>
This commit is contained in:
RainbowBird
2026-04-28 00:07:38 +08:00
committed by GitHub
co-authored by Liet Blue
parent 0346aa729e
commit 172e4ce59c
50 changed files with 3469 additions and 385 deletions
+5 -4
View File
@@ -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>
-108
View File
@@ -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>
+12
View File
@@ -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({