frontend: load config.json at runtime from public dir, to be overwritable

This commit is contained in:
Paul Bienkowski 2021-09-14 22:02:57 +02:00
parent 85d93fe598
commit c7202eadd2
12 changed files with 96 additions and 42 deletions

2
.gitignore vendored
View file

@ -1,4 +1,2 @@
local
config.json
src/config.json
data

View file

@ -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
View file

View 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>

View file

@ -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

View file

@ -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
View 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

View file

@ -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 ?? '...'} />
</>
)
}

View file

@ -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]

View file

@ -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,34 +56,41 @@ export function FileUploadStatus({
React.useEffect(
() => {
const formData = new FormData()
formData.append('body', file)
let xhr
const xhr = new XMLHttpRequest()
async function _work() {
const formData = new FormData()
formData.append('body', file)
const onProgress = (e) => {
const progress = (e.loaded || 0) / (e.total || 1)
setProgress(progress)
}
xhr = new XMLHttpRequest()
const onLoad = (e) => {
onComplete(id, xhr.response)
}
const onProgress = (e) => {
const progress = (e.loaded || 0) / (e.total || 1)
setProgress(progress)
}
xhr.responseType = 'json'
xhr.onload = onLoad
xhr.upload.onprogress = onProgress
if (slug) {
xhr.open('PUT', `${config.apiUrl}/api/tracks/${slug}`)
} else {
xhr.open('POST', `${config.apiUrl}/api/tracks`)
}
const onLoad = (e) => {
onComplete(id, xhr.response)
}
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`)
}
const accessToken = await api.getValidAccessToken()
api.getValidAccessToken().then((accessToken) => {
xhr.setRequestHeader('Authorization', accessToken)
xhr.send(formData)
})
}
_work()
return () => xhr.abort()
},
// eslint-disable-next-line react-hooks/exhaustive-deps