admin refactor
This commit is contained in:
parent
b3c747151b
commit
062939d9cc
@ -19,14 +19,14 @@ import { VNode, h } from "preact";
|
|||||||
import { Route, Router, route } from "preact-router";
|
import { Route, Router, route } from "preact-router";
|
||||||
import { useEffect } from "preact/hooks";
|
import { useEffect } from "preact/hooks";
|
||||||
import { BankFrame } from "../pages/BankFrame.js";
|
import { BankFrame } from "../pages/BankFrame.js";
|
||||||
import { BusinessAccount } from "../pages/BusinessAccount.js";
|
import { BusinessAccount } from "../pages/business/Home.js";
|
||||||
import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js";
|
import { HomePage, WithdrawalOperationPage } from "../pages/HomePage.js";
|
||||||
import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js";
|
import { PublicHistoriesPage } from "../pages/PublicHistoriesPage.js";
|
||||||
import { RegistrationPage } from "../pages/RegistrationPage.js";
|
import { RegistrationPage } from "../pages/RegistrationPage.js";
|
||||||
import { Test } from "../pages/Test.js";
|
import { Test } from "../pages/Test.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { LoginForm } from "../pages/LoginForm.js";
|
import { LoginForm } from "../pages/LoginForm.js";
|
||||||
import { AdminPage } from "../pages/AdminPage.js";
|
import { AdminHome } from "../pages/admin/Home.js";
|
||||||
|
|
||||||
export function Routing(): VNode {
|
export function Routing(): VNode {
|
||||||
const history = createHashHistory();
|
const history = createHashHistory();
|
||||||
@ -34,6 +34,7 @@ export function Routing(): VNode {
|
|||||||
|
|
||||||
if (backend.state.status === "loggedOut") {
|
if (backend.state.status === "loggedOut") {
|
||||||
return <BankFrame
|
return <BankFrame
|
||||||
|
account={undefined}
|
||||||
goToBusinessAccount={() => {
|
goToBusinessAccount={() => {
|
||||||
route("/business");
|
route("/business");
|
||||||
}}
|
}}
|
||||||
@ -63,7 +64,7 @@ export function Routing(): VNode {
|
|||||||
</Router>
|
</Router>
|
||||||
</BankFrame>
|
</BankFrame>
|
||||||
}
|
}
|
||||||
const isAdmin = backend.state.isUserAdministrator
|
const { isUserAdministrator, username } = backend.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BankFrame
|
<BankFrame
|
||||||
@ -108,14 +109,15 @@ export function Routing(): VNode {
|
|||||||
<Route
|
<Route
|
||||||
path="/account"
|
path="/account"
|
||||||
component={() => {
|
component={() => {
|
||||||
if (isAdmin) {
|
if (isUserAdministrator) {
|
||||||
return <AdminPage
|
return <AdminHome
|
||||||
onRegister={() => {
|
onRegister={() => {
|
||||||
route("/register");
|
route("/register");
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
} else {
|
} else {
|
||||||
return <HomePage
|
return <HomePage
|
||||||
|
account={username}
|
||||||
onPendingOperationFound={(wopid) => {
|
onPendingOperationFound={(wopid) => {
|
||||||
route(`/operation/${wopid}`);
|
route(`/operation/${wopid}`);
|
||||||
}}
|
}}
|
||||||
@ -130,6 +132,7 @@ export function Routing(): VNode {
|
|||||||
path="/business"
|
path="/business"
|
||||||
component={() => (
|
component={() => (
|
||||||
<BusinessAccount
|
<BusinessAccount
|
||||||
|
account={username}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
route("/account");
|
route("/account");
|
||||||
}}
|
}}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,7 @@ import { useBackendContext } from "../context/backend.js";
|
|||||||
import { getInitialBackendBaseURL } from "../hooks/backend.js";
|
import { getInitialBackendBaseURL } from "../hooks/backend.js";
|
||||||
import { useSettings } from "../hooks/settings.js";
|
import { useSettings } from "../hooks/settings.js";
|
||||||
import { AccountPage } from "./AccountPage/index.js";
|
import { AccountPage } from "./AccountPage/index.js";
|
||||||
import { AdminPage } from "./AdminPage.js";
|
import { AdminHome } from "./admin/Home.js";
|
||||||
import { LoginForm } from "./LoginForm.js";
|
import { LoginForm } from "./LoginForm.js";
|
||||||
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
|
||||||
import { error } from "console";
|
import { error } from "console";
|
||||||
@ -54,31 +54,24 @@ const logger = new Logger("AccountPage");
|
|||||||
*/
|
*/
|
||||||
export function HomePage({
|
export function HomePage({
|
||||||
onRegister,
|
onRegister,
|
||||||
|
account,
|
||||||
onPendingOperationFound,
|
onPendingOperationFound,
|
||||||
}: {
|
}: {
|
||||||
|
account: string,
|
||||||
onPendingOperationFound: (id: string) => void;
|
onPendingOperationFound: (id: string) => void;
|
||||||
onRegister: () => void;
|
onRegister: () => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const backend = useBackendContext();
|
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
if (backend.state.status === "loggedOut") {
|
|
||||||
return <LoginForm onRegister={onRegister} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.currentWithdrawalOperationId) {
|
if (settings.currentWithdrawalOperationId) {
|
||||||
onPendingOperationFound(settings.currentWithdrawalOperationId);
|
onPendingOperationFound(settings.currentWithdrawalOperationId);
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backend.state.isUserAdministrator) {
|
|
||||||
return <AdminPage onRegister={onRegister} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountPage
|
<AccountPage
|
||||||
account={backend.state.username}
|
account={account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -105,8 +98,8 @@ export function WithdrawalOperationPage({
|
|||||||
|
|
||||||
if (!parsedUri) {
|
if (!parsedUri) {
|
||||||
notifyError(
|
notifyError(
|
||||||
i18n.str`The Withdrawal URI is not valid: "${uri}"`,
|
i18n.str`The Withdrawal URI is not valid`,
|
||||||
undefined
|
uri as TranslatedString
|
||||||
);
|
);
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
143
packages/demobank-ui/src/pages/ShowAccountDetails.tsx
Normal file
143
packages/demobank-ui/src/pages/ShowAccountDetails.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { VNode,h } from "preact";
|
||||||
|
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import { buildRequestErrorMessage } from "../utils.js";
|
||||||
|
import { AccountForm } from "./admin/AccountForm.js";
|
||||||
|
|
||||||
|
export function ShowAccountDetails({
|
||||||
|
account,
|
||||||
|
onClear,
|
||||||
|
onUpdateSuccess,
|
||||||
|
onLoadNotOk,
|
||||||
|
onChangePassword,
|
||||||
|
}: {
|
||||||
|
onLoadNotOk: <T>(
|
||||||
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
|
) => VNode;
|
||||||
|
onClear?: () => void;
|
||||||
|
onChangePassword: () => void;
|
||||||
|
onUpdateSuccess: () => void;
|
||||||
|
account: string;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const result = useBusinessAccountDetails(account);
|
||||||
|
const { updateAccount } = useAdminAccountAPI();
|
||||||
|
const [update, setUpdate] = useState(false);
|
||||||
|
const [submitAccount, setSubmitAccount] = useState<
|
||||||
|
SandboxBackend.Circuit.CircuitAccountData | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
|
return onLoadNotOk(result);
|
||||||
|
}
|
||||||
|
if (result.status === HttpStatusCode.NotFound) {
|
||||||
|
return <div>account not found</div>;
|
||||||
|
}
|
||||||
|
return onLoadNotOk(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 class="nav welcome-text">
|
||||||
|
<i18n.Translate>Business account details</i18n.Translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
||||||
|
<AccountForm
|
||||||
|
template={result.data}
|
||||||
|
purpose={update ? "update" : "show"}
|
||||||
|
onChange={(a) => setSubmitAccount(a)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p class="buttons-account">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
flexFlow: "wrap-reverse",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{onClear ? (
|
||||||
|
<input
|
||||||
|
class="pure-button"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Close`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClear();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="select-exchange"
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
disabled={update && !submitAccount}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Change password`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onChangePassword();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="select-exchange"
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
disabled={update && !submitAccount}
|
||||||
|
type="submit"
|
||||||
|
value={update ? i18n.str`Confirm` : i18n.str`Update`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!update) {
|
||||||
|
setUpdate(true);
|
||||||
|
} else {
|
||||||
|
if (!submitAccount) return;
|
||||||
|
try {
|
||||||
|
await updateAccount(account, {
|
||||||
|
cashout_address: submitAccount.cashout_address,
|
||||||
|
contact_data: submitAccount.contact_data,
|
||||||
|
});
|
||||||
|
onUpdateSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The rights to change the account are not sufficient`
|
||||||
|
: status === HttpStatusCode.NotFound
|
||||||
|
? i18n.str`The username was not found`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
131
packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
Normal file
131
packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { useAdminAccountAPI, useBusinessAccountDetails } from "../hooks/circuit.js";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import { VNode,h ,Fragment} from "preact";
|
||||||
|
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
|
||||||
|
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
||||||
|
|
||||||
|
export function UpdateAccountPassword({
|
||||||
|
account,
|
||||||
|
onClear,
|
||||||
|
onUpdateSuccess,
|
||||||
|
onLoadNotOk,
|
||||||
|
}: {
|
||||||
|
onLoadNotOk: <T>(
|
||||||
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
|
) => VNode;
|
||||||
|
onClear: () => void;
|
||||||
|
onUpdateSuccess: () => void;
|
||||||
|
account: string;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const result = useBusinessAccountDetails(account);
|
||||||
|
const { changePassword } = useAdminAccountAPI();
|
||||||
|
const [password, setPassword] = useState<string | undefined>();
|
||||||
|
const [repeat, setRepeat] = useState<string | undefined>();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
|
return onLoadNotOk(result);
|
||||||
|
}
|
||||||
|
if (result.status === HttpStatusCode.NotFound) {
|
||||||
|
return <div>account not found</div>;
|
||||||
|
}
|
||||||
|
return onLoadNotOk(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = undefinedIfEmpty({
|
||||||
|
password: !password ? i18n.str`required` : undefined,
|
||||||
|
repeat: !repeat
|
||||||
|
? i18n.str`required`
|
||||||
|
: password !== repeat
|
||||||
|
? i18n.str`password doesn't match`
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 class="nav welcome-text">
|
||||||
|
<i18n.Translate>Update password for {account}</i18n.Translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
||||||
|
<form class="pure-form">
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Password`}</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPassword(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.password}
|
||||||
|
isDirty={password !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Repeat password`}</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={repeat ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRepeat(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.repeat}
|
||||||
|
isDirty={repeat !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
class="pure-button"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Close`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClear();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="select-exchange"
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
disabled={!!errors}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Confirm`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!!errors || !password) return;
|
||||||
|
try {
|
||||||
|
const r = await changePassword(account, {
|
||||||
|
new_password: password,
|
||||||
|
});
|
||||||
|
onUpdateSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(buildRequestErrorMessage(i18n, error.cause));
|
||||||
|
} else {
|
||||||
|
notifyError(i18n.str`Operation failed, please report`, (error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -317,7 +317,8 @@ export function WithdrawalConfirmationQuestion({
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
|
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
|
||||||
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{Amounts.stringifyValue(details.amount)}</dd>
|
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">To be added</dd>
|
||||||
|
{/* Amounts.stringifyValue(details.amount) */}
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
@ -100,10 +100,6 @@ export function WithdrawalQRCode({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.confirmation_done) {
|
if (data.confirmation_done) {
|
||||||
if (!settings.showWithdrawalSuccess) {
|
|
||||||
clearCurrentWithdrawal()
|
|
||||||
onContinue()
|
|
||||||
}
|
|
||||||
return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
|
return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
|
||||||
<div>
|
<div>
|
||||||
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||||
|
56
packages/demobank-ui/src/pages/admin/Account.tsx
Normal file
56
packages/demobank-ui/src/pages/admin/Account.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Amounts } from "@gnu-taler/taler-util";
|
||||||
|
import { PaytoWireTransferForm } from "../PaytoWireTransferForm.js";
|
||||||
|
import { handleNotOkResult } from "../HomePage.js";
|
||||||
|
import { useAccountDetails } from "../../hooks/access.js";
|
||||||
|
import { useBackendContext } from "../../context/backend.js";
|
||||||
|
import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
|
||||||
|
export function AdminAccount({ onRegister }: { onRegister: () => void }): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const r = useBackendContext();
|
||||||
|
const account = r.state.status === "loggedIn" ? r.state.username : "admin";
|
||||||
|
const result = useAccountDetails(account);
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
return handleNotOkResult(i18n, onRegister)(result);
|
||||||
|
}
|
||||||
|
const { data } = result;
|
||||||
|
const balance = Amounts.parseOrThrow(data.balance.amount);
|
||||||
|
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
|
||||||
|
const balanceIsDebit = result.data.balance.credit_debit_indicator == "debit";
|
||||||
|
const limit = balanceIsDebit
|
||||||
|
? Amounts.sub(debitThreshold, balance).amount
|
||||||
|
: Amounts.add(balance, debitThreshold).amount;
|
||||||
|
if (!balance) return <Fragment />;
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<section id="assets">
|
||||||
|
<div class="asset-summary">
|
||||||
|
<h2>{i18n.str`Bank account balance`}</h2>
|
||||||
|
{!balance ? (
|
||||||
|
<div class="large-amount" style={{ color: "gray" }}>
|
||||||
|
Waiting server response...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="large-amount amount">
|
||||||
|
{balanceIsDebit ? <b>-</b> : null}
|
||||||
|
<span class="value">{`${Amounts.stringifyValue(balance)}`}</span>
|
||||||
|
|
||||||
|
<span class="currency">{`${balance.currency}`}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<PaytoWireTransferForm
|
||||||
|
focus
|
||||||
|
limit={limit}
|
||||||
|
onSuccess={() => {
|
||||||
|
notifyInfo(i18n.str`Wire transfer created!`);
|
||||||
|
}}
|
||||||
|
onCancel={undefined}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
219
packages/demobank-ui/src/pages/admin/AccountForm.tsx
Normal file
219
packages/demobank-ui/src/pages/admin/AccountForm.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import { VNode,h } from "preact";
|
||||||
|
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
|
||||||
|
import { PartialButDefined, RecursivePartial, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { parsePaytoUri } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
|
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
|
||||||
|
const EMAIL_REGEX =
|
||||||
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create valid account object to update or create
|
||||||
|
* Take template as initial values for the form
|
||||||
|
* Purpose indicate if all field al read only (show), part of them (update)
|
||||||
|
* or none (create)
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function AccountForm({
|
||||||
|
template,
|
||||||
|
purpose,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
template: SandboxBackend.Circuit.CircuitAccountData | undefined;
|
||||||
|
onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
|
||||||
|
purpose: "create" | "update" | "show";
|
||||||
|
}): VNode {
|
||||||
|
const initial = initializeFromTemplate(template);
|
||||||
|
const [form, setForm] = useState(initial);
|
||||||
|
const [errors, setErrors] = useState<
|
||||||
|
RecursivePartial<typeof initial> | undefined
|
||||||
|
>(undefined);
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
function updateForm(newForm: typeof initial): void {
|
||||||
|
const parsed = !newForm.cashout_address
|
||||||
|
? undefined
|
||||||
|
: parsePaytoUri(newForm.cashout_address);
|
||||||
|
|
||||||
|
const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
|
||||||
|
cashout_address: !newForm.cashout_address
|
||||||
|
? i18n.str`required`
|
||||||
|
: !parsed
|
||||||
|
? i18n.str`does not follow the pattern`
|
||||||
|
: !parsed.isKnown || parsed.targetType !== "iban"
|
||||||
|
? i18n.str`only "IBAN" target are supported`
|
||||||
|
: !IBAN_REGEX.test(parsed.iban)
|
||||||
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||||
|
: validateIBAN(parsed.iban, i18n),
|
||||||
|
contact_data: undefinedIfEmpty({
|
||||||
|
email: !newForm.contact_data?.email
|
||||||
|
? i18n.str`required`
|
||||||
|
: !EMAIL_REGEX.test(newForm.contact_data.email)
|
||||||
|
? i18n.str`it should be an email`
|
||||||
|
: undefined,
|
||||||
|
phone: !newForm.contact_data?.phone
|
||||||
|
? i18n.str`required`
|
||||||
|
: !newForm.contact_data.phone.startsWith("+")
|
||||||
|
? i18n.str`should start with +`
|
||||||
|
: !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
|
||||||
|
? i18n.str`phone number can't have other than numbers`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
iban: !newForm.iban
|
||||||
|
? undefined //optional field
|
||||||
|
: !IBAN_REGEX.test(newForm.iban)
|
||||||
|
? i18n.str`IBAN should have just uppercased letters and numbers`
|
||||||
|
: validateIBAN(newForm.iban, i18n),
|
||||||
|
name: !newForm.name ? i18n.str`required` : undefined,
|
||||||
|
username: !newForm.username ? i18n.str`required` : undefined,
|
||||||
|
});
|
||||||
|
setErrors(errors);
|
||||||
|
setForm(newForm);
|
||||||
|
onChange(errors === undefined ? (newForm as any) : undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form class="pure-form">
|
||||||
|
<fieldset>
|
||||||
|
<label for="username">
|
||||||
|
{i18n.str`Username`}
|
||||||
|
{purpose === "create" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
disabled={purpose !== "create"}
|
||||||
|
value={form.username}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.username = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>{" "}
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.username}
|
||||||
|
isDirty={form.username !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
{i18n.str`Name`}
|
||||||
|
{purpose === "create" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
disabled={purpose !== "create"}
|
||||||
|
value={form.name ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.name = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.name}
|
||||||
|
isDirty={form.name !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
{purpose !== "create" && (
|
||||||
|
<fieldset>
|
||||||
|
<label>{i18n.str`Internal IBAN`}</label>
|
||||||
|
<input
|
||||||
|
disabled={true}
|
||||||
|
value={form.iban ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.iban = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.iban}
|
||||||
|
isDirty={form.iban !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
{i18n.str`Email`}
|
||||||
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
disabled={purpose === "show"}
|
||||||
|
value={form.contact_data.email ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.contact_data.email = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.contact_data?.email}
|
||||||
|
isDirty={form.contact_data.email !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
{i18n.str`Phone`}
|
||||||
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
disabled={purpose === "show"}
|
||||||
|
value={form.contact_data.phone ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.contact_data.phone = e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.contact_data?.phone}
|
||||||
|
isDirty={form.contact_data?.phone !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
{i18n.str`Cashout address`}
|
||||||
|
{purpose !== "show" && <b style={{ color: "red" }}>*</b>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
disabled={purpose === "show"}
|
||||||
|
value={(form.cashout_address ?? "").substring("payto://iban/".length)}
|
||||||
|
onChange={(e) => {
|
||||||
|
form.cashout_address = "payto://iban/" + e.currentTarget.value;
|
||||||
|
updateForm(structuredClone(form));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ShowInputErrorLabel
|
||||||
|
message={errors?.cashout_address}
|
||||||
|
isDirty={form.cashout_address !== undefined}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeFromTemplate(
|
||||||
|
account: SandboxBackend.Circuit.CircuitAccountData | undefined,
|
||||||
|
): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
|
||||||
|
const emptyAccount = {
|
||||||
|
cashout_address: undefined,
|
||||||
|
iban: undefined,
|
||||||
|
name: undefined,
|
||||||
|
username: undefined,
|
||||||
|
contact_data: undefined,
|
||||||
|
};
|
||||||
|
const emptyContact = {
|
||||||
|
email: undefined,
|
||||||
|
phone: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
|
||||||
|
structuredClone(account) ?? emptyAccount;
|
||||||
|
if (typeof initial.contact_data === "undefined") {
|
||||||
|
initial.contact_data = emptyContact;
|
||||||
|
}
|
||||||
|
initial.contact_data.email;
|
||||||
|
return initial as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
120
packages/demobank-ui/src/pages/admin/AccountList.tsx
Normal file
120
packages/demobank-ui/src/pages/admin/AccountList.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { h, VNode } from "preact";
|
||||||
|
import { useBusinessAccounts } from "../../hooks/circuit.js";
|
||||||
|
import { handleNotOkResult } from "../HomePage.js";
|
||||||
|
import { AccountAction } from "./Home.js";
|
||||||
|
import { Amounts } from "@gnu-taler/taler-util";
|
||||||
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onAction: (type: AccountAction, account: string) => void;
|
||||||
|
account: string | undefined;
|
||||||
|
onRegister: () => void;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountList({ account, onAction, onRegister }: Props): VNode {
|
||||||
|
const result = useBusinessAccounts({ account });
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
if (result.loading) return <div />;
|
||||||
|
if (!result.ok) {
|
||||||
|
return handleNotOkResult(i18n, onRegister)(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { customers } = result.data;
|
||||||
|
return <section
|
||||||
|
id="main"
|
||||||
|
style={{ width: 600, marginLeft: "auto", marginRight: "auto" }}
|
||||||
|
>
|
||||||
|
{!customers.length ? (
|
||||||
|
<div></div>
|
||||||
|
) : (
|
||||||
|
<article>
|
||||||
|
<h2>{i18n.str`Accounts:`}</h2>
|
||||||
|
<div class="results">
|
||||||
|
<table class="pure-table pure-table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{i18n.str`Username`}</th>
|
||||||
|
<th>{i18n.str`Name`}</th>
|
||||||
|
<th>{i18n.str`Balance`}</th>
|
||||||
|
<th>{i18n.str`Actions`}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{customers.map((item, idx) => {
|
||||||
|
const balance = !item.balance
|
||||||
|
? undefined
|
||||||
|
: Amounts.parse(item.balance.amount);
|
||||||
|
const balanceIsDebit =
|
||||||
|
item.balance &&
|
||||||
|
item.balance.credit_debit_indicator == "debit";
|
||||||
|
return (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onAction("show-details", item.username)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.username}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td>
|
||||||
|
{!balance ? (
|
||||||
|
i18n.str`unknown`
|
||||||
|
) : (
|
||||||
|
<span class="amount">
|
||||||
|
{balanceIsDebit ? <b>-</b> : null}
|
||||||
|
<span class="value">{`${Amounts.stringifyValue(
|
||||||
|
balance,
|
||||||
|
)}`}</span>
|
||||||
|
|
||||||
|
<span class="currency">{`${balance.currency}`}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onAction("update-password", item.username)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
change password
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onAction("show-cashout", item.username)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
cashouts
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onAction("remove-account", item.username)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
remove
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
}
|
107
packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
Normal file
107
packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { VNode, h, Fragment } from "preact";
|
||||||
|
import { useAdminAccountAPI } from "../../hooks/circuit.js";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { buildRequestErrorMessage } from "../../utils.js";
|
||||||
|
import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import { getRandomPassword } from "../rnd.js";
|
||||||
|
import { AccountForm } from "./AccountForm.js";
|
||||||
|
|
||||||
|
export function CreateNewAccount({
|
||||||
|
onClose,
|
||||||
|
onCreateSuccess,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onCreateSuccess: (password: string) => void;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const { createAccount } = useAdminAccountAPI();
|
||||||
|
const [submitAccount, setSubmitAccount] = useState<
|
||||||
|
SandboxBackend.Circuit.CircuitAccountData | undefined
|
||||||
|
>();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 class="nav welcome-text">
|
||||||
|
<i18n.Translate>New account</i18n.Translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ maxWidth: 600, overflowX: "hidden", margin: "auto" }}>
|
||||||
|
<AccountForm
|
||||||
|
template={undefined}
|
||||||
|
purpose="create"
|
||||||
|
onChange={(a) => {
|
||||||
|
setSubmitAccount(a);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
class="pure-button"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Close`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="select-exchange"
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
disabled={!submitAccount}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Confirm`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!submitAccount) return;
|
||||||
|
try {
|
||||||
|
const account: SandboxBackend.Circuit.CircuitAccountRequest =
|
||||||
|
{
|
||||||
|
cashout_address: submitAccount.cashout_address,
|
||||||
|
contact_data: submitAccount.contact_data,
|
||||||
|
internal_iban: submitAccount.iban,
|
||||||
|
name: submitAccount.name,
|
||||||
|
username: submitAccount.username,
|
||||||
|
password: getRandomPassword(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await createAccount(account);
|
||||||
|
onCreateSuccess(account.password);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The rights to perform the operation are not sufficient`
|
||||||
|
: status === HttpStatusCode.BadRequest
|
||||||
|
? i18n.str`Input data was invalid`
|
||||||
|
: status === HttpStatusCode.Conflict
|
||||||
|
? i18n.str`At least one registration detail was not available`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(
|
||||||
|
i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
162
packages/demobank-ui/src/pages/admin/Home.tsx
Normal file
162
packages/demobank-ui/src/pages/admin/Home.tsx
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { Fragment, VNode, h } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { Cashouts } from "../../components/Cashouts/index.js";
|
||||||
|
import { ShowCashoutDetails } from "../business/Home.js";
|
||||||
|
import { handleNotOkResult } from "../HomePage.js";
|
||||||
|
import { ShowAccountDetails } from "../ShowAccountDetails.js";
|
||||||
|
import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
|
||||||
|
import { AdminAccount } from "./Account.js";
|
||||||
|
import { AccountList } from "./AccountList.js";
|
||||||
|
import { CreateNewAccount } from "./CreateNewAccount.js";
|
||||||
|
import { RemoveAccount } from "./RemoveAccount.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query account information and show QR code if there is pending withdrawal
|
||||||
|
*/
|
||||||
|
interface Props {
|
||||||
|
onRegister: () => void;
|
||||||
|
}
|
||||||
|
export type AccountAction = "show-details" |
|
||||||
|
"show-cashout" |
|
||||||
|
"update-password" |
|
||||||
|
"remove-account" |
|
||||||
|
"show-cashouts-details";
|
||||||
|
|
||||||
|
export function AdminHome({ onRegister }: Props): VNode {
|
||||||
|
const [action, setAction] = useState<{
|
||||||
|
type: AccountAction,
|
||||||
|
account: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const [createAccount, setCreateAccount] = useState(false);
|
||||||
|
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case "show-details": return <ShowCashoutDetails
|
||||||
|
id={action.account}
|
||||||
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
|
onCancel={() => {
|
||||||
|
setAction(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
case "show-cashout": return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 class="nav welcome-text">
|
||||||
|
<i18n.Translate>Cashout for account {action.account}</i18n.Translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<Cashouts
|
||||||
|
account={action.account}
|
||||||
|
onSelected={(id) => {
|
||||||
|
setAction({
|
||||||
|
type: "show-cashouts-details",
|
||||||
|
account: action.account
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
<input
|
||||||
|
class="pure-button"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Close`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setAction(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
case "update-password": return <UpdateAccountPassword
|
||||||
|
account={action.account}
|
||||||
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
|
onUpdateSuccess={() => {
|
||||||
|
notifyInfo(i18n.str`Password changed`);
|
||||||
|
setAction(undefined);
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
setAction(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
case "remove-account": return <RemoveAccount
|
||||||
|
account={action.account}
|
||||||
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
|
onUpdateSuccess={() => {
|
||||||
|
notifyInfo(i18n.str`Account removed`);
|
||||||
|
setAction(undefined);
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
setAction(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
case "show-cashouts-details": return <ShowAccountDetails
|
||||||
|
account={action.account}
|
||||||
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
|
onChangePassword={() => {
|
||||||
|
setAction({
|
||||||
|
type: "update-password",
|
||||||
|
account: action.account,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onUpdateSuccess={() => {
|
||||||
|
notifyInfo(i18n.str`Account updated`);
|
||||||
|
setAction(undefined);
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
setAction(undefined);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createAccount) {
|
||||||
|
return (
|
||||||
|
<CreateNewAccount
|
||||||
|
onClose={() => setCreateAccount(false)}
|
||||||
|
onCreateSuccess={(password) => {
|
||||||
|
notifyInfo(
|
||||||
|
i18n.str`Account created with password "${password}". The user must change the password on the next login.`,
|
||||||
|
);
|
||||||
|
setCreateAccount(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div>
|
||||||
|
<h1 class="nav welcome-text">
|
||||||
|
<i18n.Translate>Admin panel</i18n.Translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Create account`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setCreateAccount(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<AdminAccount onRegister={onRegister} />
|
||||||
|
|
||||||
|
<AccountList account={undefined} onAction={(type,account) => setAction({account, type})} onRegister={onRegister}/>
|
||||||
|
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
112
packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
Normal file
112
packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { ErrorType, HttpResponsePaginated, RequestError, notify, notifyError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { VNode,h,Fragment } from "preact";
|
||||||
|
import { useAccountDetails } from "../../hooks/access.js";
|
||||||
|
import { useAdminAccountAPI } from "../../hooks/circuit.js";
|
||||||
|
import { Amounts, HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
|
||||||
|
import { buildRequestErrorMessage } from "../../utils.js";
|
||||||
|
|
||||||
|
export function RemoveAccount({
|
||||||
|
account,
|
||||||
|
onClear,
|
||||||
|
onUpdateSuccess,
|
||||||
|
onLoadNotOk,
|
||||||
|
}: {
|
||||||
|
onLoadNotOk: <T>(
|
||||||
|
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
|
||||||
|
) => VNode;
|
||||||
|
onClear: () => void;
|
||||||
|
onUpdateSuccess: () => void;
|
||||||
|
account: string;
|
||||||
|
}): VNode {
|
||||||
|
const { i18n } = useTranslationContext();
|
||||||
|
const result = useAccountDetails(account);
|
||||||
|
const { deleteAccount } = useAdminAccountAPI();
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
if (result.loading || result.type === ErrorType.TIMEOUT) {
|
||||||
|
return onLoadNotOk(result);
|
||||||
|
}
|
||||||
|
if (result.status === HttpStatusCode.NotFound) {
|
||||||
|
return <div>account not found</div>;
|
||||||
|
}
|
||||||
|
return onLoadNotOk(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const balance = Amounts.parse(result.data.balance.amount);
|
||||||
|
if (!balance) {
|
||||||
|
return <div>there was an error reading the balance</div>;
|
||||||
|
}
|
||||||
|
const isBalanceEmpty = Amounts.isZero(balance);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h1 class="nav welcome-text">
|
||||||
|
<i18n.Translate>Remove account: {account}</i18n.Translate>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
{/* {FXME: SHOW WARNING} */}
|
||||||
|
{/* {!isBalanceEmpty && (
|
||||||
|
<ErrorBannerFloat
|
||||||
|
error={{
|
||||||
|
title: i18n.str`Can't delete the account`,
|
||||||
|
description: i18n.str`Balance is not empty`,
|
||||||
|
}}
|
||||||
|
onClear={() => saveError(undefined)}
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
class="pure-button"
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Cancel`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClear();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="select-exchange"
|
||||||
|
class="pure-button pure-button-primary content"
|
||||||
|
disabled={!isBalanceEmpty}
|
||||||
|
type="submit"
|
||||||
|
value={i18n.str`Confirm`}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
const r = await deleteAccount(account);
|
||||||
|
onUpdateSuccess();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof RequestError) {
|
||||||
|
notify(
|
||||||
|
buildRequestErrorMessage(i18n, error.cause, {
|
||||||
|
onClientError: (status) =>
|
||||||
|
status === HttpStatusCode.Forbidden
|
||||||
|
? i18n.str`The administrator specified a institutional username`
|
||||||
|
: status === HttpStatusCode.NotFound
|
||||||
|
? i18n.str`The username was not found`
|
||||||
|
: status === HttpStatusCode.PreconditionFailed
|
||||||
|
? i18n.str`Balance was not zero`
|
||||||
|
: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notifyError(i18n.str`Operation failed, please report`,
|
||||||
|
(error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON.stringify(error)) as TranslatedString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -30,52 +30,51 @@ import {
|
|||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { Cashouts } from "../components/Cashouts/index.js";
|
import { Cashouts } from "../../components/Cashouts/index.js";
|
||||||
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
|
import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../../context/backend.js";
|
||||||
import { useAccountDetails } from "../hooks/access.js";
|
import { useAccountDetails } from "../../hooks/access.js";
|
||||||
import {
|
import {
|
||||||
useCashoutDetails,
|
useCashoutDetails,
|
||||||
useCircuitAccountAPI,
|
useCircuitAccountAPI,
|
||||||
useEstimator,
|
useEstimator,
|
||||||
useRatiosAndFeeConfig,
|
useRatiosAndFeeConfig,
|
||||||
} from "../hooks/circuit.js";
|
} from "../../hooks/circuit.js";
|
||||||
import {
|
import {
|
||||||
TanChannel,
|
TanChannel,
|
||||||
buildRequestErrorMessage,
|
buildRequestErrorMessage,
|
||||||
undefinedIfEmpty,
|
undefinedIfEmpty,
|
||||||
} from "../utils.js";
|
} from "../../utils.js";
|
||||||
import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
|
import { handleNotOkResult } from "../HomePage.js";
|
||||||
import { handleNotOkResult } from "./HomePage.js";
|
import { LoginForm } from "../LoginForm.js";
|
||||||
import { LoginForm } from "./LoginForm.js";
|
import { Amount } from "../PaytoWireTransferForm.js";
|
||||||
import { Amount } from "./PaytoWireTransferForm.js";
|
import { ShowAccountDetails } from "../ShowAccountDetails.js";
|
||||||
|
import { UpdateAccountPassword } from "../UpdateAccountPassword.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
account: string,
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRegister: () => void;
|
onRegister: () => void;
|
||||||
onLoadNotOk: () => void;
|
onLoadNotOk: () => void;
|
||||||
}
|
}
|
||||||
export function BusinessAccount({
|
export function BusinessAccount({
|
||||||
onClose,
|
onClose,
|
||||||
|
account,
|
||||||
onLoadNotOk,
|
onLoadNotOk,
|
||||||
onRegister,
|
onRegister,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const backend = useBackendContext();
|
|
||||||
const [updatePassword, setUpdatePassword] = useState(false);
|
const [updatePassword, setUpdatePassword] = useState(false);
|
||||||
const [newCashout, setNewcashout] = useState(false);
|
const [newCashout, setNewcashout] = useState(false);
|
||||||
const [showCashoutDetails, setShowCashoutDetails] = useState<
|
const [showCashoutDetails, setShowCashoutDetails] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
if (backend.state.status === "loggedOut") {
|
|
||||||
return <LoginForm onRegister={onRegister} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newCashout) {
|
if (newCashout) {
|
||||||
return (
|
return (
|
||||||
<CreateCashout
|
<CreateCashout
|
||||||
account={backend.state.username}
|
account={account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setNewcashout(false);
|
setNewcashout(false);
|
||||||
@ -104,7 +103,7 @@ export function BusinessAccount({
|
|||||||
if (updatePassword) {
|
if (updatePassword) {
|
||||||
return (
|
return (
|
||||||
<UpdateAccountPassword
|
<UpdateAccountPassword
|
||||||
account={backend.state.username}
|
account={account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
onUpdateSuccess={() => {
|
onUpdateSuccess={() => {
|
||||||
notifyInfo(i18n.str`Password changed`);
|
notifyInfo(i18n.str`Password changed`);
|
||||||
@ -119,7 +118,7 @@ export function BusinessAccount({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ShowAccountDetails
|
<ShowAccountDetails
|
||||||
account={backend.state.username}
|
account={account}
|
||||||
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
onLoadNotOk={handleNotOkResult(i18n, onRegister)}
|
||||||
onUpdateSuccess={() => {
|
onUpdateSuccess={() => {
|
||||||
notifyInfo(i18n.str`Account updated`);
|
notifyInfo(i18n.str`Account updated`);
|
||||||
@ -133,7 +132,7 @@ export function BusinessAccount({
|
|||||||
<div class="active">
|
<div class="active">
|
||||||
<h3>{i18n.str`Latest cashouts`}</h3>
|
<h3>{i18n.str`Latest cashouts`}</h3>
|
||||||
<Cashouts
|
<Cashouts
|
||||||
account={backend.state.username}
|
account={account}
|
||||||
onSelected={(id) => {
|
onSelected={(id) => {
|
||||||
setShowCashoutDetails(id);
|
setShowCashoutDetails(id);
|
||||||
}}
|
}}
|
Loading…
Reference in New Issue
Block a user