mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-22 09:18:15 +00:00
Feat/admin command (#2022)
* WIP: cli admin command * configure admin command to load ledger admin view an express service was implemented, which serves the default site from the instances test plan: run `sourcecred admin` and browse to the ledger admin instance to ensure it loads * implement GET and POST handlers for ledger.json The GET localhost:6006/data/ledger.json request will return the ledger.json file if it exists The POST request will write the submitted json to disk at ./data/ledger.json test plan: run `$ sourcecred admin` or `$ yarn admin` then post some json to the path by doing something like ``` $ curl --header "Content-Type: application/json" \ --request POST \ --data '{"username":"derp","password":"slerp"}' \ http://localhost:6006/data/ledger.json ``` and ensure there are no errors. then ``` $ curl http://localhost:6006/data/ledger.json ``` and ensure the submitted json is returned * remove unused variables * Add Ledger to the UI load result Note: This will fail if there is no ledger file. An empty JSON array is technically a valid ledger, so if you want to update the example instance just use: `echo [] > data/ledger.json` Test plan: Not yet tested, integrate into frontend * implement button to save ledger to disk test-plan: put and empty array ("[]") in the instance's ledger.json file, then start the admin service "yarn admin" or "sourcecred admin" then generate some ledger data using the identity interface and ensure it saves back to disk * Load ledger from disk for real * add text middleware to express libdefs * move ledger disk saving from application/json to text/plain ledger.json files are encoded in such a way that they are indented and escaped for some readability from the CLI. using json content-typing will discard that escaping. By treating the payload as raw text, that encoding is preserved test-plan: modify and save a canonically serialized ledger.json file from the browser frontend and observe that the spacing is preserved in the file saved to disk * remove unused deps and improve server console prompt test-plan: start the admin server and ensure the frontend can create and modify identities, then save the ledger.json file to disk successfully * comments and capitalization test-plan: `yarn flow` should still pass after this change * remove redundant static middleware we don't need to configure a static service for a subdirectory of a statically-served directory test-plan: ensure ledger.json can still be fetched either via curl or the web frontend when the admin service is running * remove address from fetch post request if we're fetching to/from the parent address that served the javascript we're calling from, an address is unnecessary test-plan: make sure updated ledger.json files still POST back to disk * add instance check attempt to load the instance configurtion. If the config cannot be loaded, an error will be throw indicating that the sourcecred.json file cannot be found test-plan: run `sourcecred admin` outside of an instance directory and ensure it fails while looking for sourcecred.json run `sourcecred admin` in an instance directory and ensure the service starts up Co-authored-by: Dandelion Mané <decentralion@dandelion.io>
This commit is contained in:
parent
e1886ff792
commit
b85f5330ee
9
flow-typed/npm/express_v4.17.x.js
vendored
9
flow-typed/npm/express_v4.17.x.js
vendored
@ -323,6 +323,14 @@ declare type express$UrlEncodedOptions = {
|
|||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare type express$TextOptions = {
|
||||||
|
defaultCharset?: string,
|
||||||
|
inflate?: boolean,
|
||||||
|
limit?: mixed,
|
||||||
|
type?: mixed,
|
||||||
|
verify?: Function,
|
||||||
|
}
|
||||||
|
|
||||||
declare module "express" {
|
declare module "express" {
|
||||||
declare export type RouterOptions = express$RouterOptions;
|
declare export type RouterOptions = express$RouterOptions;
|
||||||
declare export type CookieOptions = express$CookieOptions;
|
declare export type CookieOptions = express$CookieOptions;
|
||||||
@ -343,6 +351,7 @@ declare module "express" {
|
|||||||
// If you try to call like a function, it will use this signature
|
// If you try to call like a function, it will use this signature
|
||||||
<Req: express$Request, Res: express$Response>(): express$Application<Req, Res>,
|
<Req: express$Request, Res: express$Response>(): express$Application<Req, Res>,
|
||||||
json: (opts: ?JsonOptions) => express$Middleware<>,
|
json: (opts: ?JsonOptions) => express$Middleware<>,
|
||||||
|
text: (opts: ?express$TextOptions) => express$Middleware<>,
|
||||||
// `static` property on the function
|
// `static` property on the function
|
||||||
static: <Req: express$Request, Res: express$Response>(root: string, options?: Object) => express$Middleware<Req, Res>,
|
static: <Req: express$Request, Res: express$Response>(root: string, options?: Object) => express$Middleware<Req, Res>,
|
||||||
// `Router` property on the function
|
// `Router` property on the function
|
||||||
|
47
src/cli/admin.js
Normal file
47
src/cli/admin.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// @flow
|
||||||
|
|
||||||
|
import type {Command} from "./command";
|
||||||
|
import {loadInstanceConfig} from "./common";
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const express = require("express");
|
||||||
|
|
||||||
|
function die(std, message) {
|
||||||
|
std.err("fatal: " + message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminCommand: Command = async (args, std) => {
|
||||||
|
const basedir = process.cwd();
|
||||||
|
// check to ensure service is running within an instance directory
|
||||||
|
await loadInstanceConfig(basedir);
|
||||||
|
|
||||||
|
if (args.length !== 0) {
|
||||||
|
return die(std, "usage: sourcecred admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = express();
|
||||||
|
|
||||||
|
// serve the static admin site and all subdirectories
|
||||||
|
// also enables GETing data/ledger.json
|
||||||
|
server.use(express.static("."));
|
||||||
|
|
||||||
|
// middleware that parses text request bodies for us
|
||||||
|
server.use(express.text());
|
||||||
|
// write posted ledger.json files to disk
|
||||||
|
server.post("/data/ledger.json", (req, res) => {
|
||||||
|
try {
|
||||||
|
fs.writeFileSync("./data/ledger.json", req.body, "utf8");
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(`error saving ledger.json file: ${e}`);
|
||||||
|
}
|
||||||
|
res.status(201).end();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(6006, () => {
|
||||||
|
console.info("admin server running: navigate to http://localhost:6006");
|
||||||
|
});
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default adminCommand;
|
@ -8,6 +8,7 @@ import graph from "./graph";
|
|||||||
import score from "./score";
|
import score from "./score";
|
||||||
import site from "./site";
|
import site from "./site";
|
||||||
import go from "./go";
|
import go from "./go";
|
||||||
|
import admin from "./admin";
|
||||||
import help from "./help";
|
import help from "./help";
|
||||||
|
|
||||||
const sourcecred: Command = async (args, std) => {
|
const sourcecred: Command = async (args, std) => {
|
||||||
@ -32,6 +33,8 @@ const sourcecred: Command = async (args, std) => {
|
|||||||
return site(args.slice(1), std);
|
return site(args.slice(1), std);
|
||||||
case "go":
|
case "go":
|
||||||
return go(args.slice(1), std);
|
return go(args.slice(1), std);
|
||||||
|
case "admin":
|
||||||
|
return admin(args.slice(1), std);
|
||||||
default:
|
default:
|
||||||
std.err("fatal: unknown command: " + JSON.stringify(args[0]));
|
std.err("fatal: unknown command: " + JSON.stringify(args[0]));
|
||||||
std.err("fatal: run 'sourcecred help' for commands and usage");
|
std.err("fatal: run 'sourcecred help' for commands and usage");
|
||||||
|
@ -34,7 +34,10 @@ const customRoutes = (loadResult: LoadSuccess) => [
|
|||||||
<Redirect to="/explorer" />
|
<Redirect to="/explorer" />
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route key="admin" exact path="/admin">
|
<Route key="admin" exact path="/admin">
|
||||||
<LedgerAdmin credView={loadResult.credView} />
|
<LedgerAdmin
|
||||||
|
credView={loadResult.credView}
|
||||||
|
initialLedger={loadResult.ledger}
|
||||||
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -8,10 +8,11 @@ import {AliasSelector} from "./AliasSelector";
|
|||||||
|
|
||||||
export type Props = {|
|
export type Props = {|
|
||||||
+credView: CredView,
|
+credView: CredView,
|
||||||
|
+initialLedger: Ledger,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
export const LedgerAdmin = ({credView}: Props) => {
|
export const LedgerAdmin = ({credView, initialLedger}: Props) => {
|
||||||
const [ledger, setLedger] = useState<Ledger>(new Ledger());
|
const [ledger, setLedger] = useState<Ledger>(initialLedger);
|
||||||
const [nextIdentityName, setIdentityName] = useState<string>("");
|
const [nextIdentityName, setIdentityName] = useState<string>("");
|
||||||
const [currentIdentity, setCurrentIdentity] = useState<Identity | null>(null);
|
const [currentIdentity, setCurrentIdentity] = useState<Identity | null>(null);
|
||||||
const [promptString, setPromptString] = useState<string>("Add Identity:");
|
const [promptString, setPromptString] = useState<string>("Add Identity:");
|
||||||
@ -91,6 +92,21 @@ export const LedgerAdmin = ({credView}: Props) => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
value={currentIdentity ? "update username" : "create identity"}
|
value={currentIdentity ? "update username" : "create identity"}
|
||||||
/>
|
/>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
type="button"
|
||||||
|
value="save ledger to disk"
|
||||||
|
onClick={() => {
|
||||||
|
fetch("data/ledger.json", {
|
||||||
|
headers: {
|
||||||
|
Accept: "text/plain",
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: ledger.serialize(),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{currentIdentity && (
|
{currentIdentity && (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
|
@ -2,17 +2,23 @@
|
|||||||
import * as pluginId from "../api/pluginId";
|
import * as pluginId from "../api/pluginId";
|
||||||
import {CredView} from "../analysis/credView";
|
import {CredView} from "../analysis/credView";
|
||||||
import {fromJSON as credResultFromJSON} from "../analysis/credResult";
|
import {fromJSON as credResultFromJSON} from "../analysis/credResult";
|
||||||
|
import {Ledger, parser as ledgerParser} from "../ledger/ledger";
|
||||||
|
|
||||||
export type LoadResult = LoadSuccess | LoadFailure;
|
export type LoadResult = LoadSuccess | LoadFailure;
|
||||||
export type LoadSuccess = {|
|
export type LoadSuccess = {|
|
||||||
+type: "SUCCESS",
|
+type: "SUCCESS",
|
||||||
+credView: CredView,
|
+credView: CredView,
|
||||||
|
+ledger: Ledger,
|
||||||
+bundledPlugins: $ReadOnlyArray<pluginId.PluginId>,
|
+bundledPlugins: $ReadOnlyArray<pluginId.PluginId>,
|
||||||
|};
|
|};
|
||||||
export type LoadFailure = {|+type: "FAILURE", +error: any|};
|
export type LoadFailure = {|+type: "FAILURE", +error: any|};
|
||||||
|
|
||||||
export async function load(): Promise<LoadResult> {
|
export async function load(): Promise<LoadResult> {
|
||||||
const queries = [fetch("output/credResult.json"), fetch("/sourcecred.json")];
|
const queries = [
|
||||||
|
fetch("output/credResult.json"),
|
||||||
|
fetch("/sourcecred.json"),
|
||||||
|
fetch("data/ledger.json"),
|
||||||
|
];
|
||||||
const responses = await Promise.all(queries);
|
const responses = await Promise.all(queries);
|
||||||
|
|
||||||
for (const response of responses) {
|
for (const response of responses) {
|
||||||
@ -26,7 +32,9 @@ export async function load(): Promise<LoadResult> {
|
|||||||
const credResult = credResultFromJSON(json);
|
const credResult = credResultFromJSON(json);
|
||||||
const credView = new CredView(credResult);
|
const credView = new CredView(credResult);
|
||||||
const {bundledPlugins} = await responses[1].json();
|
const {bundledPlugins} = await responses[1].json();
|
||||||
return {type: "SUCCESS", credView, bundledPlugins};
|
const ledgerJson = await responses[2].json();
|
||||||
|
const ledger = ledgerParser.parseOrThrow(ledgerJson);
|
||||||
|
return {type: "SUCCESS", credView, bundledPlugins, ledger};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return {type: "FAILURE", error: e};
|
return {type: "FAILURE", error: e};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user