You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
140 lines
4.0 KiB
140 lines
4.0 KiB
import {
|
|
UnauthorizedError,
|
|
BadRequestError,
|
|
NotFoundError,
|
|
ServerError,
|
|
} from './errors'
|
|
|
|
import {
|
|
LOCAL_STORAGE_ACCESS_TOKEN_KEY,
|
|
LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY,
|
|
LOCAL_STORAGE_REFRESH_TOKEN_KEY,
|
|
} from '../constants'
|
|
|
|
import { FetchOptions, FormNotification, NotificationType } from '../types'
|
|
import getConfig from '../config'
|
|
|
|
interface RefreshResponse {
|
|
id: string
|
|
access: string
|
|
refresh: string
|
|
expires: number
|
|
}
|
|
|
|
interface FormError {
|
|
field?: string
|
|
message: string
|
|
}
|
|
|
|
interface ErrorResponse {
|
|
message: string
|
|
errors?: FormError[]
|
|
}
|
|
|
|
type APIFetch = <T = void>(options: FetchOptions) => Promise<T>
|
|
|
|
const mapErrorsToFormNotifications = (errors?: FormError[]): FormNotification[] => {
|
|
if (!errors) return []
|
|
|
|
return errors.map(e => ({
|
|
field: e.field,
|
|
type: NotificationType.Error,
|
|
message: e.message,
|
|
}))
|
|
}
|
|
|
|
const checkResponse = async (response: Response, retry?: () => Promise<void>) => {
|
|
switch (response.status) {
|
|
case 400: {
|
|
const { message, errors } = await response.json() as ErrorResponse
|
|
throw new BadRequestError(message, mapErrorsToFormNotifications(errors))
|
|
}
|
|
case 401: {
|
|
if (retry) return await retry()
|
|
throw new UnauthorizedError()
|
|
}
|
|
case 404: throw new NotFoundError()
|
|
default: {
|
|
throw new ServerError()
|
|
}
|
|
}
|
|
}
|
|
|
|
const getResponseData = async (response: Response) => {
|
|
try {
|
|
return await response.json()
|
|
} catch (err) {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
export const apiFetch: APIFetch = async (options: FetchOptions) => {
|
|
const { path, method = 'get', body } = options
|
|
const contentType = 'application/json'
|
|
const config = await getConfig()
|
|
|
|
const doFetch = async () => {
|
|
const headers = new Headers({
|
|
...options.headers,
|
|
'Accept': contentType,
|
|
})
|
|
|
|
if (body) headers.append('Content-Type', contentType)
|
|
|
|
const accessToken = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)
|
|
if (accessToken) headers.append('Authorization', `Bearer ${accessToken}`)
|
|
|
|
return await fetch(`${config.apiUrl}${path}`, {
|
|
headers,
|
|
method,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
})
|
|
}
|
|
|
|
const doRefresh = async () => {
|
|
const accessToken = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)
|
|
const refreshToken = localStorage.getItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY)
|
|
|
|
if (accessToken && refreshToken) {
|
|
const refreshResponse = await fetch(`${config.apiUrl}/api/refresh`, {
|
|
headers: new Headers({
|
|
'Content-Type': contentType,
|
|
'Authorization': `Bearer ${accessToken}`
|
|
}),
|
|
method: 'post',
|
|
body: JSON.stringify({
|
|
refresh: refreshToken,
|
|
}),
|
|
})
|
|
|
|
if (refreshResponse.status !== 201) {
|
|
throw new UnauthorizedError()
|
|
}
|
|
|
|
const data = await getResponseData(refreshResponse) as RefreshResponse
|
|
|
|
localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY, data.access)
|
|
localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY, data.expires.toString())
|
|
localStorage.setItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY, data.refresh)
|
|
|
|
const secondResponse = await doFetch()
|
|
if (secondResponse.ok) {
|
|
return await getResponseData(secondResponse)
|
|
}
|
|
|
|
await checkResponse(secondResponse)
|
|
}
|
|
}
|
|
|
|
const accessTokenExpiresAt = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_AT_KEY)
|
|
if (accessTokenExpiresAt && Date.now() >= parseInt(accessTokenExpiresAt, 10)) {
|
|
return await doRefresh()
|
|
}
|
|
|
|
const response = await doFetch()
|
|
if (response.ok) {
|
|
return await getResponseData(response)
|
|
}
|
|
|
|
return await checkResponse(response, doRefresh)
|
|
}
|