frontend: load config.json at runtime from public dir, to be overwritable
This commit is contained in:
parent
85d93fe598
commit
c7202eadd2
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,4 +1,2 @@
|
|||
local
|
||||
config.json
|
||||
src/config.json
|
||||
data
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
# This docker-compose file is intended for development use. You can simply run
|
||||
# `docker-compose up -d` in the repository and it should build and run all
|
||||
# required parts of the application. See README.md for details.
|
||||
#
|
||||
# For a production docker setup, please check the corresponding documentation.
|
||||
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
|
@ -75,7 +81,9 @@ services:
|
|||
- ./frontend/public:/opt/obs/frontend/public
|
||||
- ./frontend/tsconfig.json:/opt/obs/frontend/tsconfig.json
|
||||
- ./frontend/package.json:/opt/obs/frontend/package.json
|
||||
- ./frontend/src/config.dev.json:/opt/obs/frontend/src/config.json
|
||||
|
||||
# Overwrite the default config with the development mode configuration
|
||||
- ./frontend/config.dev.json:/opt/obs/frontend/public/config.json
|
||||
environment:
|
||||
- PORT=3000
|
||||
links:
|
||||
|
|
0
frontend/public/config.json
Executable file
0
frontend/public/config.json
Executable file
|
@ -6,7 +6,7 @@ import {useObservable} from 'rxjs-hooks'
|
|||
import {from} from 'rxjs'
|
||||
import {pluck} from 'rxjs/operators'
|
||||
|
||||
import config from 'config.json'
|
||||
import {useConfig} from 'config'
|
||||
import styles from './App.module.scss'
|
||||
|
||||
import {
|
||||
|
@ -38,6 +38,7 @@ function DropdownItemForLink({navigate, ...props}) {
|
|||
}
|
||||
|
||||
const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||
const config = useConfig()
|
||||
const apiVersion = useObservable(() => from(api.get('/info')).pipe(pluck('version')))
|
||||
|
||||
return (
|
||||
|
@ -148,12 +149,12 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
|||
<Header as="h5">This installation</Header>
|
||||
<List>
|
||||
<List.Item>
|
||||
<a href={config.privacyPolicyUrl} target="_blank" rel="noreferrer">
|
||||
<a href={config?.privacyPolicyUrl} target="_blank" rel="noreferrer">
|
||||
Privacy policy
|
||||
</a>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<a href={config.imprintUrl} target="_blank" rel="noreferrer">
|
||||
<a href={config?.imprintUrl} target="_blank" rel="noreferrer">
|
||||
Imprint
|
||||
</a>
|
||||
</List.Item>
|
||||
|
|
|
@ -2,7 +2,7 @@ import {stringifyParams} from 'query'
|
|||
import globalStore from 'store'
|
||||
import {setAuth, invalidateAccessToken, resetAuth} from 'reducers/auth'
|
||||
import {setLogin} from 'reducers/login'
|
||||
import config from 'config.json'
|
||||
import configPromise from 'config'
|
||||
import {create as createPkce} from 'pkce'
|
||||
import download from 'downloadjs'
|
||||
|
||||
|
@ -39,6 +39,7 @@ class API {
|
|||
* it supports PKCE.
|
||||
*/
|
||||
async getAuthorizationServerMetadata() {
|
||||
const config = await configPromise
|
||||
const url = new URL(config.auth.server)
|
||||
const pathSuffix = url.pathname.replace(/^\/+|\/+$/, '')
|
||||
url.pathname = '/.well-known/oauth-authorization-server' + (pathSuffix ? '/' + pathSuffix : '')
|
||||
|
@ -117,6 +118,7 @@ class API {
|
|||
|
||||
// Try to use the refresh token
|
||||
const {tokenEndpoint} = await this.getAuthorizationServerMetadata()
|
||||
const config = await configPromise
|
||||
const url = new URL(tokenEndpoint)
|
||||
url.searchParams.append('refresh_token', refreshToken)
|
||||
url.searchParams.append('grant_type', 'refresh_token')
|
||||
|
@ -142,6 +144,7 @@ class API {
|
|||
}
|
||||
|
||||
const {tokenEndpoint} = await this.getAuthorizationServerMetadata()
|
||||
const config = await configPromise
|
||||
const url = new URL(tokenEndpoint)
|
||||
url.searchParams.append('code', code)
|
||||
url.searchParams.append('grant_type', 'authorization_code')
|
||||
|
@ -178,6 +181,7 @@ class API {
|
|||
|
||||
async makeLoginUrl() {
|
||||
const {authorizationEndpoint} = await this.getAuthorizationServerMetadata()
|
||||
const config = await configPromise
|
||||
|
||||
const {codeVerifier, codeChallenge} = createPkce()
|
||||
localStorage.setItem('codeVerifier', codeVerifier)
|
||||
|
@ -197,6 +201,7 @@ class API {
|
|||
|
||||
async fetch(url, options = {}) {
|
||||
const accessToken = await this.getValidAccessToken()
|
||||
const config = await configPromise
|
||||
|
||||
const {returnResponse = false, ...fetchOptions} = options
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import {fromLonLat} from 'ol/proj'
|
|||
// Import styles for open layers + addons
|
||||
import 'ol/ol.css'
|
||||
|
||||
import config from 'config.json'
|
||||
import {useConfig} from 'config'
|
||||
|
||||
// Prepare projection
|
||||
proj4.defs(
|
||||
|
@ -86,6 +86,11 @@ export function TileLayer({osm, ...props}) {
|
|||
}
|
||||
|
||||
export function BaseLayer(props) {
|
||||
const config = useConfig()
|
||||
if (!config) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<TileLayer
|
||||
osm={{
|
||||
|
@ -117,26 +122,30 @@ function FitView({extent}) {
|
|||
return null
|
||||
}
|
||||
|
||||
const minZoom = config.mapTileset?.minZoom ?? 0
|
||||
const maxZoom = config.mapTileset?.maxZoom ?? 18
|
||||
const mapHomeZoom = config.mapHome?.zoom ?? 15
|
||||
const mapHomeLongitude = config.mapHome?.longitude ?? 9.1797
|
||||
const mapHomeLatitude = config.mapHome?.latitude ?? 48.7784
|
||||
|
||||
function View({...options}) {
|
||||
const map = React.useContext(MapContext)
|
||||
const config = useConfig()
|
||||
|
||||
const view = React.useMemo(
|
||||
() =>
|
||||
new OlView({
|
||||
() => {
|
||||
if (!config) return null
|
||||
|
||||
const minZoom = config.mapTileset?.minZoom ?? 0
|
||||
const maxZoom = config.mapTileset?.maxZoom ?? 18
|
||||
const mapHomeZoom = config.mapHome?.zoom ?? 15
|
||||
const mapHomeLongitude = config.mapHome?.longitude ?? 9.1797
|
||||
const mapHomeLatitude = config.mapHome?.latitude ?? 48.7784
|
||||
|
||||
return new OlView({
|
||||
minZoom,
|
||||
maxZoom,
|
||||
zoom: Math.max(Math.min(mapHomeZoom, maxZoom), minZoom),
|
||||
center: fromLonLat([mapHomeLongitude, mapHomeLatitude]),
|
||||
...options,
|
||||
}),
|
||||
})
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
[config]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
26
frontend/src/config.ts
Normal file
26
frontend/src/config.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react'
|
||||
|
||||
interface Config {
|
||||
apiUrl: string
|
||||
}
|
||||
|
||||
async function loadConfig(): Promise<Config> {
|
||||
const response = await fetch('./config.json')
|
||||
const config = await response.json()
|
||||
return config
|
||||
}
|
||||
|
||||
let _configPromise: Promise<Config> = loadConfig()
|
||||
let _configCache: null | Config = null
|
||||
|
||||
export function useConfig() {
|
||||
const [config, setConfig] = React.useState<Config>(_configCache)
|
||||
React.useEffect(() => {
|
||||
if (!_configCache) {
|
||||
_configPromise.then(setConfig)
|
||||
}
|
||||
}, [])
|
||||
return config
|
||||
}
|
||||
|
||||
export default _configPromise
|
|
@ -7,7 +7,7 @@ import {setLogin} from 'reducers/login'
|
|||
import {Page, Stats} from 'components'
|
||||
import api from 'api'
|
||||
import {findInput} from 'utils'
|
||||
import config from 'config.json'
|
||||
import {useConfig} from 'config'
|
||||
|
||||
const SettingsPage = connect((state) => ({login: state.login}), {setLogin})(function SettingsPage({login, setLogin}) {
|
||||
const {register, handleSubmit} = useForm()
|
||||
|
@ -102,6 +102,7 @@ function CopyInput({value, ...props}) {
|
|||
const selectField = findInput((ref) => ref?.select())
|
||||
|
||||
function ApiKeyDialog({login}) {
|
||||
const config = useConfig()
|
||||
const [show, setShow] = React.useState(false)
|
||||
const onClick = React.useCallback(
|
||||
(e) => {
|
||||
|
@ -131,7 +132,7 @@ function ApiKeyDialog({login}) {
|
|||
)}
|
||||
</div>
|
||||
<p>The API URL should be set to:</p>
|
||||
<CopyInput label="API URL" value={config.apiUrl} />
|
||||
<CopyInput label="API URL" value={config?.apiUrl ?? '...'} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {Fill, Stroke, Style, Text, Circle} from 'ol/style'
|
|||
|
||||
import {Map} from 'components'
|
||||
import type {TrackData, TrackPoint} from 'types'
|
||||
import config from 'config.json'
|
||||
|
||||
const isValidTrackPoint = (point: TrackPoint): boolean => {
|
||||
const longitude = point.geometry?.coordinates?.[0]
|
||||
|
|
|
@ -6,7 +6,7 @@ import {Link} from 'react-router-dom'
|
|||
import {FileUploadField, Page} from 'components'
|
||||
import type {Track} from 'types'
|
||||
import api from 'api'
|
||||
import config from '../config.json'
|
||||
import configPromise from 'config'
|
||||
|
||||
function isSameFile(a: File, b: File) {
|
||||
return a.name === b.name && a.size === b.size
|
||||
|
@ -56,10 +56,13 @@ export function FileUploadStatus({
|
|||
|
||||
React.useEffect(
|
||||
() => {
|
||||
let xhr
|
||||
|
||||
async function _work() {
|
||||
const formData = new FormData()
|
||||
formData.append('body', file)
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr = new XMLHttpRequest()
|
||||
|
||||
const onProgress = (e) => {
|
||||
const progress = (e.loaded || 0) / (e.total || 1)
|
||||
|
@ -73,17 +76,21 @@ export function FileUploadStatus({
|
|||
xhr.responseType = 'json'
|
||||
xhr.onload = onLoad
|
||||
xhr.upload.onprogress = onProgress
|
||||
|
||||
const config = await configPromise
|
||||
if (slug) {
|
||||
xhr.open('PUT', `${config.apiUrl}/api/tracks/${slug}`)
|
||||
} else {
|
||||
xhr.open('POST', `${config.apiUrl}/api/tracks`)
|
||||
}
|
||||
|
||||
api.getValidAccessToken().then((accessToken) => {
|
||||
const accessToken = await api.getValidAccessToken()
|
||||
|
||||
xhr.setRequestHeader('Authorization', accessToken)
|
||||
xhr.send(formData)
|
||||
})
|
||||
}
|
||||
|
||||
_work()
|
||||
return () => xhr.abort()
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
Loading…
Reference in a new issue