diff options
Diffstat (limited to 'packages/exchange-backoffice-ui/src/pages/Officer.tsx')
-rw-r--r-- | packages/exchange-backoffice-ui/src/pages/Officer.tsx | 313 |
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> + ); } |