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",
"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",

View File

@@ -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);

View File

@@ -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,
};

View File

@@ -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:

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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) => {

View File

@@ -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 ||

View File

@@ -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';

View File

@@ -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({

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';
// see https://www.npmjs.com/package/role-acl

View File

@@ -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,
});

View File

@@ -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<string | undefined>,
set: (key: string, value: string) => Promise<void>,
del: (...keys: string[]) => Promise<void>
interface RedisType {
client: redis.RedisClient | undefined;
get(key: string): Promise<string | undefined>;
set(key: string, value: string): Promise<void>;
del(...keys: string[]): Promise<number>;
}
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<string|null>)|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<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
return value === null ? undefined : value;
}),
set: promisify(redisClient.set),
del: promisify(redisClient.del) as unknown as (...keys: string[]) => Promise<void>,
} : {
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<void> {
if (!this._promSet) this._promSet = promisify(this.client.set);
await this._promSet(key, value);
}
async del(...keys: string[]): Promise<number> {
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<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 {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<session.SessionOptions>): 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},

View File

@@ -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')