Auto reload and bug fixes
This commit is contained in:
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
13
src/app.ts
13
src/app.ts
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
58
src/utils/auto-reload.ts
Normal 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;
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
2
src/utils/helpers/urlJoin.ts
Normal file
2
src/utils/helpers/urlJoin.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import * as properUrlJoin from 'proper-url-join';
|
||||
export const urlJoin = properUrlJoin as unknown as properUrlJoin.default;
|
||||
@@ -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';
|
||||
|
||||
@@ -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
5
src/utils/types/logging.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare namespace Express {
|
||||
interface Request {
|
||||
noLogging: boolean|undefined;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user