aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-05-26 16:52:30 -0300
committerSebastian <sebasjm@gmail.com>2023-05-26 16:52:30 -0300
commit69b66e715eae039330898f470a8993d1d154b583 (patch)
tree794cbd7ba6ae8657dbadc96b0576f33ce488546b
parentbe27647ff73d1529372a80c3e145f3ee4f229a17 (diff)
account as hook
-rw-r--r--packages/exchange-backoffice-ui/src/account.ts14
-rw-r--r--packages/exchange-backoffice-ui/src/forms/902_1e.ts2
-rw-r--r--packages/exchange-backoffice-ui/src/forms/902_4e.ts2
-rw-r--r--packages/exchange-backoffice-ui/src/forms/simplest.ts2
-rw-r--r--packages/exchange-backoffice-ui/src/hooks/useOfficer.ts100
-rw-r--r--packages/exchange-backoffice-ui/src/pages.ts4
-rw-r--r--packages/exchange-backoffice-ui/src/pages/CaseDetails.tsx (renamed from packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx)2
-rw-r--r--packages/exchange-backoffice-ui/src/pages/Cases.tsx8
-rw-r--r--packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx89
-rw-r--r--packages/exchange-backoffice-ui/src/pages/HandleAccountNotReady.tsx31
-rw-r--r--packages/exchange-backoffice-ui/src/pages/Officer.tsx247
-rw-r--r--packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx70
12 files changed, 321 insertions, 250 deletions
diff --git a/packages/exchange-backoffice-ui/src/account.ts b/packages/exchange-backoffice-ui/src/account.ts
index 05f0f8984..bd3c2003e 100644
--- a/packages/exchange-backoffice-ui/src/account.ts
+++ b/packages/exchange-backoffice-ui/src/account.ts
@@ -16,7 +16,7 @@ export interface Account {
}
/**
- * Restore previous session and unlock account
+ * Restore previous session and unlock account with password
*
* @param salt string from which crypto params will be derived
* @param key secured private key
@@ -55,7 +55,7 @@ declare const opaque_SigningKey: unique symbol;
export type SigningKey = Uint8Array & { [opaque_SigningKey]: true };
/**
- * Create new account (secured private key) under session
+ * Create new account (secured private key)
* secured with the given password
*
* @param sessionId
@@ -64,8 +64,8 @@ export type SigningKey = Uint8Array & { [opaque_SigningKey]: true };
*/
export async function createNewAccount(
password: string,
-): Promise<LockedAccount> {
- const { eddsaPriv } = createEddsaKeyPair();
+): Promise<Account & { safe: LockedAccount }> {
+ const { eddsaPriv, eddsaPub } = createEddsaKeyPair();
const key = stringToBytes(password);
@@ -76,9 +76,11 @@ export async function createNewAccount(
password,
);
- const protectedPriv = encodeCrock(protectedPrivKey);
+ const signingKey = eddsaPriv as SigningKey;
+ const accountId = encodeCrock(eddsaPub) as AccountId;
+ const safe = encodeCrock(protectedPrivKey) as LockedAccount;
- return protectedPriv as LockedAccount;
+ return { accountId, signingKey, safe };
}
export class UnwrapKeyError extends Error {
diff --git a/packages/exchange-backoffice-ui/src/forms/902_1e.ts b/packages/exchange-backoffice-ui/src/forms/902_1e.ts
index 04952a985..654085443 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_1e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_1e.ts
@@ -8,7 +8,7 @@ import { FlexibleForm, languageList } from "./index.js";
import { FormState } from "../handlers/FormProvider.js";
import { State } from "../pages/AntiMoneyLaunderingForm.js";
import { AmlState } from "../types.js";
-import { amlStateConverter } from "../pages/AccountDetails.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
import { Simplest, resolutionSection } from "./simplest.js";
export const v1 = (current: State): FlexibleForm<Form902_1.Form> => ({
diff --git a/packages/exchange-backoffice-ui/src/forms/902_4e.ts b/packages/exchange-backoffice-ui/src/forms/902_4e.ts
index 15ad17144..f77a2f63a 100644
--- a/packages/exchange-backoffice-ui/src/forms/902_4e.ts
+++ b/packages/exchange-backoffice-ui/src/forms/902_4e.ts
@@ -11,7 +11,7 @@ import { h as create } from "preact";
import { ChevronRightIcon } from "@heroicons/react/24/solid";
import { State } from "../pages/AntiMoneyLaunderingForm.js";
import { AmlState } from "../types.js";
-import { amlStateConverter } from "../pages/AccountDetails.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
import { Simplest, resolutionSection } from "./simplest.js";
export const v1 = (current: State): FlexibleForm<Form902_4.Form> => ({
diff --git a/packages/exchange-backoffice-ui/src/forms/simplest.ts b/packages/exchange-backoffice-ui/src/forms/simplest.ts
index 5da01961b..7eda03fef 100644
--- a/packages/exchange-backoffice-ui/src/forms/simplest.ts
+++ b/packages/exchange-backoffice-ui/src/forms/simplest.ts
@@ -7,7 +7,7 @@ import {
import { FormState } from "../handlers/FormProvider.js";
import { FlexibleForm } from "./index.js";
import { AmlState } from "../types.js";
-import { amlStateConverter } from "../pages/AccountDetails.js";
+import { amlStateConverter } from "../pages/CaseDetails.js";
import { State } from "../pages/AntiMoneyLaunderingForm.js";
import { DoubleColumnFormSection, UIFormField } from "../handlers/forms.js";
diff --git a/packages/exchange-backoffice-ui/src/hooks/useOfficer.ts b/packages/exchange-backoffice-ui/src/hooks/useOfficer.ts
new file mode 100644
index 000000000..2ed375846
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/hooks/useOfficer.ts
@@ -0,0 +1,100 @@
+import {
+ AbsoluteTime,
+ Codec,
+ buildCodecForObject,
+ codecForAbsoluteTime,
+ codecForString,
+} from "@gnu-taler/taler-util";
+import {
+ Account,
+ LockedAccount,
+ createNewAccount,
+ unlockAccount,
+} from "../account.js";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useMemoryStorage,
+} from "@gnu-taler/web-util/browser";
+
+export interface Officer {
+ account: LockedAccount;
+ when: AbsoluteTime;
+}
+
+const codecForLockedAccount = codecForString() as Codec<LockedAccount>;
+
+export const codecForOfficer = (): Codec<Officer> =>
+ buildCodecForObject<Officer>()
+ .property("account", codecForLockedAccount) // FIXME
+ .property("when", codecForAbsoluteTime) // FIXME
+ .build("Officer");
+
+export type OfficerState = OfficerNotReady | OfficerReady;
+export type OfficerNotReady = OfficerNotFound | OfficerLocked;
+interface OfficerNotFound {
+ state: "not-found";
+ create: (password: string) => Promise<void>;
+}
+interface OfficerLocked {
+ state: "locked";
+ forget: () => void;
+ tryUnlock: (password: string) => Promise<void>;
+}
+interface OfficerReady {
+ state: "ready";
+ account: Account;
+ forget: () => void;
+ lock: () => void;
+}
+
+const OFFICER_KEY = buildStorageKey("officer", codecForOfficer());
+const ACCOUNT_KEY = buildStorageKey<Account>("account");
+
+export function useOfficer(): OfficerState {
+ const accountStorage = useMemoryStorage(ACCOUNT_KEY);
+ const officerStorage = useLocalStorage(OFFICER_KEY);
+
+ const officer = officerStorage.value;
+ const account = accountStorage.value;
+
+ if (officer === undefined) {
+ return {
+ state: "not-found",
+ create: async (pwd: string) => {
+ const { accountId, safe, signingKey } = await createNewAccount(pwd);
+ officerStorage.update({
+ account: safe,
+ when: AbsoluteTime.now(),
+ });
+
+ accountStorage.update({ accountId, signingKey });
+ },
+ };
+ }
+
+ if (account === undefined) {
+ return {
+ state: "locked",
+ forget: () => {
+ officerStorage.reset();
+ },
+ tryUnlock: async (pwd: string) => {
+ const ac = await unlockAccount(officer.account, pwd);
+ accountStorage.update(ac);
+ },
+ };
+ }
+
+ return {
+ state: "ready",
+ account: account,
+ lock: () => {
+ accountStorage.reset();
+ },
+ forget: () => {
+ officerStorage.reset();
+ accountStorage.reset();
+ },
+ };
+}
diff --git a/packages/exchange-backoffice-ui/src/pages.ts b/packages/exchange-backoffice-ui/src/pages.ts
index 2b13ce585..18fb7a158 100644
--- a/packages/exchange-backoffice-ui/src/pages.ts
+++ b/packages/exchange-backoffice-ui/src/pages.ts
@@ -5,7 +5,7 @@ import { Welcome } from "./pages/Welcome.js";
import { PageEntry, pageDefinition } from "./route.js";
import { Officer } from "./pages/Officer.js";
import { Cases } from "./pages/Cases.js";
-import { AccountDetails } from "./pages/AccountDetails.js";
+import { CaseDetails } from "./pages/CaseDetails.js";
import { NewFormEntry } from "./pages/NewFormEntry.js";
const home: PageEntry = {
@@ -18,7 +18,7 @@ const cases: PageEntry = {
};
const account: PageEntry<{ account?: string }> = {
url: pageDefinition("#/account/:account"),
- view: AccountDetails,
+ view: CaseDetails,
};
const newFormEntry: PageEntry<{ account?: string; type?: string }> = {
diff --git a/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx b/packages/exchange-backoffice-ui/src/pages/CaseDetails.tsx
index b252d2ab0..e5fb8eaba 100644
--- a/packages/exchange-backoffice-ui/src/pages/AccountDetails.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/CaseDetails.tsx
@@ -141,7 +141,7 @@ function getEventsFromAmlHistory(
return ae.concat(ke).sort(selectSooner);
}
-export function AccountDetails({ account }: { account?: string }) {
+export function CaseDetails({ account }: { account?: string }) {
const events = getEventsFromAmlHistory(
response.aml_history,
response.kyc_attributes,
diff --git a/packages/exchange-backoffice-ui/src/pages/Cases.tsx b/packages/exchange-backoffice-ui/src/pages/Cases.tsx
index 1983769ed..28b9d2a88 100644
--- a/packages/exchange-backoffice-ui/src/pages/Cases.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/Cases.tsx
@@ -4,8 +4,10 @@ import { AmlRecords, AmlState } from "../types.js";
import { InputChoiceHorizontal } from "../handlers/InputChoiceHorizontal.js";
import { createNewForm } from "../handlers/forms.js";
import { TranslatedString } from "@gnu-taler/taler-util";
-import { amlStateConverter as amlStateConverter } from "./AccountDetails.js";
+import { amlStateConverter as amlStateConverter } from "./CaseDetails.js";
import { useState } from "preact/hooks";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
+import { useOfficer } from "../hooks/useOfficer.js";
const response: AmlRecords = {
records: [
@@ -61,6 +63,10 @@ function doFilter(
}
export function Cases() {
+ const officer = useOfficer();
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
+ }
const form = createNewForm<{
state: AmlState;
}>();
diff --git a/packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx b/packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx
new file mode 100644
index 000000000..41a1d20ff
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx
@@ -0,0 +1,89 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { createNewForm } from "../handlers/forms.js";
+
+export function CreateAccount({
+ onNewAccount,
+}: {
+ onNewAccount: (password: string) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const Form = createNewForm<{
+ password: string;
+ repeat: string;
+ }>();
+
+ 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>
+
+ <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
+ computeFormState={(v) => {
+ return {
+ password: {
+ error: !v.password
+ ? i18n.str`required`
+ : v.password.length < 8
+ ? i18n.str`should have at least 8 characters`
+ : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
+ ? i18n.str`should have lowercase and uppercase characters`
+ : !v.password.match(/\d/)
+ ? i18n.str`should have numbers`
+ : !v.password.match(/[^a-zA-Z\d]/)
+ ? i18n.str`should have at least one character which is not a number or letter`
+ : undefined,
+ },
+ repeat: {
+ // error: !v.repeat
+ // ? i18n.str`required`
+ // // : v.repeat !== v.password
+ // // ? i18n.str`doesn't match`
+ // : undefined,
+ },
+ };
+ }}
+ onSubmit={async (v) => {
+ onNewAccount(v.password);
+ }}
+ >
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Password" as TranslatedString}
+ name="password"
+ type="password"
+ help={
+ "lower and upper case letters, number and special character" as TranslatedString
+ }
+ required
+ />
+ </div>
+ <div class="mb-4">
+ <Form.InputLine
+ label={"Repeat password" as TranslatedString}
+ name="repeat"
+ type="password"
+ required
+ />
+ </div>
+
+ <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>
+ );
+}
diff --git a/packages/exchange-backoffice-ui/src/pages/HandleAccountNotReady.tsx b/packages/exchange-backoffice-ui/src/pages/HandleAccountNotReady.tsx
new file mode 100644
index 000000000..b0c430875
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/HandleAccountNotReady.tsx
@@ -0,0 +1,31 @@
+import { VNode, h } from "preact";
+import { OfficerNotReady } from "../hooks/useOfficer.js";
+import { CreateAccount } from "./CreateAccount.js";
+import { UnlockAccount } from "./UnlockAccount.js";
+
+export function HandleAccountNotReady({
+ officer,
+}: {
+ officer: OfficerNotReady;
+}): VNode {
+ if (officer.state === "not-found") {
+ return (
+ <CreateAccount
+ onNewAccount={(password) => {
+ officer.create(password);
+ }}
+ />
+ );
+ }
+
+ if (officer.state === "locked") {
+ return (
+ <UnlockAccount
+ onAccountUnlocked={(pwd) => {
+ officer.tryUnlock(pwd);
+ }}
+ />
+ );
+ }
+ throw Error(`unexpected account state ${(officer as any).state}`);
+}
diff --git a/packages/exchange-backoffice-ui/src/pages/Officer.tsx b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
index 40ec33018..5320369e4 100644
--- a/packages/exchange-backoffice-ui/src/pages/Officer.tsx
+++ b/packages/exchange-backoffice-ui/src/pages/Officer.tsx
@@ -1,83 +1,11 @@
-import {
- AbsoluteTime,
- Codec,
- TranslatedString,
- buildCodecForObject,
- codecForAbsoluteTime,
- codecForString,
-} from "@gnu-taler/taler-util";
-import {
- notifyError,
- notifyInfo,
- useLocalStorage,
- useMemoryStorage,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import {
- Account,
- LockedAccount,
- UnwrapKeyError,
- createNewAccount,
- unlockAccount,
-} from "../account.js";
-import { createNewForm } from "../handlers/forms.js";
-
-export interface Officer {
- account: LockedAccount;
- when: AbsoluteTime;
-}
-
-const codecForLockedAccount = codecForString() as Codec<LockedAccount>;
-
-export const codecForOfficer = (): Codec<Officer> =>
- buildCodecForObject<Officer>()
- .property("account", codecForLockedAccount) // FIXME
- .property("when", codecForAbsoluteTime) // FIXME
- .build("Officer");
+import { Fragment, h } from "preact";
+import { useOfficer } from "../hooks/useOfficer.js";
+import { HandleAccountNotReady } from "./HandleAccountNotReady.js";
export function Officer() {
- const password = useMemoryStorage("password");
- const officer = useLocalStorage("officer", {
- codec: codecForOfficer(),
- });
- const [keys, setKeys] = useState<Account>();
-
- useEffect(() => {
- if (officer.value === undefined || password.value === undefined) {
- return;
- }
-
- unlockAccount(officer.value.account, password.value)
- .then((keys) => setKeys(keys ?? { accountId: "", pub: "" }))
- .catch((e) => {
- if (e instanceof UnwrapKeyError) {
- console.log(e);
- }
- });
- }, [officer.value, password.value]);
-
- if (officer.value === undefined || !officer.value.account) {
- return (
- <CreateAccount
- onNewAccount={(account, pwd) => {
- password.update(pwd);
- officer.update({ account, when: AbsoluteTime.now() });
- }}
- />
- );
- }
-
- if (password.value === undefined) {
- return (
- <UnlockAccount
- lockedAccount={officer.value.account}
- onAccountUnlocked={(pwd) => {
- password.update(pwd);
- }}
- />
- );
+ const officer = useOfficer();
+ if (officer.state !== "ready") {
+ return <HandleAccountNotReady officer={officer} />;
}
return (
@@ -86,12 +14,12 @@ export function Officer() {
Public key
</h1>
<div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg">
- <p class="mt-6 font-mono break-all">{keys?.accountId}</p>
+ <p class="mt-6 font-mono break-all">{officer.account.accountId}</p>
</div>
<p>
<a
href={`mailto:aml@exchange.taler.net?body=${encodeURIComponent(
- `I want my AML account\n\n\nPubKey: ${keys?.accountId}`,
+ `I want my AML account\n\n\nPubKey: ${officer.account.accountId}`,
)}`}
target="_blank"
rel="noreferrer"
@@ -104,7 +32,7 @@ export function Officer() {
<button
type="button"
onClick={() => {
- password.reset();
+ officer.lock();
}}
class="m-4 block rounded-md border-0 bg-gray-200 px-3 py-2 text-center text-sm text-black shadow-sm "
>
@@ -115,7 +43,7 @@ export function Officer() {
<button
type="button"
onClick={() => {
- officer.reset();
+ officer.forget();
}}
class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 "
>
@@ -125,158 +53,3 @@ export function Officer() {
</div>
);
}
-
-function CreateAccount({
- onNewAccount,
-}: {
- onNewAccount: (account: LockedAccount, password: string) => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const Form = createNewForm<{
- password: string;
- repeat: string;
- }>();
-
- 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>
-
- <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
- computeFormState={(v) => {
- return {
- password: {
- error: !v.password
- ? i18n.str`required`
- : v.password.length < 8
- ? i18n.str`should have at least 8 characters`
- : !v.password.match(/[a-z]/) && v.password.match(/[A-Z]/)
- ? i18n.str`should have lowercase and uppercase characters`
- : !v.password.match(/\d/)
- ? i18n.str`should have numbers`
- : !v.password.match(/[^a-zA-Z\d]/)
- ? i18n.str`should have at least one character which is not a number or letter`
- : undefined,
- },
- repeat: {
- // error: !v.repeat
- // ? i18n.str`required`
- // // : v.repeat !== v.password
- // // ? i18n.str`doesn't match`
- // : undefined,
- },
- };
- }}
- onSubmit={async (v) => {
- const account = await createNewAccount(v.password);
- onNewAccount(account, v.password);
- }}
- >
- <div class="mb-4">
- <Form.InputLine
- label={"Password" as TranslatedString}
- name="password"
- type="password"
- help={
- "lower and upper case letters, number and special character" as TranslatedString
- }
- required
- />
- </div>
- <div class="mb-4">
- <Form.InputLine
- label={"Repeat password" as TranslatedString}
- name="repeat"
- type="password"
- required
- />
- </div>
-
- <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>
- );
-}
-
-function UnlockAccount({
- lockedAccount,
- onAccountUnlocked,
-}: {
- lockedAccount: LockedAccount;
- onAccountUnlocked: (password: string) => void;
-}): VNode {
- const Form = createNewForm<{
- password: string;
- }>();
-
- 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">
- Account locked
- </h2>
- <p class="mt-6 text-lg leading-8 text-gray-600">
- Your account is normally locked anytime you reload. To unlock type
- your password again.
- </p>
- </div>
-
- <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) => {
- try {
- // test login
- await unlockAccount(lockedAccount, v.password);
-
- 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={"Password" as TranslatedString}
- name="password"
- type="password"
- required
- />
- </div>
-
- <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>
- );
-}
diff --git a/packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx b/packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx
new file mode 100644
index 000000000..941e28627
--- /dev/null
+++ b/packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx
@@ -0,0 +1,70 @@
+import { TranslatedString } from "@gnu-taler/taler-util";
+import { notifyError, notifyInfo } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { UnwrapKeyError } from "../account.js";
+import { createNewForm } from "../handlers/forms.js";
+
+export function UnlockAccount({
+ onAccountUnlocked,
+}: {
+ onAccountUnlocked: (password: string) => void;
+}): VNode {
+ const Form = createNewForm<{
+ password: string;
+ }>();
+
+ 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">
+ Account locked
+ </h2>
+ <p class="mt-6 text-lg leading-8 text-gray-600">
+ Your account is normally locked anytime you reload. To unlock type
+ your password again.
+ </p>
+ </div>
+
+ <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) => {
+ try {
+ await 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={"Password" as TranslatedString}
+ name="password"
+ type="password"
+ required
+ />
+ </div>
+
+ <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>
+ );
+}