diff --git a/package.json b/package.json index 2ecb397..4b0c223 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "lint-fix": "eslint . --ext .ts --fix", "update-client-hash": "node -e \"require('fs').writeFileSync('public/misc/hash.txt', require('randomstring').generate())\"", "debug-client": "tsc-watch --project ./public/js-source", - "debug-server": "tsc-watch --project ./src --onSuccess \"node --enable-source-maps --use-openssl-ca --unhandled-rejections=strict ./out/index\"", + "debug-server": "tsc-watch --project . --onSuccess \"node --enable-source-maps --use-openssl-ca --unhandled-rejections=strict ./out/index\"", "debug": "concurrently npm:debug-*", "build": "tsc", "production": "node --use-openssl-ca --unhandled-rejections=strict ./out/index", diff --git a/src/app.ts b/src/app.ts index d30f384..01bcf70 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,7 +5,7 @@ import * as path from 'path'; import * as sassMiddleware from 'node-sass-middleware'; import * as compression from 'compression'; import indexRouter from './routes'; -import {HttpLogger, Config, AuthProxy, AutoReloader, Polyfill, Session} from './utils'; +import {AuthProxy, AutoReloader, DefaultConfig, HttpLogger, Polyfill} from './utils'; export const app = express(); @@ -13,7 +13,7 @@ export const app = express(); app.set('views', path.join(__dirname, '../views')); app.set('view engine', 'pug'); app.use((req, res, next) => { - res.locals.Config = Config; + res.locals.Config = DefaultConfig; next(); }); @@ -33,7 +33,7 @@ router.use(AuthProxy.router); router.use(AutoReloader.router); // session -router.use(Session.getRouter()); +//router.use(Session.getRouter()); // static config router.use("/js/polyfill.js", Polyfill.router); @@ -46,7 +46,7 @@ router.use(sassMiddleware({ router.use(express.static(path.join(__dirname, '../public'))); router.use(indexRouter); -app.use(Config.BASE_PATH, router); +app.use(DefaultConfig.BASE_PATH, router); // catch 404 and forward to error handler app.use((req, res, next) => { @@ -60,7 +60,7 @@ app.use((err: Error&{status?: number}, req: Request, res: Response, next: NextFu } // set locals, only providing error in development res.locals.message = err.message; - res.locals.error = !Config.isProduction ? err : {}; + res.locals.error = !DefaultConfig.isProduction ? err : {}; // render the error page res.status(err.status || 500); diff --git a/src/healthcheck.ts b/src/healthcheck.ts index 4306579..ddb1f95 100644 --- a/src/healthcheck.ts +++ b/src/healthcheck.ts @@ -1,12 +1,12 @@ /* eslint-disable no-process-exit,no-console */ import * as http from "http"; import urlJoin from 'proper-url-join'; -import {Config} from './utils'; +import {DefaultConfig} from './utils'; const options = { - host: Config.HOSTNAME, - port: Config.PORT, - path: urlJoin(Config.BASE_PATH, '/health'), + host: DefaultConfig.HOSTNAME, + port: DefaultConfig.PORT, + path: urlJoin(DefaultConfig.BASE_PATH, '/health'), timeout: 2000, }; diff --git a/src/index.ts b/src/index.ts index 86bd82e..ce5c2e3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,12 +4,12 @@ import * as http from 'http'; import {app} from './app'; import {HttpError} from 'http-errors'; -import {Config, Logger} from './utils'; +import {DefaultConfig, Logger} from './utils'; -app.set('port', Config.PORT); +app.set('port', DefaultConfig.PORT); const server = http.createServer(app); -app.listen(Config.PORT); +app.listen(DefaultConfig.PORT); server.on('error', onError); server.on('listening', onListening); @@ -20,11 +20,11 @@ function onError(error: HttpError): void { switch (error.code) { case 'EACCES': - Logger.error(Config.PORT + ' requires elevated privileges'); + Logger.error(DefaultConfig.PORT + ' requires elevated privileges'); process.exit(1); break; case 'EADDRINUSE': - Logger.error(Config.PORT + ' is already in use'); + Logger.error(DefaultConfig.PORT + ' is already in use'); process.exit(2); break; default: diff --git a/src/routes/index.ts b/src/routes/index.ts index 64f893b..eec512c 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,7 +1,6 @@ import * as express from 'express'; import userRouter from './api/user'; import healthRouter from './healthcheck'; -import {Config} from '../utils'; const router = express.Router(); export default router; diff --git a/src/utils/auth-proxy.ts b/src/utils/auth-proxy.ts index ef65e69..771999d 100644 --- a/src/utils/auth-proxy.ts +++ b/src/utils/auth-proxy.ts @@ -1,21 +1,22 @@ import {Request, RequestHandler} from 'express'; -import {Config, Logger} from '.'; +import {DefaultConfig, Logger} from '.'; import {Resolvable} from './helpers/resolvable'; import fetch from 'node-fetch'; import {urlJoin} from './helpers/urlJoin'; const router: RequestHandler = (req: Request, res, next) => { const resolvable = new Resolvable(async () => { - if (!Config.USERINFO_HEADER) { + if (!DefaultConfig.USERINFO_HEADER) { return undefined; } - const token = req.header(Config.USERINFO_HEADER); - const url = Config.AUTH_PROXY_USERINFO_URL || Config.AUTH_PROXY_URL && urlJoin(Config.AUTH_PROXY_URL, "userinfo"); + const token = req.header(DefaultConfig.USERINFO_HEADER); + const url = DefaultConfig.AUTH_PROXY_USERINFO_URL || + DefaultConfig.AUTH_PROXY_URL && urlJoin(DefaultConfig.AUTH_PROXY_URL, "userinfo"); if (token === undefined || url === undefined) { return undefined; } try { - const res = await fetch(url, {headers: [[Config.USERINFO_HEADER, token]]}); + const res = await fetch(url, {headers: [[DefaultConfig.USERINFO_HEADER, token]]}); return await res.json() as UserInfo; } catch (e) { Logger.warn(e); @@ -25,7 +26,8 @@ const router: RequestHandler = (req: Request, res, next) => { req.getUserInfo = () => resolvable.resolve(); res.initLogout = function() { - const url = Config.AUTH_PROXY_INIT_LOGOUT_URL || Config.AUTH_PROXY_URL && urlJoin(Config.AUTH_PROXY_URL, "init-logout"); + const url = DefaultConfig.AUTH_PROXY_INIT_LOGOUT_URL || + DefaultConfig.AUTH_PROXY_URL && urlJoin(DefaultConfig.AUTH_PROXY_URL, "init-logout"); if (url === undefined) { return false; } diff --git a/src/utils/auto-reload.ts b/src/utils/auto-reload.ts index 60f4502..47b8192 100644 --- a/src/utils/auto-reload.ts +++ b/src/utils/auto-reload.ts @@ -1,11 +1,11 @@ import {Router} from 'express'; -import {Config, Logger} from '.'; +import {DefaultConfig, Logger} from '.'; import {urlJoin} from './helpers/urlJoin'; import {v4} from 'uuid'; import Timeout = NodeJS.Timeout; const router = Router(); -if (!Config.isProduction) { +if (!DefaultConfig.isProduction) { let uuid = v4(); let updateTimeout: Timeout|undefined = undefined; import("node-watch").then((watch) => { diff --git a/src/utils/config.ts b/src/utils/config.ts index 1e134a3..e772704 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -32,10 +32,10 @@ const envs = { }; function requireEnv(name: string, onlyInProduction = false): void { - env.get(name).required(!onlyInProduction || isProduction); + env.get(name).required(!onlyInProduction || isProduction).asString(); } -export const Config = { +export const DefaultConfig = { ...envs, EXTERNAL_BASE_URL: envs.EXTERNAL_BASE_URL || diff --git a/src/utils/index.ts b/src/utils/index.ts index 10c431e..5ef6823 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -export {Config} from './config'; +export {DefaultConfig} from './config'; export {Redis} from './redis'; export {Logger, HttpLogger} from './logging'; export {AuthProxy} from './auth-proxy'; diff --git a/src/utils/logging.ts b/src/utils/logging.ts index be08134..c646f3b 100644 --- a/src/utils/logging.ts +++ b/src/utils/logging.ts @@ -3,11 +3,11 @@ import {LeveledLogMethod} from 'winston'; import prune = require('json-prune'); import {RequestHandler} from 'express'; import * as colors from 'colors'; -import {Config} from '.'; +import {DefaultConfig} from '.'; const logger = winston.createLogger({ - level: Config.isProduction ? "info" : "silly", + level: DefaultConfig.isProduction ? "info" : "silly", format: winston.format.json(), transports: [ new winston.transports.Console({ diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 93f8833..ae42301 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -1,4 +1,5 @@ -import {AccessControl, AccessControlError, IQueryInfo, Permission, Query} from 'role-acl'; +import {AccessControl, AccessControlError, IQueryInfo, Permission} from 'role-acl'; +import {Query} from 'role-acl/lib/src/core/Query'; import {Request, RequestHandler} from 'express'; // see https://www.npmjs.com/package/role-acl diff --git a/src/utils/polyfill.ts b/src/utils/polyfill.ts index 08737a2..38f192c 100644 --- a/src/utils/polyfill.ts +++ b/src/utils/polyfill.ts @@ -1,6 +1,6 @@ import {RequestHandler} from 'express'; import * as polyfillLibrary from 'polyfill-library'; -import {Config, Logger} from '.'; +import {DefaultConfig, Logger} from '.'; import {PolyfillFeatureList} from 'polyfill-library'; import {WaitForSync} from './helpers/resolvable'; import {spawn, Thread, Worker} from 'threads'; @@ -26,7 +26,7 @@ const router: RequestHandler = async (req, res) => { const polyfillBundle = await polyfillLibrary.getPolyfillString({ uaString: req.header("user-agent"), features: await features.resolve(), - minify: Config.isProduction, + minify: DefaultConfig.isProduction, unknown: "polyfill", stream: false, }); diff --git a/src/utils/redis.ts b/src/utils/redis.ts index 109c920..2b8f0bf 100644 --- a/src/utils/redis.ts +++ b/src/utils/redis.ts @@ -1,38 +1,70 @@ import * as redis from 'redis'; -import {Config} from '.'; +import {DefaultConfig} from '.'; import {promisify} from 'util'; -Config.requireEnv('REDIS_URL', true); - -const redisClient = Config.REDIS_URL && redis.createClient({url: Config.REDIS_URL}); -const inMemory: {[key: string]: string} = {}; - -type RedisType = { - client: redis.RedisClient | undefined, - get: (key: string) => Promise, - set: (key: string, value: string) => Promise, - del: (...keys: string[]) => Promise +interface RedisType { + client: redis.RedisClient | undefined; + get(key: string): Promise; + set(key: string, value: string): Promise; + del(...keys: string[]): Promise; } -export const Redis: RedisType = redisClient ? { - client: redisClient, - get: (async (key: string) => { - const value = await promisify(redisClient.get)(key); +class RealRedis implements RedisType { + private _client: redis.RedisClient|undefined; + private _promGet: ((key: string) => Promise)|undefined; + private _promSet: RedisType["set"]|undefined; + private _promDel: RedisType["del"]|undefined; + + get client(): redis.RedisClient { + if (this._client !== undefined) return this._client; + DefaultConfig.requireEnv('REDIS_URL'); + this._client = redis.createClient({url: DefaultConfig.REDIS_URL}); + return this._client; + } + + async get(key: string): Promise { + if (!this._promGet) this._promGet = promisify(this.client.get); + const value = await this._promGet(key); // eslint-disable-next-line no-null/no-null return value === null ? undefined : value; - }), - set: promisify(redisClient.set), - del: promisify(redisClient.del) as unknown as (...keys: string[]) => Promise, -} : { - client: undefined, - get: (async (key: string) => inMemory[key] || undefined), - set: (async (key: string, value: string) => { - inMemory[key] = value; - }), - del: (async (...keys: string[]) => { - for (const key of keys) { - delete inMemory[key]; - } - }), -}; + } + + async set(key: string, value: string): Promise { + if (!this._promSet) this._promSet = promisify(this.client.set); + await this._promSet(key, value); + } + + async del(...keys: string[]): Promise { + if (!this._promDel) this._promDel = promisify(this.client.del); + return await this._promDel(...keys); + } +} + +class MockRedis implements RedisType { + private inMemory: {[key: string]: string} = {}; + + get client(): undefined { + return undefined; + } + + async get(key: string): Promise { + return this.inMemory[key] || undefined; + } + + async set(key: string, value: string): Promise { + this.inMemory[key] = value; + } + + async del(...keys: string[]): Promise { + for (const key of keys) { + delete this.inMemory[key]; + } + return keys.length; + } +} + +export const Redis: RedisType = + DefaultConfig.REDIS_URL || DefaultConfig.isProduction + ? new RealRedis() + : new MockRedis(); diff --git a/src/utils/session.ts b/src/utils/session.ts index 608aa77..565e805 100644 --- a/src/utils/session.ts +++ b/src/utils/session.ts @@ -1,21 +1,22 @@ import {Store} from 'express-session'; -import {Redis, Config} from '.'; +import {Redis, DefaultConfig} from '.'; import * as redisStore from "connect-redis"; import * as session from 'express-session'; import {RequestHandler} from 'express'; -Config.requireEnv('SESSION_SECRET', true); + let sessionStore: Store|undefined = undefined; function getRouter(options?: Partial): RequestHandler { + DefaultConfig.requireEnv('SESSION_SECRET', true); if (Redis.client && sessionStore !== undefined) { const RedisStore = redisStore(session); sessionStore = new RedisStore({client: Redis.client}); } return session({ store: sessionStore, - secret: Config.SESSION_SECRET || 'keyboard cat', + secret: DefaultConfig.SESSION_SECRET || 'keyboard cat', resave: false, saveUninitialized: true, cookie: {secure: false}, diff --git a/views/layout.pug b/views/layout.pug index 0f8efd8..0228ac8 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -1,4 +1,4 @@ -- const baseUrl = Config.EXTERNAL_BASE_URL; +- const baseUrl = DefaultConfig.EXTERNAL_BASE_URL; doctype html html head @@ -13,5 +13,5 @@ html require(['src/index'], function (index) { index.setConfig({EXTERNAL_BASE_URL: '#{baseUrl}'}); }); - if !Config.isProduction + if !DefaultConfig.isProduction script(type='text/javascript', src=baseUrl+'/auto-reload/client.js')