298 lines
7.9 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.

/**
* 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<string> {
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<TokenInfo> {
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<TokenInfo> {
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<void> {
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<any> {
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<string, string> {
const accessToken = getAccessToken()
if (!accessToken) {
return {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
return {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}