From 0b30cc2b7e049a2079fadf5d04e02a354f806066 Mon Sep 17 00:00:00 2001 From: ianshaw Date: Thu, 25 Dec 2025 08:39:48 -0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E6=96=B0=E5=A2=9E=20Gemini?= =?UTF-8?q?=20OAuth=20=E6=8E=88=E6=9D=83=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /admin/gemini API 接口封装(generateAuthUrl, exchangeCode) - 新增 useGeminiOAuth composable 处理 Gemini OAuth 流程 - 新增 OAuthCallbackView 视图用于接收 OAuth 回调 - 支持 code/state 参数提取和 credentials 构建 --- frontend/src/api/admin/gemini.ts | 47 +++++++ frontend/src/composables/useGeminiOAuth.ts | 133 ++++++++++++++++++ frontend/src/views/auth/OAuthCallbackView.vue | 87 ++++++++++++ 3 files changed, 267 insertions(+) create mode 100644 frontend/src/api/admin/gemini.ts create mode 100644 frontend/src/composables/useGeminiOAuth.ts create mode 100644 frontend/src/views/auth/OAuthCallbackView.vue diff --git a/frontend/src/api/admin/gemini.ts b/frontend/src/api/admin/gemini.ts new file mode 100644 index 00000000..0a2c4a66 --- /dev/null +++ b/frontend/src/api/admin/gemini.ts @@ -0,0 +1,47 @@ +/** + * Admin Gemini API endpoints + * Handles Gemini OAuth flows for administrators + */ + +import { apiClient } from '../client' + +export interface GeminiAuthUrlResponse { + auth_url: string + session_id: string + state: string +} + +export interface GeminiAuthUrlRequest { + redirect_uri: string + proxy_id?: number +} + +export interface GeminiExchangeCodeRequest { + session_id: string + state: string + code: string + redirect_uri: string + proxy_id?: number +} + +export type GeminiTokenInfo = Record + +export async function generateAuthUrl( + payload: GeminiAuthUrlRequest +): Promise { + const { data } = await apiClient.post( + '/admin/gemini/oauth/auth-url', + payload + ) + return data +} + +export async function exchangeCode(payload: GeminiExchangeCodeRequest): Promise { + const { data } = await apiClient.post( + '/admin/gemini/oauth/exchange-code', + payload + ) + return data +} + +export default { generateAuthUrl, exchangeCode } diff --git a/frontend/src/composables/useGeminiOAuth.ts b/frontend/src/composables/useGeminiOAuth.ts new file mode 100644 index 00000000..63e5c2b9 --- /dev/null +++ b/frontend/src/composables/useGeminiOAuth.ts @@ -0,0 +1,133 @@ +import { ref } from 'vue' +import { useI18n } from 'vue-i18n' +import { useAppStore } from '@/stores/app' +import { adminAPI } from '@/api/admin' + +export interface GeminiTokenInfo { + access_token?: string + refresh_token?: string + token_type?: string + scope?: string + expires_at?: number | string + project_id?: string + [key: string]: unknown +} + +export function useGeminiOAuth() { + const appStore = useAppStore() + const { t } = useI18n() + + const authUrl = ref('') + const sessionId = ref('') + const state = ref('') + const loading = ref(false) + const error = ref('') + + const resetState = () => { + authUrl.value = '' + sessionId.value = '' + state.value = '' + loading.value = false + error.value = '' + } + + const generateAuthUrl = async ( + proxyId: number | null | undefined, + redirectUri: string + ): Promise => { + loading.value = true + authUrl.value = '' + sessionId.value = '' + state.value = '' + error.value = '' + + try { + if (!redirectUri?.trim()) { + error.value = t('admin.accounts.oauth.gemini.missingRedirectUri') + appStore.showError(error.value) + return false + } + + const payload: Record = { redirect_uri: redirectUri.trim() } + if (proxyId) payload.proxy_id = proxyId + + const response = await adminAPI.gemini.generateAuthUrl(payload as any) + authUrl.value = response.auth_url + sessionId.value = response.session_id + state.value = response.state + return true + } catch (err: any) { + error.value = err.response?.data?.detail || t('admin.accounts.oauth.gemini.failedToGenerateUrl') + appStore.showError(error.value) + return false + } finally { + loading.value = false + } + } + + const exchangeAuthCode = async (params: { + code: string + sessionId: string + state: string + redirectUri: string + proxyId?: number | null + }): Promise => { + const code = params.code?.trim() + if (!code || !params.sessionId || !params.state || !params.redirectUri?.trim()) { + error.value = t('admin.accounts.oauth.gemini.missingExchangeParams') + return null + } + + loading.value = true + error.value = '' + + try { + const payload: Record = { + session_id: params.sessionId, + state: params.state, + code, + redirect_uri: params.redirectUri.trim() + } + if (params.proxyId) payload.proxy_id = params.proxyId + + const tokenInfo = await adminAPI.gemini.exchangeCode(payload as any) + return tokenInfo as GeminiTokenInfo + } catch (err: any) { + error.value = err.response?.data?.detail || t('admin.accounts.oauth.gemini.failedToExchangeCode') + appStore.showError(error.value) + return null + } finally { + loading.value = false + } + } + + const buildCredentials = (tokenInfo: GeminiTokenInfo): Record => { + let expiresAt: string | undefined + if (typeof tokenInfo.expires_at === 'number' && Number.isFinite(tokenInfo.expires_at)) { + expiresAt = Math.floor(tokenInfo.expires_at).toString() + } else if (typeof tokenInfo.expires_at === 'string' && tokenInfo.expires_at.trim()) { + expiresAt = tokenInfo.expires_at.trim() + } + + return { + access_token: tokenInfo.access_token, + refresh_token: tokenInfo.refresh_token, + token_type: tokenInfo.token_type, + expires_at: expiresAt, + scope: tokenInfo.scope, + project_id: tokenInfo.project_id + } + } + + return { + authUrl, + sessionId, + state, + loading, + error, + resetState, + generateAuthUrl, + exchangeAuthCode, + buildCredentials + } +} diff --git a/frontend/src/views/auth/OAuthCallbackView.vue b/frontend/src/views/auth/OAuthCallbackView.vue new file mode 100644 index 00000000..64489e42 --- /dev/null +++ b/frontend/src/views/auth/OAuthCallbackView.vue @@ -0,0 +1,87 @@ + + +