account as hook
This commit is contained in:
parent
be27647ff7
commit
69b66e715e
@ -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 {
|
||||
|
@ -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> => ({
|
||||
|
@ -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> => ({
|
||||
|
@ -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";
|
||||
|
||||
|
100
packages/exchange-backoffice-ui/src/hooks/useOfficer.ts
Normal file
100
packages/exchange-backoffice-ui/src/hooks/useOfficer.ts
Normal file
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
@ -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 }> = {
|
||||
|
@ -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,
|
@ -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;
|
||||
}>();
|
||||
|
89
packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx
Normal file
89
packages/exchange-backoffice-ui/src/pages/CreateAccount.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
@ -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}`);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
70
packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx
Normal file
70
packages/exchange-backoffice-ui/src/pages/UnlockAccount.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user