requestNative.ts 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import type { FetchContext } from 'ofetch'
  2. import type { ApiResponse } from '~/types/api'
  3. import CryptoJS from 'crypto-js'
  4. import { v4 as uuidv4 } from 'uuid'
  5. import { APP_LOCALE_KEY, DEVICE_UDID_KEY } from '~/constants'
  6. import { showToast } from 'vant'
  7. import { getDeviceLanguage } from '~/utils/helpers'
  8. const MD5 = CryptoJS.MD5
  9. declare global {
  10. interface Window {
  11. GAMI_BRIDGE?: {
  12. requestHeader?: Record<string, unknown>
  13. }
  14. }
  15. }
  16. // Device and app information (fallback values)
  17. const DEVICE_INFO = {
  18. udid: '', // Will be initialized
  19. app: 'web',
  20. device: '',
  21. platform: 3,
  22. channel: 'official',
  23. api: 1,
  24. version: '1.0.0',
  25. network: 'wifi',
  26. }
  27. /**
  28. * Initialize device info (fallback for when bridge headers are missing)
  29. */
  30. const initDeviceInfo = () => {
  31. if (import.meta.client) {
  32. let udid = localStorage.getItem(DEVICE_UDID_KEY)
  33. if (!udid) {
  34. udid = uuidv4()
  35. localStorage.setItem(DEVICE_UDID_KEY, udid)
  36. }
  37. DEVICE_INFO.udid = udid
  38. DEVICE_INFO.device = (navigator.userAgent || 'unknown').slice(0, 256)
  39. interface NetworkInformation {
  40. effectiveType?: string
  41. }
  42. const nav = navigator as Navigator & {
  43. connection?: NetworkInformation
  44. mozConnection?: NetworkInformation
  45. webkitConnection?: NetworkInformation
  46. }
  47. const connection = nav.connection || nav.mozConnection || nav.webkitConnection
  48. if (connection) {
  49. DEVICE_INFO.network = connection.effectiveType || 'wifi'
  50. }
  51. }
  52. }
  53. /**
  54. * Generate request ID
  55. */
  56. const generateRequestId = (): string => {
  57. return uuidv4().replace(/-/g, '')
  58. }
  59. type SignDeviceInfo = {
  60. udid: string
  61. app: string
  62. device: string
  63. platform: string | number
  64. channel: string
  65. api: string | number
  66. version: string
  67. network: string
  68. }
  69. /**
  70. * Generate sign (same algorithm as web request.ts)
  71. */
  72. const generateSign = (id: string, time: number, token: string, secret: string, body: unknown, deviceInfo: SignDeviceInfo): string => {
  73. const { udid, app, device, platform, channel, api, version, network } = deviceInfo
  74. const bodyStr = body ? JSON.stringify(body) : ''
  75. const signStr = `${id}${udid}${app}${device}${platform}${channel}${api}${version}${network}${time}${token}${secret}${bodyStr}`
  76. return MD5(signStr).toString()
  77. }
  78. /**
  79. * Create request instance for native pages.
  80. * Related headers are read from window.GAMI_BRIDGE.requestHeader, while `id` and `sign` are generated locally.
  81. */
  82. export const createRequestNative = () => {
  83. const runtimeConfig = useRuntimeConfig()
  84. const { token, logout } = useAuth()
  85. const API_CONFIG = {
  86. baseURL: runtimeConfig.public.apiBase || '/api',
  87. timeout: 30000,
  88. secret: 'abc|abc|edg|9527|1234',
  89. identity: runtimeConfig.public.apiIdentity || 'web',
  90. }
  91. if (import.meta.client && !DEVICE_INFO.udid) {
  92. initDeviceInfo()
  93. }
  94. const getBridgeHeaders = (): Record<string, string> => {
  95. if (!import.meta.client) return {}
  96. const raw = window.GAMI_BRIDGE?.requestHeader
  97. if (!raw || typeof raw !== 'object') {
  98. console.error('Must in native context')
  99. showError('Must in native context')
  100. return {}
  101. }
  102. const out: Record<string, string> = {}
  103. for (const [k, v] of Object.entries(raw)) {
  104. if (v === undefined || v === null) continue
  105. out[k] = String(v)
  106. }
  107. return out
  108. }
  109. const onRequest = (ctx: FetchContext): void => {
  110. const { options } = ctx
  111. if (!options.baseURL) {
  112. options.baseURL = API_CONFIG.baseURL
  113. }
  114. const headers: Record<string, string> = {}
  115. if (options.headers) {
  116. const existingHeaders = options.headers as unknown as Record<string, string>
  117. Object.assign(headers, existingHeaders)
  118. }
  119. const bridgeHeaders = getBridgeHeaders()
  120. Object.assign(headers, bridgeHeaders)
  121. const requestId = generateRequestId()
  122. const time = Date.now()
  123. const tokenVal = bridgeHeaders.token || token.value || ''
  124. if (import.meta.client) {
  125. const language = localStorage.getItem(APP_LOCALE_KEY)
  126. headers['Accept-Language'] = language || getDeviceLanguage()
  127. }
  128. const signDeviceInfo: SignDeviceInfo = {
  129. udid: bridgeHeaders.udid || DEVICE_INFO.udid,
  130. app: bridgeHeaders.app || API_CONFIG.identity,
  131. device: bridgeHeaders.device || DEVICE_INFO.device,
  132. platform: bridgeHeaders.platform ?? DEVICE_INFO.platform,
  133. channel: bridgeHeaders.channel || DEVICE_INFO.channel,
  134. api: bridgeHeaders.api ?? DEVICE_INFO.api,
  135. version: bridgeHeaders.version || DEVICE_INFO.version,
  136. network: bridgeHeaders.network || DEVICE_INFO.network,
  137. }
  138. // Ensure mandatory API headers exist (from bridge or fallback), but override id/time/sign
  139. headers['id'] = requestId
  140. headers['udid'] = String(signDeviceInfo.udid)
  141. headers['app'] = String(signDeviceInfo.app)
  142. headers['device'] = String(signDeviceInfo.device)
  143. headers['platform'] = String(signDeviceInfo.platform)
  144. headers['channel'] = String(signDeviceInfo.channel)
  145. headers['api'] = String(signDeviceInfo.api)
  146. headers['version'] = String(signDeviceInfo.version)
  147. headers['network'] = String(signDeviceInfo.network)
  148. headers['time'] = String(time)
  149. if (tokenVal) headers['token'] = tokenVal
  150. else delete headers.token
  151. const body = options.body
  152. headers['sign'] = generateSign(requestId, time, tokenVal, API_CONFIG.secret, body, signDeviceInfo)
  153. if (!headers['Content-Type']) {
  154. headers['Content-Type'] = 'application/json'
  155. }
  156. // @ts-expect-error - Headers type mismatch between HeadersInit and Headers
  157. options.headers = headers
  158. }
  159. const onResponse = (ctx: FetchContext): void => {
  160. if (!ctx.response) return
  161. const data = ctx.response._data as ApiResponse
  162. if (data && typeof data === 'object' && 'code' in data) {
  163. if (data.code === 0) {
  164. ctx.response._data = data.data
  165. return
  166. }
  167. handleApiError(data.code, data.msg)
  168. throw new Error(data.msg || 'Request failed')
  169. }
  170. }
  171. const onResponseError = (ctx: FetchContext): Promise<never> => {
  172. const { error } = ctx
  173. const response = ctx.response
  174. if (response) {
  175. const status = response.status
  176. const data = response._data as { msg?: string }
  177. switch (status) {
  178. case 401:
  179. handleUnauthorized()
  180. break
  181. case 403:
  182. showError('No permission to access this resource')
  183. break
  184. case 404:
  185. showError('Resource not found')
  186. break
  187. case 500:
  188. showError('Server error, please try again later')
  189. break
  190. default:
  191. showError(data?.msg || error?.message || 'Request failed')
  192. }
  193. }
  194. else {
  195. const message = error?.message || ''
  196. if (message.includes('timeout')) showError('Request timeout, please try again')
  197. else if (message.includes('Network Error')) showError('Network error, please check your connection')
  198. else showError(message || 'Unknown error occurred')
  199. }
  200. return Promise.reject(error)
  201. }
  202. function handleApiError(code: number, message: string) {
  203. switch (code) {
  204. case 100004:
  205. handleUnauthorized()
  206. break
  207. default:
  208. if (message) showError(message)
  209. }
  210. }
  211. function handleUnauthorized() {
  212. logout()
  213. if (import.meta.client) {
  214. const route = useRoute()
  215. const redirect = route.fullPath
  216. showError('Login expired, please login again')
  217. navigateTo({
  218. path: '/login',
  219. query: redirect && redirect !== '/login' ? { redirect } : undefined,
  220. })
  221. }
  222. }
  223. function showError(message: string) {
  224. if (import.meta.client) {
  225. showToast({ message: `${message}` })
  226. }
  227. }
  228. const request = $fetch.create({
  229. baseURL: API_CONFIG.baseURL,
  230. timeout: API_CONFIG.timeout,
  231. onRequest,
  232. onResponse,
  233. onResponseError,
  234. })
  235. return request
  236. }