mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-21 08:48:14 +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 export type RouterOptions = express$RouterOptions;
|
||||
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
|
||||
<Req: express$Request, Res: express$Response>(): express$Application<Req, Res>,
|
||||
json: (opts: ?JsonOptions) => express$Middleware<>,
|
||||
text: (opts: ?express$TextOptions) => express$Middleware<>,
|
||||
// `static` property on the function
|
||||
static: <Req: express$Request, Res: express$Response>(root: string, options?: Object) => express$Middleware<Req, Res>,
|
||||
// `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 site from "./site";
|
||||
import go from "./go";
|
||||
import admin from "./admin";
|
||||
import help from "./help";
|
||||
|
||||
const sourcecred: Command = async (args, std) => {
|
||||
@ -32,6 +33,8 @@ const sourcecred: Command = async (args, std) => {
|
||||
return site(args.slice(1), std);
|
||||
case "go":
|
||||
return go(args.slice(1), std);
|
||||
case "admin":
|
||||
return admin(args.slice(1), std);
|
||||
default:
|
||||
std.err("fatal: unknown command: " + JSON.stringify(args[0]));
|
||||
std.err("fatal: run 'sourcecred help' for commands and usage");
|
||||
|
@ -34,7 +34,10 @@ const customRoutes = (loadResult: LoadSuccess) => [
|
||||
<Redirect to="/explorer" />
|
||||
</Route>,
|
||||
<Route key="admin" exact path="/admin">
|
||||
<LedgerAdmin credView={loadResult.credView} />
|
||||
<LedgerAdmin
|
||||
credView={loadResult.credView}
|
||||
initialLedger={loadResult.ledger}
|
||||
/>
|
||||
</Route>,
|
||||
];
|
||||
|
||||
|
@ -8,10 +8,11 @@ import {AliasSelector} from "./AliasSelector";
|
||||
|
||||
export type Props = {|
|
||||
+credView: CredView,
|
||||
+initialLedger: Ledger,
|
||||
|};
|
||||
|
||||
export const LedgerAdmin = ({credView}: Props) => {
|
||||
const [ledger, setLedger] = useState<Ledger>(new Ledger());
|
||||
export const LedgerAdmin = ({credView, initialLedger}: Props) => {
|
||||
const [ledger, setLedger] = useState<Ledger>(initialLedger);
|
||||
const [nextIdentityName, setIdentityName] = useState<string>("");
|
||||
const [currentIdentity, setCurrentIdentity] = useState<Identity | null>(null);
|
||||
const [promptString, setPromptString] = useState<string>("Add Identity:");
|
||||
@ -91,6 +92,21 @@ export const LedgerAdmin = ({credView}: Props) => {
|
||||
type="submit"
|
||||
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 && (
|
||||
<>
|
||||
<br />
|
||||
|
@ -2,17 +2,23 @@
|
||||
import * as pluginId from "../api/pluginId";
|
||||
import {CredView} from "../analysis/credView";
|
||||
import {fromJSON as credResultFromJSON} from "../analysis/credResult";
|
||||
import {Ledger, parser as ledgerParser} from "../ledger/ledger";
|
||||
|
||||
export type LoadResult = LoadSuccess | LoadFailure;
|
||||
export type LoadSuccess = {|
|
||||
+type: "SUCCESS",
|
||||
+credView: CredView,
|
||||
+ledger: Ledger,
|
||||
+bundledPlugins: $ReadOnlyArray<pluginId.PluginId>,
|
||||
|};
|
||||
export type LoadFailure = {|+type: "FAILURE", +error: any|};
|
||||
|
||||
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);
|
||||
|
||||
for (const response of responses) {
|
||||
@ -26,7 +32,9 @@ export async function load(): Promise<LoadResult> {
|
||||
const credResult = credResultFromJSON(json);
|
||||
const credView = new CredView(credResult);
|
||||
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) {
|
||||
console.error(e);
|
||||
return {type: "FAILURE", error: e};
|
||||
|
Loading…
x
Reference in New Issue
Block a user