From 656e735c4b1535fd9c372aaa1ac05ad25a2c7910 Mon Sep 17 00:00:00 2001 From: Richard Liu Date: Thu, 1 Feb 2024 13:28:06 -0800 Subject: [PATCH] [generic issuance] pipeline CRUD app with minimal UI (#1451) Closes #1428 TODO - [x] Request types - [x] Move requests to passport-interface - [x] Integrating with pipeline DB - [x] Integrating with user DB - [x] DB testing - [ ] proposed refreshPipeline() in-memory command https://github.com/proofcarryingdata/zupass/issues/1456 - [ ] end-to-end API tests https://github.com/proofcarryingdata/zupass/issues/1457 What's in here: - CRUD endpoints for pipeline definitions - MVP UI that allows interacting with this CRUD UI via JSON (see video below) - Basic 'permission control' for the CRUD actions, e.g., you must own a pipeline to delete it, but you can read or write to a pipeline if you are either an owner or editor - Pipeline Definition schema validation on the backend What's not in here: - The best state management / client-side fetching system - Any admin user logic - Comprehensive tests (in progress) https://github.com/proofcarryingdata/zupass/assets/36896271/c3277ca4-caca-4249-868f-cc167c80f896 --- apps/generic-issuance-client/.env.example | 5 +- apps/generic-issuance-client/build.ts | 8 +- apps/generic-issuance-client/package.json | 2 + apps/generic-issuance-client/src/constants.ts | 16 +- apps/generic-issuance-client/src/main.tsx | 4 +- .../src/pages/Dashboard.tsx | 118 +++++++++-- .../src/pages/Pipeline.tsx | 126 +++++++++++ apps/generic-issuance-client/tsconfig.json | 19 +- apps/passport-server/src/application.ts | 2 + .../database/queries/pipelineDefinitionDB.ts | 23 ++- .../src/database/queries/pipelineUserDB.ts | 71 +++++++ .../routing/routes/genericIssuanceRoutes.ts | 63 +++++- apps/passport-server/src/routing/server.ts | 12 +- .../genericIssuanceService.ts | 115 ++++++++++- .../pipelines/LemonadePipeline.ts | 77 +------ .../pipelines/PretixPipeline.ts | 55 +---- .../generic-issuance/pipelines/types.ts | 49 +---- apps/passport-server/src/types.ts | 2 + .../MockPipelineDefinitionDB.ts | 6 +- .../generic-issuance/MockPipelineUserDB.ts | 4 +- .../test/genericIssuance.spec.ts | 114 ++++++++-- .../passport-interface/src/RequestTypes.ts | 27 +++ .../passport-interface/src/api/makeRequest.ts | 111 +++++++++- .../requestGenericIssuanceDeletePipeline.ts | 26 +++ ...questGenericIssuanceGetAllUserPipelines.ts | 26 +++ .../api/requestGenericIssuanceGetPipeline.ts | 27 +++ .../requestGenericIssuanceUpsertPipeline.ts | 30 +++ .../passport-interface/src/genericIssuance.ts | 195 ++++++++++++++++++ packages/lib/passport-interface/src/index.ts | 5 + yarn.lock | 18 ++ 30 files changed, 1104 insertions(+), 252 deletions(-) create mode 100644 apps/generic-issuance-client/src/pages/Pipeline.tsx create mode 100644 packages/lib/passport-interface/src/api/requestGenericIssuanceDeletePipeline.ts create mode 100644 packages/lib/passport-interface/src/api/requestGenericIssuanceGetAllUserPipelines.ts create mode 100644 packages/lib/passport-interface/src/api/requestGenericIssuanceGetPipeline.ts create mode 100644 packages/lib/passport-interface/src/api/requestGenericIssuanceUpsertPipeline.ts create mode 100644 packages/lib/passport-interface/src/genericIssuance.ts diff --git a/apps/generic-issuance-client/.env.example b/apps/generic-issuance-client/.env.example index 782b383fe1..c6a140dbf7 100644 --- a/apps/generic-issuance-client/.env.example +++ b/apps/generic-issuance-client/.env.example @@ -1,3 +1,6 @@ # This token is used to authenticate Stytch requests for user authentication. # If you are on the team, message @rrrliu to get one. -STYTCH_PUBLIC_TOKEN= \ No newline at end of file +STYTCH_PUBLIC_TOKEN= + +PASSPORT_CLIENT_URL="http://localhost:3000" +PASSPORT_SERVER_URL="http://localhost:3002" \ No newline at end of file diff --git a/apps/generic-issuance-client/build.ts b/apps/generic-issuance-client/build.ts index 82a7d1179d..ad9c1259e6 100644 --- a/apps/generic-issuance-client/build.ts +++ b/apps/generic-issuance-client/build.ts @@ -12,7 +12,13 @@ const genericIssuanceClientAppOpts: BuildOptions = { define: { "process.env.NODE_ENV": `'${process.env.NODE_ENV}'`, "process.env.STYTCH_PUBLIC_TOKEN": `'${process.env.STYTCH_PUBLIC_TOKEN}'`, - "process.env.GENERIC_ISSUANCE_CLIENT_URL": `'${process.env.GENERIC_ISSUANCE_CLIENT_URL}'` + "process.env.GENERIC_ISSUANCE_CLIENT_URL": `'${process.env.GENERIC_ISSUANCE_CLIENT_URL}'`, + "process.env.PASSPORT_SERVER_URL": JSON.stringify( + process.env.PASSPORT_SERVER_URL || "http://localhost:3002" + ), + "process.env.PASSPORT_CLIENT_URL": JSON.stringify( + process.env.PASSPORT_CLIENT_URL || "http://localhost:3000" + ) }, entryPoints: ["src/main.tsx"], plugins: [ diff --git a/apps/generic-issuance-client/package.json b/apps/generic-issuance-client/package.json index 6add00a144..29471575eb 100644 --- a/apps/generic-issuance-client/package.json +++ b/apps/generic-issuance-client/package.json @@ -12,11 +12,13 @@ "clean": "rm -rf node_modules public/js tsconfig.tsbuildinfo" }, "dependencies": { + "@pcd/passport-interface": "0.10.0", "@stytch/react": "^15.0.0", "@stytch/vanilla-js": "^4.4.2", "dotenv": "^16.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^9.1.0", "react-router-dom": "^6.9.0", "styled-components": "^5.3.6" }, diff --git a/apps/generic-issuance-client/src/constants.ts b/apps/generic-issuance-client/src/constants.ts index 293c34ca7b..257f5dbf05 100644 --- a/apps/generic-issuance-client/src/constants.ts +++ b/apps/generic-issuance-client/src/constants.ts @@ -1,16 +1,4 @@ export const PCD_GITHUB_URL = "https://github.com/proofcarryingdata/pcd"; -export const IS_PROD = process.env.NODE_ENV === "production"; -export const IS_STAGING = process.env.NODE_ENV === "staging"; - -export const ZUPASS_URL = IS_PROD - ? "https://zupass.org/" - : IS_STAGING - ? "https://staging.zupass.org/" - : "http://localhost:3000/"; - -export const ZUPASS_SERVER_URL = IS_PROD - ? "https://api.zupass.org/" - : IS_STAGING - ? "https://api-staging.zupass.org/" - : "http://localhost:3002/"; +export const ZUPASS_URL = process.env.PASSPORT_CLIENT_URL; +export const ZUPASS_SERVER_URL = process.env.PASSPORT_SERVER_URL; diff --git a/apps/generic-issuance-client/src/main.tsx b/apps/generic-issuance-client/src/main.tsx index f3065d0c99..d18376b4b8 100644 --- a/apps/generic-issuance-client/src/main.tsx +++ b/apps/generic-issuance-client/src/main.tsx @@ -6,12 +6,14 @@ import { createHashRouter, RouterProvider } from "react-router-dom"; import { GlobalStyle } from "./components/GlobalStyle"; import Dashboard from "./pages/Dashboard"; import Home from "./pages/Home"; +import Pipeline from "./pages/Pipeline"; const stytch = new StytchUIClient(process.env.STYTCH_PUBLIC_TOKEN); const router = createHashRouter([ { path: "/", element: }, - { path: "/dashboard", element: } + { path: "/dashboard", element: }, + { path: "/pipelines/:id", element: } ]); createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/apps/generic-issuance-client/src/pages/Dashboard.tsx b/apps/generic-issuance-client/src/pages/Dashboard.tsx index 29ecd90ff1..72fa5a2fc1 100644 --- a/apps/generic-issuance-client/src/pages/Dashboard.tsx +++ b/apps/generic-issuance-client/src/pages/Dashboard.tsx @@ -1,24 +1,75 @@ +import { + PipelineDefinition, + requestGenericIssuanceGetAllUserPipelines, + requestGenericIssuanceUpsertPipeline +} from "@pcd/passport-interface"; import { useStytch, useStytchUser } from "@stytch/react"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useCallback, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; import { ZUPASS_SERVER_URL } from "../constants"; +const SAMPLE_CREATE_PIPELINE_TEXT = JSON.stringify( + { + type: "Lemonade", + editorUserIds: [], + options: { + lemonadeApiKey: "your-lemonade-api-key", + events: [] + } + }, + null, + 2 +); + export default function Dashboard(): ReactNode { const stytchClient = useStytch(); const { user } = useStytchUser(); const [isLoggingOut, setLoggingOut] = useState(false); - const [userPingMessage, setUserPingMessage] = useState(""); + // TODO: After MVP, replace with RTK hooks or a more robust state management. + const [pipelines, setPipelines] = useState([]); + const [isLoading, setLoading] = useState(true); + const [isCreatingPipeline, setCreatingPipeline] = useState(false); + const [newPipelineRaw, setNewPipelineRaw] = useState( + SAMPLE_CREATE_PIPELINE_TEXT + ); const [error, setError] = useState(""); - useEffect(() => { - setUserPingMessage("Pinging server..."); - fetch(new URL("generic-issuance/api/user/ping", ZUPASS_SERVER_URL).href, { - credentials: "include" - }) - .then((res) => res.json()) - .then((message) => setUserPingMessage(`JWT valid, received ${message}.`)) - .catch((e) => setUserPingMessage(`Error: ${e}`)); + const fetchAllPipelines = useCallback(async () => { + setLoading(true); + const res = + await requestGenericIssuanceGetAllUserPipelines(ZUPASS_SERVER_URL); + if (res.success) { + setPipelines(res.value); + } else { + // TODO: Better errors + alert(`An error occurred while fetching user pipelines: ${res.error}`); + } + setLoading(false); }, []); + useEffect(() => { + fetchAllPipelines(); + }, [fetchAllPipelines]); + + const createPipeline = async (): Promise => { + if (!newPipelineRaw) return; + const res = await requestGenericIssuanceUpsertPipeline( + ZUPASS_SERVER_URL, + JSON.parse(newPipelineRaw) + ); + await fetchAllPipelines(); + if (res.success) { + setCreatingPipeline(false); + } else { + // TODO: Better errors + alert(`An error occurred while creating pipeline: ${res.error}`); + } + }; + + if (isLoading) { + return
Loading...
; + } + if (!user) { window.location.href = "/"; } @@ -36,20 +87,53 @@ export default function Dashboard(): ReactNode {

Congrats - you are now logged in as {user.emails?.[0]?.email}.

- {userPingMessage &&

{userPingMessage}

} + +

My Pipelines

+ {!pipelines.length &&

No pipelines right now - go create some!

} + {!!pipelines.length && ( +
    + {pipelines.map((p) => ( + +
  1. + id: {p.id}, type: {p.type} +
  2. + + ))} +
+ )} +

+ + {isCreatingPipeline && ( +

+