[ABANDONED] React/Redux front end for the Flexor social network.
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.
 
 

156 lines
4.6 KiB

// fetch.ts
// Copyright (C) 2020 Dwayne Harris
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import {
UnauthorizedError,
BadRequestError,
NotFoundError,
ServerError,
} from './errors'
import { pathJoin } from '../utils'
import {
LOCAL_STORAGE_ACCESS_TOKEN_KEY,
LOCAL_STORAGE_ACCESS_TOKEN_EXPIRES_AT_KEY,
LOCAL_STORAGE_REFRESH_TOKEN_KEY,
} from '../constants'
import { FetchOptions, FormNotification, NotificationType } from '../types'
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 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(`/${pathJoin('api', 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('/api/v1/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_EXPIRES_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_EXPIRES_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)
}