Add auto resolving fields
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing

This commit is contained in:
Fabian Stamm 2020-03-24 18:33:47 +01:00
parent 68295c148d
commit 0bfdbce908
7 changed files with 245 additions and 180 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "@hibas123/realtimedb", "name": "@hibas123/realtimedb",
"version": "2.0.0-beta.18", "version": "2.0.0-beta.19",
"description": "", "description": "",
"main": "lib/index.js", "main": "lib/index.js",
"private": true, "private": true,

View File

@ -154,6 +154,7 @@ export abstract class Query {
} else if (this.permission === "write" && !perm.write) { } else if (this.permission === "write" && !perm.write) {
throw new QueryError("No permission!"); throw new QueryError("No permission!");
} }
this.query.path = perm.path;
return this._runner.call( return this._runner.call(
this, this,
collection, collection,
@ -170,10 +171,12 @@ export abstract class Query {
this.query.path, this.query.path,
this.session this.session
); );
if (this.permission === "read" && !perm.read) { if (!perm.read) {
throw new QueryError("No permission!"); throw new QueryError("No permission!");
} }
this.query.path = perm.path;
const receivedChanges = (changes: Change[]) => { const receivedChanges = (changes: Change[]) => {
let res = changes let res = changes
.filter(change => this.checkChange(change)) .filter(change => this.checkChange(change))

View File

@ -61,19 +61,21 @@ export class Rules {
hasPermission( hasPermission(
path: string[], path: string[],
session: Session session: Session
): { read: boolean; write: boolean } { ): { read: boolean; write: boolean; path: string[] } {
if (session.root) if (session.root)
return { return {
read: true, read: true,
write: true write: true,
path: path
}; };
let read = this.rules[".read"] || false; let read = this.rules[".read"] || false;
let write = this.rules[".write"] || false; let write = this.rules[".write"] || false;
let rules = this.rules; let rules = this.rules;
for (let segment of path) { for (let idx in path) {
if (segment.startsWith("$") || segment.startsWith(".")) { let segment = path[idx];
if (segment.startsWith(".")) {
read = false; read = false;
write = false; write = false;
Logging.log("Invalid query path (started with '$' or '.'):", path); Logging.log("Invalid query path (started with '$' or '.'):", path);
@ -85,6 +87,10 @@ export class Rules {
.find(e => { .find(e => {
switch (e) { switch (e) {
case "$uid": case "$uid":
if (segment === "$uid") {
path[idx] = session.uid;
return true;
}
if (segment === session.uid) return true; if (segment === session.uid) return true;
break; break;
} }
@ -108,7 +114,8 @@ export class Rules {
return { return {
read: read as boolean, read: read as boolean,
write: write as boolean write: write as boolean,
path
}; };
} }

View File

@ -5,16 +5,26 @@ interface IFormConfigField {
type: "text" | "number" | "boolean" | "textarea"; type: "text" | "number" | "boolean" | "textarea";
label: string; label: string;
value?: string; value?: string;
disabled?: boolean;
} }
type IFormConfig = { [name: string]: IFormConfigField } type IFormConfig = { [name: string]: IFormConfigField };
export default function getForm(url: string, title: string, fieldConfig: IFormConfig): (ctx: Context) => void { export default function getForm(
let fields = Object.keys(fieldConfig).map(name => ({ name, ...fieldConfig[name] })) url: string,
title: string,
fieldConfig: IFormConfig
): (ctx: Context) => void {
let fields = Object.keys(fieldConfig).map(name => ({
name,
...fieldConfig[name],
disabled: fieldConfig.disabled ? "disabled" : ""
}));
return ctx => ctx.body = getTemplate("forms")({ return ctx =>
url, (ctx.body = getTemplate("forms")({
title, url,
fields title,
}); fields
}));
} }

View File

@ -2,7 +2,11 @@ import * as Router from "koa-router";
import Settings from "../../settings"; import Settings from "../../settings";
import getForm from "../helper/form"; import getForm from "../helper/form";
import getTable from "../helper/table"; import getTable from "../helper/table";
import { BadRequestError, NoPermissionError } from "../helper/errors"; import {
BadRequestError,
NoPermissionError,
NotFoundError
} from "../helper/errors";
import { DatabaseManager } from "../../database/database"; import { DatabaseManager } from "../../database/database";
import { MP } from "../../database/query"; import { MP } from "../../database/query";
import config from "../../config"; import config from "../../config";
@ -13,10 +17,9 @@ const AdminRoute = new Router();
AdminRoute.use(async (ctx, next) => { AdminRoute.use(async (ctx, next) => {
const { key } = ctx.query; const { key } = ctx.query;
if (key !== config.admin) if (key !== config.admin) throw new NoPermissionError("No permission!");
throw new NoPermissionError("No permission!");
return next(); return next();
}) });
AdminRoute.get("/", async ctx => { AdminRoute.get("/", async ctx => {
//TODO: Main Interface //TODO: Main Interface
@ -33,26 +36,24 @@ AdminRoute.get("/settings", async ctx => {
let res = [["key", "value"]]; let res = [["key", "value"]];
stream.on("data", ({ key, value }) => { stream.on("data", ({ key, value }) => {
res.push([key, value]); res.push([key, value]);
}) });
stream.on("error", no); stream.on("error", no);
stream.on("end", () => yes(res)) stream.on("end", () => yes(res));
}) });
if (ctx.query.view) { if (ctx.query.view) {
return getTable("Settings", res, ctx); return getTable("Settings", res, ctx);
} else { } else {
ctx.body = res; ctx.body = res;
} }
}) });
AdminRoute.get("/data", async ctx => { AdminRoute.get("/data", async ctx => {
const { database } = ctx.query; const { database } = ctx.query;
let db = DatabaseManager.getDatabase(database); let db = DatabaseManager.getDatabase(database);
if (!db) if (!db) throw new BadRequestError("Database not found");
throw new BadRequestError("Database not found");
let res = await new Promise<string[][]>((yes, no) => { let res = await new Promise<string[][]>((yes, no) => {
const stream = db.data.createReadStream({ const stream = db.data.createReadStream({
keys: true, keys: true,
values: true, values: true,
@ -61,73 +62,71 @@ AdminRoute.get("/data", async ctx => {
limit: 1000 limit: 1000
}); });
let res = [["key", "value"]]; let res = [["key", "value"]];
stream.on("data", ({ key, value }: { key: string, value: Buffer }) => { stream.on("data", ({ key, value }: { key: string; value: Buffer }) => {
res.push([key, key.split("/").length > 2 ? value.toString() : JSON.stringify(MP.decode(value))]); res.push([
}) key,
key.split("/").length > 2
? value.toString()
: JSON.stringify(MP.decode(value))
]);
});
stream.on("error", no); stream.on("error", no);
stream.on("end", () => yes(res)) stream.on("end", () => yes(res));
}) });
if (ctx.query.view) { if (ctx.query.view) {
return getTable("Data from " + database, res, ctx); return getTable("Data from " + database, res, ctx);
} else { } else {
ctx.body = res; ctx.body = res;
} }
}) });
AdminRoute AdminRoute.get("/database", ctx => {
.get("/database", ctx => { const isFull = ctx.query.full === "true" || ctx.query.full === "1";
const isFull = ctx.query.full === "true" || ctx.query.full === "1"; let res;
let res; if (isFull) {
if (isFull) { //TODO: Better than JSON.parse / JSON.stringify
//TODO: Better than JSON.parse / JSON.stringify res = Array.from(DatabaseManager.databases.entries()).map(
res = Array.from(DatabaseManager.databases.entries()).map(([name, config]) => ({ name, ...(JSON.parse(JSON.stringify(config))) })); ([name, config]) => ({
} else { name,
res = Array.from(DatabaseManager.databases.keys()); ...JSON.parse(JSON.stringify(config))
} })
);
} else {
res = Array.from(DatabaseManager.databases.keys());
}
if (ctx.query.view) { if (ctx.query.view) {
return getTable("Databases" + (isFull ? "" : " small"), res, ctx); return getTable("Databases" + (isFull ? "" : " small"), res, ctx);
} else { } else {
ctx.body = res; ctx.body = res;
} }
}) }).post("/database", async ctx => {
.post("/database", async ctx => { const { name, rules, publickey, accesskey, rootkey } = ctx.request.body;
const { name, rules, publickey, accesskey, rootkey } = ctx.request.body;
if (!name) if (!name) throw new BadRequestError("Name must be set!");
throw new BadRequestError("Name must be set!");
let db = DatabaseManager.getDatabase(name); let db = DatabaseManager.getDatabase(name);
if (!db) if (!db) db = await DatabaseManager.addDatabase(name);
db = await DatabaseManager.addDatabase(name);
if (publickey) if (publickey) await db.setPublicKey(publickey);
await db.setPublicKey(publickey);
if (rules) if (rules) await db.setRules(rules);
await db.setRules(rules);
if (accesskey) if (accesskey) await db.setAccessKey(accesskey);
await db.setAccessKey(accesskey);
if (rootkey) await db.setRootKey(rootkey);
if (rootkey) ctx.body = "Success";
await db.setRootKey(rootkey); });
ctx.body = "Success";
})
AdminRoute.get("/collections", async ctx => { AdminRoute.get("/collections", async ctx => {
const { database } = ctx.query; const { database } = ctx.query;
let db = DatabaseManager.getDatabase(database); let db = DatabaseManager.getDatabase(database);
if (!db) if (!db) throw new BadRequestError("Database not found");
throw new BadRequestError("Database not found");
let res = await new Promise<string[]>((yes, no) => { let res = await new Promise<string[]>((yes, no) => {
const stream = db.collections.createKeyStream({ const stream = db.collections.createKeyStream({
keyAsBuffer: false, keyAsBuffer: false,
limit: 1000 limit: 1000
@ -135,24 +134,23 @@ AdminRoute.get("/collections", async ctx => {
let res = []; let res = [];
stream.on("data", (key: string) => { stream.on("data", (key: string) => {
res.push(key); res.push(key);
}) });
stream.on("error", no); stream.on("error", no);
stream.on("end", () => yes(res)) stream.on("end", () => yes(res));
}) });
if (ctx.query.view) { if (ctx.query.view) {
return getTable("Databases", res, ctx); return getTable("Databases", res, ctx);
} else { } else {
ctx.body = res; ctx.body = res;
} }
}) });
AdminRoute.get("/collections/cleanup", async ctx => { AdminRoute.get("/collections/cleanup", async ctx => {
const { database } = ctx.query; const { database } = ctx.query;
let db = DatabaseManager.getDatabase(database); let db = DatabaseManager.getDatabase(database);
if (!db) if (!db) throw new BadRequestError("Database not found");
throw new BadRequestError("Database not found");
let deleted = await db.runCleanup(); let deleted = await db.runCleanup();
if (ctx.query.view) { if (ctx.query.view) {
@ -160,14 +158,55 @@ AdminRoute.get("/collections/cleanup", async ctx => {
} else { } else {
ctx.body = deleted; ctx.body = deleted;
} }
}) });
AdminRoute.get("/database/new", getForm("/v1/admin/database", "New/Change Database", { AdminRoute.get(
name: { label: "Name", type: "text", }, "/database/new",
accesskey: { label: "Access Key", type: "text" }, getForm("/v1/admin/database", "New Database", {
rootkey: { label: "Root access key", type: "text" }, name: { label: "Name", type: "text" },
rules: { label: "Rules", type: "textarea", value: `{\n ".write": true, \n ".read": true \n}` }, accesskey: { label: "Access Key", type: "text" },
publickey: { label: "Public Key", type: "textarea" } rootkey: { label: "Root access key", type: "text" },
})) rules: {
label: "Rules",
type: "textarea",
value: `{\n ".write": true, \n ".read": true \n}`
},
publickey: { label: "Public Key", type: "textarea" }
})
);
AdminRoute.get("/database/update", async ctx => {
const { database } = ctx.query;
let db = DatabaseManager.getDatabase(database);
if (!db) throw new NotFoundError("Database not found!");
getForm("/v1/admin/database", "Change Database", {
name: {
label: "Name",
type: "text",
value: db.name,
disabled: true
},
accesskey: {
label: "Access Key",
type: "text",
value: db.accesskey
},
rootkey: {
label: "Root access key",
type: "text",
value: db.rootkey
},
rules: {
label: "Rules",
type: "textarea",
value: db.rules.toJSON()
},
publickey: {
label: "Public Key",
type: "textarea",
value: db.publickey
}
})(ctx);
});
export default AdminRoute; export default AdminRoute;

View File

@ -1,112 +1,118 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Admin Interface</title>
<link
rel="stylesheet"
href="https://unpkg.com/@hibas123/theme/out/base.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/@hibas123/theme/out/light.css"
/>
<head> <script src="https://unpkg.com/handlebars/dist/handlebars.min.js"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Admin Interface</title>
<link rel="stylesheet" href="https://unpkg.com/@hibas123/theme/out/base.css">
<link rel="stylesheet" href="https://unpkg.com/@hibas123/theme/out/light.css">
<script src="https://unpkg.com/handlebars/dist/handlebars.min.js"></script> <style>
#message {
visibility: hidden;
background-color: lightgreen;
border: 1px solid lime;
border-radius: 0.5rem;
padding: 1rem;
font-size: 1.5rem;
margin-bottom: 1rem;
}
<style> .grid {
#message { display: grid;
visibility: hidden; height: 100vh;
background-color: lightgreen; grid-template-columns: 360px auto;
border: 1px solid lime; }
border-radius: .5rem;
padding: 1rem;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.grid { #content {
display: grid; position: absolute;
height: 100vh; top: 0;
grid-template-columns: 360px auto; left: 0;
} width: 100%;
height: 100%;
#content { border: 0;
position: absolute; }
top: 0; </style>
left: 0; </head>
width: 100%;
height: 100%;
border: 0;
}
</style>
</head>
<body>
<div class="grid">
<div style="border-right: 1px solid darkgrey; padding: 1rem;">
<h2>Navigation: </h2>
<ul class="list list-clickable">
<li onclick="loadView('settings');">Settings</li>
<li onclick="loadView('database', {full:true});">Databases</li>
<li onclick="loadView('database/new');">New Database</li>
</ul>
Databases:
<div id="dbs" class="list list-clickable" style="margin: 1rem;"></div>
<body>
<div class="grid">
<div style="border-right: 1px solid darkgrey; padding: 1rem;">
<h2>Navigation:</h2>
<ul class="list list-clickable">
<li onclick="loadView('settings');">Settings</li>
<li onclick="loadView('database', {full:true});">Databases</li>
<li onclick="loadView('database/new');">New Database</li>
</ul>
Databases:
<div
id="dbs"
class="list list-clickable"
style="margin: 1rem;"
></div>
</div>
<div style="position:relative;">
<iframe id="content"></iframe>
</div>
</div> </div>
<div style="position:relative;">
<iframe id="content"></iframe>
</div>
</div>
<template> <template> </template>
</template> <script>
const key = new URL(window.location.href).searchParams.get("key");
const content = document.getElementById("content");
const base = new URL(window.location.href).host;
<script> function getUrl(name, params, view = true) {
const key = new URL(window.location.href).searchParams.get("key"); const url = new URL(window.location.href);
const content = document.getElementById("content"); url.pathname = "/v1/admin/" + name;
const base = new URL(window.location.href).host; for (let key in params || {})
url.searchParams.set(key, params[key]);
function getUrl(name, params, view = true) { url.searchParams.set("key", key);
const url = new URL(window.location.href); if (view) url.searchParams.set("view", "true");
url.pathname = "/v1/admin/" + name;
for (let key in params || {})
url.searchParams.set(key, params[key]);
url.searchParams.set("key", key); return url.href;
if (view) }
url.searchParams.set("view", "true");
return url.href; function loadView(name, params) {
} content.src = getUrl(name, params);
}
function loadView(name, params) { loadView("settings");
content.src = getUrl(name, params);
}
loadView("settings") const dbsul = document.getElementById("dbs");
function reloadDBs() {
const dbsul = document.getElementById("dbs"); fetch(getUrl("database", {}, false))
function reloadDBs() { .then(res => res.json())
fetch(getUrl("database", {}, false)) .then(databases =>
.then(res => res.json()) databases.map(
.then(databases => databases.map(database => ` database => `
<div class="card margin elv-4"> <div class="card margin elv-4">
<h3>${database}</h3> <h3>${database}</h3>
<button class=btn onclick="loadView('data', {database:'${database}'})">Data</button> <button class=btn onclick="loadView('data', {database:'${database}'})">Data</button>
<button class=btn onclick="loadView('collections', {database:'${database}'})">Collections</button> <button class=btn onclick="loadView('collections', {database:'${database}'})">Collections</button>
<button class=btn onclick="loadView('collections/cleanup', {database:'${database}'})">Clean</button> <button class=btn onclick="loadView('collections/cleanup', {database:'${database}'})">Clean</button>
<button class=btn onclick="loadView('database/update', {database:'${database}'})">Change</button>
</div>` </div>`
)) )
.then(d => d.join("\n")) )
.then(d => dbsul.innerHTML = d) .then(d => d.join("\n"))
.catch(console.error) .then(d => (dbsul.innerHTML = d))
.catch(console.error);
}
}
reloadDBs();
setInterval(reloadDBs, 5000);
</script>
</body>
reloadDBs();
setInterval(reloadDBs, 5000);
</script>
</body>
</html> </html>

View File

@ -32,19 +32,19 @@
<div class="input-group"> <div class="input-group">
<label>{{label}}</label> <label>{{label}}</label>
{{#ifCond type "===" "text"}} {{#ifCond type "===" "text"}}
<input type="text" placeholder="{{label}}" name="{{name}}" value="{{value}}" /> <input type="text" placeholder="{{label}}" name="{{name}}" value="{{value}}" {{disabled}} />
{{/ifCond}} {{/ifCond}}
{{#ifCond type "===" "number"}} {{#ifCond type "===" "number"}}
<input type="number" placeholder="{{label}}" name="{{name}}" value="{{value}}" /> <input type="number" placeholder="{{label}}" name="{{name}}" value="{{value}}" {{disabled}} />
{{/ifCond}} {{/ifCond}}
{{#ifCond type "===" "boolean"}} {{#ifCond type "===" "boolean"}}
<input type="checkbox" name="{{name}}" checked="{{value}}" /> <input type="checkbox" name="{{name}}" checked="{{value}}" {{disabled}} />
{{/ifCond}} {{/ifCond}}
{{#ifCond type "===" "textarea"}} {{#ifCond type "===" "textarea"}}
<textarea class="inp" name="{{name}}" rows="20">{{value}}</textarea> <textarea class="inp" name="{{name}}" rows="20" {{disabled}}>{{value}}</textarea>
{{/ifCond}} {{/ifCond}}
</div> </div>
{{/each}} {{/each}}