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
|
local
|
||||||
config.json
|
|
||||||
src/config.json
|
|
||||||
data
|
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'
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
@ -75,7 +81,9 @@ services:
|
||||||
- ./frontend/public:/opt/obs/frontend/public
|
- ./frontend/public:/opt/obs/frontend/public
|
||||||
- ./frontend/tsconfig.json:/opt/obs/frontend/tsconfig.json
|
- ./frontend/tsconfig.json:/opt/obs/frontend/tsconfig.json
|
||||||
- ./frontend/package.json:/opt/obs/frontend/package.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:
|
environment:
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
links:
|
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 {from} from 'rxjs'
|
||||||
import {pluck} from 'rxjs/operators'
|
import {pluck} from 'rxjs/operators'
|
||||||
|
|
||||||
import config from 'config.json'
|
import {useConfig} from 'config'
|
||||||
import styles from './App.module.scss'
|
import styles from './App.module.scss'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -38,6 +38,7 @@ function DropdownItemForLink({navigate, ...props}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = connect((state) => ({login: state.login}))(function App({login}) {
|
const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||||
|
const config = useConfig()
|
||||||
const apiVersion = useObservable(() => from(api.get('/info')).pipe(pluck('version')))
|
const apiVersion = useObservable(() => from(api.get('/info')).pipe(pluck('version')))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -148,12 +149,12 @@ const App = connect((state) => ({login: state.login}))(function App({login}) {
|
||||||
<Header as="h5">This installation</Header>
|
<Header as="h5">This installation</Header>
|
||||||
<List>
|
<List>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<a href={config.privacyPolicyUrl} target="_blank" rel="noreferrer">
|
<a href={config?.privacyPolicyUrl} target="_blank" rel="noreferrer">
|
||||||
Privacy policy
|
Privacy policy
|
||||||
</a>
|
</a>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<a href={config.imprintUrl} target="_blank" rel="noreferrer">
|
<a href={config?.imprintUrl} target="_blank" rel="noreferrer">
|
||||||
Imprint
|
Imprint
|
||||||
</a>
|
</a>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {stringifyParams} from 'query'
|
||||||
import globalStore from 'store'
|
import globalStore from 'store'
|
||||||
import {setAuth, invalidateAccessToken, resetAuth} from 'reducers/auth'
|
import {setAuth, invalidateAccessToken, resetAuth} from 'reducers/auth'
|
||||||
import {setLogin} from 'reducers/login'
|
import {setLogin} from 'reducers/login'
|
||||||
import config from 'config.json'
|
import configPromise from 'config'
|
||||||
import {create as createPkce} from 'pkce'
|
import {create as createPkce} from 'pkce'
|
||||||
import download from 'downloadjs'
|
import download from 'downloadjs'
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ class API {
|
||||||
* it supports PKCE.
|
* it supports PKCE.
|
||||||
*/
|
*/
|
||||||
async getAuthorizationServerMetadata() {
|
async getAuthorizationServerMetadata() {
|
||||||
|
const config = await configPromise
|
||||||
const url = new URL(config.auth.server)
|
const url = new URL(config.auth.server)
|
||||||
const pathSuffix = url.pathname.replace(/^\/+|\/+$/, '')
|
const pathSuffix = url.pathname.replace(/^\/+|\/+$/, '')
|
||||||
url.pathname = '/.well-known/oauth-authorization-server' + (pathSuffix ? '/' + pathSuffix : '')
|
url.pathname = '/.well-known/oauth-authorization-server' + (pathSuffix ? '/' + pathSuffix : '')
|
||||||
|
@ -117,6 +118,7 @@ class API {
|
||||||
|
|
||||||
// Try to use the refresh token
|
// Try to use the refresh token
|
||||||
const {tokenEndpoint} = await this.getAuthorizationServerMetadata()
|
const {tokenEndpoint} = await this.getAuthorizationServerMetadata()
|
||||||
|
const config = await configPromise
|
||||||
const url = new URL(tokenEndpoint)
|
const url = new URL(tokenEndpoint)
|
||||||
url.searchParams.append('refresh_token', refreshToken)
|
url.searchParams.append('refresh_token', refreshToken)
|
||||||
url.searchParams.append('grant_type', 'refresh_token')
|
url.searchParams.append('grant_type', 'refresh_token')
|
||||||
|
@ -142,6 +144,7 @@ class API {
|
||||||
}
|
}
|
||||||
|
|
||||||
const {tokenEndpoint} = await this.getAuthorizationServerMetadata()
|
const {tokenEndpoint} = await this.getAuthorizationServerMetadata()
|
||||||
|
const config = await configPromise
|
||||||
const url = new URL(tokenEndpoint)
|
const url = new URL(tokenEndpoint)
|
||||||
url.searchParams.append('code', code)
|
url.searchParams.append('code', code)
|
||||||
url.searchParams.append('grant_type', 'authorization_code')
|
url.searchParams.append('grant_type', 'authorization_code')
|
||||||
|
@ -178,6 +181,7 @@ class API {
|
||||||
|
|
||||||
async makeLoginUrl() {
|
async makeLoginUrl() {
|
||||||
const {authorizationEndpoint} = await this.getAuthorizationServerMetadata()
|
const {authorizationEndpoint} = await this.getAuthorizationServerMetadata()
|
||||||
|
const config = await configPromise
|
||||||
|
|
||||||
const {codeVerifier, codeChallenge} = createPkce()
|
const {codeVerifier, codeChallenge} = createPkce()
|
||||||
localStorage.setItem('codeVerifier', codeVerifier)
|
localStorage.setItem('codeVerifier', codeVerifier)
|
||||||
|
@ -197,6 +201,7 @@ class API {
|
||||||
|
|
||||||
async fetch(url, options = {}) {
|
async fetch(url, options = {}) {
|
||||||
const accessToken = await this.getValidAccessToken()
|
const accessToken = await this.getValidAccessToken()
|
||||||
|
const config = await configPromise
|
||||||
|
|
||||||
const {returnResponse = false, ...fetchOptions} = options
|
const {returnResponse = false, ...fetchOptions} = options
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {fromLonLat} from 'ol/proj'
|
||||||
// Import styles for open layers + addons
|
// Import styles for open layers + addons
|
||||||
import 'ol/ol.css'
|
import 'ol/ol.css'
|
||||||
|
|
||||||
import config from 'config.json'
|
import {useConfig} from 'config'
|
||||||
|
|
||||||
// Prepare projection
|
// Prepare projection
|
||||||
proj4.defs(
|
proj4.defs(
|
||||||
|
@ -86,6 +86,11 @@ export function TileLayer({osm, ...props}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BaseLayer(props) {
|
export function BaseLayer(props) {
|
||||||
|
const config = useConfig()
|
||||||
|
if (!config) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TileLayer
|
<TileLayer
|
||||||
osm={{
|
osm={{
|
||||||
|
@ -117,26 +122,30 @@ function FitView({extent}) {
|
||||||
return null
|
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}) {
|
function View({...options}) {
|
||||||
const map = React.useContext(MapContext)
|
const map = React.useContext(MapContext)
|
||||||
|
const config = useConfig()
|
||||||
|
|
||||||
const view = React.useMemo(
|
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,
|
minZoom,
|
||||||
maxZoom,
|
maxZoom,
|
||||||
zoom: Math.max(Math.min(mapHomeZoom, maxZoom), minZoom),
|
zoom: Math.max(Math.min(mapHomeZoom, maxZoom), minZoom),
|
||||||
center: fromLonLat([mapHomeLongitude, mapHomeLatitude]),
|
center: fromLonLat([mapHomeLongitude, mapHomeLatitude]),
|
||||||
...options,
|
...options,
|
||||||
}),
|
})
|
||||||
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[]
|
[config]
|
||||||
)
|
)
|
||||||
|
|
||||||
React.useEffect(() => {
|
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 {Page, Stats} from 'components'
|
||||||
import api from 'api'
|
import api from 'api'
|
||||||
import {findInput} from 'utils'
|
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 SettingsPage = connect((state) => ({login: state.login}), {setLogin})(function SettingsPage({login, setLogin}) {
|
||||||
const {register, handleSubmit} = useForm()
|
const {register, handleSubmit} = useForm()
|
||||||
|
@ -102,6 +102,7 @@ function CopyInput({value, ...props}) {
|
||||||
const selectField = findInput((ref) => ref?.select())
|
const selectField = findInput((ref) => ref?.select())
|
||||||
|
|
||||||
function ApiKeyDialog({login}) {
|
function ApiKeyDialog({login}) {
|
||||||
|
const config = useConfig()
|
||||||
const [show, setShow] = React.useState(false)
|
const [show, setShow] = React.useState(false)
|
||||||
const onClick = React.useCallback(
|
const onClick = React.useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
|
@ -131,7 +132,7 @@ function ApiKeyDialog({login}) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p>The API URL should be set to:</p>
|
<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 {Map} from 'components'
|
||||||
import type {TrackData, TrackPoint} from 'types'
|
import type {TrackData, TrackPoint} from 'types'
|
||||||
import config from 'config.json'
|
|
||||||
|
|
||||||
const isValidTrackPoint = (point: TrackPoint): boolean => {
|
const isValidTrackPoint = (point: TrackPoint): boolean => {
|
||||||
const longitude = point.geometry?.coordinates?.[0]
|
const longitude = point.geometry?.coordinates?.[0]
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {Link} from 'react-router-dom'
|
||||||
import {FileUploadField, Page} from 'components'
|
import {FileUploadField, Page} from 'components'
|
||||||
import type {Track} from 'types'
|
import type {Track} from 'types'
|
||||||
import api from 'api'
|
import api from 'api'
|
||||||
import config from '../config.json'
|
import configPromise from 'config'
|
||||||
|
|
||||||
function isSameFile(a: File, b: File) {
|
function isSameFile(a: File, b: File) {
|
||||||
return a.name === b.name && a.size === b.size
|
return a.name === b.name && a.size === b.size
|
||||||
|
@ -56,10 +56,13 @@ export function FileUploadStatus({
|
||||||
|
|
||||||
React.useEffect(
|
React.useEffect(
|
||||||
() => {
|
() => {
|
||||||
|
let xhr
|
||||||
|
|
||||||
|
async function _work() {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('body', file)
|
formData.append('body', file)
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest()
|
xhr = new XMLHttpRequest()
|
||||||
|
|
||||||
const onProgress = (e) => {
|
const onProgress = (e) => {
|
||||||
const progress = (e.loaded || 0) / (e.total || 1)
|
const progress = (e.loaded || 0) / (e.total || 1)
|
||||||
|
@ -73,17 +76,21 @@ export function FileUploadStatus({
|
||||||
xhr.responseType = 'json'
|
xhr.responseType = 'json'
|
||||||
xhr.onload = onLoad
|
xhr.onload = onLoad
|
||||||
xhr.upload.onprogress = onProgress
|
xhr.upload.onprogress = onProgress
|
||||||
|
|
||||||
|
const config = await configPromise
|
||||||
if (slug) {
|
if (slug) {
|
||||||
xhr.open('PUT', `${config.apiUrl}/api/tracks/${slug}`)
|
xhr.open('PUT', `${config.apiUrl}/api/tracks/${slug}`)
|
||||||
} else {
|
} else {
|
||||||
xhr.open('POST', `${config.apiUrl}/api/tracks`)
|
xhr.open('POST', `${config.apiUrl}/api/tracks`)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.getValidAccessToken().then((accessToken) => {
|
const accessToken = await api.getValidAccessToken()
|
||||||
|
|
||||||
xhr.setRequestHeader('Authorization', accessToken)
|
xhr.setRequestHeader('Authorization', accessToken)
|
||||||
xhr.send(formData)
|
xhr.send(formData)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
_work()
|
||||||
return () => xhr.abort()
|
return () => xhr.abort()
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
Loading…
Reference in a new issue