Files
web/src/actions/base.ts
2026-04-14 11:34:28 +08:00

178 lines
4.5 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, ApiResponse, CLIENT_ID, CLIENT_SECRET} from '@/lib/api'
import {add, isBefore} from 'date-fns'
import {cookies, headers} from 'next/headers'
import {cache} from 'react'
export type TokenResp = {
access_token: string
refresh_token: string
expires_in: number
token_type: string
scope?: string
}
export async function getApiUrl() {
return {
success: true,
data: API_BASE_URL,
} satisfies ApiResponse<string>
}
// ======================
// public
// ======================
async function callPublic<R = undefined>(
endpoint: string,
data?: unknown,
): Promise<ApiResponse<R>> {
return _callPublic(endpoint, data ? JSON.stringify(data) : undefined)
}
const _callPublic = cache(async <R = undefined>(
endpoint: string,
data?: string,
): Promise<ApiResponse<R>> => {
return call(`${API_BASE_URL}${endpoint}`, data)
})
// ======================
// device
// ======================
let token: string | null = null
let token_expire: Date | null = null
async function callByDevice<R = undefined>(
endpoint: string,
data: unknown,
): Promise<ApiResponse<R>> {
return _callByDevice(endpoint, data ? JSON.stringify(data) : undefined)
}
const _callByDevice = cache(async <R = undefined>(
endpoint: string,
data?: string,
): Promise<ApiResponse<R>> => {
if (!token || !token_expire || isBefore(token_expire, new Date())) {
const basic = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64url')
const resp = await call<TokenResp>(`${API_BASE_URL}/api/auth/token`, JSON.stringify({
grant_type: 'client_credentials',
}), `Basic ${basic}`)
if (!resp.success) {
return resp
}
token = resp.data.access_token
token_expire = add(new Date(), {seconds: resp.data.expires_in})
}
// 发起请求
return call(`${API_BASE_URL}${endpoint}`, data, `Bearer ${token}`)
})
// ======================
// user
// ======================
async function callByUser<R = undefined>(
endpoint: string,
data?: unknown,
): Promise<ApiResponse<R>> {
return _callByUser(endpoint, data ? JSON.stringify(data) : undefined)
}
const _callByUser = cache(async <R = undefined>(
endpoint: string,
data?: string,
): Promise<ApiResponse<R>> => {
// 获取用户令牌
const cookie = await cookies()
const token = cookie.get('auth_token')?.value
if (!token) {
return {
success: false,
status: 401,
message: '会话已失效',
}
}
// 发起请求
return await call<R>(`${API_BASE_URL}${endpoint}`, data, `Bearer ${token}`)
})
// ======================
// call
// ======================
async function call<R = undefined>(url: string, body: RequestInit['body'], auth?: string): Promise<ApiResponse<R>> {
let response: Response
try {
const reqHeaders = await headers()
const reqIP = reqHeaders.get('x-forwarded-for')
const reqUA = reqHeaders.get('user-agent')
const callHeaders: RequestInit['headers'] = {
'Content-Type': 'application/json',
}
if (auth) callHeaders['Authorization'] = auth
if (reqIP) callHeaders['X-Forwarded-For'] = reqIP
if (reqUA) callHeaders['User-Agent'] = reqUA
response = await fetch(url, {
method: 'POST',
headers: callHeaders,
body,
})
}
catch (e) {
console.error('后端请求失败', url, (e as Error).message)
throw new Error(`请求失败,网络错误`)
}
const type = response.headers.get('Content-Type') ?? 'text/plain'
if (type.indexOf('text/plain') !== -1) {
const text = await response.text()
if (!response.ok) {
console.log('后端请求失败', url, `status=${response.status}`, text)
return {
success: false,
status: response.status,
message: text || '请求失败',
}
}
if (!!text?.trim()?.length) {
console.log('未处理的响应成功', `type=text`, `text=${text}`)
}
return {
success: true,
data: undefined as R, // 强转类型,考虑优化
}
}
else if (type.indexOf('application/json') !== -1) {
const json = await response.json()
if (!response.ok) {
console.log('后端请求失败', url, `status=${response.status}`, json)
return {
success: false,
status: response.status,
message: json.message || json.error_description || '请求失败', // 业务错误message或者 oauth 错误error_description
}
}
return {
success: true,
data: json,
}
}
throw new Error(`无法解析响应数据,未处理的 Content-Type: ${type}`)
}
// 导出
export {
callPublic,
callByDevice,
callByUser,
}