inital #2
This commit is contained in:
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
out
|
||||||
40
.eslintrc.js
Normal file
40
.eslintrc.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: [
|
||||||
|
'@typescript-eslint',
|
||||||
|
'no-null',
|
||||||
|
'promise',
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:promise/recommended",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"no-console": "error",
|
||||||
|
"max-len": ["error", {"code": 128}],
|
||||||
|
"no-process-env": "error",
|
||||||
|
"no-process-exit": "error",
|
||||||
|
"no-null/no-null": "error",
|
||||||
|
"no-useless-return": "error",
|
||||||
|
"prefer-arrow-callback": "warn",
|
||||||
|
"object-curly-spacing": "error",
|
||||||
|
"consistent-return": "error",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
|
"error", {
|
||||||
|
"allowExpressions": true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"no-void": "error",
|
||||||
|
"comma-spacing": "error",
|
||||||
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
"comma-style": "error",
|
||||||
|
"semi": "error",
|
||||||
|
"no-implicit-coercion": "error",
|
||||||
|
|
||||||
|
"promise/always-return": "off",
|
||||||
|
},
|
||||||
|
};
|
||||||
28
.idea/codeStyles/Project.xml
generated
Normal file
28
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<DBN-PSQL>
|
||||||
|
<case-options enabled="true">
|
||||||
|
<option name="KEYWORD_CASE" value="lower" />
|
||||||
|
<option name="FUNCTION_CASE" value="lower" />
|
||||||
|
<option name="PARAMETER_CASE" value="lower" />
|
||||||
|
<option name="DATATYPE_CASE" value="lower" />
|
||||||
|
<option name="OBJECT_CASE" value="preserve" />
|
||||||
|
</case-options>
|
||||||
|
<formatting-settings enabled="false" />
|
||||||
|
</DBN-PSQL>
|
||||||
|
<DBN-SQL>
|
||||||
|
<case-options enabled="true">
|
||||||
|
<option name="KEYWORD_CASE" value="lower" />
|
||||||
|
<option name="FUNCTION_CASE" value="lower" />
|
||||||
|
<option name="PARAMETER_CASE" value="lower" />
|
||||||
|
<option name="DATATYPE_CASE" value="lower" />
|
||||||
|
<option name="OBJECT_CASE" value="preserve" />
|
||||||
|
</case-options>
|
||||||
|
<formatting-settings enabled="false">
|
||||||
|
<option name="STATEMENT_SPACING" value="one_line" />
|
||||||
|
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
|
||||||
|
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
|
||||||
|
</formatting-settings>
|
||||||
|
</DBN-SQL>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/node-pkg-express-utils.iml" filepath="$PROJECT_DIR$/.idea/node-pkg-express-utils.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
.idea/node-pkg-express-utils.iml
generated
Normal file
12
.idea/node-pkg-express-utils.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
15
README.md
Normal file
15
README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Express Utils for Node Projects
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
##### 1. Install the package
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install git+https://git.biotronik.int/scm/coe-bs-website/node-pkg-express-utils.git
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 2. Copy the types
|
||||||
|
|
||||||
|
Copy the folder `src/types` to some location within the source folder of your project.
|
||||||
|
It just needs to sit there with no more action required.
|
||||||
|
|
||||||
5
out/auth-proxy.d.ts
vendored
Normal file
5
out/auth-proxy.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="qs" />
|
||||||
|
import { RequestHandler } from 'express';
|
||||||
|
export declare const AuthProxy: {
|
||||||
|
router: RequestHandler<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs>;
|
||||||
|
};
|
||||||
49
out/auth-proxy.js
Normal file
49
out/auth-proxy.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"use strict";
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.AuthProxy = void 0;
|
||||||
|
const _1 = require(".");
|
||||||
|
const node_fetch_1 = require("node-fetch");
|
||||||
|
const router = (req, res, next) => {
|
||||||
|
const resolvable = new _1.Resolvable(() => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
|
if (!_1.DefaultConfig.USERINFO_HEADER) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const token = req.header(_1.DefaultConfig.USERINFO_HEADER);
|
||||||
|
const url = _1.DefaultConfig.AUTH_PROXY_USERINFO_URL ||
|
||||||
|
_1.DefaultConfig.AUTH_PROXY_URL && _1.urlJoin(_1.DefaultConfig.AUTH_PROXY_URL, "userinfo");
|
||||||
|
if (token === undefined || url === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = yield node_fetch_1.default(url, { headers: [[_1.DefaultConfig.USERINFO_HEADER, token]] });
|
||||||
|
return yield res.json();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
_1.Logger.warn(e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
req.getUserInfo = () => resolvable.resolve();
|
||||||
|
res.initLogout = function () {
|
||||||
|
const url = _1.DefaultConfig.AUTH_PROXY_INIT_LOGOUT_URL ||
|
||||||
|
_1.DefaultConfig.AUTH_PROXY_URL && _1.urlJoin(_1.DefaultConfig.AUTH_PROXY_URL, "init-logout");
|
||||||
|
if (url === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.redirect(307, url);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
exports.AuthProxy = {
|
||||||
|
router,
|
||||||
|
};
|
||||||
3
out/auto-reload.d.ts
vendored
Normal file
3
out/auto-reload.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export declare const AutoReloader: {
|
||||||
|
router: import("express-serve-static-core").Router;
|
||||||
|
};
|
||||||
61
out/auto-reload.js
Normal file
61
out/auto-reload.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.AutoReloader = void 0;
|
||||||
|
const express_1 = require("express");
|
||||||
|
const _1 = require(".");
|
||||||
|
const uuid_1 = require("uuid");
|
||||||
|
const router = express_1.Router();
|
||||||
|
if (!_1.DefaultConfig.isProduction) {
|
||||||
|
let uuid = uuid_1.v4();
|
||||||
|
let updateTimeout = undefined;
|
||||||
|
Promise.resolve().then(() => require("node-watch")).then((watch) => {
|
||||||
|
watch.default('public', { recursive: true }, () => {
|
||||||
|
if (updateTimeout !== undefined)
|
||||||
|
clearTimeout(updateTimeout);
|
||||||
|
updateTimeout = setTimeout(() => {
|
||||||
|
uuid = uuid_1.v4();
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}).catch((err) => { _1.Logger.error(err); });
|
||||||
|
router.get("/auto-reload/client.js", (req, res) => {
|
||||||
|
_1.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+'${_1.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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.AutoReloader = {
|
||||||
|
router,
|
||||||
|
};
|
||||||
17
out/config.d.ts
vendored
Normal file
17
out/config.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
declare function requireEnv(name: string, onlyInProduction?: boolean): void;
|
||||||
|
export declare const DefaultConfig: {
|
||||||
|
EXTERNAL_BASE_URL: string;
|
||||||
|
isProduction: boolean;
|
||||||
|
requireEnv: typeof requireEnv;
|
||||||
|
NODE_ENV: string;
|
||||||
|
PORT: number;
|
||||||
|
HOSTNAME: string;
|
||||||
|
BASE_PATH: string;
|
||||||
|
REDIS_URL: string | undefined;
|
||||||
|
SESSION_SECRET: string | undefined;
|
||||||
|
USERINFO_HEADER: string | undefined;
|
||||||
|
AUTH_PROXY_URL: string | undefined;
|
||||||
|
AUTH_PROXY_USERINFO_URL: string | undefined;
|
||||||
|
AUTH_PROXY_INIT_LOGOUT_URL: string | undefined;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
36
out/config.js
Normal file
36
out/config.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.DefaultConfig = void 0;
|
||||||
|
const env = require("env-var");
|
||||||
|
const _1 = require(".");
|
||||||
|
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, onlyInProduction = false) {
|
||||||
|
env.get(name).required(!onlyInProduction || isProduction).asString();
|
||||||
|
}
|
||||||
|
exports.DefaultConfig = Object.assign(Object.assign({}, envs), { EXTERNAL_BASE_URL: envs.EXTERNAL_BASE_URL ||
|
||||||
|
_1.urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH), isProduction,
|
||||||
|
requireEnv });
|
||||||
27
out/helpers/resolvable.d.ts
vendored
Normal file
27
out/helpers/resolvable.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
declare enum ResolvableState {
|
||||||
|
WAITING = 0,
|
||||||
|
PENDING = 1,
|
||||||
|
ERROR = 2,
|
||||||
|
DONE = 3
|
||||||
|
}
|
||||||
|
declare class FetchOnce<T, U extends Array<unknown>> {
|
||||||
|
protected fetchMethod?: ((...args: U) => Promise<T>) | undefined;
|
||||||
|
protected data: T | undefined;
|
||||||
|
protected error: unknown | undefined;
|
||||||
|
protected state: ResolvableState;
|
||||||
|
protected pendings: [(res: Promise<T> | T) => void, (reason: unknown) => void][];
|
||||||
|
constructor(fetchMethod?: ((...args: U) => Promise<T>) | undefined);
|
||||||
|
resolve(...args: U): Promise<T>;
|
||||||
|
protected isFinished(): boolean;
|
||||||
|
protected parsePromise(promise: Promise<T>): void;
|
||||||
|
}
|
||||||
|
export declare class Resolvable<T, U extends Array<unknown>> extends FetchOnce<T, U> {
|
||||||
|
constructor(fetchMethod: (...args: U) => Promise<T>);
|
||||||
|
}
|
||||||
|
export declare class WaitForSync<T> extends FetchOnce<T, never> {
|
||||||
|
protected state: ResolvableState;
|
||||||
|
constructor();
|
||||||
|
setData(data: T): void;
|
||||||
|
setError(error: unknown): void;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
85
out/helpers/resolvable.js
Normal file
85
out/helpers/resolvable.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"use strict";
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.WaitForSync = exports.Resolvable = void 0;
|
||||||
|
var ResolvableState;
|
||||||
|
(function (ResolvableState) {
|
||||||
|
ResolvableState[ResolvableState["WAITING"] = 0] = "WAITING";
|
||||||
|
ResolvableState[ResolvableState["PENDING"] = 1] = "PENDING";
|
||||||
|
ResolvableState[ResolvableState["ERROR"] = 2] = "ERROR";
|
||||||
|
ResolvableState[ResolvableState["DONE"] = 3] = "DONE";
|
||||||
|
})(ResolvableState || (ResolvableState = {}));
|
||||||
|
class FetchOnce {
|
||||||
|
constructor(fetchMethod) {
|
||||||
|
this.fetchMethod = fetchMethod;
|
||||||
|
this.state = ResolvableState.WAITING;
|
||||||
|
this.pendings = [];
|
||||||
|
}
|
||||||
|
resolve(...args) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
isFinished() {
|
||||||
|
return this.state === ResolvableState.DONE || this.state === ResolvableState.ERROR;
|
||||||
|
}
|
||||||
|
parsePromise(promise) {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Resolvable extends FetchOnce {
|
||||||
|
constructor(fetchMethod) {
|
||||||
|
super(fetchMethod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.Resolvable = Resolvable;
|
||||||
|
class WaitForSync extends FetchOnce {
|
||||||
|
constructor() {
|
||||||
|
super(undefined);
|
||||||
|
this.state = ResolvableState.PENDING;
|
||||||
|
}
|
||||||
|
setData(data) {
|
||||||
|
if (!this.isFinished()) {
|
||||||
|
this.parsePromise((() => __awaiter(this, void 0, void 0, function* () { return data; }))());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setError(error) {
|
||||||
|
if (!this.isFinished()) {
|
||||||
|
this.parsePromise((() => __awaiter(this, void 0, void 0, function* () { throw error; }))());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.WaitForSync = WaitForSync;
|
||||||
2
out/helpers/urlJoin.d.ts
vendored
Normal file
2
out/helpers/urlJoin.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import * as properUrlJoin from 'proper-url-join';
|
||||||
|
export declare const urlJoin: properUrlJoin.default;
|
||||||
5
out/helpers/urlJoin.js
Normal file
5
out/helpers/urlJoin.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.urlJoin = void 0;
|
||||||
|
const properUrlJoin = require("proper-url-join");
|
||||||
|
exports.urlJoin = properUrlJoin;
|
||||||
10
out/helpers/userinfo.d.ts
vendored
Normal file
10
out/helpers/userinfo.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export declare type UserInfo = {
|
||||||
|
email: string;
|
||||||
|
email_verified: boolean;
|
||||||
|
family_name: string;
|
||||||
|
given_name: string;
|
||||||
|
groups: string[];
|
||||||
|
name: string;
|
||||||
|
preferred_username: string;
|
||||||
|
sub: string;
|
||||||
|
};
|
||||||
2
out/helpers/userinfo.js
Normal file
2
out/helpers/userinfo.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
11
out/index.d.ts
vendored
Normal file
11
out/index.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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';
|
||||||
|
export { UserInfo } from './helpers/userinfo';
|
||||||
25
out/index.js
Normal file
25
out/index.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Permissions = exports.Session = exports.Polyfill = exports.AutoReloader = exports.urlJoin = exports.WaitForSync = exports.Resolvable = exports.AuthProxy = exports.HttpLogger = exports.Logger = exports.Redis = exports.DefaultConfig = void 0;
|
||||||
|
var config_1 = require("./config");
|
||||||
|
Object.defineProperty(exports, "DefaultConfig", { enumerable: true, get: function () { return config_1.DefaultConfig; } });
|
||||||
|
var redis_1 = require("./redis");
|
||||||
|
Object.defineProperty(exports, "Redis", { enumerable: true, get: function () { return redis_1.Redis; } });
|
||||||
|
var logging_1 = require("./logging");
|
||||||
|
Object.defineProperty(exports, "Logger", { enumerable: true, get: function () { return logging_1.Logger; } });
|
||||||
|
Object.defineProperty(exports, "HttpLogger", { enumerable: true, get: function () { return logging_1.HttpLogger; } });
|
||||||
|
var auth_proxy_1 = require("./auth-proxy");
|
||||||
|
Object.defineProperty(exports, "AuthProxy", { enumerable: true, get: function () { return auth_proxy_1.AuthProxy; } });
|
||||||
|
var resolvable_1 = require("./helpers/resolvable");
|
||||||
|
Object.defineProperty(exports, "Resolvable", { enumerable: true, get: function () { return resolvable_1.Resolvable; } });
|
||||||
|
Object.defineProperty(exports, "WaitForSync", { enumerable: true, get: function () { return resolvable_1.WaitForSync; } });
|
||||||
|
var urlJoin_1 = require("./helpers/urlJoin");
|
||||||
|
Object.defineProperty(exports, "urlJoin", { enumerable: true, get: function () { return urlJoin_1.urlJoin; } });
|
||||||
|
var auto_reload_1 = require("./auto-reload");
|
||||||
|
Object.defineProperty(exports, "AutoReloader", { enumerable: true, get: function () { return auto_reload_1.AutoReloader; } });
|
||||||
|
var polyfill_1 = require("./polyfill");
|
||||||
|
Object.defineProperty(exports, "Polyfill", { enumerable: true, get: function () { return polyfill_1.Polyfill; } });
|
||||||
|
var session_1 = require("./session");
|
||||||
|
Object.defineProperty(exports, "Session", { enumerable: true, get: function () { return session_1.Session; } });
|
||||||
|
var permissions_1 = require("./permissions");
|
||||||
|
Object.defineProperty(exports, "Permissions", { enumerable: true, get: function () { return permissions_1.Permissions; } });
|
||||||
13
out/logging.d.ts
vendored
Normal file
13
out/logging.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import * as winston from 'winston';
|
||||||
|
import { RequestHandler } from 'express';
|
||||||
|
export declare const Logger: {
|
||||||
|
error: (...args: unknown[]) => winston.Logger;
|
||||||
|
http: (...args: unknown[]) => winston.Logger;
|
||||||
|
info: (...args: unknown[]) => winston.Logger;
|
||||||
|
silly: (...args: unknown[]) => winston.Logger;
|
||||||
|
warn: (...args: unknown[]) => winston.Logger;
|
||||||
|
verbose: (...args: unknown[]) => winston.Logger;
|
||||||
|
debug: (...args: unknown[]) => winston.Logger;
|
||||||
|
log: (...args: unknown[]) => winston.Logger;
|
||||||
|
};
|
||||||
|
export declare const HttpLogger: RequestHandler;
|
||||||
47
out/logging.js
Normal file
47
out/logging.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.HttpLogger = exports.Logger = void 0;
|
||||||
|
const winston = require("winston");
|
||||||
|
const colors = require("colors");
|
||||||
|
const _1 = require(".");
|
||||||
|
const prune = require("json-prune");
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: _1.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"];
|
||||||
|
const wrapper = (original) => {
|
||||||
|
return (...args) => {
|
||||||
|
return original(args.map((obj) => typeof obj === "string" ? obj : prune(obj)).join(" "));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
exports.Logger = {};
|
||||||
|
for (const level of levels) {
|
||||||
|
exports.Logger[level] = wrapper(logger[level]);
|
||||||
|
}
|
||||||
|
exports.Logger.log = wrapper(logger["silly"]);
|
||||||
|
exports.HttpLogger = (req, res, next) => {
|
||||||
|
const start = Date.now();
|
||||||
|
const path = req.path;
|
||||||
|
const end = res.end;
|
||||||
|
res.end = function (...args) {
|
||||||
|
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)
|
||||||
|
exports.Logger.http(`${status} ${method} ${responseTime}ms ${path}`);
|
||||||
|
end.apply(res, args);
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
};
|
||||||
24
out/permissions.d.ts
vendored
Normal file
24
out/permissions.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { AccessControl, IQueryInfo, Permission } from 'role-acl';
|
||||||
|
import { Query } from 'role-acl/lib/src/core/Query';
|
||||||
|
import { Request, RequestHandler } from 'express';
|
||||||
|
declare class PermissionManager extends AccessControl {
|
||||||
|
can(roleOrRequest: Request | string | string[] | IQueryInfo): PermQuery;
|
||||||
|
getRouter(resource: string, opts: Partial<RermRouterOpts>): RequestHandler;
|
||||||
|
}
|
||||||
|
export declare type RermRouterOpts = {
|
||||||
|
context: unknown;
|
||||||
|
action: string;
|
||||||
|
skipConditions: boolean;
|
||||||
|
};
|
||||||
|
export declare class PermQuery extends Query {
|
||||||
|
protected resolveRequest: Request | undefined;
|
||||||
|
constructor(grants: unknown, roleOrRequest: Request | string | string[] | IQueryInfo);
|
||||||
|
on(resource: string, skipConditions?: boolean): Promise<Permission>;
|
||||||
|
context(context: unknown): PermQuery;
|
||||||
|
skipConditions(value: boolean): PermQuery;
|
||||||
|
with(context: unknown): PermQuery;
|
||||||
|
execute(action: string): PermQuery;
|
||||||
|
sync(): PermQuery;
|
||||||
|
}
|
||||||
|
export declare const Permissions: PermissionManager;
|
||||||
|
export {};
|
||||||
92
out/permissions.js
Normal file
92
out/permissions.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"use strict";
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Permissions = exports.PermQuery = void 0;
|
||||||
|
const role_acl_1 = require("role-acl");
|
||||||
|
const Query_1 = require("role-acl/lib/src/core/Query");
|
||||||
|
// see https://www.npmjs.com/package/role-acl
|
||||||
|
class PermissionManager extends role_acl_1.AccessControl {
|
||||||
|
can(roleOrRequest) {
|
||||||
|
return new PermQuery(this.getGrants(), roleOrRequest);
|
||||||
|
}
|
||||||
|
getRouter(resource, opts) {
|
||||||
|
return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
|
||||||
|
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 = yield query.on(resource);
|
||||||
|
if (permission.granted) {
|
||||||
|
req.permissionDetails = permission;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.sendStatus(403);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class PermQuery extends Query_1.Query {
|
||||||
|
constructor(grants, roleOrRequest) {
|
||||||
|
function isRequest(obj) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on(resource, skipConditions) {
|
||||||
|
const _super = Object.create(null, {
|
||||||
|
on: { get: () => super.on }
|
||||||
|
});
|
||||||
|
var _a;
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
if (this.resolveRequest) {
|
||||||
|
const userInfo = yield this.resolveRequest.getUserInfo();
|
||||||
|
this.role((_a = userInfo === null || userInfo === void 0 ? void 0 : userInfo.groups) !== null && _a !== void 0 ? _a : []);
|
||||||
|
}
|
||||||
|
if (typeof this._.role === 'object' && this._.role.includes('noaccess') ||
|
||||||
|
typeof this._.role === 'string' && this._.role === 'noaccess') {
|
||||||
|
this.role([]);
|
||||||
|
}
|
||||||
|
return _super.on.call(this, resource, skipConditions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
context(context) {
|
||||||
|
super.context(context);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
skipConditions(value) {
|
||||||
|
super.skipConditions(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
with(context) {
|
||||||
|
super.with(context);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
execute(action) {
|
||||||
|
super.execute(action);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
sync() {
|
||||||
|
throw new role_acl_1.AccessControlError("Sync method is not allowed on PermissionManager!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.PermQuery = PermQuery;
|
||||||
|
exports.Permissions = new PermissionManager();
|
||||||
1
out/polyfill-worker.d.ts
vendored
Normal file
1
out/polyfill-worker.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export {};
|
||||||
25
out/polyfill-worker.js
Normal file
25
out/polyfill-worker.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
// workers/add.js
|
||||||
|
const worker_1 = require("threads/worker");
|
||||||
|
const fs = require("fs");
|
||||||
|
const polyfill_analyzer_1 = require("@10xjs/polyfill-analyzer");
|
||||||
|
const polyfills_1 = require("@10xjs/polyfill-analyzer/dist/polyfills");
|
||||||
|
worker_1.expose(() => {
|
||||||
|
const exclude = [
|
||||||
|
"console.markTimeline",
|
||||||
|
"console.timeline",
|
||||||
|
"console.timelineEnd",
|
||||||
|
];
|
||||||
|
const featureList = polyfill_analyzer_1.analyze({
|
||||||
|
source: fs.readFileSync('./public/js/bundle.js', 'utf-8'),
|
||||||
|
include: polyfills_1.default.filter(x => !exclude.includes(x)),
|
||||||
|
// Not all features listed by polyfillLibrary.listAllPolyfills()` can be detected.
|
||||||
|
unsupportedPolyfill: 'ignore',
|
||||||
|
});
|
||||||
|
const feats = {};
|
||||||
|
for (const feature of featureList) {
|
||||||
|
feats[feature] = {};
|
||||||
|
}
|
||||||
|
return feats;
|
||||||
|
});
|
||||||
7
out/polyfill.d.ts
vendored
Normal file
7
out/polyfill.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { RequestHandler } from 'express';
|
||||||
|
import { PolyfillOptions } from 'polyfill-library';
|
||||||
|
declare function getRouter(opts?: Partial<PolyfillOptions>): RequestHandler;
|
||||||
|
export declare const Polyfill: {
|
||||||
|
getRouter: typeof getRouter;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
39
out/polyfill.js
Normal file
39
out/polyfill.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"use strict";
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Polyfill = void 0;
|
||||||
|
const polyfillLibrary = require("polyfill-library");
|
||||||
|
const _1 = require(".");
|
||||||
|
const threads_1 = require("threads");
|
||||||
|
const features = new _1.WaitForSync();
|
||||||
|
(() => __awaiter(void 0, void 0, void 0, function* () {
|
||||||
|
const worker = yield threads_1.spawn(new threads_1.Worker("./polyfill-worker"));
|
||||||
|
const feats = yield worker();
|
||||||
|
yield threads_1.Thread.terminate(worker);
|
||||||
|
return feats;
|
||||||
|
}))()
|
||||||
|
.then(feats => {
|
||||||
|
feats["fetch"] = {};
|
||||||
|
_1.Logger.debug("Polyfill analysed:", Object.keys(feats));
|
||||||
|
features.setData(feats);
|
||||||
|
})
|
||||||
|
.catch(err => features.setError(err));
|
||||||
|
function getRouter(opts) {
|
||||||
|
const options = Object.assign({ minify: _1.DefaultConfig.isProduction, unknown: "polyfill" }, opts);
|
||||||
|
return (req, res) => __awaiter(this, void 0, void 0, function* () {
|
||||||
|
const polyfillBundle = yield polyfillLibrary.getPolyfillString(Object.assign(Object.assign({}, options), { uaString: req.header("user-agent"), features: yield features.resolve(), stream: false }));
|
||||||
|
res.setHeader('Content-Type', 'text/javascript');
|
||||||
|
res.send(polyfillBundle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
exports.Polyfill = {
|
||||||
|
getRouter: getRouter,
|
||||||
|
};
|
||||||
9
out/redis.d.ts
vendored
Normal file
9
out/redis.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import * as redis from 'redis';
|
||||||
|
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 declare const Redis: RedisType;
|
||||||
|
export {};
|
||||||
76
out/redis.js
Normal file
76
out/redis.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"use strict";
|
||||||
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||||||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Redis = void 0;
|
||||||
|
const redis = require("redis");
|
||||||
|
const _1 = require(".");
|
||||||
|
const util_1 = require("util");
|
||||||
|
class RealRedis {
|
||||||
|
get client() {
|
||||||
|
if (this._client !== undefined)
|
||||||
|
return this._client;
|
||||||
|
_1.DefaultConfig.requireEnv('REDIS_URL');
|
||||||
|
this._client = redis.createClient({ url: _1.DefaultConfig.REDIS_URL });
|
||||||
|
return this._client;
|
||||||
|
}
|
||||||
|
get(key) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
if (!this._promGet)
|
||||||
|
this._promGet = util_1.promisify(this.client.get);
|
||||||
|
const value = yield this._promGet(key);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
return value === null ? undefined : value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
set(key, value) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
if (!this._promSet)
|
||||||
|
this._promSet = util_1.promisify(this.client.set);
|
||||||
|
yield this._promSet(key, value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
del(...keys) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
if (!this._promDel)
|
||||||
|
this._promDel = util_1.promisify(this.client.del);
|
||||||
|
return yield this._promDel(...keys);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class MockRedis {
|
||||||
|
constructor() {
|
||||||
|
this.inMemory = {};
|
||||||
|
}
|
||||||
|
get client() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
get(key) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
return this.inMemory[key] || undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
set(key, value) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
this.inMemory[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
del(...keys) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
for (const key of keys) {
|
||||||
|
delete this.inMemory[key];
|
||||||
|
}
|
||||||
|
return keys.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.Redis = _1.DefaultConfig.REDIS_URL || _1.DefaultConfig.isProduction
|
||||||
|
? new RealRedis()
|
||||||
|
: new MockRedis();
|
||||||
7
out/session.d.ts
vendored
Normal file
7
out/session.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import * as session from 'express-session';
|
||||||
|
import { RequestHandler } from 'express';
|
||||||
|
declare function getRouter(options?: Partial<session.SessionOptions>): RequestHandler;
|
||||||
|
export declare const Session: {
|
||||||
|
getRouter: typeof getRouter;
|
||||||
|
};
|
||||||
|
export {};
|
||||||
18
out/session.js
Normal file
18
out/session.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.Session = void 0;
|
||||||
|
const session = require("express-session");
|
||||||
|
const _1 = require(".");
|
||||||
|
const redisStore = require("connect-redis");
|
||||||
|
let sessionStore = undefined;
|
||||||
|
function getRouter(options) {
|
||||||
|
_1.DefaultConfig.requireEnv('SESSION_SECRET', true);
|
||||||
|
if (_1.Redis.client && sessionStore !== undefined) {
|
||||||
|
const RedisStore = redisStore(session);
|
||||||
|
sessionStore = new RedisStore({ client: _1.Redis.client });
|
||||||
|
}
|
||||||
|
return session(Object.assign({ store: sessionStore, secret: _1.DefaultConfig.SESSION_SECRET || 'keyboard cat', resave: false, saveUninitialized: true, cookie: { secure: false } }, options));
|
||||||
|
}
|
||||||
|
exports.Session = {
|
||||||
|
getRouter,
|
||||||
|
};
|
||||||
3392
package-lock.json
generated
Normal file
3392
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
59
package.json
Normal file
59
package.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "pkg-express-utils",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "out/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint . --ext .ts",
|
||||||
|
"lint-fix": "eslint . --ext .ts --fix",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"install-debug": "npm install && npm run build",
|
||||||
|
"install-prod": "npm install --only=dev && npm install --only=prod && npm run build && npm run del-node-module && npm install --only=prod",
|
||||||
|
"build": "tsc",
|
||||||
|
"del-node-module:default": "rm -r node_modules",
|
||||||
|
"del-node-module:windows": "if exist node_modules rmdir /Q/S node_modules",
|
||||||
|
"del-node-module": "run-script-os"
|
||||||
|
},
|
||||||
|
"author": "Sebastian Seedorf",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@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",
|
||||||
|
"threads": "^1.6.3",
|
||||||
|
"tiny-worker": "^2.3.0",
|
||||||
|
"uuid": "^8.3.1",
|
||||||
|
"winston": "^3.3.3",
|
||||||
|
"run-script-os": "^1.1.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/connect-redis": "0.0.15",
|
||||||
|
"@types/express": "^4.17.8",
|
||||||
|
"@types/express-session": "^1.17.1",
|
||||||
|
"@types/http-errors": "^1.8.0",
|
||||||
|
"@types/node": "^14.14.7",
|
||||||
|
"@types/node-fetch": "^2.5.7",
|
||||||
|
"@types/proper-url-join": "^2.0.0",
|
||||||
|
"@types/redis": "^2.8.28",
|
||||||
|
"@types/uuid": "^8.3.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.7.0",
|
||||||
|
"@typescript-eslint/parser": "^4.7.0",
|
||||||
|
"concurrently": "^5.3.0",
|
||||||
|
"eslint": "^7.13.0",
|
||||||
|
"eslint-plugin-no-null": "^1.0.2",
|
||||||
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
|
"node-watch": "^0.7.0",
|
||||||
|
"stream": "0.0.2",
|
||||||
|
"tsc-watch": "^4.2.9",
|
||||||
|
"typescript": "^4.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/auth-proxy.ts
Normal file
40
src/auth-proxy.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {Request, RequestHandler} from 'express';
|
||||||
|
import {DefaultConfig, Logger, Resolvable, urlJoin, UserInfo} 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,
|
||||||
|
};
|
||||||
62
src/auto-reload.ts
Normal file
62
src/auto-reload.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
44
src/config.ts
Normal file
44
src/config.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
79
src/helpers/resolvable.ts
Normal file
79
src/helpers/resolvable.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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; })());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/helpers/urlJoin.ts
Normal file
3
src/helpers/urlJoin.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import * as properUrlJoin from 'proper-url-join';
|
||||||
|
|
||||||
|
export const urlJoin = properUrlJoin as unknown as properUrlJoin.default;
|
||||||
10
src/helpers/userinfo.ts
Normal file
10
src/helpers/userinfo.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export type UserInfo = {
|
||||||
|
email: string,
|
||||||
|
email_verified: boolean,
|
||||||
|
family_name: string,
|
||||||
|
given_name: string,
|
||||||
|
groups: string[],
|
||||||
|
name: string,
|
||||||
|
preferred_username: string,
|
||||||
|
sub: string,
|
||||||
|
};
|
||||||
12
src/index.ts
Normal file
12
src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export { UserInfo } from './helpers/userinfo';
|
||||||
55
src/logging.ts
Normal file
55
src/logging.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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();
|
||||||
|
};
|
||||||
92
src/permissions.ts
Normal file
92
src/permissions.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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();
|
||||||
25
src/polyfill-worker.ts
Normal file
25
src/polyfill-worker.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// 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;
|
||||||
|
});
|
||||||
43
src/polyfill.ts
Normal file
43
src/polyfill.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import {RequestHandler} from 'express';
|
||||||
|
import * as polyfillLibrary from 'polyfill-library';
|
||||||
|
import {PolyfillFeatureList, PolyfillOptions} 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));
|
||||||
|
|
||||||
|
|
||||||
|
function getRouter(opts?: Partial<PolyfillOptions>): RequestHandler {
|
||||||
|
const options: Partial<PolyfillOptions> = {
|
||||||
|
minify: DefaultConfig.isProduction,
|
||||||
|
unknown: "polyfill",
|
||||||
|
...opts
|
||||||
|
}
|
||||||
|
return async (req, res) => {
|
||||||
|
const polyfillBundle = await polyfillLibrary.getPolyfillString({
|
||||||
|
...options,
|
||||||
|
uaString: req.header("user-agent"),
|
||||||
|
features: await features.resolve(),
|
||||||
|
stream: false,
|
||||||
|
});
|
||||||
|
res.setHeader('Content-Type', 'text/javascript');
|
||||||
|
res.send(polyfillBundle);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const Polyfill = {
|
||||||
|
getRouter: getRouter,
|
||||||
|
};
|
||||||
70
src/redis.ts
Normal file
70
src/redis.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
28
src/session.ts
Normal file
28
src/session.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
9
src/types/auth-proxy.d.ts
vendored
Normal file
9
src/types/auth-proxy.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
interface Request {
|
||||||
|
getUserInfo(): Promise<import('../helpers/userinfo').UserInfo|undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
initLogout(): boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/types/json-prune.d.ts
vendored
Normal file
5
src/types/json-prune.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare module "json-prune" {
|
||||||
|
function prune(object: unknown): string;
|
||||||
|
|
||||||
|
export = prune;
|
||||||
|
}
|
||||||
5
src/types/logging.d.ts
vendored
Normal file
5
src/types/logging.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
interface Request {
|
||||||
|
noLogging: boolean|undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/types/permissions.d.ts
vendored
Normal file
5
src/types/permissions.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace Express {
|
||||||
|
interface Request {
|
||||||
|
permissionDetails?: import('role-acl').Permission;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/types/polyfill.d.ts
vendored
Normal file
37
src/types/polyfill.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
declare module "polyfill-library" {
|
||||||
|
import {Readable} from 'stream';
|
||||||
|
|
||||||
|
function listAllPolyfills(): string[];
|
||||||
|
function describePolyfill(featureName: string): Promise<PolyfillMetadata|undefined>;
|
||||||
|
function getOptions(opts: Partial<PolyfillOptions>): PolyfillOptions;
|
||||||
|
function getPolyfills(opts: Partial<PolyfillOptions>): Promise<PolyfillFeatures>;
|
||||||
|
function getPolyfillString(opts: Partial<PolyfillOptions>&{stream?: true}): Readable;
|
||||||
|
function getPolyfillString(opts: Partial<PolyfillOptions>&{stream: false}): Promise<string>;
|
||||||
|
|
||||||
|
type PolyfillMetadata = {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
type PolyfillFeatureList = {
|
||||||
|
[featureName: string]: {
|
||||||
|
flags?: string[]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type PolyfillOptions = {
|
||||||
|
minify: boolean,
|
||||||
|
unknown: 'polyfill'|'ignore',
|
||||||
|
features: PolyfillFeatureList,
|
||||||
|
excludes: string[],
|
||||||
|
uaString: string,
|
||||||
|
rum: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
type PolyfillFeature = {
|
||||||
|
flags: string[],
|
||||||
|
dependencyOf: string[],
|
||||||
|
aliasOf: string[],
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolyfillFeatures = { [featureName: string]: PolyfillFeature };
|
||||||
|
}
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6",
|
||||||
|
"outDir": "./out",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"./node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user