mirror of
https://github.com/sbrl/powahroot.git
synced 2024-11-21 14:13:01 +00:00
Import from Nibriboard-Panel
This commit is contained in:
commit
2fe1f30fad
9 changed files with 592 additions and 0 deletions
91
.gitignore
vendored
Normal file
91
.gitignore
vendored
Normal file
|
@ -0,0 +1,91 @@
|
|||
|
||||
# Created by https://www.gitignore.io/api/node
|
||||
# Edit at https://www.gitignore.io/?templates=node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# End of https://www.gitignore.io/api/node
|
29
.tern-project
Normal file
29
.tern-project
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"ecmaVersion": 8,
|
||||
"libs": [],
|
||||
"loadEagerly": [
|
||||
"**.mjs"
|
||||
],
|
||||
"dontLoad": [],
|
||||
"plugins": {
|
||||
"doc_comment": true,
|
||||
"node": {
|
||||
"dontLoad": "",
|
||||
"load": "",
|
||||
"modules": ""
|
||||
},
|
||||
"node_resolve": {},
|
||||
"modules": {
|
||||
"dontLoad": "",
|
||||
"load": "",
|
||||
"modules": ""
|
||||
},
|
||||
"es_modules": {},
|
||||
"requirejs": {
|
||||
"baseURL": "",
|
||||
"paths": "",
|
||||
"override": ""
|
||||
},
|
||||
"commonjs": {}
|
||||
}
|
||||
}
|
105
Client/Router.mjs
Normal file
105
Client/Router.mjs
Normal file
|
@ -0,0 +1,105 @@
|
|||
"use strict";
|
||||
|
||||
import EventEmitter from 'event-emitter-es6';
|
||||
|
||||
class ClientRouter extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
/***** Settings *****/
|
||||
this.listen_pushstate = true;
|
||||
this.verbose = false;
|
||||
|
||||
/********************/
|
||||
|
||||
for(let key in options) {
|
||||
this[key] = options[key];
|
||||
}
|
||||
|
||||
/********************/
|
||||
|
||||
/** Whether we should handle popstate events or not. @type {Boolean} */
|
||||
this.handle_popstates = true;
|
||||
this.routes = [];
|
||||
|
||||
window.addEventListener("popstate", ((event) => {
|
||||
if(!this.handle_popstates) return;
|
||||
|
||||
console.info(`[ClientRouter] Handling popstate - path: ${window.location.hash.substr(1)} (state`, event.state, `)`);
|
||||
this.navigate(window.location.hash.substr(1));
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
add_404(callback) {
|
||||
this.callback_404 = callback;
|
||||
}
|
||||
|
||||
add_page(routespec, callback) {
|
||||
this.routes.push({
|
||||
spec: routespec,
|
||||
match: this.pathspec_to_regex(routespec),
|
||||
callback
|
||||
});
|
||||
}
|
||||
|
||||
navigate(path) {
|
||||
for(let route_info of this.routes) {
|
||||
const matches = path.match(route_info.match.regex);
|
||||
if(matches) {
|
||||
if(this.verbose) console.log(`%c[Router] Matched against ${route_info.match.regex}!`, "color: hsl(331, 76%, 40%); font-weight: bold;");
|
||||
|
||||
// Build the parameters object
|
||||
let params = {};
|
||||
for(let i = 1; i < matches.length; i++) { // Skip the top-level group match
|
||||
params[route_info.match.tokens[i-1]] = matches[i];
|
||||
}
|
||||
|
||||
// Don't handle any popstates potentially generated by changing the hash
|
||||
this.handle_popstates = false;
|
||||
window.location.hash = `#${path}`;
|
||||
this.handle_popstates = true;
|
||||
|
||||
route_info.callback(params);
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.verbose) console.debug(`%c[Router] No match against ${route_info.match.regex} - moving on.`, "color: hsl(331, 76%, 40%)");
|
||||
}
|
||||
|
||||
if(this.verbose) console.warn(`Couldn't find a match for '${path}'.`);
|
||||
if(typeof this.callback_404 == "function")
|
||||
this.callback_404(path);
|
||||
}
|
||||
|
||||
navigate_current_hash() {
|
||||
this.navigate(window.location.hash.substr(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a path specification into a regular expression.
|
||||
* From the server-side sibling of this (client-side) router.
|
||||
* @param {string} pathspec The path specification to convert.
|
||||
* @return {RegExp} The resulting regular expression
|
||||
*/
|
||||
pathspec_to_regex(pathspec) {
|
||||
if(pathspec == "*") // Support wildcards
|
||||
return { regex: /^/, tokens: [] };
|
||||
|
||||
let tokens = [];
|
||||
let regex = new RegExp("^" + pathspec.replace(/::?([a-zA-Z0-9\-_]+)/g, (substr/*, index, template (not used)*/) => {
|
||||
tokens.push(substr.replace(/:/g, ""));
|
||||
|
||||
// FUTURE: We could add optional param support here too
|
||||
if(substr.startsWith("::"))
|
||||
return `(.+)`;
|
||||
else
|
||||
return `([^\/]+)`;
|
||||
}) + "$", "i");
|
||||
|
||||
if(this.verbose) console.info("[router/verbose] Created regex", regex);
|
||||
|
||||
return { regex, tokens };
|
||||
}
|
||||
}
|
||||
|
||||
export default ClientRouter;
|
169
Server/Router.mjs
Normal file
169
Server/Router.mjs
Normal file
|
@ -0,0 +1,169 @@
|
|||
"use strict";
|
||||
|
||||
import RouterContext from './RouterContext.mjs';
|
||||
|
||||
/**
|
||||
* A standalone HTTP router that's based on the principle of middleware.
|
||||
* Based on rill (see the npm package bearing the name), but stripped down and
|
||||
* simplified.
|
||||
*/
|
||||
class Router
|
||||
{
|
||||
constructor(verbose = false) {
|
||||
/** The actions to run in turn. */
|
||||
this.actions = [];
|
||||
/** Whether to activate versbose mode. Useful for debugging the router. */
|
||||
this.verbose = verbose;
|
||||
|
||||
this.default_action = async (ctx) => {
|
||||
let message = `No route was found for '${ctx.request.url}'.`;
|
||||
if(this.verbose) {
|
||||
message += `\n\nRegistered Actions:\n`;
|
||||
for(let action of this.actions) {
|
||||
message += ` - ${action.toString()}\n`;
|
||||
}
|
||||
}
|
||||
ctx.send.plain(404, message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Shortcut function for attaching an action to any request method. Full function: on */
|
||||
any(pathspec, action) { this.on("*", pathspec, action); }
|
||||
/** Shortcut function for attaching an action to head requests. Full function: on */
|
||||
head(pathspec, action) { this.on(["head"], pathspec, action); }
|
||||
/** Shortcut function for attaching an action to get requests. Full function: on */
|
||||
get(pathspec, action) { this.on(["get"], pathspec, action); }
|
||||
/** Shortcut function for attaching an action to post requests. Full function: on */
|
||||
post(pathspec, action) { this.on(["post"], pathspec, action); }
|
||||
/** Shortcut function for attaching an action to put requests. Full function: on */
|
||||
put(pathspec, action) { this.on(["put"], pathspec, action); }
|
||||
/** Shortcut function for attaching an action to delete requests. Full function: on */
|
||||
delete(pathspec, action) { this.on(["delete"], pathspec, action); }
|
||||
/** Shortcut function for attaching an action to options requests. Full function: on */
|
||||
options(pathspec, action) { this.on(["options"], pathspec, action); }
|
||||
|
||||
/**
|
||||
* Execute the specified action for requests matching the given parameters.
|
||||
* TODO: Consider merging with on_all and refactoring to take an object literal which we cna destructure instead
|
||||
* @param {array} methods The HTTP methods to run the action for. Include '*' to specify all methods.
|
||||
* @param {string|regex} pathspec The specification of the paths that this action should match against. May be a regular expression.
|
||||
* @param {Function} action The action to execute. Will be passed the parameters `context` (Object) and `next` (Function).
|
||||
*/
|
||||
on(methods, pathspec, action) {
|
||||
let regex_info = pathspec instanceof RegExp ? {regex: pathspec, tokens: [] } : this.pathspec_to_regex(pathspec);
|
||||
|
||||
// next must be a generator that returns each action in turn
|
||||
this.actions.push(async (context, next) => {
|
||||
const matches = context.url.pathname.match(regex_info.regex);
|
||||
|
||||
if(this.verbose) console.error(`[router/verbose] [${methods.join(", ")} -> ${pathspec} ] Matches: `, matches);
|
||||
|
||||
if((methods.indexOf(context.request.method.toLowerCase()) > -1 || methods.indexOf("*") > -1) && matches) {
|
||||
if(this.verbose) console.error(`[router/verbose] Match found! Executing action.`);
|
||||
for(let i = 1; i < matches.length; i++) { // Skip the top-level group
|
||||
context.params[regex_info.tokens[i-1]] = matches[i];
|
||||
}
|
||||
await action(context, next);
|
||||
}
|
||||
else {
|
||||
if(this.verbose) console.error(`[router/verbose] Nope, didn't match. Moving on`);
|
||||
await next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
on_all(action) {
|
||||
this.actions.push(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified action for requests if the provided testing function
|
||||
* returns true.
|
||||
* @param {Function} test The testing function. Will be passed the context as it's only parameter.
|
||||
* @param {Function} action The action to run if the test returns true.
|
||||
*/
|
||||
onif(test, action) {
|
||||
this.actions.push(async (context, next) => {
|
||||
let test_result = test(context);
|
||||
if(this.verbose) console.error("[router/verbose] Test action result: ", test_result);
|
||||
if(test_result)
|
||||
await action(context, next);
|
||||
else
|
||||
await next(context);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a path specification into a regular expression.
|
||||
* @param {string} pathspec The path specification to convert.
|
||||
* @return {RegExp} The resulting regular expression
|
||||
*/
|
||||
pathspec_to_regex(pathspec) {
|
||||
if(pathspec == "*") // Support wildcards
|
||||
return { regex: /^/, tokens: [] };
|
||||
|
||||
let tokens = [];
|
||||
let regex = new RegExp("^" + pathspec.replace(/::?([a-zA-Z0-9\-_]+)/g, (substr/*, index, template (not actually used)*/) => {
|
||||
tokens.push(substr.replace(/:/g, ""));
|
||||
|
||||
// FUTURE: We could add optional param support here too
|
||||
if(substr.startsWith("::"))
|
||||
return `(.+)`;
|
||||
else
|
||||
return `([^\/]+)`;
|
||||
}) + "$", "i");
|
||||
|
||||
/*if(this.verbose)*/ console.error("[router/verbose] Created regex", regex);
|
||||
|
||||
return { regex, tokens };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the specified request.
|
||||
* @param {http.ClientRequest} request The request to handle.
|
||||
* @param {http.ServerResponse} response The response object to use to send the response.
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
async handle(request, response) {
|
||||
let context = new RouterContext(request, response),
|
||||
iterator = this.iterate();
|
||||
|
||||
// Begin the middleware execution
|
||||
this.gen_next(iterator, context)();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an anonymous function that, when called, will execute the next
|
||||
* item of middleware.
|
||||
* It achieves this via a combination of a generator, anonymous function
|
||||
* scope abuse, being recursive, and magic.
|
||||
* @param {Generator} iterator The generator that emits the middleware.
|
||||
* @param {Object} context The context of the request.
|
||||
* @return {Function} A magic next function.
|
||||
*/
|
||||
gen_next(iterator, context) {
|
||||
let next_raw = iterator.next();
|
||||
// Return the default action if we've reached the end of the line
|
||||
if(next_raw.done)
|
||||
return async () => {
|
||||
this.default_action(context);
|
||||
};
|
||||
|
||||
return (async () => {
|
||||
if(this.verbose) console.error(`[router/verbose] Executing ${next_raw.value}`);
|
||||
await next_raw.value(context, this.gen_next(iterator, context));
|
||||
}).bind(this); // Don't forget to bind each successive function to this context
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all the generated middleware.
|
||||
* @return {Generator} A generator that returns each successive piece of middleware in turn.
|
||||
*/
|
||||
*iterate() {
|
||||
for(let action of this.actions) {
|
||||
yield action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Router;
|
73
Server/RouterContext.mjs
Normal file
73
Server/RouterContext.mjs
Normal file
|
@ -0,0 +1,73 @@
|
|||
"use strict";
|
||||
|
||||
// core
|
||||
import url from 'url';
|
||||
// npm
|
||||
import cookie from 'cookie';
|
||||
// files
|
||||
import Sender from './Sender.mjs';
|
||||
|
||||
/**
|
||||
* Contains context information about a single request / response pair.
|
||||
*/
|
||||
class RouterContext {
|
||||
constructor(in_request, in_response) {
|
||||
/**
|
||||
* The Node.JS request object
|
||||
* @type {http.ClientRequest}
|
||||
*/
|
||||
this.request = in_request;
|
||||
/**
|
||||
* The Node.JS response object
|
||||
* @type {http.ServerResponse}
|
||||
*/
|
||||
this.response = in_response;
|
||||
/**
|
||||
* The parsed request URL
|
||||
* @type {URL}
|
||||
*/
|
||||
this.url = url.parse(this.request.url, true);
|
||||
/**
|
||||
* The url parameters parsed out by the router
|
||||
* @type {Object}
|
||||
*/
|
||||
this.params = {};
|
||||
/**
|
||||
* An object containing some utitlity methods for quickly sending responses
|
||||
* @type {Sender}
|
||||
*/
|
||||
this.send = new Sender(this.response);
|
||||
|
||||
// FUTURE: Refactor the default population of this object elsewhere
|
||||
/**
|
||||
* The environment object.
|
||||
* State variables that need to be attached to a specific request can
|
||||
* go in here.
|
||||
* @type {Object}
|
||||
*/
|
||||
this.env = {
|
||||
/**
|
||||
* Whether the user is logged in or not.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
logged_in: false,
|
||||
/**
|
||||
* The user's name. Guaranteed to be specified - if only as "anonymous".
|
||||
* @type {String}
|
||||
*/
|
||||
username: "anonymous",
|
||||
/**
|
||||
* The parsed cookie object
|
||||
* @type {Object}
|
||||
*/
|
||||
cookie: cookie.parse(this.request.headers["cookie"] || ""),
|
||||
/**
|
||||
* The parsed post data as an object, if applicable.
|
||||
* @type {Object|null}
|
||||
*/
|
||||
post_data: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default RouterContext;
|
56
Server/Sender.mjs
Normal file
56
Server/Sender.mjs
Normal file
|
@ -0,0 +1,56 @@
|
|||
"use strict";
|
||||
|
||||
import { NightInkFile } from 'nightink';
|
||||
|
||||
class Sender {
|
||||
constructor(response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a HTML response, rendering a NightInk template.
|
||||
* Don't forget to await this!
|
||||
* @param {number} status_code The status code to return.
|
||||
* @param {string} template_filename The path to the filename containing the template to render.
|
||||
* @param {Object} data The data to use whilst rendering the template.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async html(status_code, template_filename, data) {
|
||||
let response_html = await NightInkFile(template_filename, data);
|
||||
this.response.writeHead(status_code, {
|
||||
"content-type": "text/html",
|
||||
"content-length": Buffer.byteLength(response_html, "utf8"),
|
||||
});
|
||||
this.response.end(response_html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a plain text response.
|
||||
* @param {number} status_code The HTTP status code to return.
|
||||
* @param {string} data The data to send.
|
||||
*/
|
||||
plain(status_code, data) {
|
||||
this.response.writeHead(status_code, {
|
||||
"content-type": "text/plain",
|
||||
"content-length": Buffer.byteLength(data, "utf8"),
|
||||
});
|
||||
this.response.end(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a redirect.
|
||||
* @param {number} status_code The HTTP status code to send.
|
||||
* @param {string} new_path The (possibly relative) uri to redirect the client to.
|
||||
* @param {string} message The informational plain-text message to return, just in case.
|
||||
*/
|
||||
redirect(status_code, new_path, message) {
|
||||
this.response.writeHead(status_code, {
|
||||
"location": new_path,
|
||||
"content-type": "text/plain",
|
||||
"content-length": message.length
|
||||
});
|
||||
this.response.end(message);
|
||||
}
|
||||
}
|
||||
|
||||
export default Sender;
|
7
index.mjs
Normal file
7
index.mjs
Normal file
|
@ -0,0 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
import Router as ServerRouter from './Server/Router.mjs';
|
||||
|
||||
export {
|
||||
ServerRouter
|
||||
};
|
33
package-lock.json
generated
Normal file
33
package-lock.json
generated
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "powerroot",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"await-fs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/await-fs/-/await-fs-1.0.0.tgz",
|
||||
"integrity": "sha1-QAnTAIYz/WYlqgCfCm8aujY38wE="
|
||||
},
|
||||
"event-emitter-es6": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/event-emitter-es6/-/event-emitter-es6-1.1.5.tgz",
|
||||
"integrity": "sha1-75UxGy4Xqjm+djsDHOSvfunLeEk=",
|
||||
"dev": true
|
||||
},
|
||||
"html-entities": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
|
||||
"integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
|
||||
},
|
||||
"nightink": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/nightink/-/nightink-0.1.2.tgz",
|
||||
"integrity": "sha512-nAyyf1EvghaFtmwD4ox2rFiY0eWqAsW6rulIPaPC5IMv3YaNKYdQ4F7zUZ6E7eONEVQGtiCGTv6YksY9wHHs1g==",
|
||||
"requires": {
|
||||
"await-fs": "^1.0.0",
|
||||
"html-entities": "^1.2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
29
package.json
Normal file
29
package.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "powerroot",
|
||||
"version": "1.0.0",
|
||||
"description": "Client and server-side routing micro frameworks",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sbrl/powerroot.git"
|
||||
},
|
||||
"keywords": [
|
||||
"routing",
|
||||
"micro-framework"
|
||||
],
|
||||
"author": "Starbeamrainbowlabs",
|
||||
"license": "MPL-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/sbrl/powerroot/issues"
|
||||
},
|
||||
"homepage": "https://github.com/sbrl/powerroot#readme",
|
||||
"dependencies": {
|
||||
"nightink": "^0.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"event-emitter-es6": "^1.1.5"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue