No require any ENV_VAR

This commit is contained in:
Sebastian Seedorf
2020-11-15 23:57:04 +01:00
parent 72b81cae5e
commit 6a21fc1169
15 changed files with 102 additions and 67 deletions

View File

@@ -7,7 +7,7 @@
"lint-fix": "eslint . --ext .ts --fix", "lint-fix": "eslint . --ext .ts --fix",
"update-client-hash": "node -e \"require('fs').writeFileSync('public/misc/hash.txt', require('randomstring').generate())\"", "update-client-hash": "node -e \"require('fs').writeFileSync('public/misc/hash.txt', require('randomstring').generate())\"",
"debug-client": "tsc-watch --project ./public/js-source", "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-*", "debug": "concurrently npm:debug-*",
"build": "tsc", "build": "tsc",
"production": "node --use-openssl-ca --unhandled-rejections=strict ./out/index", "production": "node --use-openssl-ca --unhandled-rejections=strict ./out/index",

View File

@@ -5,7 +5,7 @@ import * as path from 'path';
import * as sassMiddleware from 'node-sass-middleware'; import * as sassMiddleware from 'node-sass-middleware';
import * as compression from 'compression'; import * as compression from 'compression';
import indexRouter from './routes'; 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(); export const app = express();
@@ -13,7 +13,7 @@ export const app = express();
app.set('views', path.join(__dirname, '../views')); app.set('views', path.join(__dirname, '../views'));
app.set('view engine', 'pug'); app.set('view engine', 'pug');
app.use((req, res, next) => { app.use((req, res, next) => {
res.locals.Config = Config; res.locals.Config = DefaultConfig;
next(); next();
}); });
@@ -33,7 +33,7 @@ router.use(AuthProxy.router);
router.use(AutoReloader.router); router.use(AutoReloader.router);
// session // session
router.use(Session.getRouter()); //router.use(Session.getRouter());
// static config // static config
router.use("/js/polyfill.js", Polyfill.router); router.use("/js/polyfill.js", Polyfill.router);
@@ -46,7 +46,7 @@ router.use(sassMiddleware({
router.use(express.static(path.join(__dirname, '../public'))); router.use(express.static(path.join(__dirname, '../public')));
router.use(indexRouter); router.use(indexRouter);
app.use(Config.BASE_PATH, router); app.use(DefaultConfig.BASE_PATH, router);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use((req, res, next) => { 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 // set locals, only providing error in development
res.locals.message = err.message; res.locals.message = err.message;
res.locals.error = !Config.isProduction ? err : {}; res.locals.error = !DefaultConfig.isProduction ? err : {};
// render the error page // render the error page
res.status(err.status || 500); res.status(err.status || 500);

View File

@@ -1,12 +1,12 @@
/* eslint-disable no-process-exit,no-console */ /* eslint-disable no-process-exit,no-console */
import * as http from "http"; import * as http from "http";
import urlJoin from 'proper-url-join'; import urlJoin from 'proper-url-join';
import {Config} from './utils'; import {DefaultConfig} from './utils';
const options = { const options = {
host: Config.HOSTNAME, host: DefaultConfig.HOSTNAME,
port: Config.PORT, port: DefaultConfig.PORT,
path: urlJoin(Config.BASE_PATH, '/health'), path: urlJoin(DefaultConfig.BASE_PATH, '/health'),
timeout: 2000, timeout: 2000,
}; };

View File

@@ -4,12 +4,12 @@
import * as http from 'http'; import * as http from 'http';
import {app} from './app'; import {app} from './app';
import {HttpError} from 'http-errors'; 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); const server = http.createServer(app);
app.listen(Config.PORT); app.listen(DefaultConfig.PORT);
server.on('error', onError); server.on('error', onError);
server.on('listening', onListening); server.on('listening', onListening);
@@ -20,11 +20,11 @@ function onError(error: HttpError): void {
switch (error.code) { switch (error.code) {
case 'EACCES': case 'EACCES':
Logger.error(Config.PORT + ' requires elevated privileges'); Logger.error(DefaultConfig.PORT + ' requires elevated privileges');
process.exit(1); process.exit(1);
break; break;
case 'EADDRINUSE': case 'EADDRINUSE':
Logger.error(Config.PORT + ' is already in use'); Logger.error(DefaultConfig.PORT + ' is already in use');
process.exit(2); process.exit(2);
break; break;
default: default:

View File

@@ -1,7 +1,6 @@
import * as express from 'express'; import * as express from 'express';
import userRouter from './api/user'; import userRouter from './api/user';
import healthRouter from './healthcheck'; import healthRouter from './healthcheck';
import {Config} from '../utils';
const router = express.Router(); const router = express.Router();
export default router; export default router;

View File

@@ -1,21 +1,22 @@
import {Request, RequestHandler} from 'express'; import {Request, RequestHandler} from 'express';
import {Config, Logger} from '.'; import {DefaultConfig, Logger} from '.';
import {Resolvable} from './helpers/resolvable'; import {Resolvable} from './helpers/resolvable';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import {urlJoin} from './helpers/urlJoin'; import {urlJoin} from './helpers/urlJoin';
const router: RequestHandler = (req: Request, res, next) => { const router: RequestHandler = (req: Request, res, next) => {
const resolvable = new Resolvable(async () => { const resolvable = new Resolvable(async () => {
if (!Config.USERINFO_HEADER) { if (!DefaultConfig.USERINFO_HEADER) {
return undefined; return undefined;
} }
const token = req.header(Config.USERINFO_HEADER); const token = req.header(DefaultConfig.USERINFO_HEADER);
const url = Config.AUTH_PROXY_USERINFO_URL || Config.AUTH_PROXY_URL && urlJoin(Config.AUTH_PROXY_URL, "userinfo"); const url = DefaultConfig.AUTH_PROXY_USERINFO_URL ||
DefaultConfig.AUTH_PROXY_URL && urlJoin(DefaultConfig.AUTH_PROXY_URL, "userinfo");
if (token === undefined || url === undefined) { if (token === undefined || url === undefined) {
return undefined; return undefined;
} }
try { 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; return await res.json() as UserInfo;
} catch (e) { } catch (e) {
Logger.warn(e); Logger.warn(e);
@@ -25,7 +26,8 @@ const router: RequestHandler = (req: Request, res, next) => {
req.getUserInfo = () => resolvable.resolve(); req.getUserInfo = () => resolvable.resolve();
res.initLogout = function() { 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) { if (url === undefined) {
return false; return false;
} }

View File

@@ -1,11 +1,11 @@
import {Router} from 'express'; import {Router} from 'express';
import {Config, Logger} from '.'; import {DefaultConfig, Logger} from '.';
import {urlJoin} from './helpers/urlJoin'; import {urlJoin} from './helpers/urlJoin';
import {v4} from 'uuid'; import {v4} from 'uuid';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
const router = Router(); const router = Router();
if (!Config.isProduction) { if (!DefaultConfig.isProduction) {
let uuid = v4(); let uuid = v4();
let updateTimeout: Timeout|undefined = undefined; let updateTimeout: Timeout|undefined = undefined;
import("node-watch").then((watch) => { import("node-watch").then((watch) => {

View File

@@ -32,10 +32,10 @@ const envs = {
}; };
function requireEnv(name: string, onlyInProduction = false): void { 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, ...envs,
EXTERNAL_BASE_URL: EXTERNAL_BASE_URL:
envs.EXTERNAL_BASE_URL || envs.EXTERNAL_BASE_URL ||

View File

@@ -1,4 +1,4 @@
export {Config} from './config'; export {DefaultConfig} from './config';
export {Redis} from './redis'; export {Redis} from './redis';
export {Logger, HttpLogger} from './logging'; export {Logger, HttpLogger} from './logging';
export {AuthProxy} from './auth-proxy'; export {AuthProxy} from './auth-proxy';

View File

@@ -3,11 +3,11 @@ import {LeveledLogMethod} from 'winston';
import prune = require('json-prune'); import prune = require('json-prune');
import {RequestHandler} from 'express'; import {RequestHandler} from 'express';
import * as colors from 'colors'; import * as colors from 'colors';
import {Config} from '.'; import {DefaultConfig} from '.';
const logger = winston.createLogger({ const logger = winston.createLogger({
level: Config.isProduction ? "info" : "silly", level: DefaultConfig.isProduction ? "info" : "silly",
format: winston.format.json(), format: winston.format.json(),
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({

View File

@@ -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'; import {Request, RequestHandler} from 'express';
// see https://www.npmjs.com/package/role-acl // see https://www.npmjs.com/package/role-acl

View File

@@ -1,6 +1,6 @@
import {RequestHandler} from 'express'; import {RequestHandler} from 'express';
import * as polyfillLibrary from 'polyfill-library'; import * as polyfillLibrary from 'polyfill-library';
import {Config, Logger} from '.'; import {DefaultConfig, Logger} from '.';
import {PolyfillFeatureList} from 'polyfill-library'; import {PolyfillFeatureList} from 'polyfill-library';
import {WaitForSync} from './helpers/resolvable'; import {WaitForSync} from './helpers/resolvable';
import {spawn, Thread, Worker} from 'threads'; import {spawn, Thread, Worker} from 'threads';
@@ -26,7 +26,7 @@ const router: RequestHandler = async (req, res) => {
const polyfillBundle = await polyfillLibrary.getPolyfillString({ const polyfillBundle = await polyfillLibrary.getPolyfillString({
uaString: req.header("user-agent"), uaString: req.header("user-agent"),
features: await features.resolve(), features: await features.resolve(),
minify: Config.isProduction, minify: DefaultConfig.isProduction,
unknown: "polyfill", unknown: "polyfill",
stream: false, stream: false,
}); });

View File

@@ -1,38 +1,70 @@
import * as redis from 'redis'; import * as redis from 'redis';
import {Config} from '.'; import {DefaultConfig} from '.';
import {promisify} from 'util'; import {promisify} from 'util';
Config.requireEnv('REDIS_URL', true); interface RedisType {
client: redis.RedisClient | undefined;
const redisClient = Config.REDIS_URL && redis.createClient({url: Config.REDIS_URL}); get(key: string): Promise<string | undefined>;
const inMemory: {[key: string]: string} = {}; set(key: string, value: string): Promise<void>;
del(...keys: string[]): Promise<number>;
type RedisType = {
client: redis.RedisClient | undefined,
get: (key: string) => Promise<string | undefined>,
set: (key: string, value: string) => Promise<void>,
del: (...keys: string[]) => Promise<void>
} }
export const Redis: RedisType = redisClient ? { class RealRedis implements RedisType {
client: redisClient, private _client: redis.RedisClient|undefined;
get: (async (key: string) => { private _promGet: ((key: string) => Promise<string|null>)|undefined;
const value = await promisify(redisClient.get)(key); 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<string | undefined> {
if (!this._promGet) this._promGet = promisify(this.client.get);
const value = await this._promGet(key);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
return value === null ? undefined : value; return value === null ? undefined : value;
}), }
set: promisify(redisClient.set),
del: promisify(redisClient.del) as unknown as (...keys: string[]) => Promise<void>, async set(key: string, value: string): Promise<void> {
} : { if (!this._promSet) this._promSet = promisify(this.client.set);
client: undefined, await this._promSet(key, value);
get: (async (key: string) => inMemory[key] || undefined), }
set: (async (key: string, value: string) => {
inMemory[key] = value; async del(...keys: string[]): Promise<number> {
}), if (!this._promDel) this._promDel = promisify(this.client.del);
del: (async (...keys: string[]) => { return await this._promDel(...keys);
for (const key of keys) { }
delete inMemory[key]; }
}
}), class MockRedis implements RedisType {
}; private inMemory: {[key: string]: string} = {};
get client(): undefined {
return undefined;
}
async get(key: string): Promise<string | undefined> {
return this.inMemory[key] || undefined;
}
async set(key: string, value: string): Promise<void> {
this.inMemory[key] = value;
}
async del(...keys: string[]): Promise<number> {
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();

View File

@@ -1,21 +1,22 @@
import {Store} from 'express-session'; import {Store} from 'express-session';
import {Redis, Config} from '.'; import {Redis, DefaultConfig} from '.';
import * as redisStore from "connect-redis"; import * as redisStore from "connect-redis";
import * as session from 'express-session'; import * as session from 'express-session';
import {RequestHandler} from 'express'; import {RequestHandler} from 'express';
Config.requireEnv('SESSION_SECRET', true);
let sessionStore: Store|undefined = undefined; let sessionStore: Store|undefined = undefined;
function getRouter(options?: Partial<session.SessionOptions>): RequestHandler { function getRouter(options?: Partial<session.SessionOptions>): RequestHandler {
DefaultConfig.requireEnv('SESSION_SECRET', true);
if (Redis.client && sessionStore !== undefined) { if (Redis.client && sessionStore !== undefined) {
const RedisStore = redisStore(session); const RedisStore = redisStore(session);
sessionStore = new RedisStore({client: Redis.client}); sessionStore = new RedisStore({client: Redis.client});
} }
return session({ return session({
store: sessionStore, store: sessionStore,
secret: Config.SESSION_SECRET || 'keyboard cat', secret: DefaultConfig.SESSION_SECRET || 'keyboard cat',
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: {secure: false}, cookie: {secure: false},

View File

@@ -1,4 +1,4 @@
- const baseUrl = Config.EXTERNAL_BASE_URL; - const baseUrl = DefaultConfig.EXTERNAL_BASE_URL;
doctype html doctype html
html html
head head
@@ -13,5 +13,5 @@ html
require(['src/index'], function (index) { require(['src/index'], function (index) {
index.setConfig({EXTERNAL_BASE_URL: '#{baseUrl}'}); index.setConfig({EXTERNAL_BASE_URL: '#{baseUrl}'});
}); });
if !Config.isProduction if !DefaultConfig.isProduction
script(type='text/javascript', src=baseUrl+'/auto-reload/client.js') script(type='text/javascript', src=baseUrl+'/auto-reload/client.js')