feat(frontend): 新增 Gemini OAuth 授权流程

- 新增 /admin/gemini API 接口封装(generateAuthUrl, exchangeCode)
- 新增 useGeminiOAuth composable 处理 Gemini OAuth 流程
- 新增 OAuthCallbackView 视图用于接收 OAuth 回调
- 支持 code/state 参数提取和 credentials 构建
This commit is contained in:
ianshaw
2025-12-25 08:39:48 -08:00
parent 03a8ae62e5
commit 0b30cc2b7e
3 changed files with 267 additions and 0 deletions

View File

@@ -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<boolean> => {
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<string, unknown> = { 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<GeminiTokenInfo | null> => {
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<string, unknown> = {
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<string, unknown> => {
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
}
}