Added Permission Middleware, Renaming
This commit is contained in:
@@ -33,6 +33,8 @@ module.exports = {
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-style": "error",
|
||||
"semi": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
|
||||
"promise/always-return": "off",
|
||||
},
|
||||
};
|
||||
|
||||
24
src/app.ts
24
src/app.ts
@@ -2,13 +2,10 @@ import * as createError from 'http-errors';
|
||||
import * as express from 'express';
|
||||
import {NextFunction, Request, Response} from 'express';
|
||||
import * as path from 'path';
|
||||
import * as redisStore from 'connect-redis';
|
||||
import * as session from 'express-session';
|
||||
import * as sassMiddleware from 'node-sass-middleware';
|
||||
import * as compression from 'compression';
|
||||
import indexRouter from './routes';
|
||||
import {HttpLogger, Redis, Config, setupAuthProxy, getReloadRouter, polyfillRoute} from './utils';
|
||||
import {Store} from 'express-session';
|
||||
import {HttpLogger, Config, AuthProxy, AutoReloader, Polyfill, Session} from './utils';
|
||||
|
||||
export const app = express();
|
||||
|
||||
@@ -31,26 +28,15 @@ app.use(express.urlencoded({extended: false}));
|
||||
app.use(compression());
|
||||
|
||||
// auth proxy middleware
|
||||
router.use(setupAuthProxy);
|
||||
router.use(AuthProxy.router);
|
||||
// auto reloader (when running in debug mode)
|
||||
router.use(getReloadRouter());
|
||||
router.use(AutoReloader.router);
|
||||
|
||||
// session
|
||||
let sessionStore: Store|undefined = undefined;
|
||||
if (Redis.client) {
|
||||
const RedisStore = redisStore(session);
|
||||
sessionStore = new RedisStore({client: Redis.client});
|
||||
}
|
||||
router.use(session({
|
||||
store: sessionStore,
|
||||
secret: Config.SESSION_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {secure: false},
|
||||
}));
|
||||
router.use(Session.getRouter());
|
||||
|
||||
// static config
|
||||
router.use("/js/polyfill.js", polyfillRoute);
|
||||
router.use("/js/polyfill.js", Polyfill.router);
|
||||
router.use(sassMiddleware({
|
||||
src: path.join(__dirname, '../public'),
|
||||
dest: path.join(__dirname, '../public'),
|
||||
|
||||
@@ -4,7 +4,7 @@ import {Resolvable} from './helpers/resolvable';
|
||||
import fetch from 'node-fetch';
|
||||
import {urlJoin} from './helpers/urlJoin';
|
||||
|
||||
export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
|
||||
const router: RequestHandler = (req: Request, res, next) => {
|
||||
const resolvable = new Resolvable(async () => {
|
||||
if (!Config.USERINFO_HEADER) {
|
||||
return undefined;
|
||||
@@ -34,3 +34,7 @@ export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
|
||||
};
|
||||
next();
|
||||
};
|
||||
|
||||
export const AuthProxy = {
|
||||
router,
|
||||
};
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
import {Router} from 'express';
|
||||
import {Config} from './config';
|
||||
import {Logger} from './logging';
|
||||
import {Config, Logger} from '.';
|
||||
import {urlJoin} from './helpers/urlJoin';
|
||||
import {v4} from 'uuid';
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
export function getReloadRouter(): Router {
|
||||
const reloadRouter = Router();
|
||||
if (!Config.isProduction) {
|
||||
let uuid = v4();
|
||||
let updateTimeout: Timeout|undefined = undefined;
|
||||
import("node-watch").then((watch) => {
|
||||
watch.default('public', {recursive: true}, (evt, name) => {
|
||||
if (updateTimeout !== undefined) clearTimeout(updateTimeout);
|
||||
updateTimeout = setTimeout(() => {
|
||||
uuid = v4();
|
||||
}, 200);
|
||||
});
|
||||
}).catch((err) => { Logger.error(err); });
|
||||
|
||||
reloadRouter.get("/auto-reload/client.js", (req, res) => {
|
||||
Logger.debug(req.url, req.originalUrl, req.baseUrl);
|
||||
res.setHeader('Content-Type', "application/javascript");
|
||||
// language=JavaScript
|
||||
res.send(`
|
||||
const loc = window.location;
|
||||
const url = loc.protocol+'//'+loc.host+'${urlJoin(req.baseUrl, "/auto-reload")}';
|
||||
// const parse = async res => (await res.json()).uuid;
|
||||
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, ""));
|
||||
const router = Router();
|
||||
if (!Config.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); });
|
||||
|
||||
reloadRouter.get("/auto-reload", (req, res) => {
|
||||
req.noLogging = true;
|
||||
res.json({uuid});
|
||||
});
|
||||
}
|
||||
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, ""));
|
||||
});
|
||||
|
||||
return reloadRouter;
|
||||
router.get("/auto-reload", (req, res) => {
|
||||
req.noLogging = true;
|
||||
res.json({uuid});
|
||||
});
|
||||
}
|
||||
|
||||
export const AutoReloader = {
|
||||
router,
|
||||
};
|
||||
|
||||
@@ -16,10 +16,10 @@ const envs = {
|
||||
// external base url
|
||||
EXTERNAL_BASE_URL: env.get('EXTERNAL_BASE_URL').asString(),
|
||||
|
||||
// url of redis session store
|
||||
REDIS_URL: env.get('REDIS_URL').required(isProduction).asString() || undefined,
|
||||
// cookie secret for the session id
|
||||
SESSION_SECRET: env.get('REDIS_URL').required(isProduction).default('keyboard cat').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,
|
||||
@@ -29,14 +29,17 @@ const envs = {
|
||||
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);
|
||||
}
|
||||
|
||||
export const Config = {
|
||||
...envs,
|
||||
EXTERNAL_BASE_URL:
|
||||
envs.EXTERNAL_BASE_URL ||
|
||||
urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH),
|
||||
isProduction,
|
||||
EXTERNAL_BASE_URL:
|
||||
envs.EXTERNAL_BASE_URL ||
|
||||
urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH),
|
||||
}
|
||||
requireEnv,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export {Config} from './config';
|
||||
export {Redis} from './redis';
|
||||
export {Logger, HttpLogger} from './logging';
|
||||
export {setupAuthProxy} from './auth-proxy';
|
||||
export {AuthProxy} from './auth-proxy';
|
||||
export {Resolvable, WaitForSync} from './helpers/resolvable';
|
||||
export {getReloadRouter} from './auto-reload';
|
||||
export {polyfillRoute} from './polyfill';
|
||||
export {AutoReloader} from './auto-reload';
|
||||
export {Polyfill} from './polyfill';
|
||||
export {Session} from './session';
|
||||
export {Permissions} from './permissions';
|
||||
|
||||
85
src/utils/permissions.ts
Normal file
85
src/utils/permissions.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import {AccessControl, AccessControlError, IQueryInfo, Permission, Query} from 'role-acl';
|
||||
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 ?? []);
|
||||
}
|
||||
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,8 +1,7 @@
|
||||
import {RequestHandler} from 'express';
|
||||
import * as polyfillLibrary from 'polyfill-library';
|
||||
import {Config} from './config';
|
||||
import {Config, Logger} from '.';
|
||||
import {PolyfillFeatureList} from 'polyfill-library';
|
||||
import {Logger} from './logging';
|
||||
import {WaitForSync} from './helpers/resolvable';
|
||||
import {spawn, Thread, Worker} from 'threads';
|
||||
import {WorkerFunction} from 'threads/dist/types/worker';
|
||||
@@ -23,7 +22,7 @@ const features = new WaitForSync<PolyfillFeatureList>();
|
||||
.catch(err => features.setError(err));
|
||||
|
||||
|
||||
export const polyfillRoute: RequestHandler = async (req, res) => {
|
||||
const router: RequestHandler = async (req, res) => {
|
||||
const polyfillBundle = await polyfillLibrary.getPolyfillString({
|
||||
uaString: req.header("user-agent"),
|
||||
features: await features.resolve(),
|
||||
@@ -34,3 +33,7 @@ export const polyfillRoute: RequestHandler = async (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/javascript');
|
||||
res.send(polyfillBundle);
|
||||
};
|
||||
|
||||
export const Polyfill = {
|
||||
router: router,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as redis from 'redis';
|
||||
import {Config} from '.';
|
||||
import {promisify} from 'util';
|
||||
|
||||
Config.requireEnv('REDIS_URL', true);
|
||||
|
||||
const redisClient = Config.REDIS_URL && redis.createClient({url: Config.REDIS_URL});
|
||||
const inMemory: {[key: string]: string} = {};
|
||||
|
||||
|
||||
28
src/utils/session.ts
Normal file
28
src/utils/session.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {Store} from 'express-session';
|
||||
import {Redis, Config} from '.';
|
||||
import * as redisStore from "connect-redis";
|
||||
import * as session from 'express-session';
|
||||
import {RequestHandler} from 'express';
|
||||
|
||||
Config.requireEnv('SESSION_SECRET', true);
|
||||
|
||||
let sessionStore: Store|undefined = undefined;
|
||||
|
||||
function getRouter(options?: Partial<session.SessionOptions>): RequestHandler {
|
||||
if (Redis.client && sessionStore !== undefined) {
|
||||
const RedisStore = redisStore(session);
|
||||
sessionStore = new RedisStore({client: Redis.client});
|
||||
}
|
||||
return session({
|
||||
store: sessionStore,
|
||||
secret: Config.SESSION_SECRET || 'keyboard cat',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {secure: false},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export const Session = {
|
||||
getRouter,
|
||||
};
|
||||
5
src/utils/types/permissions.d.ts
vendored
Normal file
5
src/utils/types/permissions.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare namespace Express {
|
||||
interface Request {
|
||||
permissionDetails?: import('role-acl').Permission;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user