2025-12-22 22:58:31 +08:00
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
|
import { useAppStore } from '@/stores/app'
|
|
|
|
|
|
import { adminAPI } from '@/api/admin'
|
|
|
|
|
|
|
|
|
|
|
|
export interface OpenAITokenInfo {
|
|
|
|
|
|
access_token?: string
|
|
|
|
|
|
refresh_token?: string
|
2026-02-28 15:01:20 +08:00
|
|
|
|
client_id?: string
|
2025-12-22 22:58:31 +08:00
|
|
|
|
id_token?: string
|
|
|
|
|
|
token_type?: string
|
|
|
|
|
|
expires_in?: number
|
|
|
|
|
|
expires_at?: number
|
|
|
|
|
|
scope?: string
|
|
|
|
|
|
email?: string
|
|
|
|
|
|
name?: string
|
2026-03-24 12:42:17 +08:00
|
|
|
|
plan_type?: string
|
2026-03-24 14:39:33 +08:00
|
|
|
|
privacy_mode?: string
|
2025-12-22 22:58:31 +08:00
|
|
|
|
// OpenAI specific IDs (extracted from ID Token)
|
|
|
|
|
|
chatgpt_account_id?: string
|
|
|
|
|
|
chatgpt_user_id?: string
|
|
|
|
|
|
organization_id?: string
|
|
|
|
|
|
[key: string]: unknown
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-05 17:11:01 +08:00
|
|
|
|
export type OpenAIOAuthPlatform = 'openai'
|
2026-02-19 08:02:56 +08:00
|
|
|
|
|
|
|
|
|
|
interface UseOpenAIOAuthOptions {
|
|
|
|
|
|
platform?: OpenAIOAuthPlatform
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-05 04:11:30 +08:00
|
|
|
|
export function useOpenAIOAuth(_options?: UseOpenAIOAuthOptions) {
|
2025-12-22 22:58:31 +08:00
|
|
|
|
const appStore = useAppStore()
|
2026-04-05 17:11:01 +08:00
|
|
|
|
const endpointPrefix = '/admin/openai'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
|
const authUrl = ref('')
|
|
|
|
|
|
const sessionId = ref('')
|
2026-02-19 08:02:56 +08:00
|
|
|
|
const oauthState = ref('')
|
2025-12-22 22:58:31 +08:00
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const error = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
// Reset state
|
|
|
|
|
|
const resetState = () => {
|
|
|
|
|
|
authUrl.value = ''
|
|
|
|
|
|
sessionId.value = ''
|
2026-02-19 08:02:56 +08:00
|
|
|
|
oauthState.value = ''
|
2025-12-22 22:58:31 +08:00
|
|
|
|
loading.value = false
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Generate auth URL for OpenAI OAuth
|
|
|
|
|
|
const generateAuthUrl = async (
|
|
|
|
|
|
proxyId?: number | null,
|
|
|
|
|
|
redirectUri?: string
|
|
|
|
|
|
): Promise<boolean> => {
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
authUrl.value = ''
|
|
|
|
|
|
sessionId.value = ''
|
2026-02-19 08:02:56 +08:00
|
|
|
|
oauthState.value = ''
|
2025-12-22 22:58:31 +08:00
|
|
|
|
error.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload: Record<string, unknown> = {}
|
|
|
|
|
|
if (proxyId) {
|
|
|
|
|
|
payload.proxy_id = proxyId
|
|
|
|
|
|
}
|
|
|
|
|
|
if (redirectUri) {
|
|
|
|
|
|
payload.redirect_uri = redirectUri
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 08:41:43 -08:00
|
|
|
|
const response = await adminAPI.accounts.generateAuthUrl(
|
2026-02-19 08:02:56 +08:00
|
|
|
|
`${endpointPrefix}/generate-auth-url`,
|
2025-12-25 08:41:43 -08:00
|
|
|
|
payload
|
|
|
|
|
|
)
|
2025-12-22 22:58:31 +08:00
|
|
|
|
authUrl.value = response.auth_url
|
|
|
|
|
|
sessionId.value = response.session_id
|
2026-02-19 08:02:56 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const parsed = new URL(response.auth_url)
|
|
|
|
|
|
oauthState.value = parsed.searchParams.get('state') || ''
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
oauthState.value = ''
|
|
|
|
|
|
}
|
2025-12-22 22:58:31 +08:00
|
|
|
|
return true
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
error.value = err.response?.data?.detail || 'Failed to generate OpenAI auth URL'
|
|
|
|
|
|
appStore.showError(error.value)
|
|
|
|
|
|
return false
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Exchange auth code for tokens
|
|
|
|
|
|
const exchangeAuthCode = async (
|
|
|
|
|
|
code: string,
|
|
|
|
|
|
currentSessionId: string,
|
2026-02-19 08:02:56 +08:00
|
|
|
|
state: string,
|
2025-12-22 22:58:31 +08:00
|
|
|
|
proxyId?: number | null
|
|
|
|
|
|
): Promise<OpenAITokenInfo | null> => {
|
2026-02-19 08:02:56 +08:00
|
|
|
|
if (!code.trim() || !currentSessionId || !state.trim()) {
|
|
|
|
|
|
error.value = 'Missing auth code, session ID, or state'
|
2025-12-22 22:58:31 +08:00
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-19 08:02:56 +08:00
|
|
|
|
const payload: { session_id: string; code: string; state: string; proxy_id?: number } = {
|
2025-12-22 22:58:31 +08:00
|
|
|
|
session_id: currentSessionId,
|
2026-02-19 08:02:56 +08:00
|
|
|
|
code: code.trim(),
|
|
|
|
|
|
state: state.trim()
|
2025-12-22 22:58:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (proxyId) {
|
|
|
|
|
|
payload.proxy_id = proxyId
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 08:02:56 +08:00
|
|
|
|
const tokenInfo = await adminAPI.accounts.exchangeCode(`${endpointPrefix}/exchange-code`, payload)
|
2025-12-22 22:58:31 +08:00
|
|
|
|
return tokenInfo as OpenAITokenInfo
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
|
error.value = err.response?.data?.detail || 'Failed to exchange OpenAI auth code'
|
|
|
|
|
|
appStore.showError(error.value)
|
|
|
|
|
|
return null
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 16:10:15 +08:00
|
|
|
|
// Validate refresh token and get full token info
|
2026-03-24 12:35:32 +08:00
|
|
|
|
// clientId: 指定 OAuth client_id(用于第三方渠道获取的 RT,如 app_LlGpXReQgckcGGUo2JrYvtJK)
|
2026-02-08 16:10:15 +08:00
|
|
|
|
const validateRefreshToken = async (
|
|
|
|
|
|
refreshToken: string,
|
2026-03-24 12:35:32 +08:00
|
|
|
|
proxyId?: number | null,
|
|
|
|
|
|
clientId?: string
|
2026-02-08 16:10:15 +08:00
|
|
|
|
): Promise<OpenAITokenInfo | null> => {
|
|
|
|
|
|
if (!refreshToken.trim()) {
|
|
|
|
|
|
error.value = 'Missing refresh token'
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
error.value = ''
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Use dedicated refresh-token endpoint
|
2026-02-19 08:02:56 +08:00
|
|
|
|
const tokenInfo = await adminAPI.accounts.refreshOpenAIToken(
|
|
|
|
|
|
refreshToken.trim(),
|
|
|
|
|
|
proxyId,
|
2026-03-24 12:35:32 +08:00
|
|
|
|
`${endpointPrefix}/refresh-token`,
|
|
|
|
|
|
clientId
|
2026-02-19 08:02:56 +08:00
|
|
|
|
)
|
2026-02-08 16:10:15 +08:00
|
|
|
|
return tokenInfo as OpenAITokenInfo
|
|
|
|
|
|
} catch (err: any) {
|
2026-03-24 12:35:32 +08:00
|
|
|
|
error.value = err.response?.data?.detail || err.message || 'Failed to validate refresh token'
|
2026-02-08 16:10:15 +08:00
|
|
|
|
appStore.showError(error.value)
|
|
|
|
|
|
return null
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 12:42:17 +08:00
|
|
|
|
// Build credentials for OpenAI OAuth account (aligned with backend BuildAccountCredentials)
|
2025-12-22 22:58:31 +08:00
|
|
|
|
const buildCredentials = (tokenInfo: OpenAITokenInfo): Record<string, unknown> => {
|
|
|
|
|
|
const creds: Record<string, unknown> = {
|
|
|
|
|
|
access_token: tokenInfo.access_token,
|
2026-03-24 12:42:17 +08:00
|
|
|
|
expires_at: tokenInfo.expires_at
|
2025-12-22 22:58:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 12:42:17 +08:00
|
|
|
|
// 仅在返回了新的 refresh_token 时才写入,防止用空值覆盖已有令牌
|
|
|
|
|
|
if (tokenInfo.refresh_token) {
|
|
|
|
|
|
creds.refresh_token = tokenInfo.refresh_token
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tokenInfo.id_token) {
|
|
|
|
|
|
creds.id_token = tokenInfo.id_token
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tokenInfo.email) {
|
|
|
|
|
|
creds.email = tokenInfo.email
|
2026-02-28 15:01:20 +08:00
|
|
|
|
}
|
2025-12-22 22:58:31 +08:00
|
|
|
|
if (tokenInfo.chatgpt_account_id) {
|
|
|
|
|
|
creds.chatgpt_account_id = tokenInfo.chatgpt_account_id
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tokenInfo.chatgpt_user_id) {
|
|
|
|
|
|
creds.chatgpt_user_id = tokenInfo.chatgpt_user_id
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tokenInfo.organization_id) {
|
|
|
|
|
|
creds.organization_id = tokenInfo.organization_id
|
|
|
|
|
|
}
|
2026-03-24 12:42:17 +08:00
|
|
|
|
if (tokenInfo.plan_type) {
|
|
|
|
|
|
creds.plan_type = tokenInfo.plan_type
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tokenInfo.client_id) {
|
|
|
|
|
|
creds.client_id = tokenInfo.client_id
|
|
|
|
|
|
}
|
2025-12-22 22:58:31 +08:00
|
|
|
|
|
|
|
|
|
|
return creds
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build extra info from token response
|
|
|
|
|
|
const buildExtraInfo = (tokenInfo: OpenAITokenInfo): Record<string, string> | undefined => {
|
|
|
|
|
|
const extra: Record<string, string> = {}
|
|
|
|
|
|
if (tokenInfo.email) {
|
|
|
|
|
|
extra.email = tokenInfo.email
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tokenInfo.name) {
|
|
|
|
|
|
extra.name = tokenInfo.name
|
|
|
|
|
|
}
|
2026-03-24 14:39:33 +08:00
|
|
|
|
if (tokenInfo.privacy_mode) {
|
|
|
|
|
|
extra.privacy_mode = tokenInfo.privacy_mode
|
|
|
|
|
|
}
|
2025-12-22 22:58:31 +08:00
|
|
|
|
return Object.keys(extra).length > 0 ? extra : undefined
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
// State
|
|
|
|
|
|
authUrl,
|
|
|
|
|
|
sessionId,
|
2026-02-19 08:02:56 +08:00
|
|
|
|
oauthState,
|
2025-12-22 22:58:31 +08:00
|
|
|
|
loading,
|
|
|
|
|
|
error,
|
|
|
|
|
|
// Methods
|
|
|
|
|
|
resetState,
|
|
|
|
|
|
generateAuthUrl,
|
|
|
|
|
|
exchangeAuthCode,
|
2026-02-08 16:10:15 +08:00
|
|
|
|
validateRefreshToken,
|
2025-12-22 22:58:31 +08:00
|
|
|
|
buildCredentials,
|
|
|
|
|
|
buildExtraInfo
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|