/** * OAuth2 API */ import axios from 'axios' import { generatePKCE, storeAccessToken, storeRefreshToken, getAccessToken, getRefreshToken, clearTokens, storeCodeVerifier, getCodeVerifier, clearCodeVerifier, type TokenInfo } from '../utils/oauth2' // 从环境变量或配置获取 OAuth2 配置 const getOAuth2Config = () => { // 从环境变量或配置中获取 let backendUrl = import.meta.env.VITE_JINGROW_BACKEND_URL || 'https://cloud.jingrow.com' // 确保 URL 不以 /dashboard 结尾(API 路径应该直接在根路径下) // 如果用户设置了 /dashboard,自动移除 backendUrl = backendUrl.replace(/\/dashboard\/?$/, '') const clientId = import.meta.env.VITE_OAUTH2_CLIENT_ID || 'i679osfadt' const redirectUri = `${window.location.origin}/oauth/callback` return { backendUrl, clientId, redirectUri, scopes: 'all openid desk:read desk:write' } } /** * 获取 API URL(开发环境使用相对路径通过代理,生产环境使用绝对 URL) */ const getApiUrl = (apiPath: string): string => { const config = getOAuth2Config() // 在开发环境中,使用相对路径通过 Vite 代理,避免 CORS 问题 // 在生产环境中,使用绝对 URL if (import.meta.env.DEV) { // 开发环境:使用相对路径,Vite 代理会转发到后端 return apiPath } else { // 生产环境:使用绝对 URL return `${config.backendUrl}${apiPath}` } } /** * 获取授权 URL */ export async function getAuthorizationUrl(state?: string): Promise { const config = getOAuth2Config() const { codeVerifier, codeChallenge } = await generatePKCE() // 存储 code verifier 用于后续交换 token storeCodeVerifier(codeVerifier) // 调试日志:输出 redirect_uri 以便检查后端配置 console.log('OAuth2 授权 URL 配置:', { backendUrl: config.backendUrl, clientId: config.clientId, redirectUri: config.redirectUri, scopes: config.scopes, currentOrigin: window.location.origin }) const params = new URLSearchParams({ client_id: config.clientId, redirect_uri: config.redirectUri, response_type: 'code', scope: config.scopes, code_challenge: codeChallenge, code_challenge_method: 'S256', ...(state && { state }) }) // 确保 API 路径正确(不应该包含 /dashboard) const apiPath = '/api/action/jingrow.integrations.oauth2.authorize' const authUrl = `${config.backendUrl}${apiPath}?${params.toString()}` console.log('OAuth2 授权 URL 配置:', { backendUrl: config.backendUrl, apiPath, fullUrl: authUrl, redirectUri: config.redirectUri, clientId: config.clientId }) console.log('期望的重定向 URI:', config.redirectUri) console.log('请确保后端 OAuth Client 的 redirect_uris 包含:', config.redirectUri) return authUrl } /** * 用授权码换取 access token */ export async function exchangeCodeForToken(code: string): Promise { const config = getOAuth2Config() const codeVerifier = getCodeVerifier() if (!codeVerifier) { throw new Error('Code verifier not found. Please restart the authorization flow.') } // 调试日志:确认使用的 redirect_uri console.log('OAuth2 Token 交换配置:', { redirectUri: config.redirectUri, clientId: config.clientId, backendUrl: config.backendUrl }) const params = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: config.redirectUri, client_id: config.clientId, code_verifier: codeVerifier }) try { // 使用 getApiUrl 获取正确的 API URL(开发环境通过代理,生产环境使用绝对 URL) const apiUrl = getApiUrl('/api/action/jingrow.integrations.oauth2.get_token') const response = await axios.post( apiUrl, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' } } ) const tokenInfo: TokenInfo = response.data // 存储 token storeAccessToken(tokenInfo.access_token, tokenInfo.expires_in) if (tokenInfo.refresh_token) { storeRefreshToken(tokenInfo.refresh_token) } // 清除 code verifier clearCodeVerifier() return tokenInfo } catch (error: any) { clearCodeVerifier() throw new Error( error.response?.data?.error_description || error.response?.data?.error || error.message || 'Failed to exchange authorization code for token' ) } } /** * 刷新 access token */ export async function refreshAccessToken(): Promise { const config = getOAuth2Config() const refreshToken = getRefreshToken() if (!refreshToken) { throw new Error('No refresh token available') } const params = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: config.clientId }) try { // 使用 getApiUrl 获取正确的 API URL(开发环境通过代理,生产环境使用绝对 URL) const apiUrl = getApiUrl('/api/action/jingrow.integrations.oauth2.get_token') const response = await axios.post( apiUrl, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' } } ) const tokenInfo: TokenInfo = response.data // 更新存储的 token storeAccessToken(tokenInfo.access_token, tokenInfo.expires_in) if (tokenInfo.refresh_token) { storeRefreshToken(tokenInfo.refresh_token) } return tokenInfo } catch (error: any) { // 刷新失败,清除所有 token clearTokens() throw new Error( error.response?.data?.error_description || error.response?.data?.error || error.message || 'Failed to refresh access token' ) } } /** * 撤销 token */ export async function revokeToken(token?: string, tokenTypeHint: 'access_token' | 'refresh_token' = 'access_token'): Promise { const config = getOAuth2Config() const tokenToRevoke = token || (tokenTypeHint === 'access_token' ? getAccessToken() : getRefreshToken()) if (!tokenToRevoke) { return } const params = new URLSearchParams({ token: tokenToRevoke, token_type_hint: tokenTypeHint }) try { // 使用 getApiUrl 获取正确的 API URL(开发环境通过代理,生产环境使用绝对 URL) const apiUrl = getApiUrl('/api/action/jingrow.integrations.oauth2.revoke_token') await axios.post( apiUrl, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } ) // 清除本地存储的 token clearTokens() } catch (error: any) { // 即使撤销失败,也清除本地 token clearTokens() console.error('Failed to revoke token:', error) } } /** * 获取用户信息(OpenID Connect userinfo) */ export async function getUserInfo(): Promise { const config = getOAuth2Config() const accessToken = getAccessToken() if (!accessToken) { throw new Error('No access token available') } // 使用 getApiUrl 获取正确的 API URL(开发环境通过代理,生产环境使用绝对 URL) const apiUrl = getApiUrl('/api/action/jingrow.integrations.oauth2.openid_profile') const response = await axios.get( apiUrl, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/json' } } ) return response.data } /** * 获取 Bearer token 认证头 */ export function getBearerTokenHeaders(): Record { const accessToken = getAccessToken() if (!accessToken) { return { 'Content-Type': 'application/json', 'Accept': 'application/json' } } return { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }