diff --git a/server/resources/configResource.ts b/server/resources/configResource.ts new file mode 100644 index 0000000..17c2928 --- /dev/null +++ b/server/resources/configResource.ts @@ -0,0 +1,10 @@ +import {success} from "../utils/resources"; +import {config} from "../config"; +import {Request, Response} from "express"; + +export function get (req: Request, res: Response): void { + success( + res, + config.client + ); +} diff --git a/server/router.ts b/server/router.ts index 49e25ef..b1f0c4d 100644 --- a/server/router.ts +++ b/server/router.ts @@ -3,6 +3,7 @@ import express from "express" import {app} from "./app" import {config} from "./config" +import * as ConfigResource from "./resources/configResource" import * as VersionResource from "./resources/versionResource" import * as StatisticsResource from "./resources/statisticsResource" import * as FrontendResource from "./resources/frontendResource" @@ -16,6 +17,7 @@ export function init (): void { router.post('/', FrontendResource.render); + router.get('/api/config', ConfigResource.get); router.get('/api/version', VersionResource.get); router.post('/api/node', NodeResource.create); diff --git a/server/types/config.ts b/server/types/config.ts index f369ccb..d550516 100644 --- a/server/types/config.ts +++ b/server/types/config.ts @@ -1,4 +1,5 @@ import {ArrayField, Field, RawJsonField} from "sparkson" +import {ClientConfig} from "./shared"; // TODO: Replace string types by more specific types like URL, Password, etc. @@ -50,73 +51,9 @@ export class ServerConfig { ) {} } -export class CommunityConfig { - constructor( - @Field("name") public name: string, - @Field("domain") public domain: string, - @Field("contactEmail") public contactEmail: string, - @ArrayField("sites", String) public sites: string[], - @ArrayField("domains", String) public domains: string[], - ) {} -} - -export class LegalConfig { - constructor( - @Field("privacyUrl", true) public privacyUrl?: string, - @Field("imprintUrl", true) public imprintUrl?: string, - ) {} -} - -export class ClientMapConfig { - constructor( - @Field("mapUrl") public mapUrl: string, - ) {} -} -export class MonitoringConfig { - constructor( - @Field("enabled") public enabled: boolean, - ) {} -} - -export class Coords { - constructor( - @Field("lat") public lat: number, - @Field("lng") public lng: number, - ) {} -} - -export class CoordsSelectorConfig { - constructor( - @Field("lat") public lat: number, - @Field("lng") public lng: number, - @Field("defaultZoom") public defaultZoom: number, - @RawJsonField("layers") public layers: any, // TODO: Better types! - ) {} -} - -export class OtherCommunityInfoConfig { - constructor( - @Field("showInfo") public showInfo: boolean, - @Field("showBorderForDebugging") public showBorderForDebugging: boolean, - @ArrayField("localCommunityPolygon", Coords) public localCommunityPolygon: Coords[], - ) {} -} - -export class ClientConfig { - constructor( - @Field("community") public community: CommunityConfig, - @Field("legal") public legal: LegalConfig, - @Field("map") public map: ClientMapConfig, - @Field("monitoring") public monitoring: MonitoringConfig, - @Field("coordsSelector") public coordsSelector: CoordsSelectorConfig, - @Field("otherCommunityInfo") public otherCommunityInfo: OtherCommunityInfoConfig, - @Field("rootPath", true, undefined, "/") public rootPath: string, - ) {} -} - export class Config { constructor( @Field("server") public server: ServerConfig, - @Field("client") public client: ClientConfig + @Field("client") public client: ClientConfig, ) {} } diff --git a/server/types/shared.ts b/server/types/shared.ts index 4d143bd..2078f1b 100644 --- a/server/types/shared.ts +++ b/server/types/shared.ts @@ -1,9 +1,27 @@ +import {ArrayField, Field, RawJsonField} from "sparkson"; + // Types shared with the client. export function isObject(arg: unknown): arg is object { return arg !== null && typeof arg === "object"; } +export function isArray(arg: unknown, isT: (arg: unknown) => arg is T): arg is Array { + if (!Array.isArray(arg)) { + return false; + } + for (const element of arg) { + if (!isT(element)) { + return false + } + } + return true; +} + +export function isString(arg: unknown): arg is string { + return typeof arg === "string" +} + export type Version = string; export function isVersion(arg: unknown): arg is Version { @@ -43,3 +61,162 @@ export interface Statistics { export function isStatistics(arg: unknown): arg is Statistics { return isObject(arg) && isNodeStatistics((arg as Statistics).nodes); } + +export class CommunityConfig { + constructor( + @Field("name") public name: string, + @Field("domain") public domain: string, + @Field("contactEmail") public contactEmail: string, + @ArrayField("sites", String) public sites: string[], + @ArrayField("domains", String) public domains: string[], + ) {} +} + +export function isCommunityConfig(arg: unknown): arg is CommunityConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as CommunityConfig; + return ( + typeof cfg.name === "string" && + typeof cfg.domain === "string" && + typeof cfg.contactEmail === "string" && + isArray(cfg.sites, isString) && + isArray(cfg.domains, isString) + ); +} + +export class LegalConfig { + constructor( + @Field("privacyUrl", true) public privacyUrl?: string, + @Field("imprintUrl", true) public imprintUrl?: string, + ) {} +} + +export function isLegalConfig(arg: unknown): arg is LegalConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as LegalConfig; + return ( + (cfg.privacyUrl === undefined || typeof cfg.privacyUrl === "string") && + (cfg.imprintUrl === undefined || typeof cfg.imprintUrl === "string") + ); +} + +export class ClientMapConfig { + constructor( + @Field("mapUrl") public mapUrl: string, + ) {} +} + +export function isClientMapConfig(arg: unknown): arg is ClientMapConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as ClientMapConfig; + return typeof cfg.mapUrl === "string"; +} + +export class MonitoringConfig { + constructor( + @Field("enabled") public enabled: boolean, + ) {} +} + +export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as MonitoringConfig; + return typeof cfg.enabled === "boolean"; +} + +export class Coords { + constructor( + @Field("lat") public lat: number, + @Field("lng") public lng: number, + ) {} +} + +export function isCoords(arg: unknown): arg is Coords { + if (!isObject(arg)) { + return false; + } + const coords = arg as Coords; + return ( + typeof coords.lat === "number" && + typeof coords.lng === "number" + ); +} + +export class CoordsSelectorConfig { + constructor( + @Field("lat") public lat: number, + @Field("lng") public lng: number, + @Field("defaultZoom") public defaultZoom: number, + @RawJsonField("layers") public layers: any, // TODO: Better types! + ) {} +} + +export function isCoordsSelectorConfig(arg: unknown): arg is CoordsSelectorConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as CoordsSelectorConfig; + return ( + typeof cfg.lat === "number" && + typeof cfg.lng === "number" && + typeof cfg.defaultZoom === "number" && + isObject(cfg.layers) // TODO: Better types! + ); +} + +export class OtherCommunityInfoConfig { + constructor( + @Field("showInfo") public showInfo: boolean, + @Field("showBorderForDebugging") public showBorderForDebugging: boolean, + @ArrayField("localCommunityPolygon", Coords) public localCommunityPolygon: Coords[], + ) {} +} + +export function isOtherCommunityInfoConfig(arg: unknown): arg is OtherCommunityInfoConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as OtherCommunityInfoConfig; + return ( + typeof cfg.showInfo === "boolean" && + typeof cfg.showBorderForDebugging === "boolean" && + isArray(cfg.localCommunityPolygon, isCoords) + ); +} + +export class ClientConfig { + constructor( + @Field("community") public community: CommunityConfig, + @Field("legal") public legal: LegalConfig, + @Field("map") public map: ClientMapConfig, + @Field("monitoring") public monitoring: MonitoringConfig, + @Field("coordsSelector") public coordsSelector: CoordsSelectorConfig, + @Field("otherCommunityInfo") public otherCommunityInfo: OtherCommunityInfoConfig, + @Field("rootPath", true, undefined, "/") public rootPath: string, + ) { + } +} + +export function isClientConfig(arg: unknown): arg is ClientConfig { + if (!isObject(arg)) { + return false; + } + const cfg = arg as ClientConfig; + return ( + isCommunityConfig(cfg.community) && + isLegalConfig(cfg.legal) && + isClientMapConfig(cfg.map) && + isMonitoringConfig(cfg.monitoring) && + isCoordsSelectorConfig(cfg.coordsSelector) && + isOtherCommunityInfoConfig(cfg.otherCommunityInfo) && + typeof cfg.rootPath === "string" + ); +}