feat(frontend): 新增 Gemini OAuth 授权流程
- 新增 /admin/gemini API 接口封装(generateAuthUrl, exchangeCode) - 新增 useGeminiOAuth composable 处理 Gemini OAuth 流程 - 新增 OAuthCallbackView 视图用于接收 OAuth 回调 - 支持 code/state 参数提取和 credentials 构建
This commit is contained in:
87
frontend/src/views/auth/OAuthCallbackView.vue
Normal file
87
frontend/src/views/auth/OAuthCallbackView.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-50 px-4 py-10 dark:bg-dark-900">
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="card p-6">
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">OAuth Callback</h1>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Copy the <code>code</code> (and <code>state</code> if needed) back to the admin
|
||||
authorization flow.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 space-y-4">
|
||||
<div>
|
||||
<label class="input-label">Code</label>
|
||||
<div class="flex gap-2">
|
||||
<input class="input flex-1 font-mono text-sm" :value="code" readonly />
|
||||
<button class="btn btn-secondary" type="button" :disabled="!code" @click="copy(code)">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">State</label>
|
||||
<div class="flex gap-2">
|
||||
<input class="input flex-1 font-mono text-sm" :value="state" readonly />
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
type="button"
|
||||
:disabled="!state"
|
||||
@click="copy(state)"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">Full URL</label>
|
||||
<div class="flex gap-2">
|
||||
<input class="input flex-1 font-mono text-xs" :value="fullUrl" readonly />
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
type="button"
|
||||
:disabled="!fullUrl"
|
||||
@click="copy(fullUrl)"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="error"
|
||||
class="rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30"
|
||||
>
|
||||
<p class="text-sm text-red-600 dark:text-red-400">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
|
||||
const route = useRoute()
|
||||
const { copyToClipboard } = useClipboard()
|
||||
|
||||
const code = computed(() => (route.query.code as string) || '')
|
||||
const state = computed(() => (route.query.state as string) || '')
|
||||
const error = computed(
|
||||
() => (route.query.error as string) || (route.query.error_description as string) || ''
|
||||
)
|
||||
|
||||
const fullUrl = computed(() => {
|
||||
if (typeof window === 'undefined') return ''
|
||||
return window.location.href
|
||||
})
|
||||
|
||||
const copy = (value: string) => {
|
||||
if (!value) return
|
||||
copyToClipboard(value, 'Copied')
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user