[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

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. // fetch.ts
  2. // Copyright (C) 2020 Dwayne Harris
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU General Public License for more details.
  11. // You should have received a copy of the GNU General Public License
  12. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. import {
  14. UnauthorizedError,
  15. BadRequestError,
  16. NotFoundError,
  17. ServerError,
  18. } from './errors'
  19. import { pathJoin } from '../utils'
  20. import {
  21. LOCAL_STORAGE_ACCESS_TOKEN_KEY,
  22. LOCAL_STORAGE_ACCESS_TOKEN_EXPIRES_AT_KEY,
  23. LOCAL_STORAGE_REFRESH_TOKEN_KEY,
  24. } from '../constants'
  25. import { FetchOptions, FormNotification, NotificationType } from '../types'
  26. interface RefreshResponse {
  27. id: string
  28. access: string
  29. refresh: string
  30. expires: number
  31. }
  32. interface FormError {
  33. field?: string
  34. message: string
  35. }
  36. interface ErrorResponse {
  37. message: string
  38. errors?: FormError[]
  39. }
  40. type APIFetch = <T = void>(options: FetchOptions) => Promise<T>
  41. const mapErrorsToFormNotifications = (errors?: FormError[]): FormNotification[] => {
  42. if (!errors) return []
  43. return errors.map(e => ({
  44. field: e.field,
  45. type: NotificationType.Error,
  46. message: e.message,
  47. }))
  48. }
  49. const checkResponse = async (response: Response, retry?: () => Promise<void>) => {
  50. switch (response.status) {
  51. case 400: {
  52. const { message, errors } = await response.json() as ErrorResponse
  53. throw new BadRequestError(message, mapErrorsToFormNotifications(errors))
  54. }
  55. case 401: {
  56. if (retry) return await retry()
  57. throw new UnauthorizedError()
  58. }
  59. case 404: throw new NotFoundError()
  60. default: {
  61. throw new ServerError()
  62. }
  63. }
  64. }
  65. const getResponseData = async (response: Response) => {
  66. try {
  67. return await response.json()
  68. } catch (err) {
  69. return {}
  70. }
  71. }
  72. export const apiFetch: APIFetch = async (options: FetchOptions) => {
  73. const { path, method = 'get', body } = options
  74. const contentType = 'application/json'
  75. const doFetch = async () => {
  76. const headers = new Headers({
  77. ...options.headers,
  78. 'Accept': contentType,
  79. })
  80. if (body) headers.append('Content-Type', contentType)
  81. const accessToken = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)
  82. if (accessToken) headers.append('Authorization', `Bearer ${accessToken}`)
  83. return await fetch(`/${pathJoin('api', path)}`, {
  84. headers,
  85. method,
  86. body: body ? JSON.stringify(body) : undefined,
  87. })
  88. }
  89. const doRefresh = async () => {
  90. const accessToken = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY)
  91. const refreshToken = localStorage.getItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY)
  92. if (accessToken && refreshToken) {
  93. const refreshResponse = await fetch('/api/v1/refresh', {
  94. headers: new Headers({
  95. 'Content-Type': contentType,
  96. 'Authorization': `Bearer ${accessToken}`
  97. }),
  98. method: 'post',
  99. body: JSON.stringify({
  100. refresh: refreshToken,
  101. }),
  102. })
  103. if (refreshResponse.status !== 201) {
  104. throw new UnauthorizedError()
  105. }
  106. const data = await getResponseData(refreshResponse) as RefreshResponse
  107. localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_KEY, data.access)
  108. localStorage.setItem(LOCAL_STORAGE_ACCESS_TOKEN_EXPIRES_AT_KEY, data.expires.toString())
  109. localStorage.setItem(LOCAL_STORAGE_REFRESH_TOKEN_KEY, data.refresh)
  110. const secondResponse = await doFetch()
  111. if (secondResponse.ok) {
  112. return await getResponseData(secondResponse)
  113. }
  114. await checkResponse(secondResponse)
  115. }
  116. }
  117. const accessTokenExpiresAt = localStorage.getItem(LOCAL_STORAGE_ACCESS_TOKEN_EXPIRES_AT_KEY)
  118. if (accessTokenExpiresAt && Date.now() >= parseInt(accessTokenExpiresAt, 10)) {
  119. return await doRefresh()
  120. }
  121. const response = await doFetch()
  122. if (response.ok) {
  123. return await getResponseData(response)
  124. }
  125. return await checkResponse(response, doRefresh)
  126. }