aboutsummaryrefslogtreecommitdiff
path: root/packages/exchange-backoffice-ui/src/pages/Officer.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/exchange-backoffice-ui/src/pages/Officer.tsx')
-rw-r--r--packages/exchange-backoffice-ui/src/pages/Officer.tsx313
1 files changed, 231 insertions, 82 deletions
diff --git a/packages/exchange-backoffice-ui/src/pages/Officer.tsx b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
index c72ca0720..4d8b90228 100644
--- a/packages/exchange-backoffice-ui/src/pages/Officer.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
@@ -1,116 +1,265 @@
-import { useLocalStorage } from "@gnu-taler/web-util/browser";
-import { h } from "preact";
+import { TranslatedString } from "@gnu-taler/taler-util";
+import {
+ notifyError,
+ notifyInfo,
+ useLocalStorage,
+ useMemoryStorage,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
+import {
+ UnwrapKeyError,
+ createNewAccount,
+ createNewSessionId,
+ unlockAccount,
+} from "../account.js";
+import { createNewForm } from "../handlers/forms.js";
-const oldKey =
- "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDPQVq8F0Ce6kTXKQ5Ea2fZRoap6poFYs0FOln8o8+ehGI8rDdMBzNU3pLIlOMKs/vKvhDNMG4m4xxb92wDbvefDxkxaEkbRSZnRiJd4MIbh8Lx8zvFbLp03rkXu9KPN8IprKOXxgN7xbxm0KKcu03rtqLiOvC1gMqja2LMIPCi32nyNneduszHZ57d+CqIKZdVnaqAcXOSMAQsVoEq2joBOeIaSAnIJHg+T8HQ+VcLV8Y722jhX/bH84IyEMup9e7mhgVFnHgINc77c6TONH8H+dHlXCQ+hMPGw9wM+wgpJgIDzrhIN+QSjn283EOXD6z6dpiWBdEYfJRLHwEWk8wNAgMBAAECggEAB/anZrMasQsoXP9qBG1Uvq+r4fXZODFtK5vBNGi+RAWAhCX2iU3SMPB3wbby0wj1DlESR91qBhrTjqG+/TzIzUxLuARyoVZysiTVkjeIzdJVcRgwU5bTbUUs5da6MaA/WNGWMZvoALFUMBEpMQ4uDCC8OSbG8/prDtoZSuWjHrxBhsqSyIoJ3Q0iPQxPT0ShC9d5T56QuhsRQeRIWhQVtFlytXl1lqEbqljhIEOzkvS5QOcXcS3OBo/Nvdit+vi9kkLuiP8z2p6WAiVZCgCXfffNH3EEbQG/BEpIOynkchiDy1L31mFRFk1oYJRs9xD8+oF/N75GhlmYO7IbxeHw0wKBgQDnYZWjGlRM2oHpeiPSII5m9rC7qohO0ImxqifYZPp47vdRMbBWrdbxX68SqdzGfSzXcDPLfBAObG4QR8Xol1LMNJUT9og9pERZHgob+yWkTd68lLSdxfCJEKRJaDmD8dHgSrBYe86ADUeAj+fC4dycYXH//fwed1gt/G8iXtdU9wKBgQDlTp9752+tEh9fMlUdINbZXmGbjHBrZMTnAYJI509iJLIvJvYroU5TvRMsp+rACDc2Zy2nbsaCM5Xzd5wUxRBvF+PiBCFoi7c/EBaLCtb9+vyXtHAIHtzHeYUP/1cq7MOdTwrWvZqzIoW6xm7L9HRX/5i+n+rVUSxnzYIxgTlaGwKBgQC0INgpXbn7CrDQXnG8h/PUXIBB2QS8tsQ7N8hFQndr5j1LTG+HS1ZmGqNk2DAzpgdewM7RvweQ8wDMU9PSutuOdfEI1YhC1LsQ1b3xApfPTX/1N59UpGAZlIcRTr5X5c4J2ptmhxu/vJbJkz5ODR997q6dJ9E6tpZDVp3+F+9zCQKBgQCrp+OzuVjcUoixltgoagDrz7951fQCMPlFhNenA6FlctsAeUYm+yXLgersrvcIsh3C2BJRGJf5t+w0ygFJewwGXff1pensfUq8Jqr5gy/WCSE135lOOuxDVzDI/Pif5YW6KQWQI3e/ScSaQRmIDINbrLcHXGdLMOzw9+LSdE4eqQKBgQDe86MfzwMLPoDH07WC09dCcoIUSYMThYrFwUK3qgEiYaJMZJvdAIwr12szVwVRYIX4wHBObFsQZLTaY5+O/REnze6Q1AQa2H6eH1TalC1r6jBS5/LhIrVWl/0VSdsUIe41tc8xPDWrm9hmLeJLZk+xb5/hAm3REsDM1Iif9O7zzg==";
export function Officer() {
- const storage = useLocalStorage("officer");
- const [keys, setKeys] = useState({ priv: "", pub: "" });
+ const password = useMemoryStorage("password");
+ const session = useLocalStorage("session");
+ const officer = useLocalStorage("officer");
+ const [keys, setKeys] = useState({ accountId: "", pub: "" });
+
useEffect(() => {
- loadPreviousSession(oldKey).then((keys) =>
- setKeys(keys ?? { priv: "", pub: "" }),
- );
- // generateNewId().then((keys) => setKeys(keys));
+ if (
+ officer.value === undefined ||
+ session.value === undefined ||
+ password.value === undefined
+ ) {
+ return;
+ }
+ unlockAccount(session.value, officer.value, password.value)
+ .then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
+ .catch((e) => {
+ if (e instanceof UnwrapKeyError) {
+ console.log(e);
+ }
+ });
+ }, [officer.value, session.value, password.value]);
+
+ useEffect(() => {
+ if (!session.value) {
+ session.update(createNewSessionId());
+ }
}, []);
- console.log(keys.pub);
- console.log(keys.priv);
+ const { value: sessionId } = session;
+ if (!sessionId) {
+ return <div>loading...</div>;
+ }
+
+ if (officer.value === undefined) {
+ return (
+ <CreateAccount
+ sessionId={sessionId}
+ onNewAccount={(id) => {
+ password.reset();
+ officer.update(id);
+ }}
+ />
+ );
+ }
+
+ console.log("pwd", password.value);
+ if (password.value === undefined) {
+ return (
+ <UnlockAccount
+ sessionId={sessionId}
+ accountId={officer.value}
+ onAccountUnlocked={(pwd) => {
+ password.update(pwd);
+ }}
+ />
+ );
+ }
+
return (
<div>
<div>Officer</div>
+ <h1>{sessionId}</h1>
<h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
Public key
</h1>
<div>
- -----BEGIN PUBLIC KEY-----
- <p class="mt-6 leading-8 text-gray-700 break-all">{keys.pub}</p>
- -----END PUBLIC KEY-----
+ <p class="mt-6 leading-8 text-gray-700 break-all">
+ -----BEGIN PUBLIC KEY-----
+ <div>{keys.pub}</div>
+ -----END PUBLIC KEY-----
+ </p>
</div>
<h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 ">
Private key
</h1>
<div>
- -----BEGIN PRIVATE KEY-----
- <p class="mt-6 leading-8 text-gray-700 break-all">{keys.priv}</p>
- -----END PRIVATE KEY-----
+ <p class="mt-6 leading-8 text-gray-700 break-all">
+ -----BEGIN PRIVATE KEY-----
+ <div>{keys.accountId}</div>
+ -----END PRIVATE KEY-----
+ </p>
</div>
</div>
);
}
-const rsaAlgorithm: RsaHashedKeyGenParams = {
- name: "RSA-OAEP",
- modulusLength: 2048,
- publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
- hash: "SHA-256",
-};
-
-async function generateNewId() {
- const key = await crypto.subtle.generateKey(rsaAlgorithm, true, [
- "encrypt",
- "decrypt",
- ]);
-
- if (key instanceof CryptoKey) {
- throw Error("unexpected key without pair");
- }
- const { privateKey, publicKey } = key;
- const privRaw = await crypto.subtle.exportKey("pkcs8", privateKey);
-
- const pubRaw = await crypto.subtle.exportKey("spki", publicKey);
+function CreateAccount({
+ sessionId,
+ onNewAccount,
+}: {
+ sessionId: string;
+ onNewAccount: (accountId: string) => void;
+}): VNode {
+ const Form = createNewForm<{
+ email: string;
+ password: string;
+ }>();
- const priv = btoa(ab2str(privRaw));
-
- const pub = btoa(ab2str(pubRaw));
- return { priv, pub };
-}
+ return (
+ <div class="flex min-h-full flex-col ">
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
+ <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
+ Create account
+ </h2>
+ </div>
-async function loadPreviousSession(priv: string) {
- const key = str2ab(window.atob(priv));
- const privateKey = await window.crypto.subtle
- .importKey("pkcs8", key, rsaAlgorithm, true, ["decrypt"])
- .catch(throwErrorWithStack);
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+ <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+ <Form.Provider
+ onSubmit={async (v) => {
+ const keys = await createNewAccount(sessionId, v.password);
+ onNewAccount(keys.accountId);
+ }}
+ >
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Email" as TranslatedString}
+ name="email"
+ type="email"
+ required
+ />
+ </div>
- if (!privateKey) return undefined;
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Password" as TranslatedString}
+ name="password"
+ type="password"
+ required
+ />
+ </div>
- // export private key to JWK
- const jwk = await crypto.subtle
- .exportKey("jwk", privateKey)
- .catch(throwErrorWithStack);
+ <div class="mt-8">
+ <button
+ type="submit"
+ class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ Create
+ </button>
+ </div>
+ </Form.Provider>
+ </div>
+ </div>
+ </div>
+ );
+}
- // remove private data from JWK
- delete jwk.d;
- delete jwk.dp;
- delete jwk.dq;
- delete jwk.q;
- delete jwk.qi;
- jwk.key_ops = ["encrypt"];
+function UnlockAccount({
+ sessionId,
+ accountId,
+ onAccountUnlocked,
+}: {
+ sessionId: string;
+ accountId: string;
+ onAccountUnlocked: (password: string) => void;
+}): VNode {
+ const Form = createNewForm<{
+ sessionId: string;
+ accountId: string;
+ password: string;
+ }>();
- const publicKey = await crypto.subtle
- .importKey("jwk", jwk, rsaAlgorithm, true, ["encrypt"])
- .catch(throwErrorWithStack);
+ return (
+ <div class="flex min-h-full flex-col ">
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
+ <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">
+ Unlock account
+ </h2>
+ </div>
- const pubRaw = await crypto.subtle
- .exportKey("spki", publicKey)
- .catch(throwErrorWithStack);
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] ">
+ <div class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12">
+ <Form.Provider
+ initialValue={{
+ sessionId,
+ accountId:
+ accountId.substring(0, 6) +
+ "..." +
+ accountId.substring(accountId.length - 6),
+ }}
+ computeFormState={(v) => {
+ return {
+ accountId: {
+ disabled: true,
+ },
+ sessionId: {
+ disabled: true,
+ },
+ };
+ }}
+ onSubmit={async (v) => {
+ try {
+ // test login
+ await unlockAccount(sessionId, accountId, v.password);
- const pub = btoa(ab2str(pubRaw));
+ onAccountUnlocked(v.password ?? "");
+ notifyInfo("Account unlocked" as TranslatedString);
+ } catch (e) {
+ if (e instanceof UnwrapKeyError) {
+ notifyError(
+ "Could not unlock account" as any,
+ e.message as any,
+ );
+ } else {
+ throw e;
+ }
+ }
+ }}
+ >
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Session" as TranslatedString}
+ name="sessionId"
+ type="text"
+ />
+ </div>
+ <div class="mb-4">
+ <Form.InputLine
+ label={"AccountId" as TranslatedString}
+ name="accountId"
+ type="text"
+ />
+ </div>
- return { priv, pub };
-}
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Password" as TranslatedString}
+ name="password"
+ type="password"
+ required
+ />
+ </div>
-function ab2str(buf: ArrayBuffer) {
- return String.fromCharCode.apply(null, Array.from(new Uint8Array(buf)));
-}
-function str2ab(str: string) {
- const buf = new ArrayBuffer(str.length);
- const bufView = new Uint8Array(buf);
- for (let i = 0, strLen = str.length; i < strLen; i++) {
- bufView[i] = str.charCodeAt(i);
- }
- return buf;
-}
-function throwErrorWithStack(e: Error): never {
- throw new Error(e.message);
+ <div class="mt-8">
+ <button
+ type="submit"
+ class="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ >
+ Unlock
+ </button>
+ </div>
+ </Form.Provider>
+ </div>
+ </div>
+ </div>
+ );
}