Skip to content

Commit

Permalink
rework API access to support local and deployed BE (#550)
Browse files Browse the repository at this point in the history
* rework API access to support local and deployed BE

* add /api prefix

* fix infinite redirect on root path

* add headers to solve server-side issues
  • Loading branch information
rtrembecky authored Dec 17, 2024
1 parent bab8a68 commit c380de5
Show file tree
Hide file tree
Showing 44 changed files with 301 additions and 150 deletions.
23 changes: 20 additions & 3 deletions .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
NEXT_PUBLIC_BE_PROTOCOL=http
NEXT_PUBLIC_BE_HOSTNAME=localhost
NEXT_PUBLIC_BE_PORT=8000
# uncomment for debug server logs
# DEBUG=true

## developovanie proti lokalnemu BE
BE_PROTOCOL=http
BE_HOSTNAME=localhost
BE_PORT=8000
BE_PREFIX=/api

## developovanie proti BE na test.strom.sk
# BE_PROTOCOL=https
# BE_HOSTNAME=test.strom.sk
# BE_PORT=
# BE_PREFIX=/api

## developovanie proti BE na strom.sk
# BE_PROTOCOL=https
# BE_HOSTNAME=strom.sk
# BE_PORT=
# BE_PREFIX=/api
5 changes: 4 additions & 1 deletion deployment/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
FROM node:20

ARG NEXT_PUBLIC_BE_PORT
ARG BE_PROTOCOL
ARG BE_HOSTNAME
ARG BE_PORT
ARG BE_PREFIX

WORKDIR /app

Expand Down
12 changes: 9 additions & 3 deletions deployment/compose-test.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
version: "3"
version: '3'

services:
webstrom-frontend:
build:
dockerfile: deployment/Dockerfile
context: ..
args:
- NEXT_PUBLIC_BE_PORT=8920
- BE_PROTOCOL=http
- BE_HOSTNAME=localhost
- BE_PORT=8920
- BE_PREFIX=/api

image: webstrom-test-frontend

environment:
- PORT=8922
- NEXT_PUBLIC_BE_PORT=8920
- BE_PROTOCOL=http
- BE_HOSTNAME=localhost
- BE_PORT=8920
- BE_PREFIX=/api

network_mode: host

Expand Down
6 changes: 4 additions & 2 deletions deployment/local-mac/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
FROM node:20

ARG NEXT_PUBLIC_BE_PORT
ARG NEXT_PUBLIC_BE_HOSTNAME
ARG BE_PROTOCOL
ARG BE_HOSTNAME
ARG BE_PORT
ARG BE_PREFIX

WORKDIR /app

Expand Down
4 changes: 3 additions & 1 deletion deployment/local-mac/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ Treba mat samozrejme Docker a docker-compose atd.

## Urobene zmeny oproti compose-test

TODO: Docker Desktop 4.34 priniesol [host networking](https://docs.docker.com/engine/network/drivers/host/#docker-desktop), treba revisitnut

Ked som chcel pustit Docker na macu, musel som urobil nasledujuce zmeny (poradilo ChatGPT):

### `compose.yml`

- zmenil som aj meno suboru (lebo nebuildime "test" environment a.k.a test.strom.sk)
- vymena `network_mode: host` za `ports: - '3000:3000'` - network_mode vraj na macu nefunguje
- vymena `network_mode: host` za explicitne `ports: - '3000:3000'` - network_mode vraj na macu nefunguje
- pridanie `- NEXT_PUBLIC_BE_HOSTNAME=host.docker.internal` - aby sa to vedelo pripojit na lokalny BE - Docker pouziva takyto hostname pre host machine localhost
- zmenil som porty na 3000/8000 ako standardne pouzivame pre development
- `dockerfile` a `context` cesty podla file struktury novych suborov
Expand Down
12 changes: 8 additions & 4 deletions deployment/local-mac/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ services:
dockerfile: deployment/local-mac/Dockerfile
context: ../..
args:
- NEXT_PUBLIC_BE_PORT=8000
- NEXT_PUBLIC_BE_HOSTNAME=host.docker.internal
- BE_PROTOCOL=http
- BE_HOSTNAME=host.docker.internal
- BE_PORT=8000
- BE_PREFIX=/api

image: webstrom-test-frontend

environment:
- PORT=3000
- NEXT_PUBLIC_BE_PORT=8000
- NEXT_PUBLIC_BE_HOSTNAME=host.docker.internal
- BE_PROTOCOL=http
- BE_HOSTNAME=host.docker.internal
- BE_PORT=8000
- BE_PREFIX=/api

ports:
- '3000:3000'
Expand Down
24 changes: 8 additions & 16 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
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: [
{
// TODO: neviem, preco to krici, treba overit, ci funguju obrazky pri ulohach
// @ts-ignore
protocol: process.env.NEXT_PUBLIC_BE_PROTOCOL,
// @ts-ignore
hostname: process.env.NEXT_PUBLIC_BE_HOSTNAME,
port: process.env.NEXT_PUBLIC_BE_PORT,
protocol: process.env.BE_PROTOCOL as 'http' | 'https' | undefined,
hostname: process.env.BE_HOSTNAME ?? 'localhost',
port: process.env.BE_PORT,
pathname: '/media/**',
},
],
Expand Down Expand Up @@ -51,6 +38,11 @@ const nextConfig: NextConfig = {

return config
},
// typecheck aj eslint pustame v CI pred buildom osobitne, nemusi ich pustat aj next
typescript: {ignoreBuildErrors: true},
eslint: {ignoreDuringBuilds: true},
// https://nextjs.org/docs/app/building-your-application/routing/middleware#advanced-middleware-flags
skipTrailingSlashRedirect: true,
experimental: {
turbo: {
rules: {
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
42 changes: 42 additions & 0 deletions src/api/apiAxios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import axios from 'axios'

import {apiInterceptor} from '@/api/apiInterceptor'
import {debugServer} from '@/utils/debugServer'
import {getInternalBeServerUrl} from '@/utils/urlBase'

export const newApiAxios = (base: 'server' | 'client') => {
const instance = axios.create({
baseURL: base === 'server' ? getInternalBeServerUrl() : '/api',
// 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,
})

if (base === 'server') {
// prvy definovany interceptor bezi posledny. logujeme finalnu URL
instance.interceptors.request.use((config) => {
const {method, url, baseURL} = config

debugServer('[SERVER API]', method?.toUpperCase(), url && baseURL ? new URL(url, baseURL).href : url)

config.headers['X-Forwarded-Host'] = 'test.strom.sk'
config.headers['X-Forwarded-Proto'] = 'https'

return config
})
}

instance.interceptors.request.use(apiInterceptor)

return instance
}

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

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

import {addApiTrailingSlash} from '@/utils/addApiTrailingSlash'

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

export const apiInterceptor: RequestInterceptor = (config) => {
if (config.url) {
const [pathname, query] = config.url.split('?')

const newPathname = addApiTrailingSlash(pathname)

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

return config
}
25 changes: 13 additions & 12 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 @@ -34,8 +36,6 @@ const parseError = (error: unknown) => {
return 'Nastala neznáma chyba'
}

const apiUrl = '/api'

// skopirovane a dost upravene z https://github.com/bmihelac/ra-data-django-rest-framework/blob/master/src/index.ts
export const dataProvider: DataProvider = {
getList: async (resource, {filter, sort, pagination}) => {
Expand All @@ -48,7 +48,8 @@ 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[]>(`/${resource}${stringifiedQuery ? `/?${stringifiedQuery}` : ''}`)

// client-side pagination
let pagedData = data
Expand All @@ -67,15 +68,15 @@ export const dataProvider: DataProvider = {
},
getOne: async (resource, params) => {
try {
const {data} = await axios.get(`${apiUrl}/${resource}/${params.id}`)
const {data} = await apiAxios.get(`/${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(`/${resource}/${id}`)))
return {data: data.map(({data}) => data)}
} catch (e) {
throw new Error(parseError(e))
Expand All @@ -88,7 +89,7 @@ export const dataProvider: DataProvider = {
}

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

try {
const {data} = await axios.patch(`${apiUrl}/${resource}/${id}`, body)
const {data} = await apiAxios.patch(`/${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(`/${resource}/${id}`, params.data)))
return {data: data.map(({data}) => data)}
} catch (e) {
throw new Error(parseError(e))
Expand All @@ -125,23 +126,23 @@ export const dataProvider: DataProvider = {
const body = formData ?? input

try {
const {data} = await axios.post(`${apiUrl}/${resource}`, body)
const {data} = await apiAxios.post(`/${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(`/${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(`/${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[]>(`/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[]>(`/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
Loading

0 comments on commit c380de5

Please sign in to comment.