File size: 3,548 Bytes
276ef31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
 * API client. Uses Next.js rewrite to /api/v1/* (see next.config.mjs).
 * Auth token is read from localStorage and attached as Bearer.
 */

import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import { useAuthStore } from '@/stores/auth'

const TOKEN_KEY = 'wormgpt.access'
const REFRESH_KEY = 'wormgpt.refresh'

export function getAccessToken() {
  if (typeof window === 'undefined') return null
  return localStorage.getItem(TOKEN_KEY)
}

export function getRefreshToken() {
  if (typeof window === 'undefined') return null
  return localStorage.getItem(REFRESH_KEY)
}

export function setTokens(access?: string, refresh?: string) {
  if (typeof window === 'undefined') return
  if (access !== undefined) localStorage.setItem(TOKEN_KEY, access)
  if (refresh !== undefined) localStorage.setItem(REFRESH_KEY, refresh)
}

export function clearTokens() {
  if (typeof window === 'undefined') return
  localStorage.removeItem(TOKEN_KEY)
  localStorage.removeItem(REFRESH_KEY)
}

export const api: AxiosInstance = axios.create({
  baseURL: '/api/v1',
  timeout: 60_000,
  headers: { 'Content-Type': 'application/json' },
})

function readCsrfToken(): string | null {
  if (typeof document === 'undefined') return null
  const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]*)/)
  return match ? decodeURIComponent(match[1]) : null
}

api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const token = getAccessToken()
  if (token) config.headers.set('Authorization', `Bearer ${token}`)
  const method = (config.method || 'get').toLowerCase()
  if (!['get', 'head', 'options'].includes(method)) {
    const csrf = readCsrfToken()
    if (csrf) config.headers.set('x-csrf-token', csrf)
  }
  return config
})

let refreshing: Promise<string | null> | null = null

async function doRefresh(): Promise<string | null> {
  const refresh = getRefreshToken()
  if (!refresh) return null
  try {
    const r = await axios.post('/api/v1/auth/refresh', { refreshToken: refresh })
    const { accessToken, refreshToken } = r.data
    setTokens(accessToken, refreshToken)
    return accessToken
  } catch {
    clearTokens()
    useAuthStore.getState().logout()
    return null
  }
}

function isAuthEndpoint(url?: string): boolean {
  if (!url) return false
  return (
    url.includes('/auth/login') ||
    url.includes('/auth/register') ||
    url.includes('/auth/refresh') ||
    url.includes('/auth/logout')
  )
}

api.interceptors.response.use(
  (r) => r,
  async (error: AxiosError) => {
    const status = error.response?.status
    const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean }
    if (
      status === 401 &&
      !original._retry &&
      !isAuthEndpoint(original.url)
    ) {
      original._retry = true
      const refreshPromise = (refreshing ??= doRefresh().finally(() => {
        refreshing = null
      }))
      const token = await refreshPromise
      if (token) {
        original.headers.set('Authorization', `Bearer ${token}`)
        return api.request(original)
      }
    }
    return Promise.reject(error)
  }
)

export function apiError(e: unknown): string {
  if (axios.isAxiosError(e)) {
    const data: any = e.response?.data
    if (typeof data === 'string') return data
    if (data?.detail) return Array.isArray(data.detail) ? data.detail.map((d: any) => d.msg).join(', ') : data.detail
    if (data?.error) return data.error
    return e.message
  }
  if (e instanceof Error) return e.message
  return 'unknown error'
}