|
|
// 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) }
|