diff --git a/package-lock.json b/package-lock.json index cfca00d..aed71dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,27 @@ "@types/node": "*" } }, + "@types/command-line-args": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.0.0.tgz", + "integrity": "sha512-4eOPXyn5DmP64MCMF8ePDvdlvlzt2a+F8ZaVjqmh2yFCpGjc1kI3kGnCFYX9SCsGTjQcWIyVZ86IHCEyjy/MNg==", + "dev": true + }, + "@types/command-line-usage": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.1.tgz", + "integrity": "sha512-/xUgezxxYePeXhg5S04hUjxG9JZi+rJTs1+4NwpYPfSaS7BeDa6tVJkH6lN9Cb6rl8d24Fi2uX0s0Ngg2JT6gg==", + "dev": true + }, + "@types/compression": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.0.tgz", + "integrity": "sha512-3LzWUM+3k3XdWOUk/RO+uSjv7YWOatYq2QADJntK1pjkk4DfVP0KrIEPDnXRJxAAGKe0VpIPRmlINLDuCedZWw==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/connect": { "version": "3.4.33", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", @@ -62,6 +83,12 @@ "@types/node": "*" } }, + "@types/deep-extend": { + "version": "0.4.31", + "resolved": "https://registry.npmjs.org/@types/deep-extend/-/deep-extend-0.4.31.tgz", + "integrity": "sha512-0stqrMZB7vxsRTe//XEpr6A9+fThL2y/g6qzDG5ZgMJwuOceqOBDfh+g99tuN/XS58V52aPNTfWVS49Xvh842w==", + "dev": true + }, "@types/express": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.4.tgz", @@ -93,6 +120,12 @@ "@types/node": "*" } }, + "@types/lodash": { + "version": "4.14.149", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", + "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", + "dev": true + }, "@types/mime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", @@ -111,6 +144,15 @@ "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==", "dev": true }, + "@types/nodemailer": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.0.tgz", + "integrity": "sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/q": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", @@ -7786,6 +7828,11 @@ "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==" }, + "reflect-metadata": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz", + "integrity": "sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==" + }, "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", @@ -8370,6 +8417,22 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "sparkson": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sparkson/-/sparkson-1.3.3.tgz", + "integrity": "sha512-GPtKnPTPMiObe+/iTwjV640ropNCZi7mx7s9mwNa0KBoRWKOrNlmIepeiAd+75FONB7z+nOGGJsVAW21bXGAhg==", + "requires": { + "lodash": "4.17.4", + "reflect-metadata": "0.1.12" + }, + "dependencies": { + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + } + } + }, "spdx-correct": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", diff --git a/package.json b/package.json index 9a16fe1..1ab4fd0 100644 --- a/package.json +++ b/package.json @@ -46,15 +46,20 @@ "request": "^2.88.0", "scribe-js": "^2.0.4", "serve-static": "^1.14.1", + "sparkson": "^1.3.3", "sqlite": "^3.0.3", "sqlite3": "^4.1.1" }, "devDependencies": { + "@types/command-line-args": "^5.0.0", + "@types/command-line-usage": "^5.0.1", "@types/compression": "^1.7.0", + "@types/deep-extend": "^0.4.31", "@types/express": "^4.17.4", "@types/graceful-fs": "^4.1.3", "@types/lodash": "^4.14.149", "@types/node": "^13.11.0", + "@types/nodemailer": "^6.4.0", "bower": "^1.8.8", "escape-string-regexp": "^2.0.0", "grunt": "^1.0.4", diff --git a/server/config.js b/server/config.js deleted file mode 100644 index c605508..0000000 --- a/server/config.js +++ /dev/null @@ -1,158 +0,0 @@ -'use strict'; - -module.exports = (() => { - const commandLineArgs = require('command-line-args'); - const commandLineUsage = require('command-line-usage'); - - const commandLineDefs = [ - { name: 'help', alias: 'h', type: Boolean, description: 'Show this help' }, - { name: 'config', alias: 'c', type: String, description: 'Location of config.json' }, - { name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version' } - ]; - - let commandLineOptions; - try { - commandLineOptions = commandLineArgs(commandLineDefs); - } catch (e) { - console.error(e.message); - console.error('Try \'--help\' for more information.'); - process.exit(1); - } - - const fs = require('graceful-fs'); - - const packageJsonFile = __dirname + '/../package.json'; - let version = 'unknown'; - if (fs.existsSync(packageJsonFile)) { - version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version; - } - - function usage () { - console.log(commandLineUsage([ - { - header: 'ffffng - ' + version + ' - Freifunk node management form', - optionList: commandLineDefs - } - ])); - } - - if (commandLineOptions.help) { - usage(); - process.exit(0); - } - - if (commandLineOptions.version) { - console.log('ffffng - ' + version); - process.exit(0); - } - - if (!commandLineOptions.config) { - usage(); - process.exit(1); - } - - const deepExtend = require('deep-extend'); - - const defaultConfig = { - server: { - baseUrl: 'http://localhost:8080', - port: 8080, - - databaseFile: '/tmp/ffffng.sqlite', - peersPath: '/tmp/peers', - - logging: { - directory: '/tmp/logs', - debug: false, - profile: false, - logRequests: false - }, - - internal: { - active: false, - user: 'admin', - password: 'secret' - }, - - email: { - from: 'Freifunk Knotenformular ', - - // For details see: https://nodemailer.com/2-0-0-beta/setup-smtp/ - smtp: { - host: 'mail.example.com', - port: '465', - secure: true, - auth: { - user: 'user@example.com', - pass: 'pass' - } - } - }, - - map: { - nodesJsonUrl: ['http://map.musterstadt.freifunk.net/nodes.json'] - } - }, - client: { - community: { - name: 'Freifunk Musterstadt', - domain: 'musterstadt.freifunk.net', - contactEmail: 'kontakt@musterstadt.freifunk.net', - sites: [], - domains: [] - }, - legal: { - privacyUrl: null, - imprintUrl: null - }, - map: { - mapUrl: 'http://map.musterstadt.freifunk.net' - }, - monitoring: { - enabled: false - }, - coordsSelector: { - showInfo: false, - showBorderForDebugging: false, - localCommunityPolygon: [], - lat: 53.565278, - lng: 10.001389, - defaultZoom: 10, - layers: {} - } - } - }; - - const configJSONFile = commandLineOptions.config; - let configJSON = {}; - - if (fs.existsSync(configJSONFile)) { - configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8')); - } else { - console.error('config.json not found: ' + configJSONFile); - process.exit(1); - } - - const _ = require('lodash'); - - function stripTrailingSlash (obj, field) { - const url = obj[field]; - if (_.isString(url) && _.last(url) === '/') { - obj[field] = url.substr(0, url.length - 1); - } - } - - const config = deepExtend({}, defaultConfig, configJSON); - - stripTrailingSlash(config.server, 'baseUrl'); - stripTrailingSlash(config.client.map, 'mapUrl'); - - const url = require('url'); - config.server.rootPath = url.parse(config.server.baseUrl).pathname; - config.client.rootPath = config.server.rootPath; - - return { - config, - version - } -})() diff --git a/server/config.ts b/server/config.ts new file mode 100644 index 0000000..d3ebbbb --- /dev/null +++ b/server/config.ts @@ -0,0 +1,207 @@ +import commandLineArgs from "command-line-args" +import commandLineUsage from "command-line-usage" +import fs from "graceful-fs" +import url from "url" +import {ArrayField, Field, parse, RawJsonField} from "sparkson" + +// TODO: Replace string types by more specific types like URL, Password, etc. + +export class LoggingConfig { + constructor( + @Field("directory") public directory: string, + @Field("debug") public debug: boolean, + @Field("profile") public profile: boolean, + @Field("logRequests") public logRequests: boolean, + ) {} +} + +export class InternalConfig { + constructor( + @Field("active") public active: boolean, + @Field("user") public user: string, + @Field("password") public password: string, + ) {} +} + +export class EmailConfig { + constructor( + @Field("from") public from: string, + + // For details see: https://nodemailer.com/2-0-0-beta/setup-smtp/ + @RawJsonField("smtp") public smtp: any, // TODO: Better types! + ) {} +} + +export class ServerMapConfig { + constructor( + @ArrayField("nodesJsonUrl", String) public nodesJsonUrl: string[], + ) {} +} + +export class ServerConfig { + constructor( + @Field("baseUrl") public baseUrl: string, + @Field("port") public port: number, + + @Field("databaseFile") public databaseFile: string, + @Field("peersPath") public peersPath: string, + + @Field("logging") public logging: LoggingConfig, + @Field("internal") public internal: InternalConfig, + @Field("email") public email: EmailConfig, + @Field("map") public map: ServerMapConfig, + + @Field("rootPath", true, undefined, "/") public rootPath: string, + ) {} +} + +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 + ) {} +} + +function parseCommandLine(): {config: Config, version: string} { + const commandLineDefs = [ + { name: 'help', alias: 'h', type: Boolean, description: 'Show this help' }, + { name: 'config', alias: 'c', type: String, description: 'Location of config.json' }, + { name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version' } + ]; + + let commandLineOptions; + try { + commandLineOptions = commandLineArgs(commandLineDefs); + } catch (e) { + console.error(e.message); + console.error('Try \'--help\' for more information.'); + process.exit(1); + } + + const packageJsonFile = __dirname + '/../package.json'; + let version = 'unknown'; + if (fs.existsSync(packageJsonFile)) { + version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version; + } + + function usage () { + console.log(commandLineUsage([ + { + header: 'ffffng - ' + version + ' - Freifunk node management form', + optionList: commandLineDefs + } + ])); + } + + if (commandLineOptions.help) { + usage(); + process.exit(0); + } + + if (commandLineOptions.version) { + console.log('ffffng - ' + version); + process.exit(0); + } + + if (!commandLineOptions.config) { + usage(); + process.exit(1); + } + + const configJSONFile = commandLineOptions.config; + let configJSON = {}; + + if (fs.existsSync(configJSONFile)) { + configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8')); + } else { + console.error('config.json not found: ' + configJSONFile); + process.exit(1); + } + + const config: Config = parse(Config, configJSON); + + function stripTrailingSlash(url: string): string { + return url.endsWith("/") ? url.substr(0, url.length - 1) : url; + } + + config.server.baseUrl = stripTrailingSlash(config.server.baseUrl); + config.client.map.mapUrl = stripTrailingSlash(config.client.map.mapUrl); + + config.server.rootPath = url.parse(config.server.baseUrl).pathname || "/"; + config.client.rootPath = config.server.rootPath; + + return { + config, + version + } +} + +const {config, version} = parseCommandLine(); + +export {config}; +export {version}; + diff --git a/server/tsconfig.json b/server/tsconfig.json index a20dad5..9abae53 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -60,8 +60,8 @@ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ /* Advanced Options */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */