Initial commit

This commit is contained in:
2024-04-17 22:54:13 +02:00
commit a8fdb455f4
162 changed files with 49158 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
/**
* @typedef {Object} AuthorizeOptions
* @property {string} [role] - Role which the user should be in
* @property {string[]} [roles] - Array of roles which the user should be in
*/
export class AuthorizeOptions {
/** @type {?string} **/
role;
/** @type {string[]} **/
roles;
constructor(roles) {
if(typeof roles === 'string') {
this.role = roles;
}
if (roles instanceof Array) {
this.roles = roles;
}
}
}
+154
View File
@@ -0,0 +1,154 @@
function normalizeIp(ip){
// Oddly enough, FF passing in the clientIp is different from chrome?!
return ip === "::ffff:127.0.0.1" ? "::1" : ip;
}
/**
* @typedef File
* @property {Buffer} buffer
* @property {string} encoding
* @property {string} fileName
* @property {string} mimetype
*/
/**
* Request context
*/
export class RequestContext{
/** @param {RequestContext} c */
constructor(c){
if(c){
this.parameters = c.parameters??{};
this.queryParameters = c.queryParameters??{};
this.body = c.body;
this.session = c.session;
this.method = c.method;
this.endpoint = c.endpoint;
this.clientIp = c.clientIp;
this.host = c.host;
this.headers = c.headers;
}
}
/** @type {Object<string,string>} */
parameters = {};
/** @type {Object<string,string>} */
queryParameters = {};
/** @type {*} */
body = undefined;
/** @type {AuthSession} */
session;
/** @type {'GET'|'POST'|'PATCH'|'DELETE'|'PUT'} */
method;
/** @type {string} */
endpoint;
/** @type {string} */
clientIp;
/** @type {string} */
host;
/** @type {Object} */
headers;
/** @type {string} */
rawBody = undefined;
get queryString(){
let query = Array.from(Object.entries(this.queryParameters));
if(query.length>0){
return "?" + query.map(
([key,value])=>
value!==null&&value!==undefined
? [encodeURIComponent(key), encodeURIComponent(value)].join('=')
: encodeURIComponent(key)
).join("&");
}else{
return "";
}
}
}
export class KoaRequestContext extends RequestContext{
constructor(koaCtx){
super({
parameters: koaCtx.params,
queryParameters: koaCtx.query,
body: koaCtx.request?.body,
rawBody: koaCtx.request?.rawBody,
session: koaCtx.state?.session,
method: koaCtx.method,
endpoint: koaCtx.path? koaCtx.path.slice((koaCtx.path.indexOf("/api/")??0) + '/api'.length) : '/',
clientIp: normalizeIp((
koaCtx.request.headers["X-Forwarded-For".toLowerCase()]
?? koaCtx.request.headers["X-Real-IP".toLowerCase()]
?? koaCtx.request.headers["X-Orig-IP".toLowerCase()]
?? koaCtx.request.ip
)?.toString() ?? undefined),
host: (koaCtx.request.headers["host"])?.toString() ?? undefined,
headers: koaCtx.request.headers
});
this.#koaCtx = koaCtx;
}
// Private
#koaCtx;
set(key, value) {
this.#koaCtx.set(key, value);
}
/** @type {request.Request} */
get req(){
return this.#koaCtx.req;
}
/** @type {request.Response} */
get res(){
return this.#koaCtx.res;
}
/** @type {string} */
get path(){
return this.#koaCtx.path;
}
set attachment(fileName){
return this.#koaCtx.attachment(fileName);
}
set type(type){
return this.#koaCtx.type = type;
}
/** @type {File} */
get file(){
return this.#koaCtx.file;
}
/** @type {[File]} */
get files(){
return this.#koaCtx.files;
}
set status(value) {
this.#koaCtx.status = value;
}
/** @type {Instance} */
get currentInstance(){
return this.#koaCtx.currentInstance;
}
/** @param {Instance} value */
set currentInstance(value) {
this.#koaCtx.currentInstance = value;
}
/** @type {AuthorizationInfo} */
get authorizationInfo() {
return this.#koaCtx.authorizationInfo;
}
set authorizationInfo(authorizationInfo) {
this.#koaCtx.authorizationInfo = authorizationInfo;
}
/** @type {UserInfo} */
get userInfo() {
return this.#koaCtx.userInfo;
}
set userInfo(userInfo) {
this.#koaCtx.userInfo = userInfo;
}
}
+110
View File
@@ -0,0 +1,110 @@
import "reflect-metadata";
/**
* @typedef {'GET'|'POST'|'PATCH'|'DELETE'|'PUT'} HttpMethod
*/
/**
* @typedef {EndpointOptions}
* @property {[function]} middlewares - Additional middlewares
* @property {AuthorizeOptions} authorization - Authorization info
*/
export class EndpointOptions {
/** @type {function[]} **/
middlewares = [];
/** @type {?AuthorizeOptions} **/
authorizeOptions;
/**
* @param {function[]} middlewares
* @param {?AuthorizeOptions=} [authorizeOptions]
*/
constructor(middlewares, authorizeOptions= null) {
this.middlewares = middlewares;
this.authorizeOptions = authorizeOptions;
}
}
/**
* @typedef {HttpEndpoint}
* @property {HttpMethod} method
* @property {string} route
* @property {?EndpointOptions} options
*/
export class HttpEndpoint {
/** @type {HttpMethod} **/
method;
/** @type {string} **/
route;
/** @type {?EndpointOptions} **/
options;
/** @type {function(KoaRequestContext, function)} **/
handler;
/**
* @param {HttpMethod} method
* @param {string} route
* @param {function(KoaRequestContext, function)} handler
* @param {?EndpointOptions=} [options]
*/
constructor(method, route, handler, options = null) {
this.method = method;
this.route = route;
this.handler = handler;
this.options = options;
}
}
export class HttpGet extends HttpEndpoint {
/**
* @param {string} route
* @param {function(KoaRequestContext, function)} handler
* @param {?EndpointOptions=} [options]
*/
constructor(route, handler, options = null) {
super('GET', route, handler, options);
}
}
export class HttpPost extends HttpEndpoint {
/**
* @param {string} route
* @param {function(KoaRequestContext, function)} handler
* @param {?EndpointOptions=} [options]
*/
constructor(route, handler, options = null) {
super('POST', route, handler, options);
}
}
export class HttpPatch extends HttpEndpoint {
/**
* @param {string} route
* @param {function(KoaRequestContext, function)} handler
* @param {?EndpointOptions=} [options]
*/
constructor(route, handler, options = null) {
super('PATCH', route, handler, options);
}
}
export class HttpPut extends HttpEndpoint {
/**
* @param {string} route
* @param {function(KoaRequestContext, function)} handler
* @param {?EndpointOptions=} [options]
*/
constructor(route, handler, options = null) {
super('PUT', route, handler, options);
}
}
export class HttpDelete extends HttpEndpoint {
/**
* @param {string} route
* @param {function(KoaRequestContext, function)} handler
* @param {?EndpointOptions=} [options]
*/
constructor(route, handler, options = null) {
super('DELETE', route, handler, options);
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./authorize";
export * from "./endpoint";
export * from "./router.js";
export * from "./context";
+104
View File
@@ -0,0 +1,104 @@
import {KoaRequestContext} from "./context";
import {hasRole, hasSite} from "../../middleware/authorization";
import Router from "@koa/router";
/**
*
* @param {string} path
*/
export class ApiRouter {
/** @type {string} **/
path;
/** @type {HttpEndpoint[]} **/
endpoints = [];
/** @type {?AuthorizeOptions} **/
authorizeOptions;
/** @type {Router} **/
koaRouter;
routerAuthValidator;
/**
* @param {string} path
* @param {?AuthorizeOptions=} [authorizeOptions]
*/
constructor(path, authorizeOptions = null) {
this.path = path;
// Define a matching router
this.koaRouter = new Router();
this.authorizeOptions = authorizeOptions;
this.routerAuthValidator = this.generateAuthorizeHandler(authorizeOptions);
}
/**
* @param {HttpEndpoint} endpoint
*/
registerEndpoint(endpoint) {
let method = endpoint.method;
let route = endpoint.route;
let authValidator = this.generateAuthorizeHandler(endpoint.options?.authorizeOptions);
let handler = async (ctx, next) => {
let reqCtx = new KoaRequestContext(ctx);
if(this.routerAuthValidator) {
await this.routerAuthValidator(reqCtx);
}
if(authValidator) {
await authValidator(reqCtx);
}
ctx.body = await Promise.resolve(endpoint.handler(reqCtx, next));
};
let methodMapping = {
'GET': 'get',
'POST': 'post',
'PATCH': 'patch',
'PUT': 'put',
'DELETE': 'delete'
};
if(endpoint.options?.middlewares) {
this.koaRouter[methodMapping[method]](route, ...endpoint.options.middlewares, handler);
} else {
this.koaRouter[methodMapping[method]](route, handler);
}
}
/**
* @param {?AuthorizeOptions} authorizeOptions
* @returns {(function(*, *): Promise<void>)|*}
*/
generateAuthorizeHandler(authorizeOptions) {
if (authorizeOptions) {
return async (/** @param {KoaRequestContext} **/ctx) => {
if (authorizeOptions.role || authorizeOptions.roles) {
const roles = [
authorizeOptions.role,
...(authorizeOptions.roles || [])
].filter(x => x);
await hasRole(roles, ctx);
const {parameters: {site}} = ctx;
if(site){
await hasSite(site, ctx);
}
}
}
}
return null;
}
/**
* @param {Router} app
*/
register(app) {
// Register it as middleware in the app (which may in turn have been a KoaRouter)
app.use(this.path, this.koaRouter.routes(), this.koaRouter.allowedMethods());
}
}