Skip to content

Commit

Permalink
rework API access to support local and deployed BE
Browse files Browse the repository at this point in the history
  • Loading branch information
rtrembecky committed Dec 16, 2024
1 parent bab8a68 commit 22a156d
Show file tree
Hide file tree
Showing 34 changed files with 198 additions and 126 deletions.
21 changes: 20 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
## pre server-side requesty
BE_PROTOCOL=http
BE_HOSTNAME=localhost
BE_PORT=8000

## localhost:8000
NEXT_PUBLIC_BE_PROTOCOL=http
NEXT_PUBLIC_BE_HOSTNAME=localhost
NEXT_PUBLIC_BE_PORT=8000
NEXT_PUBLIC_BE_PORT=8000
NEXT_PUBLIC_BE_PREFIX=

## test.strom.sk
# NEXT_PUBLIC_BE_PROTOCOL=https
# NEXT_PUBLIC_BE_HOSTNAME=test.strom.sk
# NEXT_PUBLIC_BE_PORT=
# NEXT_PUBLIC_BE_PREFIX=/api

## strom.sk
# NEXT_PUBLIC_BE_PROTOCOL=https
# NEXT_PUBLIC_BE_HOSTNAME=strom.sk
# NEXT_PUBLIC_BE_PORT=
# NEXT_PUBLIC_BE_PREFIX=/api
10 changes: 0 additions & 10 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import type {NextConfig} from 'next'

const nextConfig: NextConfig = {
// docs: https://nextjs.org/docs/api-reference/next.config.js/rewrites
async rewrites() {
return [
// rewrite API requestov na django BE (podstatne aj koncove lomitko)
{
source: '/api/:path*',
destination: `${process.env.NEXT_PUBLIC_BE_PROTOCOL}://${process.env.NEXT_PUBLIC_BE_HOSTNAME}:${process.env.NEXT_PUBLIC_BE_PORT}/:path*/`,
},
]
},
images: {
remotePatterns: [
{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.7.7",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"katex": "^0.16.11",
"luxon": "^3.5.0",
Expand Down
29 changes: 29 additions & 0 deletions src/api/apiAxios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import axios from 'axios'

import {apiInterceptor} from '@/api/apiInterceptor'
import {getInternalServerUrl, getPublicServerUrl} from '@/utils/urlBase'

export const newApiAxios = (base: 'server' | 'client') => {
const baseUrl = base === 'server' ? getInternalServerUrl() : getPublicServerUrl()

const instance = axios.create({
baseURL: baseUrl,
// auth pozostava z comba:
// 1. `sessionid` httpOnly cookie - nastavuje a maze su server pri login/logout
// 2. CSRF hlavicka - server nastavuje cookie, ktorej hodnotu treba vlozit do hlavicky. axios riesi automaticky podla tohto configu
withXSRFToken: true,
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'X-CSRFToken',
// bez tohto sa neposielaju v requeste cookies
withCredentials: true,
})

instance.interceptors.request.use(apiInterceptor)

return instance
}

// nasa "globalna" API instancia
export const apiAxios = newApiAxios('client')

export const serverApiAxios = newApiAxios('server')
25 changes: 25 additions & 0 deletions src/api/apiInterceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import axios from 'axios'

type RequestInterceptor = Parameters<typeof axios.interceptors.request.use>[0]

export const apiInterceptor: RequestInterceptor = (config) => {
if (config.url?.startsWith('/')) {
const [pathname, search] = config.url.split('?')
let newPathname = pathname

// nahrad prefix "/api" (ked existuje) za iny prefix (lokalne "", na deployed BE "/api")
newPathname = newPathname.replace(/^\/api/, process.env.NEXT_PUBLIC_BE_PREFIX ?? '')

// BE Django server ocakava trailing slash (aj pred query params).
// priklady:
// - /api/cms/post -> /api/cms/post/
// - /api/cms/post?search=textik -> /api/cms/post/?search=textik
if (!newPathname.endsWith('/')) {
newPathname = `${newPathname}/`
}

config.url = `${newPathname}${search ? `?${search}` : ''}`
}

return config
}
25 changes: 15 additions & 10 deletions src/components/Admin/dataProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import axios, {isAxiosError} from 'axios'
import {isAxiosError} from 'axios'
import {stringify} from 'querystring'
import {DataProvider, FilterPayload, /* PaginationPayload, */ SortPayload} from 'react-admin'

import {apiAxios} from '@/api/apiAxios'

const getFilterQuery = ({q, ...otherSearchParams}: FilterPayload) => ({
...otherSearchParams,
search: q,
Expand Down Expand Up @@ -48,7 +50,10 @@ export const dataProvider: DataProvider = {
const stringifiedQuery = stringify(query)

try {
const {data} = await axios.get<any[]>(`${apiUrl}/${resource}${stringifiedQuery ? `/?${stringifiedQuery}` : ''}`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const {data} = await apiAxios.get<any[]>(
`${apiUrl}/${resource}${stringifiedQuery ? `/?${stringifiedQuery}` : ''}`,
)

// client-side pagination
let pagedData = data
Expand All @@ -67,15 +72,15 @@ export const dataProvider: DataProvider = {
},
getOne: async (resource, params) => {
try {
const {data} = await axios.get(`${apiUrl}/${resource}/${params.id}`)
const {data} = await apiAxios.get(`${apiUrl}/${resource}/${params.id}`)
return {data}
} catch (e) {
throw new Error(parseError(e))
}
},
getMany: async (resource, params) => {
try {
const data = await Promise.all(params.ids.map((id) => axios.get(`${apiUrl}/${resource}/${id}`)))
const data = await Promise.all(params.ids.map((id) => apiAxios.get(`${apiUrl}/${resource}/${id}`)))
return {data: data.map(({data}) => data)}
} catch (e) {
throw new Error(parseError(e))
Expand All @@ -88,7 +93,7 @@ export const dataProvider: DataProvider = {
}

try {
const {data} = await axios.get(`${apiUrl}/${resource}/?${stringify(query)}`)
const {data} = await apiAxios.get(`${apiUrl}/${resource}/?${stringify(query)}`)
return {
data: data,
total: data.length,
Expand All @@ -105,15 +110,15 @@ export const dataProvider: DataProvider = {
const body = formData ?? input

try {
const {data} = await axios.patch(`${apiUrl}/${resource}/${id}`, body)
const {data} = await apiAxios.patch(`${apiUrl}/${resource}/${id}`, body)
return {data}
} catch (e) {
throw new Error(parseError(e))
}
},
updateMany: async (resource, params) => {
try {
const data = await Promise.all(params.ids.map((id) => axios.patch(`${apiUrl}/${resource}/${id}`, params.data)))
const data = await Promise.all(params.ids.map((id) => apiAxios.patch(`${apiUrl}/${resource}/${id}`, params.data)))
return {data: data.map(({data}) => data)}
} catch (e) {
throw new Error(parseError(e))
Expand All @@ -125,23 +130,23 @@ export const dataProvider: DataProvider = {
const body = formData ?? input

try {
const {data} = await axios.post(`${apiUrl}/${resource}`, body)
const {data} = await apiAxios.post(`${apiUrl}/${resource}`, body)
return {data}
} catch (e) {
throw new Error(parseError(e))
}
},
delete: async (resource, params) => {
try {
const {data} = await axios.delete(`${apiUrl}/${resource}/${params.id}`)
const {data} = await apiAxios.delete(`${apiUrl}/${resource}/${params.id}`)
return {data}
} catch (e) {
throw new Error(parseError(e))
}
},
deleteMany: async (resource, params) => {
try {
const data = await Promise.all(params.ids.map((id) => axios.delete(`${apiUrl}/${resource}/${id}`)))
const data = await Promise.all(params.ids.map((id) => apiAxios.delete(`${apiUrl}/${resource}/${id}`)))
return {data: data.map(({data}) => data.id)}
} catch (e) {
throw new Error(parseError(e))
Expand Down
2 changes: 1 addition & 1 deletion src/components/Admin/useAuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const useAuthProvider = () => {
await testAuthRequest()
},
checkError: async (error) => {
// rovnaky handling ako v `responseIntercepto`r v `AuthContainer`
// rovnaky handling ako v `responseInterceptor` v `AuthContainer`
const status = error.response?.status

if (status === 403) {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Archive/Archive.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Stack, Typography} from '@mui/material'
import {useQuery} from '@tanstack/react-query'
import axios from 'axios'
import {FC} from 'react'

import {apiAxios} from '@/api/apiAxios'
import {Event, Publication} from '@/types/api/competition'
import {useSeminarInfo} from '@/utils/useSeminarInfo'

Expand Down Expand Up @@ -60,7 +60,7 @@ export const Archive: FC = () => {

const {data: eventListData, isLoading: eventListIsLoading} = useQuery({
queryKey: ['competition', 'event', `competition=${seminarId}`],
queryFn: () => axios.get<MyEvent[]>(`/api/competition/event/?competition=${seminarId}`),
queryFn: () => apiAxios.get<MyEvent[]>(`/api/competition/event/?competition=${seminarId}`),
})
const eventList = eventListData?.data ?? []

Expand Down
4 changes: 2 additions & 2 deletions src/components/CompetitionPage/CompetitionPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Stack, Typography} from '@mui/material'
import Grid from '@mui/material/Unstable_Grid2'
import {useQuery} from '@tanstack/react-query'
import axios from 'axios'
import {useRouter} from 'next/router'
import {FC, Fragment, useEffect} from 'react'

import {apiAxios} from '@/api/apiAxios'
import {Link} from '@/components/Clickable/Link'
import {Competition, Event, PublicationTypes} from '@/types/api/competition'
import {BannerContainer} from '@/utils/BannerContainer'
Expand Down Expand Up @@ -33,7 +33,7 @@ export const CompetitionPage: FC<CompetitionPageProps> = ({

const {data: bannerMessage, isLoading: isBannerLoading} = useQuery({
queryKey: ['cms', 'info-banner', 'competition', id],
queryFn: () => axios.get<string[]>(`/api/cms/info-banner/competition/${id}`),
queryFn: () => apiAxios.get<string[]>(`/api/cms/info-banner/competition/${id}`),
enabled: id !== -1,
})

Expand Down
5 changes: 3 additions & 2 deletions src/components/FileUploader/FileUploader.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {Upload} from '@mui/icons-material'
import {useMutation} from '@tanstack/react-query'
import axios from 'axios'
import {FC, useCallback} from 'react'
import {Accept, DropzoneOptions, useDropzone} from 'react-dropzone'

import {apiAxios} from '@/api/apiAxios'

interface FileUploaderProps {
uploadLink: string
acceptedFormats?: Accept
Expand All @@ -13,7 +14,7 @@ interface FileUploaderProps {

export const FileUploader: FC<FileUploaderProps> = ({uploadLink, acceptedFormats, adjustFormData, refetch}) => {
const {mutate: fileUpload} = useMutation({
mutationFn: (formData: FormData) => axios.post(uploadLink, formData),
mutationFn: (formData: FormData) => apiAxios.post(uploadLink, formData),
onSuccess: () => refetch(),
})

Expand Down
6 changes: 3 additions & 3 deletions src/components/PageLayout/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {Stack} from '@mui/material'
import Grid from '@mui/material/Unstable_Grid2'
import {useQuery} from '@tanstack/react-query'
import axios from 'axios'
import {FC} from 'react'

import {apiAxios} from '@/api/apiAxios'
import {Link} from '@/components/Clickable/Link'
import {Loading} from '@/components/Loading/Loading'
import {ILogo, Logo} from '@/components/PageLayout/Footer/Logo'
Expand All @@ -19,7 +19,7 @@ export const Footer: FC = () => {
error: menuItemsError,
} = useQuery({
queryKey: ['cms', 'menu-item', 'on-site', seminarId, '?footer'],
queryFn: () => axios.get<MenuItemShort[]>(`/api/cms/menu-item/on-site/${seminarId}?type=footer`),
queryFn: () => apiAxios.get<MenuItemShort[]>(`/api/cms/menu-item/on-site/${seminarId}?type=footer`),
})
const menuItems = menuItemsData?.data ?? []

Expand All @@ -29,7 +29,7 @@ export const Footer: FC = () => {
error: logosError,
} = useQuery({
queryKey: ['cms', 'logo'],
queryFn: () => axios.get<ILogo[]>('/api/cms/logo'),
queryFn: () => apiAxios.get<ILogo[]>('/api/cms/logo'),
})
const logos = logosData?.data.filter((logo) => !logo.disabled) ?? []

Expand Down
4 changes: 2 additions & 2 deletions src/components/PageLayout/MenuMain/MenuMain.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {Box, Drawer, Stack, Theme, useMediaQuery} from '@mui/material'
import {useQuery} from '@tanstack/react-query'
import axios from 'axios'
import {useRouter} from 'next/router'
import {FC, useEffect, useState} from 'react'

import {apiAxios} from '@/api/apiAxios'
import {Link} from '@/components/Clickable/Link'
import {CloseButton} from '@/components/CloseButton/CloseButton'
import {Loading} from '@/components/Loading/Loading'
Expand Down Expand Up @@ -43,7 +43,7 @@ export const MenuMain: FC = () => {

const {data: menuItemsData, isLoading: menuItemsIsLoading} = useQuery({
queryKey: ['cms', 'menu-item', 'on-site', seminarId, '?menu'],
queryFn: () => axios.get<MenuItemShort[]>(`/api/cms/menu-item/on-site/${seminarId}?type=menu`),
queryFn: () => apiAxios.get<MenuItemShort[]>(`/api/cms/menu-item/on-site/${seminarId}?type=menu`),
})
const menuItems = menuItemsData?.data ?? []

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {Stack} from '@mui/material'
import {useMutation} from '@tanstack/react-query'
import axios from 'axios'
import {FC} from 'react'
import {SubmitHandler, useForm} from 'react-hook-form'

import {apiAxios} from '@/api/apiAxios'
import {Button} from '@/components/Clickable/Button'
import {FormInput} from '@/components/FormItems/FormInput/FormInput'
import {IGeneralPostResponse} from '@/types/api/general'
Expand All @@ -27,7 +27,7 @@ export const PasswordResetRequestForm: FC<PasswordResetRequestFormmProps> = ({cl

const {mutate: submitFormData} = useMutation({
mutationFn: (data: PasswordResetRequestFormValues) => {
return axios.post<IGeneralPostResponse>('/api/user/password/reset', data)
return apiAxios.post<IGeneralPostResponse>('/api/user/password/reset', data)
},

onSuccess: () => {
Expand Down
4 changes: 2 additions & 2 deletions src/components/PasswordReset/PasswordReset.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Stack, Typography} from '@mui/material'
import {useMutation} from '@tanstack/react-query'
import axios from 'axios'
import {useRouter} from 'next/router'
import {FC} from 'react'
import {SubmitHandler, useForm} from 'react-hook-form'

import {apiAxios} from '@/api/apiAxios'
import {Button} from '@/components/Clickable/Button'
import {FormInput} from '@/components/FormItems/FormInput/FormInput'
import {IGeneralPostResponse} from '@/types/api/general'
Expand Down Expand Up @@ -46,7 +46,7 @@ export const PasswordResetForm: FC<PasswordResetFormProps> = ({uid, token}) => {

const {mutate: submitFormData, isSuccess: isReset} = useMutation({
mutationFn: (data: PasswordResetForm) => {
return axios.post<IGeneralPostResponse>('/api/user/password/reset/confirm', transformFormData(data))
return apiAxios.post<IGeneralPostResponse>('/api/user/password/reset/confirm', transformFormData(data))
},
})

Expand Down
4 changes: 2 additions & 2 deletions src/components/Posts/Posts.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Stack, Typography} from '@mui/material'
import {useQuery} from '@tanstack/react-query'
import axios from 'axios'
import {FC} from 'react'

import {apiAxios} from '@/api/apiAxios'
import {useSeminarInfo} from '@/utils/useSeminarInfo'

import {Loading} from '../Loading/Loading'
Expand All @@ -15,7 +15,7 @@ export const Posts: FC = () => {
error: postsError,
} = useQuery({
queryKey: ['cms', 'post', 'visible'],
queryFn: () => axios.get<IPost[]>(`/api/cms/post/visible?sites=${seminarId}`),
queryFn: () => apiAxios.get<IPost[]>(`/api/cms/post/visible?sites=${seminarId}`),
})
const posts = postsData?.data ?? []

Expand Down
Loading

0 comments on commit 22a156d

Please sign in to comment.