diff --git a/backend/src/auth.ts b/backend/src/auth.ts index 920ad9a..8af3771 100644 --- a/backend/src/auth.ts +++ b/backend/src/auth.ts @@ -1,21 +1,11 @@ import { Request } from 'express'; +import { IAuthResponse, IAuthResponseOptional } from '../../common' import { generate as shortUuid } from 'short-uuid'; import * as MatchService from './matchService'; import * as Storage from './storage'; const tokens: Map = new Map(); -export interface IAuthResponse { - type: 'GLOBAL' | 'MATCH'; - comment?: string; -} - -export type IAuthResponseOptional = - | IAuthResponse - | { - type: 'UNAUTHORIZED'; - }; - export type ExpressRequest = Request & { user: T; }; @@ -127,3 +117,28 @@ export const isAuthorized = async ( return false; }; + +export const getTokenType = async ( + token: string, +): Promise => { + const t = getGlobalToken(token); + if (t) { + return { + type: 'GLOBAL', + comment: t.comment, + }; + } + + const allMatches = await MatchService.getAll(); + for (const match of allMatches.live.concat(allMatches.notLive)) { + if (match.tmtSecret === token) { + return { + type: 'MATCH' + }; + } + } + + return { + type: 'UNAUTHORIZED', + } +} \ No newline at end of file diff --git a/backend/src/authController.ts b/backend/src/authController.ts new file mode 100644 index 0000000..d8c3720 --- /dev/null +++ b/backend/src/authController.ts @@ -0,0 +1,21 @@ +import { Controller, Get, NoSecurity, Route, Request, Security, Post } from '@tsoa/runtime'; +import { IAuthResponseOptional } from '../../common'; +import * as Auth from './auth'; + +@Route('/api/auth') +export class AuthController extends Controller { + /** + * Get the authentication type for a token. + */ + @Get() + @NoSecurity() + async getAuthType( + @Request() req: Auth.ExpressRequest, + ): Promise { + const token = req.get('authorization'); + if (!token) { + return { type: 'UNAUTHORIZED' }; + } + return Auth.getTokenType(token); + } +} \ No newline at end of file diff --git a/backend/src/config.ts b/backend/src/config.ts new file mode 100644 index 0000000..0025433 --- /dev/null +++ b/backend/src/config.ts @@ -0,0 +1,68 @@ +import { IConfig, IConfigUpdateDto } from "../../common"; +import { checkAndNormalizeLogAddress } from "./match"; +import * as Storage from './storage'; + +const FILE_NAME = 'config.json'; + +// These are not defaults, they're temporary until setup() is called +let config: IConfig = { + tmtLogAddress: null, + allowUnregisteredMatchCreation: false +}; + +const defaults = (): IConfig => { + let tmtLogAdress = null; + if (!process.env['TMT_LOG_ADDRESS']) { + console.warn('Environment variable TMT_LOG_ADDRESS is not set'); + console.warn('Every match must be init with tmtLogAddress'); + } else { + tmtLogAdress = checkAndNormalizeLogAddress(process.env['TMT_LOG_ADDRESS']); + if (!tmtLogAdress) { + throw 'invalid environment variable: TMT_LOG_ADDRESS'; + } + } + + return { + tmtLogAddress: tmtLogAdress, + allowUnregisteredMatchCreation: false + } +}; + +export const get = async (): Promise => { + return config; +} + +export const set = async (data: IConfigUpdateDto) => { + let tmtLogAdress = null; + if ('tmtLogAddress' in data && data.tmtLogAddress) { + tmtLogAdress = checkAndNormalizeLogAddress(data.tmtLogAddress); + if (!tmtLogAdress) { + throw 'invalid tmtLogAddress'; + } + } else if ('tmtLogAddress' in data) { + tmtLogAdress = null; + } else { + tmtLogAdress = config.tmtLogAddress; + } + + let allowUnregisteredMatchCreation = data.allowUnregisteredMatchCreation ?? config.allowUnregisteredMatchCreation; + + config = { + tmtLogAddress: tmtLogAdress, + allowUnregisteredMatchCreation: allowUnregisteredMatchCreation + }; + + await write(); + + return config; +}; + +const write = async () => { + await Storage.write(FILE_NAME, config); +}; + +export const setup = async () => { + // Only get the defaults if the config does not already exist + const data = await Storage.read(FILE_NAME, defaults()); + set(data) +}; \ No newline at end of file diff --git a/backend/src/configController.ts b/backend/src/configController.ts index cb42f92..18102a0 100644 --- a/backend/src/configController.ts +++ b/backend/src/configController.ts @@ -1,18 +1,26 @@ -import { Controller, Get, NoSecurity, Route, Security } from '@tsoa/runtime'; -import { TMT_LOG_ADDRESS } from '.'; -import { IConfig } from '../../common'; +import { Body, Controller, Get, NoSecurity, Patch, Route, Security } from '@tsoa/runtime'; +import { IConfig, IConfigUpdateDto } from '../../common'; +import * as Config from './config'; @Route('/api/config') @Security('bearer_token') export class ConfigController extends Controller { /** - * Get some internal config variables. Currently only the set TMT_LOG_ADDRESS. + * Get some internal config variables. */ @Get() @NoSecurity() async getConfig(): Promise { - return { - tmtLogAddress: TMT_LOG_ADDRESS, - }; + return Config.get(); + } + + /** + * Update the configuration. + */ + @Patch() + async updateConfig( + @Body() requestBody: IConfigUpdateDto, + ): Promise { + return await Config.set(requestBody); } } diff --git a/backend/src/debugController.ts b/backend/src/debugController.ts index 11e9022..fad5575 100644 --- a/backend/src/debugController.ts +++ b/backend/src/debugController.ts @@ -1,8 +1,9 @@ import { Controller, Get, Route, Security } from '@tsoa/runtime'; -import { COMMIT_SHA, IMAGE_BUILD_TIMESTAMP, PORT, TMT_LOG_ADDRESS, VERSION } from '.'; +import { COMMIT_SHA, IMAGE_BUILD_TIMESTAMP, PORT, VERSION } from '.'; import { IDebugResponse } from '../../common'; import { Settings } from './settings'; import { STORAGE_FOLDER } from './storage'; +import * as Config from './config'; import * as WebSocket from './webSocket'; @Route('/api/debug') @@ -24,7 +25,7 @@ export class DebugController extends Controller { tmtImageBuildTimestamp: IMAGE_BUILD_TIMESTAMP, tmtStorageFolder: STORAGE_FOLDER, tmtPort: PORT, - tmtLogAddress: TMT_LOG_ADDRESS, + tmtLogAddress: (await Config.get()).tmtLogAddress, tmtSayPrefix: Settings.SAY_PREFIX, webSockets: WebSocket.getClients(), }; diff --git a/backend/src/index.ts b/backend/src/index.ts index 51e9642..de38371 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,10 +4,10 @@ import { existsSync, readFileSync } from 'fs'; import http from 'http'; import path from 'path'; import * as Auth from './auth'; +import * as Config from './config'; import * as Election from './election'; import * as ManagedGameServers from './managedGameServers'; import * as Match from './match'; -import { checkAndNormalizeLogAddress } from './match'; import * as MatchMap from './matchMap'; import * as MatchService from './matchService'; import * as Presets from './presets'; @@ -15,19 +15,6 @@ import { RegisterRoutes } from './routes'; import * as Storage from './storage'; import * as WebSocket from './webSocket'; -export const TMT_LOG_ADDRESS: string | null = (() => { - if (!process.env['TMT_LOG_ADDRESS']) { - console.warn('Environment variable TMT_LOG_ADDRESS is not set'); - console.warn('Every match must be init with tmtLogAddress'); - return null; - } - const addr = checkAndNormalizeLogAddress(process.env['TMT_LOG_ADDRESS']); - if (!addr) { - throw 'invalid environment variable: TMT_LOG_ADDRESS'; - } - return addr; -})(); - const APP_DIR = (() => { if (__dirname.endsWith(path.join('/backend/dist/backend/src'))) { // in production: __dirname = /app/backend/dist/backend/src @@ -128,6 +115,7 @@ const main = async () => { await Auth.setup(); await WebSocket.setup(httpServer); await ManagedGameServers.setup(); + await Config.setup(); await Presets.setup(); Match.registerCommandHandlers(); MatchMap.registerCommandHandlers(); diff --git a/backend/src/loginController.ts b/backend/src/loginController.ts deleted file mode 100644 index c26d4a5..0000000 --- a/backend/src/loginController.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Controller, Post, Route, Security } from '@tsoa/runtime'; - -@Route('/api/login') -@Security('bearer_token') -export class LoginController extends Controller { - /** - * Dummy endpoint to check if given token is valid without executing anything. - */ - @Post() - async login() {} -} diff --git a/backend/src/match.ts b/backend/src/match.ts index e9cdaf3..154ec0e 100644 --- a/backend/src/match.ts +++ b/backend/src/match.ts @@ -1,6 +1,6 @@ import { ValidateError } from '@tsoa/runtime'; import { generate as shortUuid } from 'short-uuid'; -import { COMMIT_SHA, IMAGE_BUILD_TIMESTAMP, TMT_LOG_ADDRESS, VERSION } from '.'; +import { COMMIT_SHA, IMAGE_BUILD_TIMESTAMP, VERSION } from '.'; import { IMatch, IMatchCreateDto, @@ -17,6 +17,7 @@ import { } from '../../common'; import { addChangeListener } from './changeListener'; import * as commands from './commands'; +import * as Config from './config'; import * as Election from './election'; import * as Events from './events'; import * as GameServer from './gameServer'; @@ -66,7 +67,7 @@ export const createFromData = async (data: IMatch, logMessage?: string) => { throw 'invalid tmtLogAddress'; } match.data.tmtLogAddress = la; - } else if (!TMT_LOG_ADDRESS) { + } else if ((await Config.get()).tmtLogAddress) { throw 'tmtLogAddress must be set'; } @@ -254,7 +255,7 @@ export const checkAndNormalizeLogAddress = (url: string): string | null => { }; const ensureLogAddressIsRegistered = async (match: Match) => { - const logAddress = `${match.data.tmtLogAddress || TMT_LOG_ADDRESS}/api/matches/${ + const logAddress = `${match.data.tmtLogAddress || (await Config.get()).tmtLogAddress}/api/matches/${ match.data.id }/server/log/${match.data.logSecret}`; diff --git a/backend/src/matchesController.ts b/backend/src/matchesController.ts index f05945f..256ce78 100644 --- a/backend/src/matchesController.ts +++ b/backend/src/matchesController.ts @@ -20,7 +20,9 @@ import { IMatchResponse, IMatchUpdateDto, } from '../../common'; -import { ExpressRequest, IAuthResponse, IAuthResponseOptional } from './auth'; +import { ExpressRequest, isAuthorized } from './auth'; +import { IAuthResponse, IAuthResponseOptional } from '../../common'; +import * as Config from './config'; import * as Events from './events'; import * as Match from './match'; import * as MatchMap from './matchMap'; @@ -58,7 +60,13 @@ export class MatchesController extends Controller { async createMatch( @Body() requestBody: IMatchCreateDto, @Request() req: ExpressRequest - ): Promise { + ): Promise { + if ((await Config.get()).allowUnregisteredMatchCreation === false) { + if (!isAuthorized(req.get('authorization'))) { + this.setStatus(401); + return; + } + } if (requestBody.gameServer === null) { checkRconCommands(requestBody.rconCommands, req.user.type === 'GLOBAL'); } diff --git a/backend/src/presetsController.ts b/backend/src/presetsController.ts index 15040b2..b0d1e95 100644 --- a/backend/src/presetsController.ts +++ b/backend/src/presetsController.ts @@ -11,7 +11,8 @@ import { SuccessResponse, } from '@tsoa/runtime'; import { IPreset, IPresetCreateDto } from '../../common'; -import { ExpressRequest, IAuthResponse } from './auth'; +import { ExpressRequest } from './auth'; +import { IAuthResponse } from '../../common'; import * as Presets from './presets'; @Route('/api/presets') diff --git a/backend/src/routes.ts b/backend/src/routes.ts index f3d0ff4..01d0718 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -3,8 +3,6 @@ // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { TsoaRoute, fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -import { LoginController } from './loginController'; -// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { MatchesController } from './matchesController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { GameServersController } from './gameServersController'; @@ -14,6 +12,8 @@ import { DebugController } from './debugController'; import { ConfigController } from './configController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa import { PresetsController } from './presetsController'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { AuthController } from './authController'; import { expressAuthentication } from './auth'; // @ts-ignore - no great way to install types from subpackage import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express'; @@ -1223,6 +1223,19 @@ const models: TsoaRoute.Models = { subSchemas: [{ dataType: 'string' }, { dataType: 'enum', enums: [null] }], required: true, }, + allowUnregisteredMatchCreation: { dataType: 'boolean', required: true }, + }, + additionalProperties: false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + IConfigUpdateDto: { + dataType: 'refObject', + properties: { + tmtLogAddress: { + dataType: 'union', + subSchemas: [{ dataType: 'string' }, { dataType: 'enum', enums: [null] }], + }, + allowUnregisteredMatchCreation: { dataType: 'boolean' }, }, additionalProperties: false, }, @@ -1248,6 +1261,39 @@ const models: TsoaRoute.Models = { additionalProperties: false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + IAuthResponse: { + dataType: 'refObject', + properties: { + type: { + dataType: 'union', + subSchemas: [ + { dataType: 'enum', enums: ['GLOBAL'] }, + { dataType: 'enum', enums: ['MATCH'] }, + ], + required: true, + }, + comment: { dataType: 'string' }, + }, + additionalProperties: false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + IAuthResponseOptional: { + dataType: 'refAlias', + type: { + dataType: 'union', + subSchemas: [ + { ref: 'IAuthResponse' }, + { + dataType: 'nestedObjectLiteral', + nestedProperties: { + type: { dataType: 'enum', enums: ['UNAUTHORIZED'], required: true }, + }, + }, + ], + validators: {}, + }, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa }; const templateService = new ExpressTemplateService(models, { noImplicitAdditionalProperties: 'silently-remove-extras', @@ -1262,37 +1308,6 @@ export function RegisterRoutes(app: Router) { // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa // ########################################################################################################### - app.post( - '/api/login', - authenticateMiddleware([{ bearer_token: [] }]), - ...fetchMiddlewares(LoginController), - ...fetchMiddlewares(LoginController.prototype.login), - - async function LoginController_login(request: ExRequest, response: ExResponse, next: any) { - const args: Record = {}; - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - let validatedArgs: any[] = []; - try { - validatedArgs = templateService.getValidatedArgs({ args, request, response }); - - const controller = new LoginController(); - - await templateService.apiHandler({ - methodName: 'login', - controller, - response, - next, - validatedArgs, - successStatus: undefined, - }); - } catch (err) { - return next(err); - } - } - ); - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.post( '/api/matches', authenticateMiddleware([{ bearer_token_optional: [] }]), @@ -2131,6 +2146,48 @@ export function RegisterRoutes(app: Router) { } ); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.patch( + '/api/config', + authenticateMiddleware([{ bearer_token: [] }]), + ...fetchMiddlewares(ConfigController), + ...fetchMiddlewares(ConfigController.prototype.updateConfig), + + async function ConfigController_updateConfig( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + requestBody: { + in: 'body', + name: 'requestBody', + required: true, + ref: 'IConfigUpdateDto', + }, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args, request, response }); + + const controller = new ConfigController(); + + await templateService.apiHandler({ + methodName: 'updateConfig', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + } + ); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get( '/api/presets', authenticateMiddleware([{ bearer_token_optional: [] }]), @@ -2287,6 +2344,42 @@ export function RegisterRoutes(app: Router) { } ); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.get( + '/api/auth', + ...fetchMiddlewares(AuthController), + ...fetchMiddlewares(AuthController.prototype.getAuthType), + + async function AuthController_getAuthType( + request: ExRequest, + response: ExResponse, + next: any + ) { + const args: Record = { + req: { in: 'request', name: 'req', required: true, dataType: 'object' }, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args, request, response }); + + const controller = new AuthController(); + + await templateService.apiHandler({ + methodName: 'getAuthType', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + } + ); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git a/backend/swagger.json b/backend/swagger.json index dbb3d35..862737d 100644 --- a/backend/swagger.json +++ b/backend/swagger.json @@ -2071,9 +2071,25 @@ "tmtLogAddress": { "type": "string", "nullable": true + }, + "allowUnregisteredMatchCreation": { + "type": "boolean" + } + }, + "required": ["tmtLogAddress", "allowUnregisteredMatchCreation"], + "type": "object", + "additionalProperties": false + }, + "IConfigUpdateDto": { + "properties": { + "tmtLogAddress": { + "type": "string", + "nullable": true + }, + "allowUnregisteredMatchCreation": { + "type": "boolean" } }, - "required": ["tmtLogAddress"], "type": "object", "additionalProperties": false }, @@ -2111,6 +2127,38 @@ "required": ["name", "data"], "type": "object", "additionalProperties": false + }, + "IAuthResponse": { + "properties": { + "type": { + "type": "string", + "enum": ["GLOBAL", "MATCH"] + }, + "comment": { + "type": "string" + } + }, + "required": ["type"], + "type": "object", + "additionalProperties": false + }, + "IAuthResponseOptional": { + "anyOf": [ + { + "$ref": "#/components/schemas/IAuthResponse" + }, + { + "properties": { + "type": { + "type": "string", + "enum": ["UNAUTHORIZED"], + "nullable": false + } + }, + "required": ["type"], + "type": "object" + } + ] } }, "securitySchemes": { @@ -2135,23 +2183,6 @@ } }, "paths": { - "/api/login": { - "post": { - "operationId": "Login", - "responses": { - "204": { - "description": "No content" - } - }, - "description": "Dummy endpoint to check if given token is valid without executing anything.", - "security": [ - { - "bearer_token": [] - } - ], - "parameters": [] - } - }, "/api/matches": { "post": { "operationId": "CreateMatch", @@ -2161,7 +2192,12 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IMatch" + "anyOf": [ + { + "$ref": "#/components/schemas/IMatch" + }, + {} + ] } } } @@ -2961,9 +2997,41 @@ } } }, - "description": "Get some internal config variables. Currently only the set TMT_LOG_ADDRESS.", + "description": "Get some internal config variables.", "security": [], "parameters": [] + }, + "patch": { + "operationId": "UpdateConfig", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IConfig" + } + } + } + } + }, + "description": "Update the configuration.", + "security": [ + { + "bearer_token": [] + } + ], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IConfigUpdateDto" + } + } + } + } } }, "/api/presets": { @@ -3075,6 +3143,26 @@ } ] } + }, + "/api/auth": { + "get": { + "operationId": "GetAuthType", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IAuthResponseOptional" + } + } + } + } + }, + "description": "Get the authentication type for a token.", + "security": [], + "parameters": [] + } } }, "servers": [ diff --git a/common/types/auth.ts b/common/types/auth.ts new file mode 100644 index 0000000..a9542e7 --- /dev/null +++ b/common/types/auth.ts @@ -0,0 +1,10 @@ +export interface IAuthResponse { + type: 'GLOBAL' | 'MATCH'; + comment?: string; +} + +export type IAuthResponseOptional = + | IAuthResponse + | { + type: 'UNAUTHORIZED'; + }; \ No newline at end of file diff --git a/common/types/config.ts b/common/types/config.ts index 27ff1b1..1f03cf1 100644 --- a/common/types/config.ts +++ b/common/types/config.ts @@ -1,3 +1,9 @@ export interface IConfig { tmtLogAddress: string | null; + allowUnregisteredMatchCreation: boolean; +} + +export interface IConfigUpdateDto { + tmtLogAddress?: string | null; + allowUnregisteredMatchCreation?: boolean; } diff --git a/common/types/index.ts b/common/types/index.ts index ee427de..1d24fed 100644 --- a/common/types/index.ts +++ b/common/types/index.ts @@ -1,3 +1,4 @@ +export * from './auth'; export * from './config'; export * from './debug'; export * from './election'; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bc69771..58af945 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,12 @@ import { A, AnchorProps, RouteSectionProps } from '@solidjs/router'; -import { Component, Match, Switch, onMount } from 'solid-js'; +import { Component, Match, Show, Switch, createEffect, createSignal, onMount } from 'solid-js'; import { SvgComputer, SvgDarkMode, SvgLightMode } from './assets/Icons'; import logo from './assets/logo.svg'; -import { isLoggedIn } from './utils/fetcher'; +import { loginType, createFetcher } from './utils/fetcher'; import { t } from './utils/locale'; import { currentTheme, cycleDarkMode, updateDarkClasses } from './utils/theme'; +import { IConfig } from '../../common'; const NavLink = (props: AnchorProps) => { return ( @@ -16,6 +17,15 @@ const NavLink = (props: AnchorProps) => { }; const NavBar: Component = () => { + const fetcher = createFetcher(); + const [config, setConfig] = createSignal(); + + createEffect(() => { + fetcher('GET', `/api/config`).then((c) => { + setConfig(c); + }); + }); + return (