298 lines
7.9 KiB
TypeScript
298 lines
7.9 KiB
TypeScript
/**
|
||
* 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'
|
||
}
|
||
}
|