Files
tokens-reef/frontend/src/composables/useOpenAIOAuth.ts
QTom 1f05d9f79d fix(openai): buildCredentials 对齐后端 BuildAccountCredentials 字段
补齐前端 buildCredentials 缺失的 id_token、email、plan_type 字段,
与后端 BuildAccountCredentials 保持一致。修复手动 RT 创建的账号
缺少订阅类型等关键信息的问题。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 14:50:03 +08:00

254 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ref } from 'vue'
import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
export interface OpenAITokenInfo {
access_token?: string
refresh_token?: string
client_id?: string
id_token?: string
token_type?: string
expires_in?: number
expires_at?: number
scope?: string
email?: string
name?: string
plan_type?: string
// OpenAI specific IDs (extracted from ID Token)
chatgpt_account_id?: string
chatgpt_user_id?: string
organization_id?: string
[key: string]: unknown
}
export type OpenAIOAuthPlatform = 'openai' | 'sora'
interface UseOpenAIOAuthOptions {
platform?: OpenAIOAuthPlatform
}
export function useOpenAIOAuth(options?: UseOpenAIOAuthOptions) {
const appStore = useAppStore()
const oauthPlatform = options?.platform ?? 'openai'
const endpointPrefix = oauthPlatform === 'sora' ? '/admin/sora' : '/admin/openai'
// State
const authUrl = ref('')
const sessionId = ref('')
const oauthState = ref('')
const loading = ref(false)
const error = ref('')
// Reset state
const resetState = () => {
authUrl.value = ''
sessionId.value = ''
oauthState.value = ''
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 = ''
oauthState.value = ''
error.value = ''
try {
const payload: Record<string, unknown> = {}
if (proxyId) {
payload.proxy_id = proxyId
}
if (redirectUri) {
payload.redirect_uri = redirectUri
}
const response = await adminAPI.accounts.generateAuthUrl(
`${endpointPrefix}/generate-auth-url`,
payload
)
authUrl.value = response.auth_url
sessionId.value = response.session_id
try {
const parsed = new URL(response.auth_url)
oauthState.value = parsed.searchParams.get('state') || ''
} catch {
oauthState.value = ''
}
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,
state: string,
proxyId?: number | null
): Promise<OpenAITokenInfo | null> => {
if (!code.trim() || !currentSessionId || !state.trim()) {
error.value = 'Missing auth code, session ID, or state'
return null
}
loading.value = true
error.value = ''
try {
const payload: { session_id: string; code: string; state: string; proxy_id?: number } = {
session_id: currentSessionId,
code: code.trim(),
state: state.trim()
}
if (proxyId) {
payload.proxy_id = proxyId
}
const tokenInfo = await adminAPI.accounts.exchangeCode(`${endpointPrefix}/exchange-code`, payload)
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
}
}
// Validate refresh token and get full token info
// clientId: 指定 OAuth client_id用于第三方渠道获取的 RT如 app_LlGpXReQgckcGGUo2JrYvtJK
const validateRefreshToken = async (
refreshToken: string,
proxyId?: number | null,
clientId?: string
): Promise<OpenAITokenInfo | null> => {
if (!refreshToken.trim()) {
error.value = 'Missing refresh token'
return null
}
loading.value = true
error.value = ''
try {
// Use dedicated refresh-token endpoint
const tokenInfo = await adminAPI.accounts.refreshOpenAIToken(
refreshToken.trim(),
proxyId,
`${endpointPrefix}/refresh-token`,
clientId
)
return tokenInfo as OpenAITokenInfo
} catch (err: any) {
error.value = err.response?.data?.detail || err.message || 'Failed to validate refresh token'
appStore.showError(error.value)
return null
} finally {
loading.value = false
}
}
// Validate Sora session token and get access token
const validateSessionToken = async (
sessionToken: string,
proxyId?: number | null
): Promise<OpenAITokenInfo | null> => {
if (!sessionToken.trim()) {
error.value = 'Missing session token'
return null
}
loading.value = true
error.value = ''
try {
const tokenInfo = await adminAPI.accounts.validateSoraSessionToken(
sessionToken.trim(),
proxyId,
`${endpointPrefix}/st2at`
)
return tokenInfo as OpenAITokenInfo
} catch (err: any) {
error.value = err.response?.data?.detail || 'Failed to validate session token'
appStore.showError(error.value)
return null
} finally {
loading.value = false
}
}
// Build credentials for OpenAI OAuth account (aligned with backend BuildAccountCredentials)
const buildCredentials = (tokenInfo: OpenAITokenInfo): Record<string, unknown> => {
const creds: Record<string, unknown> = {
access_token: tokenInfo.access_token,
expires_at: tokenInfo.expires_at
}
// 仅在返回了新的 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
}
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
}
if (tokenInfo.plan_type) {
creds.plan_type = tokenInfo.plan_type
}
if (tokenInfo.client_id) {
creds.client_id = tokenInfo.client_id
}
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
}
return Object.keys(extra).length > 0 ? extra : undefined
}
return {
// State
authUrl,
sessionId,
oauthState,
loading,
error,
// Methods
resetState,
generateAuthUrl,
exchangeAuthCode,
validateRefreshToken,
validateSessionToken,
buildCredentials,
buildExtraInfo
}
}