Moved utils to another repository
This commit is contained in:
@@ -25,7 +25,7 @@ services:
|
|||||||
- "CLIENT_SCOPE=openid email profile roles groups"
|
- "CLIENT_SCOPE=openid email profile roles groups"
|
||||||
- "NODE_ENV=debug"
|
- "NODE_ENV=debug"
|
||||||
- "SSL_VERIFY=false"
|
- "SSL_VERIFY=false"
|
||||||
- "EXT_RESOURCE_URI=http://localhost"
|
- "EXT_RESOURCE_URI=http://localhost:3001"
|
||||||
- "REDIS_URL=redis://redis:6379"
|
- "REDIS_URL=redis://redis:6379"
|
||||||
- "NO_PROXY=redis:6379"
|
- "NO_PROXY=redis:6379"
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
34
package-lock.json
generated
34
package-lock.json
generated
@@ -3173,6 +3173,29 @@
|
|||||||
"pinkie": "^2.0.0"
|
"pinkie": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pkg-express-utils": {
|
||||||
|
"version": "git+https://git.biotronik.int/scm/coe-bs-website/node-pkg-express-utils.git#99527fdb494c464c9351626548bf446b39c155f4",
|
||||||
|
"from": "git+https://git.biotronik.int/scm/coe-bs-website/node-pkg-express-utils.git",
|
||||||
|
"requires": {
|
||||||
|
"@10xjs/polyfill-analyzer": "^0.1.0",
|
||||||
|
"connect-redis": "^5.0.0",
|
||||||
|
"cookie-parser": "~1.4.4",
|
||||||
|
"env-var": "^6.3.0",
|
||||||
|
"express": "~4.16.1",
|
||||||
|
"express-session": "^1.17.1",
|
||||||
|
"json-prune": "^1.1.0",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"polyfill-library": "^3.97.0",
|
||||||
|
"proper-url-join": "^2.1.1",
|
||||||
|
"redis": "^3.0.2",
|
||||||
|
"role-acl": "^4.5.4",
|
||||||
|
"run-script-os": "^1.1.3",
|
||||||
|
"threads": "^1.6.3",
|
||||||
|
"tiny-worker": "^2.3.0",
|
||||||
|
"uuid": "^8.3.1",
|
||||||
|
"winston": "^3.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"polyfill-library": {
|
"polyfill-library": {
|
||||||
"version": "3.97.0",
|
"version": "3.97.0",
|
||||||
"resolved": "https://registry.npmjs.org/polyfill-library/-/polyfill-library-3.97.0.tgz",
|
"resolved": "https://registry.npmjs.org/polyfill-library/-/polyfill-library-3.97.0.tgz",
|
||||||
@@ -3643,6 +3666,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz",
|
||||||
"integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw=="
|
"integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw=="
|
||||||
},
|
},
|
||||||
|
"run-script-os": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-xPlzE6533nvWVea5z7e5J7+JAIepfpxTu/HLGxcjJYlemVukOCWJBaRCod/DWXJFRIWEFOgSGbjd2m1QWTJi5w=="
|
||||||
|
},
|
||||||
"rxjs": {
|
"rxjs": {
|
||||||
"version": "6.6.3",
|
"version": "6.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
|
||||||
@@ -4606,12 +4634,6 @@
|
|||||||
"mkdirp": "^0.5.1"
|
"mkdirp": "^0.5.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ws": {
|
|
||||||
"version": "7.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz",
|
|
||||||
"integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"y18n": {
|
"y18n": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"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())\"",
|
|
||||||
"debug-client": "tsc-watch --project ./public/js-source",
|
"debug-client": "tsc-watch --project ./public/js-source",
|
||||||
"debug-server": "tsc-watch --project . --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-*",
|
||||||
@@ -27,6 +26,7 @@
|
|||||||
"json-prune": "^1.1.0",
|
"json-prune": "^1.1.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-sass-middleware": "0.11.0",
|
"node-sass-middleware": "0.11.0",
|
||||||
|
"pkg-express-utils": "git+https://git.biotronik.int/scm/coe-bs-website/node-pkg-express-utils.git",
|
||||||
"polyfill-library": "^3.97.0",
|
"polyfill-library": "^3.97.0",
|
||||||
"proper-url-join": "^2.1.1",
|
"proper-url-join": "^2.1.1",
|
||||||
"pug": "^3.0.0",
|
"pug": "^3.0.0",
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"node-watch": "^0.7.0",
|
"node-watch": "^0.7.0",
|
||||||
"tsc-watch": "^4.2.9",
|
"tsc-watch": "^4.2.9",
|
||||||
"typescript": "^4.0.5",
|
"typescript": "^4.0.5"
|
||||||
"ws": "^7.4.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {AuthProxy, AutoReloader, DefaultConfig, HttpLogger, Polyfill} from './utils';
|
import {AuthProxy, AutoReloader, DefaultConfig, HttpLogger, Polyfill, Permissions} from 'pkg-express-utils';
|
||||||
|
|
||||||
export const app = express();
|
export const app = express();
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ router.use(AutoReloader.router);
|
|||||||
//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.getRouter('./public/js/bundle.js'));
|
||||||
router.use(sassMiddleware({
|
router.use(sassMiddleware({
|
||||||
src: path.join(__dirname, '../public'),
|
src: path.join(__dirname, '../public'),
|
||||||
dest: path.join(__dirname, '../public'),
|
dest: path.join(__dirname, '../public'),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* 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 {DefaultConfig} from './utils';
|
import {DefaultConfig} from 'pkg-express-utils';
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
host: DefaultConfig.HOSTNAME,
|
host: DefaultConfig.HOSTNAME,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
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 {DefaultConfig, Logger} from './utils';
|
import {DefaultConfig, Logger} from 'pkg-express-utils';
|
||||||
|
|
||||||
app.set('port', DefaultConfig.PORT);
|
app.set('port', DefaultConfig.PORT);
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
|
|||||||
28
src/types/extend-request.d.ts
vendored
Normal file
28
src/types/extend-request.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// extend-request.d.ts
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface UserInfo {
|
||||||
|
email: string,
|
||||||
|
email_verified: boolean,
|
||||||
|
family_name: string,
|
||||||
|
given_name: string,
|
||||||
|
groups: string[],
|
||||||
|
name: string,
|
||||||
|
preferred_username: string,
|
||||||
|
sub: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Request {
|
||||||
|
getUserInfo(): Promise<UserInfo|undefined>;
|
||||||
|
noLogging: boolean|undefined;
|
||||||
|
permissionDetails?: import('role-acl').Permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
initLogout(): boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import {Request, RequestHandler} from 'express';
|
|
||||||
import {DefaultConfig, Logger, Resolvable, urlJoin} from '.';
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
|
|
||||||
const router: RequestHandler = (req: Request, res, next) => {
|
|
||||||
const resolvable = new Resolvable(async () => {
|
|
||||||
if (!DefaultConfig.USERINFO_HEADER) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
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: [[DefaultConfig.USERINFO_HEADER, token]]});
|
|
||||||
return await res.json() as UserInfo;
|
|
||||||
} catch (e) {
|
|
||||||
Logger.warn(e);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
req.getUserInfo = () => resolvable.resolve();
|
|
||||||
res.initLogout = function() {
|
|
||||||
const url = DefaultConfig.AUTH_PROXY_INIT_LOGOUT_URL ||
|
|
||||||
DefaultConfig.AUTH_PROXY_URL && urlJoin(DefaultConfig.AUTH_PROXY_URL, "init-logout");
|
|
||||||
if (url === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.redirect(307, url);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AuthProxy = {
|
|
||||||
router,
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import {Router} from 'express';
|
|
||||||
import {DefaultConfig, Logger, urlJoin} from '.';
|
|
||||||
import {v4} from 'uuid';
|
|
||||||
import Timeout = NodeJS.Timeout;
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
if (!DefaultConfig.isProduction) {
|
|
||||||
let uuid = v4();
|
|
||||||
let updateTimeout: Timeout|undefined = undefined;
|
|
||||||
import("node-watch").then((watch) => {
|
|
||||||
watch.default('public', {recursive: true}, () => {
|
|
||||||
if (updateTimeout !== undefined) clearTimeout(updateTimeout);
|
|
||||||
updateTimeout = setTimeout(() => {
|
|
||||||
uuid = v4();
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}).catch((err) => { Logger.error(err); });
|
|
||||||
|
|
||||||
router.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;
|
|
||||||
const parse = function(res) {
|
|
||||||
return res.json()
|
|
||||||
.then(function(json) {return json.uuid;}) }
|
|
||||||
let hash = undefined;
|
|
||||||
let hadError = false;
|
|
||||||
setInterval(function() {
|
|
||||||
try {
|
|
||||||
fetch(url)
|
|
||||||
.then(function(res) { return parse(res) })
|
|
||||||
.then(function(data) {
|
|
||||||
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, ""));
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get("/auto-reload", (req, res) => {
|
|
||||||
req.noLogging = true;
|
|
||||||
res.json({uuid});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AutoReloader = {
|
|
||||||
router,
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import * as env from 'env-var';
|
|
||||||
import {urlJoin} from '.';
|
|
||||||
|
|
||||||
const NODE_ENV = env.get('NODE_ENV').default("development").asString();
|
|
||||||
const isProduction = NODE_ENV === 'production';
|
|
||||||
|
|
||||||
const envs = {
|
|
||||||
NODE_ENV,
|
|
||||||
// port of the server
|
|
||||||
PORT: env.get('PORT').default('3000').asPortNumber(),
|
|
||||||
// hostname of the server ('0.0.0.0' listens on all network interfaces)
|
|
||||||
HOSTNAME: env.get('HOSTNAME').default('0.0.0.0').asString(),
|
|
||||||
// base path
|
|
||||||
BASE_PATH: env.get('BASE_PATH').default('/').asString(),
|
|
||||||
// external base url
|
|
||||||
EXTERNAL_BASE_URL: env.get('EXTERNAL_BASE_URL').asString(),
|
|
||||||
|
|
||||||
// url of redis session store (required in production using InMemory)
|
|
||||||
REDIS_URL: env.get('REDIS_URL').asString() || undefined,
|
|
||||||
// cookie secret for the session id (required in production using Session)
|
|
||||||
SESSION_SECRET: env.get('SESSION_SECRET').asString() || undefined,
|
|
||||||
|
|
||||||
// header where user info token is stored to request auth proxy
|
|
||||||
USERINFO_HEADER: env.get('USERINFO_HEADER').asString() || undefined,
|
|
||||||
// base url to init a logout or request user info
|
|
||||||
AUTH_PROXY_URL: env.get('AUTH_PROXY_URL').asString() || undefined,
|
|
||||||
// override base url to request user info
|
|
||||||
AUTH_PROXY_USERINFO_URL: env.get('AUTH_PROXY_USERINFO_URL').asString() || undefined,
|
|
||||||
// override base url to init a logout
|
|
||||||
AUTH_PROXY_INIT_LOGOUT_URL: env.get('AUTH_PROXY_INIT_LOGOUT_URL').asString() || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
function requireEnv(name: string, onlyInProduction = false): void {
|
|
||||||
env.get(name).required(!onlyInProduction || isProduction).asString();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultConfig = {
|
|
||||||
...envs,
|
|
||||||
EXTERNAL_BASE_URL:
|
|
||||||
envs.EXTERNAL_BASE_URL ||
|
|
||||||
urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH),
|
|
||||||
isProduction,
|
|
||||||
requireEnv,
|
|
||||||
};
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
enum ResolvableState {
|
|
||||||
WAITING,
|
|
||||||
PENDING,
|
|
||||||
ERROR,
|
|
||||||
DONE
|
|
||||||
}
|
|
||||||
|
|
||||||
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?: (...args: U) => 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(...args));
|
|
||||||
break;
|
|
||||||
case ResolvableState.PENDING:
|
|
||||||
this.pendings.push([resolve, reject]);
|
|
||||||
break;
|
|
||||||
case ResolvableState.DONE:
|
|
||||||
resolve(this.data);
|
|
||||||
break;
|
|
||||||
case ResolvableState.ERROR:
|
|
||||||
reject(this.error);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected isFinished(): boolean {
|
|
||||||
return this.state === ResolvableState.DONE || this.state === ResolvableState.ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected parsePromise(promise: Promise<T>): void {
|
|
||||||
promise.then((data) => {
|
|
||||||
this.data = data;
|
|
||||||
this.state = ResolvableState.DONE;
|
|
||||||
this.pendings.forEach(pending => pending[0](data));
|
|
||||||
}).catch(err => {
|
|
||||||
this.error = err;
|
|
||||||
this.state = ResolvableState.ERROR;
|
|
||||||
this.pendings.forEach(pending => pending[1](err));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, never> {
|
|
||||||
protected state: ResolvableState = ResolvableState.PENDING;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setData(data: T): void {
|
|
||||||
if (!this.isFinished()) {
|
|
||||||
this.parsePromise((async () => data)());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setError(error: unknown): void {
|
|
||||||
if (!this.isFinished()) {
|
|
||||||
this.parsePromise((async () => { throw error; })());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import * as properUrlJoin from 'proper-url-join';
|
|
||||||
|
|
||||||
export const urlJoin = properUrlJoin as unknown as properUrlJoin.default;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export {DefaultConfig} from './config';
|
|
||||||
export {Redis} from './redis';
|
|
||||||
export {Logger, HttpLogger} from './logging';
|
|
||||||
export {AuthProxy} from './auth-proxy';
|
|
||||||
export {Resolvable, WaitForSync} from './helpers/resolvable';
|
|
||||||
export {urlJoin} from './helpers/urlJoin';
|
|
||||||
export {AutoReloader} from './auto-reload';
|
|
||||||
export {Polyfill} from './polyfill';
|
|
||||||
export {Session} from './session';
|
|
||||||
export {Permissions} from './permissions';
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import * as winston from 'winston';
|
|
||||||
import {LeveledLogMethod} from 'winston';
|
|
||||||
import {RequestHandler} from 'express';
|
|
||||||
import * as colors from 'colors';
|
|
||||||
import {DefaultConfig} from '.';
|
|
||||||
import prune = require('json-prune');
|
|
||||||
|
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
|
||||||
level: DefaultConfig.isProduction ? "info" : "silly",
|
|
||||||
format: winston.format.json(),
|
|
||||||
transports: [
|
|
||||||
new winston.transports.Console({
|
|
||||||
format: winston.format.simple(),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const levels = ["error", "warn", "info", "http", "verbose", "debug", "silly"] as const;
|
|
||||||
type LogLevels = typeof levels[number]|"log";
|
|
||||||
const wrapper = (original: LeveledLogMethod) => {
|
|
||||||
return (...args: unknown[]) => {
|
|
||||||
return original(args.map((obj) => typeof obj === "string" ? obj : prune(obj)).join(" "));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Logger = {
|
|
||||||
} as {[level in LogLevels]: ReturnType<typeof wrapper>};
|
|
||||||
|
|
||||||
for (const level of levels) {
|
|
||||||
Logger[level] = wrapper(logger[level]);
|
|
||||||
}
|
|
||||||
Logger.log = wrapper(logger["silly"]);
|
|
||||||
|
|
||||||
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]) {
|
|
||||||
const statusCode = res.statusCode;
|
|
||||||
const colorFunction = statusCode >= 500 ? colors.red
|
|
||||||
: statusCode >= 400 ? colors.yellow
|
|
||||||
: statusCode >= 300 ? colors.cyan
|
|
||||||
: statusCode >= 200 ? colors.green
|
|
||||||
: colors.gray;
|
|
||||||
const status = colorFunction(res.statusCode.toString(10));
|
|
||||||
const method = req.method.toUpperCase().padEnd(6, " ");
|
|
||||||
const responseTime = (Date.now()-start).toString(10).padStart(3, " ");
|
|
||||||
if (!req.noLogging)
|
|
||||||
Logger.http(`${status} ${method} ${responseTime}ms ${path}`);
|
|
||||||
end.apply(res, args);
|
|
||||||
};
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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
|
|
||||||
class PermissionManager extends AccessControl {
|
|
||||||
public can(roleOrRequest: Request|string|string[]|IQueryInfo): PermQuery {
|
|
||||||
return new PermQuery(this.getGrants(), roleOrRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRouter(resource: string, opts: Partial<RermRouterOpts>): RequestHandler {
|
|
||||||
return async (req: Request, res, next) => {
|
|
||||||
let query = this.can(req);
|
|
||||||
if (opts.context)
|
|
||||||
query = query.context(opts.context);
|
|
||||||
if (opts.action)
|
|
||||||
query = query.execute(opts.action);
|
|
||||||
if (opts.skipConditions)
|
|
||||||
query = query.skipConditions(opts.skipConditions);
|
|
||||||
const permission = await query.on(resource);
|
|
||||||
if (permission.granted) {
|
|
||||||
req.permissionDetails = permission;
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.sendStatus(403);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RermRouterOpts = {
|
|
||||||
context: unknown,
|
|
||||||
action: string,
|
|
||||||
skipConditions: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PermQuery extends Query {
|
|
||||||
protected resolveRequest: Request|undefined;
|
|
||||||
|
|
||||||
constructor(grants: unknown, roleOrRequest: Request|string|string[]|IQueryInfo) {
|
|
||||||
function isRequest(obj: unknown): obj is Request {
|
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
|
||||||
return typeof obj === 'object' && obj && obj.hasOwnProperty('path') || false;
|
|
||||||
}
|
|
||||||
if (isRequest(roleOrRequest)) {
|
|
||||||
super(grants, []);
|
|
||||||
this.resolveRequest = roleOrRequest;
|
|
||||||
} else {
|
|
||||||
super(grants, roleOrRequest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async on(resource: string, skipConditions?: boolean): Promise<Permission> {
|
|
||||||
if (this.resolveRequest) {
|
|
||||||
const userInfo = await this.resolveRequest.getUserInfo();
|
|
||||||
this.role(userInfo?.groups ?? []);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof this._.role === 'object' && this._.role.includes('noaccess') ||
|
|
||||||
typeof this._.role === 'string' && this._.role === 'noaccess'
|
|
||||||
) {
|
|
||||||
this.role([]);
|
|
||||||
}
|
|
||||||
return super.on(resource, skipConditions);
|
|
||||||
}
|
|
||||||
|
|
||||||
public context(context: unknown): PermQuery {
|
|
||||||
super.context(context);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public skipConditions(value: boolean): PermQuery {
|
|
||||||
super.skipConditions(value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public with(context: unknown): PermQuery {
|
|
||||||
super.with(context);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public execute(action: string): PermQuery {
|
|
||||||
super.execute(action);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sync(): PermQuery {
|
|
||||||
throw new AccessControlError("Sync method is not allowed on PermissionManager!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Permissions = new PermissionManager();
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// workers/add.js
|
|
||||||
import {expose} from "threads/worker";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import {analyze} from '@10xjs/polyfill-analyzer';
|
|
||||||
import allPolyfills from '@10xjs/polyfill-analyzer/dist/polyfills';
|
|
||||||
import {PolyfillFeatureList} from "polyfill-library";
|
|
||||||
|
|
||||||
expose(() => {
|
|
||||||
const exclude = [
|
|
||||||
"console.markTimeline",
|
|
||||||
"console.timeline",
|
|
||||||
"console.timelineEnd",
|
|
||||||
];
|
|
||||||
const featureList = analyze({
|
|
||||||
source: fs.readFileSync('./public/js/bundle.js', 'utf-8'),
|
|
||||||
include: allPolyfills.filter(x => !exclude.includes(x)),
|
|
||||||
// Not all features listed by polyfillLibrary.listAllPolyfills()` can be detected.
|
|
||||||
unsupportedPolyfill: 'ignore',
|
|
||||||
});
|
|
||||||
const feats: PolyfillFeatureList = {};
|
|
||||||
for (const feature of featureList) {
|
|
||||||
feats[feature] = {};
|
|
||||||
}
|
|
||||||
return feats;
|
|
||||||
});
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import {RequestHandler} from 'express';
|
|
||||||
import * as polyfillLibrary from 'polyfill-library';
|
|
||||||
import {PolyfillFeatureList} from 'polyfill-library';
|
|
||||||
import {DefaultConfig, Logger, WaitForSync} from '.';
|
|
||||||
import {spawn, Thread, Worker} from 'threads';
|
|
||||||
import {WorkerFunction} from 'threads/dist/types/worker';
|
|
||||||
|
|
||||||
const features = new WaitForSync<PolyfillFeatureList>();
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const worker = await spawn<WorkerFunction>(new Worker("./polyfill-worker"));
|
|
||||||
const feats = await worker() as PolyfillFeatureList;
|
|
||||||
await Thread.terminate(worker);
|
|
||||||
return feats;
|
|
||||||
})()
|
|
||||||
.then(feats => {
|
|
||||||
feats["fetch"] = {};
|
|
||||||
Logger.debug("Polyfill analysed:", Object.keys(feats));
|
|
||||||
features.setData(feats);
|
|
||||||
})
|
|
||||||
.catch(err => features.setError(err));
|
|
||||||
|
|
||||||
|
|
||||||
const router: RequestHandler = async (req, res) => {
|
|
||||||
const polyfillBundle = await polyfillLibrary.getPolyfillString({
|
|
||||||
uaString: req.header("user-agent"),
|
|
||||||
features: await features.resolve(),
|
|
||||||
minify: DefaultConfig.isProduction,
|
|
||||||
unknown: "polyfill",
|
|
||||||
stream: false,
|
|
||||||
});
|
|
||||||
res.setHeader('Content-Type', 'text/javascript');
|
|
||||||
res.send(polyfillBundle);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Polyfill = {
|
|
||||||
router: router,
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import * as redis from 'redis';
|
|
||||||
import {DefaultConfig} from '.';
|
|
||||||
import {promisify} from 'util';
|
|
||||||
|
|
||||||
interface RedisType {
|
|
||||||
client: redis.RedisClient | undefined;
|
|
||||||
get(key: string): Promise<string | undefined>;
|
|
||||||
set(key: string, value: string): Promise<void>;
|
|
||||||
del(...keys: string[]): Promise<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import * as session from 'express-session';
|
|
||||||
import {Store} from 'express-session';
|
|
||||||
import {DefaultConfig, Redis} from '.';
|
|
||||||
import * as redisStore from "connect-redis";
|
|
||||||
import {RequestHandler} from 'express';
|
|
||||||
|
|
||||||
|
|
||||||
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: DefaultConfig.SESSION_SECRET || 'keyboard cat',
|
|
||||||
resave: false,
|
|
||||||
saveUninitialized: true,
|
|
||||||
cookie: {secure: false},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Session = {
|
|
||||||
getRouter,
|
|
||||||
};
|
|
||||||
10
src/utils/types/userinfo.d.ts
vendored
10
src/utils/types/userinfo.d.ts
vendored
@@ -1,10 +0,0 @@
|
|||||||
type UserInfo = {
|
|
||||||
email: string,
|
|
||||||
email_verified: boolean,
|
|
||||||
family_name: string,
|
|
||||||
given_name: string,
|
|
||||||
groups: string[],
|
|
||||||
name: string,
|
|
||||||
preferred_username: string,
|
|
||||||
sub: string,
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
declare namespace Express {
|
declare namespace Express {
|
||||||
interface Request {
|
interface Request {
|
||||||
getUserInfo(): Promise<UserInfo|undefined>;
|
getUserInfo(): Promise<import('pkg-express-utils').UserInfo|undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Response {
|
interface Response {
|
||||||
Reference in New Issue
Block a user