Files
web/src/actions/base.ts

290 lines
7.2 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.
'use server'
import {API_BASE_URL, CLIENT_ID, CLIENT_SECRET, ApiResponse, UnauthorizedError} from '@/lib/api'
import {cookies} from 'next/headers'
import {redirect} from 'next/navigation'
// OAuth令牌缓存
interface TokenCache {
token: string
expires: number // 过期时间戳
}
// ======================
// region device token
// ======================
let tokenCache: TokenCache | null = null
// 获取OAuth2访问令牌
async function getDeviceToken(forceRefresh = false): Promise<string> {
try {
// 检查缓存的令牌是否可用
if (!forceRefresh && tokenCache && tokenCache.expires > Date.now()) {
return tokenCache.token
}
const addr = `${API_BASE_URL}/api/auth/token`
const body = {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
grant_type: 'client_credentials',
}
const response = await fetch(addr, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
throw new Error(`OAuth token request failed: ${response.status} ${await response.text()}`)
}
const data = await response.json()
// 缓存令牌和过期时间
// 通常后端会返回expires_in秒为单位
tokenCache = {
token: data.access_token,
expires: Date.now() + data.expires_in * 1000,
}
return tokenCache.token
}
catch (error) {
console.error('Failed to get access token:', error)
throw new Error('认证服务暂时不可用')
}
}
// 通用的API调用函数
async function callByDevice<R = undefined>(endpoint: string, data: unknown): Promise<ApiResponse<R>> {
try {
// 发送请求
let accessToken = getDeviceToken()
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await accessToken}`,
},
body: JSON.stringify(data),
}
let response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
// 如果返回401未授权尝试刷新令牌并重试一次
if (response.status === 401) {
accessToken = getDeviceToken(true) // 强制刷新令牌
// 使用新令牌重试请求
requestOptions.headers['Authorization'] = `Bearer ${await accessToken}`
response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
}
// 检查响应状态
if (!response.ok) {
console.log('响应不成功', `status=${response.status}`, await response.text())
return {
success: false,
status: response.status,
message: '请求失败',
}
}
// 检查响应状态
return handleResponse(response)
}
catch (e) {
console.error('API call failed:', e)
throw new Error('服务调用失败', {cause: e})
}
}
// endregion
// ======================
// region user token
// ======================
async function getUserToken(refresh = false): Promise<string> {
// 从 cookie 中获取用户令牌
const cookie = await cookies()
const userToken = cookie.get('auth_token')?.value
const userRefresh = cookie.get('auth_refresh')?.value
// 检查缓存的令牌是否可用
if (!refresh && userToken) {
return userToken
}
// 如果没有刷新令牌,抛出异常
if (!userRefresh) {
throw UnauthorizedError
}
// 请求刷新访问令牌
const addr = `${API_BASE_URL}/api/auth/token`
const body = {
grant_type: 'refresh_token',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: userRefresh,
}
const response = await fetch(addr, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
console.log('刷新令牌失败', `status=${response.status}`, await response.text())
throw UnauthorizedError
}
// 保存新的用户令牌到 cookie
const data = await response.json()
const nextAccessToken = data.access_token
const nextRefreshToken = data.refresh_token
const expiresIn = data.expires_in
cookie.set('auth_token', nextAccessToken, {
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
maxAge: Math.max(expiresIn, 0),
})
cookie.set('auth_refresh', nextRefreshToken, {
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
maxAge: 7 * 24 * 3600, // 7天
})
return nextAccessToken
}
// 使用用户令牌的API调用函数
async function callByUser<R = undefined>(
endpoint: string,
data: unknown,
): Promise<ApiResponse<R>> {
try {
let token = await getUserToken()
// 发送请求
let response: Response
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(data),
}
response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
if (response.status === 401) {
token = await getUserToken(true)
requestOptions.headers['Authorization'] = `Bearer ${token}`
response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
}
// 检查响应状态
if (!response.ok) {
const body = await response.text()
console.log('响应不成功', `status=${response.status}`, body)
if (response.status === 401) {
return redirect('/login')
}
if (response.status >= 400 && response.status < 500) {
return {
status: response.status,
success: false,
message: body,
}
}
if (response.status >= 500) {
return {
status: response.status,
success: false,
message: '服务器错误',
}
}
return {
status: response.status,
success: false,
message: `请求失败status = ${response.status}`,
}
}
return handleResponse(response)
}
catch (e) {
console.error('API call with user token failed:', e)
throw new Error('服务调用失败', {cause: e})
}
}
// endregion
// 统一响应解析
async function handleResponse<R = undefined>(response: Response): Promise<ApiResponse<R>> {
// 解析响应数据
const type = response.headers.get('Content-Type') ?? 'text/plain'
if (type.indexOf('application/json') !== -1) {
const json = await response.json()
if (!response.ok) {
console.log('响应不成功', `status=${response.status}`, json)
return {
success: false,
status: response.status,
message: json.message || '请求失败',
}
}
return {
success: true,
data: json,
}
}
else if (type.indexOf('text/plain') !== -1) {
const text = await response.text()
if (!response.ok) {
console.log('响应不成功', `status=${response.status}`, text)
return {
success: false,
status: response.status,
message: text || '请求失败',
}
}
return {
success: true,
data: undefined as unknown as R, // 强转类型,考虑优化
}
}
else {
throw new Error(`无法解析响应数据,未处理的 Content-Type: ${type}`)
}
}
// 预定义错误
// 导出
export {
getDeviceToken,
getUserToken,
callByDevice,
callByUser,
}