| <template> |
| <BaseDialog |
| :show="show" |
| :title="t('admin.accounts.reAuthorizeAccount')" |
| width="normal" |
| @close="handleClose" |
| > |
| <div v-if="account" class="space-y-4"> |
| |
| <div |
| class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700" |
| > |
| <div class="flex items-center gap-3"> |
| <div |
| :class="[ |
| 'flex h-10 w-10 items-center justify-center rounded-lg bg-gradient-to-br', |
| isOpenAILike |
| ? 'from-green-500 to-green-600' |
| : isGemini |
| ? 'from-blue-500 to-blue-600' |
| : isAntigravity |
| ? 'from-purple-500 to-purple-600' |
| : 'from-orange-500 to-orange-600' |
| ]" |
| > |
| <Icon name="sparkles" size="md" class="text-white" /> |
| </div> |
| <div> |
| <span class="block font-semibold text-gray-900 dark:text-white">{{ |
| account.name |
| }}</span> |
| <span class="text-sm text-gray-500 dark:text-gray-400"> |
| {{ |
| isOpenAI |
| ? t('admin.accounts.openaiAccount') |
| : isSora |
| ? t('admin.accounts.soraAccount') |
| : isGemini |
| ? t('admin.accounts.geminiAccount') |
| : isAntigravity |
| ? t('admin.accounts.antigravityAccount') |
| : t('admin.accounts.claudeCodeAccount') |
| }} |
| </span> |
| </div> |
| </div> |
| </div> |
| |
| |
| <fieldset v-if="isAnthropic" class="border-0 p-0"> |
| <legend class="input-label">{{ t('admin.accounts.oauth.authMethod') }}</legend> |
| <div class="mt-2 flex gap-4"> |
| <label class="flex cursor-pointer items-center"> |
| <input |
| v-model="addMethod" |
| type="radio" |
| value="oauth" |
| class="mr-2 text-primary-600 focus:ring-primary-500" |
| /> |
| <span class="text-sm text-gray-700 dark:text-gray-300">{{ |
| t('admin.accounts.types.oauth') |
| }}</span> |
| </label> |
| <label class="flex cursor-pointer items-center"> |
| <input |
| v-model="addMethod" |
| type="radio" |
| value="setup-token" |
| class="mr-2 text-primary-600 focus:ring-primary-500" |
| /> |
| <span class="text-sm text-gray-700 dark:text-gray-300">{{ |
| t('admin.accounts.setupTokenLongLived') |
| }}</span> |
| </label> |
| </div> |
| </fieldset> |
| |
| |
| <div v-if="isGemini" class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700"> |
| <div class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300"> |
| {{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }} |
| </div> |
| <div class="flex items-center gap-3"> |
| <div |
| :class="[ |
| 'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', |
| geminiOAuthType === 'google_one' |
| ? 'bg-purple-500 text-white' |
| : geminiOAuthType === 'code_assist' |
| ? 'bg-blue-500 text-white' |
| : 'bg-amber-500 text-white' |
| ]" |
| > |
| <Icon v-if="geminiOAuthType === 'google_one'" name="user" size="sm" /> |
| <Icon v-else-if="geminiOAuthType === 'code_assist'" name="cloud" size="sm" /> |
| <Icon v-else name="sparkles" size="sm" /> |
| </div> |
| <div> |
| <span class="block text-sm font-medium text-gray-900 dark:text-white"> |
| {{ |
| geminiOAuthType === 'google_one' |
| ? 'Google One' |
| : geminiOAuthType === 'code_assist' |
| ? t('admin.accounts.gemini.oauthType.builtInTitle') |
| : t('admin.accounts.gemini.oauthType.customTitle') |
| }} |
| </span> |
| <span class="text-xs text-gray-500 dark:text-gray-400"> |
| {{ |
| geminiOAuthType === 'google_one' |
| ? '个人账号' |
| : geminiOAuthType === 'code_assist' |
| ? t('admin.accounts.gemini.oauthType.builtInDesc') |
| : t('admin.accounts.gemini.oauthType.customDesc') |
| }} |
| </span> |
| </div> |
| </div> |
| </div> |
| |
| <OAuthAuthorizationFlow |
| ref="oauthFlowRef" |
| :add-method="addMethod" |
| :auth-url="currentAuthUrl" |
| :session-id="currentSessionId" |
| :loading="currentLoading" |
| :error="currentError" |
| :show-help="isAnthropic" |
| :show-proxy-warning="isAnthropic" |
| :show-cookie-option="isAnthropic" |
| :allow-multiple="false" |
| :method-label="t('admin.accounts.inputMethod')" |
| :platform="isOpenAI ? 'openai' : isSora ? 'sora' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'" |
| :show-project-id="isGemini && geminiOAuthType === 'code_assist'" |
| @generate-url="handleGenerateUrl" |
| @cookie-auth="handleCookieAuth" |
| /> |
| |
| </div> |
| |
| <template #footer> |
| <div v-if="account" class="flex justify-between gap-3"> |
| <button type="button" class="btn btn-secondary" @click="handleClose"> |
| {{ t('common.cancel') }} |
| </button> |
| <button |
| v-if="isManualInputMethod" |
| type="button" |
| :disabled="!canExchangeCode" |
| class="btn btn-primary" |
| @click="handleExchangeCode" |
| > |
| <svg |
| v-if="currentLoading" |
| class="-ml-1 mr-2 h-4 w-4 animate-spin" |
| fill="none" |
| viewBox="0 0 24 24" |
| > |
| <circle |
| class="opacity-25" |
| cx="12" |
| cy="12" |
| r="10" |
| stroke="currentColor" |
| stroke-width="4" |
| ></circle> |
| <path |
| class="opacity-75" |
| fill="currentColor" |
| d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" |
| ></path> |
| </svg> |
| {{ |
| currentLoading |
| ? t('admin.accounts.oauth.verifying') |
| : t('admin.accounts.oauth.completeAuth') |
| }} |
| </button> |
| </div> |
| </template> |
| </BaseDialog> |
| </template> |
| |
| <script setup lang="ts"> |
| import { ref, computed, watch } from 'vue' |
| import { useI18n } from 'vue-i18n' |
| import { useAppStore } from '@/stores/app' |
| import { adminAPI } from '@/api/admin' |
| import { |
| useAccountOAuth, |
| type AddMethod, |
| type AuthInputMethod |
| } from '@/composables/useAccountOAuth' |
| import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth' |
| import { useGeminiOAuth } from '@/composables/useGeminiOAuth' |
| import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth' |
| import type { Account } from '@/types' |
| import BaseDialog from '@/components/common/BaseDialog.vue' |
| import Icon from '@/components/icons/Icon.vue' |
| import OAuthAuthorizationFlow from '@/components/account/OAuthAuthorizationFlow.vue' |
| |
| |
| |
| interface OAuthFlowExposed { |
| authCode: string |
| oauthState: string |
| projectId: string |
| sessionKey: string |
| inputMethod: AuthInputMethod |
| reset: () => void |
| } |
| |
| interface Props { |
| show: boolean |
| account: Account | null |
| } |
| |
| const props = defineProps<Props>() |
| const emit = defineEmits<{ |
| close: [] |
| reauthorized: [account: Account] |
| }>() |
| |
| const appStore = useAppStore() |
| const { t } = useI18n() |
| |
| |
| const claudeOAuth = useAccountOAuth() |
| const openaiOAuth = useOpenAIOAuth({ platform: 'openai' }) |
| const soraOAuth = useOpenAIOAuth({ platform: 'sora' }) |
| const geminiOAuth = useGeminiOAuth() |
| const antigravityOAuth = useAntigravityOAuth() |
| |
| |
| const oauthFlowRef = ref<OAuthFlowExposed | null>(null) |
| |
| |
| const addMethod = ref<AddMethod>('oauth') |
| const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_assist') |
| |
| |
| const isOpenAI = computed(() => props.account?.platform === 'openai') |
| const isSora = computed(() => props.account?.platform === 'sora') |
| const isOpenAILike = computed(() => isOpenAI.value || isSora.value) |
| const isGemini = computed(() => props.account?.platform === 'gemini') |
| const isAnthropic = computed(() => props.account?.platform === 'anthropic') |
| const isAntigravity = computed(() => props.account?.platform === 'antigravity') |
| const activeOpenAIOAuth = computed(() => (isSora.value ? soraOAuth : openaiOAuth)) |
| |
| |
| const currentAuthUrl = computed(() => { |
| if (isOpenAILike.value) return activeOpenAIOAuth.value.authUrl.value |
| if (isGemini.value) return geminiOAuth.authUrl.value |
| if (isAntigravity.value) return antigravityOAuth.authUrl.value |
| return claudeOAuth.authUrl.value |
| }) |
| const currentSessionId = computed(() => { |
| if (isOpenAILike.value) return activeOpenAIOAuth.value.sessionId.value |
| if (isGemini.value) return geminiOAuth.sessionId.value |
| if (isAntigravity.value) return antigravityOAuth.sessionId.value |
| return claudeOAuth.sessionId.value |
| }) |
| const currentLoading = computed(() => { |
| if (isOpenAILike.value) return activeOpenAIOAuth.value.loading.value |
| if (isGemini.value) return geminiOAuth.loading.value |
| if (isAntigravity.value) return antigravityOAuth.loading.value |
| return claudeOAuth.loading.value |
| }) |
| const currentError = computed(() => { |
| if (isOpenAILike.value) return activeOpenAIOAuth.value.error.value |
| if (isGemini.value) return geminiOAuth.error.value |
| if (isAntigravity.value) return antigravityOAuth.error.value |
| return claudeOAuth.error.value |
| }) |
| |
| |
| const isManualInputMethod = computed(() => { |
| |
| return isOpenAILike.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual' |
| }) |
| |
| const canExchangeCode = computed(() => { |
| const authCode = oauthFlowRef.value?.authCode || '' |
| const sessionId = currentSessionId.value |
| const loading = currentLoading.value |
| return authCode.trim() && sessionId && !loading |
| }) |
| |
| |
| watch( |
| () => props.show, |
| (newVal) => { |
| if (newVal && props.account) { |
| |
| if ( |
| isAnthropic.value && |
| (props.account.type === 'oauth' || props.account.type === 'setup-token') |
| ) { |
| addMethod.value = props.account.type as AddMethod |
| } |
| if (isGemini.value) { |
| const creds = (props.account.credentials || {}) as Record<string, unknown> |
| geminiOAuthType.value = |
| creds.oauth_type === 'google_one' |
| ? 'google_one' |
| : creds.oauth_type === 'ai_studio' |
| ? 'ai_studio' |
| : 'code_assist' |
| } |
| } else { |
| resetState() |
| } |
| } |
| ) |
| |
| |
| const resetState = () => { |
| addMethod.value = 'oauth' |
| geminiOAuthType.value = 'code_assist' |
| claudeOAuth.resetState() |
| openaiOAuth.resetState() |
| soraOAuth.resetState() |
| geminiOAuth.resetState() |
| antigravityOAuth.resetState() |
| oauthFlowRef.value?.reset() |
| } |
| |
| const handleClose = () => { |
| emit('close') |
| } |
| |
| const handleGenerateUrl = async () => { |
| if (!props.account) return |
| |
| if (isOpenAILike.value) { |
| await activeOpenAIOAuth.value.generateAuthUrl(props.account.proxy_id) |
| } else if (isGemini.value) { |
| const creds = (props.account.credentials || {}) as Record<string, unknown> |
| const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined |
| const projectId = geminiOAuthType.value === 'code_assist' ? oauthFlowRef.value?.projectId : undefined |
| await geminiOAuth.generateAuthUrl(props.account.proxy_id, projectId, geminiOAuthType.value, tierId) |
| } else if (isAntigravity.value) { |
| await antigravityOAuth.generateAuthUrl(props.account.proxy_id) |
| } else { |
| await claudeOAuth.generateAuthUrl(addMethod.value, props.account.proxy_id) |
| } |
| } |
| |
| const handleExchangeCode = async () => { |
| if (!props.account) return |
| |
| const authCode = oauthFlowRef.value?.authCode || '' |
| if (!authCode.trim()) return |
| |
| if (isOpenAILike.value) { |
| |
| const oauthClient = activeOpenAIOAuth.value |
| const sessionId = oauthClient.sessionId.value |
| if (!sessionId) return |
| const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim() |
| if (!stateToUse) { |
| oauthClient.error.value = t('admin.accounts.oauth.authFailed') |
| appStore.showError(oauthClient.error.value) |
| return |
| } |
| |
| const tokenInfo = await oauthClient.exchangeAuthCode( |
| authCode.trim(), |
| sessionId, |
| stateToUse, |
| props.account.proxy_id |
| ) |
| if (!tokenInfo) return |
| |
| |
| const credentials = oauthClient.buildCredentials(tokenInfo) |
| const extra = oauthClient.buildExtraInfo(tokenInfo) |
| |
| try { |
| |
| await adminAPI.accounts.update(props.account.id, { |
| type: 'oauth', |
| credentials, |
| extra |
| }) |
| |
| |
| const updatedAccount = await adminAPI.accounts.clearError(props.account.id) |
| |
| appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) |
| emit('reauthorized', updatedAccount) |
| handleClose() |
| } catch (error: any) { |
| oauthClient.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') |
| appStore.showError(oauthClient.error.value) |
| } |
| } else if (isGemini.value) { |
| const sessionId = geminiOAuth.sessionId.value |
| if (!sessionId) return |
| |
| const stateFromInput = oauthFlowRef.value?.oauthState || '' |
| const stateToUse = stateFromInput || geminiOAuth.state.value |
| if (!stateToUse) return |
| |
| const tokenInfo = await geminiOAuth.exchangeAuthCode({ |
| code: authCode.trim(), |
| sessionId, |
| state: stateToUse, |
| proxyId: props.account.proxy_id, |
| oauthType: geminiOAuthType.value, |
| tierId: typeof (props.account.credentials as any)?.tier_id === 'string' ? ((props.account.credentials as any).tier_id as string) : undefined |
| }) |
| if (!tokenInfo) return |
| |
| const credentials = geminiOAuth.buildCredentials(tokenInfo) |
| |
| try { |
| await adminAPI.accounts.update(props.account.id, { |
| type: 'oauth', |
| credentials |
| }) |
| const updatedAccount = await adminAPI.accounts.clearError(props.account.id) |
| appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) |
| emit('reauthorized', updatedAccount) |
| handleClose() |
| } catch (error: any) { |
| geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') |
| appStore.showError(geminiOAuth.error.value) |
| } |
| } else if (isAntigravity.value) { |
| |
| const sessionId = antigravityOAuth.sessionId.value |
| if (!sessionId) return |
| |
| const stateFromInput = oauthFlowRef.value?.oauthState || '' |
| const stateToUse = stateFromInput || antigravityOAuth.state.value |
| if (!stateToUse) return |
| |
| const tokenInfo = await antigravityOAuth.exchangeAuthCode({ |
| code: authCode.trim(), |
| sessionId, |
| state: stateToUse, |
| proxyId: props.account.proxy_id |
| }) |
| if (!tokenInfo) return |
| |
| const credentials = antigravityOAuth.buildCredentials(tokenInfo) |
| |
| try { |
| await adminAPI.accounts.update(props.account.id, { |
| type: 'oauth', |
| credentials |
| }) |
| const updatedAccount = await adminAPI.accounts.clearError(props.account.id) |
| appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) |
| emit('reauthorized', updatedAccount) |
| handleClose() |
| } catch (error: any) { |
| antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') |
| appStore.showError(antigravityOAuth.error.value) |
| } |
| } else { |
| |
| const sessionId = claudeOAuth.sessionId.value |
| if (!sessionId) return |
| |
| claudeOAuth.loading.value = true |
| claudeOAuth.error.value = '' |
| |
| try { |
| const proxyConfig = props.account.proxy_id ? { proxy_id: props.account.proxy_id } : {} |
| const endpoint = |
| addMethod.value === 'oauth' |
| ? '/admin/accounts/exchange-code' |
| : '/admin/accounts/exchange-setup-token-code' |
| |
| const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, { |
| session_id: sessionId, |
| code: authCode.trim(), |
| ...proxyConfig |
| }) |
| |
| const extra = claudeOAuth.buildExtraInfo(tokenInfo) |
| |
| |
| await adminAPI.accounts.update(props.account.id, { |
| type: addMethod.value, |
| credentials: tokenInfo, |
| extra |
| }) |
| |
| |
| const updatedAccount = await adminAPI.accounts.clearError(props.account.id) |
| |
| appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) |
| emit('reauthorized', updatedAccount) |
| handleClose() |
| } catch (error: any) { |
| claudeOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed') |
| appStore.showError(claudeOAuth.error.value) |
| } finally { |
| claudeOAuth.loading.value = false |
| } |
| } |
| } |
| |
| const handleCookieAuth = async (sessionKey: string) => { |
| if (!props.account || isOpenAILike.value) return |
| |
| claudeOAuth.loading.value = true |
| claudeOAuth.error.value = '' |
| |
| try { |
| const proxyConfig = props.account.proxy_id ? { proxy_id: props.account.proxy_id } : {} |
| const endpoint = |
| addMethod.value === 'oauth' |
| ? '/admin/accounts/cookie-auth' |
| : '/admin/accounts/setup-token-cookie-auth' |
| |
| const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, { |
| session_id: '', |
| code: sessionKey.trim(), |
| ...proxyConfig |
| }) |
| |
| const extra = claudeOAuth.buildExtraInfo(tokenInfo) |
| |
| |
| await adminAPI.accounts.update(props.account.id, { |
| type: addMethod.value, |
| credentials: tokenInfo, |
| extra |
| }) |
| |
| |
| const updatedAccount = await adminAPI.accounts.clearError(props.account.id) |
| |
| appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess')) |
| emit('reauthorized', updatedAccount) |
| handleClose() |
| } catch (error: any) { |
| claudeOAuth.error.value = |
| error.response?.data?.detail || t('admin.accounts.oauth.cookieAuthFailed') |
| } finally { |
| claudeOAuth.loading.value = false |
| } |
| } |
| </script> |
| |