Auto reload and bug fixes

This commit is contained in:
Sebastian Seedorf
2020-11-14 19:38:16 +01:00
parent 5a56fc26d2
commit 6189b95b6e
22 changed files with 265 additions and 47 deletions

View File

@@ -11,6 +11,7 @@ module.exports = {
'eslint:recommended',
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:promise/recommended",
],
rules: {
"no-console": "error",
@@ -30,16 +31,8 @@ module.exports = {
"no-void": "error",
"comma-spacing": "error",
"comma-dangle": ["error", "always-multiline"],
"promise/no-return-wrap": "error",
"promise/param-names": "error",
"promise/catch-or-return": "error",
"promise/no-native": "off",
"promise/no-nesting": "warn",
"promise/no-promise-in-callback": "warn",
"promise/no-callback-in-promise": "warn",
"promise/avoid-new": "warn",
"promise/no-new-statics": "error",
"promise/no-return-in-finally": "warn",
"promise/valid-params": "warn",
"comma-style": "error",
"semi": "error",
"promise/always-return": "off",
},
};

View File

@@ -6,15 +6,18 @@ import * as redisStore from 'connect-redis';
import * as session from 'express-session';
import * as sassMiddleware from 'node-sass-middleware';
import indexRouter from './routes';
import {HttpLogger, Redis, Config, setupAuthProxy} from './utils';
import {HttpLogger, Redis, Config, setupAuthProxy, getReloadRouter} from './utils';
import {Store} from 'express-session';
export const app = express();
// view engine setup
app.set('views', path.join(__dirname, '../views'));
app.set('view engine', 'pug');
app.use((req, res, next) => {
res.locals.Config = Config;
next();
});
const router = express.Router();
@@ -24,7 +27,9 @@ app.use(express.json());
app.use(express.urlencoded({extended: false}));
// auth proxy middleware
app.use(setupAuthProxy);
router.use(setupAuthProxy);
// auto reloader (when running in debug mode)
router.use(getReloadRouter());
// session
let sessionStore: Store|undefined = undefined;
@@ -32,7 +37,7 @@ if (Redis.client) {
const RedisStore = redisStore(session);
sessionStore = new RedisStore({client: Redis.client});
}
app.use(session({
router.use(session({
store: sessionStore,
secret: Config.SESSION_SECRET,
resave: false,

View File

@@ -9,7 +9,7 @@ import {Config, Logger} from './utils';
app.set('port', Config.PORT);
const server = http.createServer(app);
server.listen(Config.PORT);
app.listen(Config.PORT);
server.on('error', onError);
server.on('listening', onListening);

View File

@@ -12,7 +12,7 @@ router.use("/health", healthRouter);
/* GET home page. */
router.get('/', async (req, res) => {
const email = (await req.getUserInfo())?.email ?? "No email found!";
res.render('index', {title: 'Express', email, externalUrl: Config.EXTERNAL_BASE_URL});
res.render('index', {title: 'Express', email});
});
router.get('/logout', (req, res) => {

View File

@@ -2,11 +2,10 @@ import {Request, RequestHandler} from 'express';
import {Config, Logger} from '.';
import {Resolvable} from './helpers/resolvable';
import fetch from 'node-fetch';
import * as properUrlJoin from 'proper-url-join';
const urlJoin = properUrlJoin as unknown as properUrlJoin.default;
import {urlJoin} from './helpers/urlJoin';
export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
const resolvable = new Resolvable<UserInfo|undefined>(async () => {
const resolvable = new Resolvable(async () => {
if (!Config.USERINFO_HEADER) {
return undefined;
}
@@ -17,7 +16,7 @@ export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
}
try {
const res = await fetch(url, {headers: [[Config.USERINFO_HEADER, token]]});
return await res.json();
return await res.json() as UserInfo;
} catch (e) {
Logger.warn(e);
return undefined;
@@ -32,6 +31,6 @@ export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
}
this.redirect(307, url);
return true;
}
};
next();
}
};

58
src/utils/auto-reload.ts Normal file
View File

@@ -0,0 +1,58 @@
import {Router} from 'express';
import {Config} from './config';
import {Logger} from './logging';
import {urlJoin} from './helpers/urlJoin';
import {v4} from 'uuid';
import Timeout = NodeJS.Timeout;
export function getReloadRouter(): Router {
const reloadRouter = Router();
if (!Config.isProduction) {
let uuid = v4();
let updateTimeout: Timeout|undefined = undefined;
import("node-watch").then((watch) => {
watch.default('public', {recursive: true}, (evt, name) => {
if (updateTimeout !== undefined) clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
uuid = v4();
}, 200);
});
}).catch((err) => { Logger.error(err); });
reloadRouter.get("/auto-reload/client.js", (req, res) => {
Logger.debug(req.url, req.originalUrl, req.baseUrl);
res.setHeader('Content-Type', "application/javascript");
// language=JavaScript
res.send(`
const loc = window.location;
const url = loc.protocol+'//'+loc.host+'${urlJoin(req.baseUrl, "/auto-reload")}';
const parse = async res => (await res.json()).uuid;
let hash = undefined;
let hadError = false;
setInterval(async () => {
try {
const data = await parse(await fetch(url));
if (data) {
hash = hash === undefined ? data : hash;
if (hash !== data) {
window.location.reload();
}
}
} catch (e) {
if (hadError === false) {
console.log(e);
hadError = true;
}
}
}, 3000);
`.replace(/\t/g, ""));
});
reloadRouter.get("/auto-reload", (req, res) => {
req.noLogging = true;
res.json({uuid});
});
}
return reloadRouter;
}

View File

@@ -36,5 +36,7 @@ const envs = {
export const Config = {
...envs,
isProduction,
EXTERNAL_BASE_URL: envs.EXTERNAL_BASE_URL || urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`),
EXTERNAL_BASE_URL:
envs.EXTERNAL_BASE_URL ||
urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH),
}

View File

@@ -5,22 +5,22 @@ enum ResolvableState {
DONE
}
class FetchOnce<T> {
class FetchOnce<T, U extends Array<unknown>> {
protected data: T|undefined;
protected error: unknown|undefined;
protected state: ResolvableState = ResolvableState.WAITING;
protected pendings: [(res: Promise<T>|T) => void, (reason: unknown) => void][] = [];
constructor(protected fetchMethod?: () => Promise<T>) { }
constructor(protected fetchMethod?: (...args: U) => Promise<T>) { }
public resolve(): Promise<T> {
public resolve(...args: U): Promise<T> {
// eslint-disable-next-line promise/avoid-new
return new Promise((resolve, reject) => {
switch (this.state) {
case ResolvableState.WAITING:
this.state = ResolvableState.PENDING;
this.pendings.push([resolve, reject]);
if (this.fetchMethod) this.parsePromise(this.fetchMethod());
if (this.fetchMethod) this.parsePromise(this.fetchMethod(...args));
break;
case ResolvableState.PENDING:
this.pendings.push([resolve, reject]);
@@ -52,13 +52,13 @@ class FetchOnce<T> {
}
}
export class Resolvable<T> extends FetchOnce<T> {
constructor(fetchMethod: () => Promise<T>) {
export class Resolvable<T, U extends Array<unknown>> extends FetchOnce<T, U> {
constructor(fetchMethod: (...args: U) => Promise<T>) {
super(fetchMethod);
}
}
export class WaitForSync<T> extends FetchOnce<T> {
export class WaitForSync<T, U extends Array<unknown>> extends FetchOnce<T, U> {
protected state: ResolvableState = ResolvableState.PENDING;
constructor() {

View File

@@ -0,0 +1,2 @@
import * as properUrlJoin from 'proper-url-join';
export const urlJoin = properUrlJoin as unknown as properUrlJoin.default;

View File

@@ -2,3 +2,5 @@ export {Config} from './config';
export {Redis} from './redis';
export {Logger, HttpLogger} from './logging';
export {setupAuthProxy} from './auth-proxy';
export {Resolvable, WaitForSync} from './helpers/resolvable';
export {getReloadRouter} from './auto-reload';

View File

@@ -33,6 +33,7 @@ for (const level of levels) {
export const HttpLogger: RequestHandler = (req, res, next) => {
const start = Date.now();
const path = req.path;
type Callback = (() => void)|undefined;
const end = res.end;
res.end = function(...args: [Callback] & [unknown, Callback] & [unknown, BufferEncoding, Callback]) {
@@ -45,8 +46,9 @@ export const HttpLogger: RequestHandler = (req, res, next) => {
const status = colorFunction(res.statusCode.toString(10));
const method = req.method.toUpperCase().padEnd(6, " ");
const responseTime = (Date.now()-start).toString(10).padStart(3, " ");
Logger.http(`${status} ${method} ${responseTime}ms ${req.path}`);
if (!req.noLogging)
Logger.http(`${status} ${method} ${responseTime}ms ${path}`);
end.apply(res, args);
};
next();
}
};

5
src/utils/types/logging.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace Express {
interface Request {
noLogging: boolean|undefined;
}
}