test login with an endpoint and cleaner calculation

This commit is contained in:
Sebastian 2023-03-15 09:25:23 -03:00
parent 0700bbe9d1
commit 0bf92a44df
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
5 changed files with 146 additions and 49 deletions

View File

@ -16,6 +16,7 @@
import { canonicalizeBaseUrl } from "@gnu-taler/taler-util";
import {
ErrorType,
RequestError,
useLocalStorage,
} from "@gnu-taler/web-util/lib/index.browser";
@ -192,6 +193,33 @@ export function usePublicBackend(): useBackendType {
};
}
export function useCredentialsChecker() {
const { request } = useApiContext();
const baseUrl = getInitialBackendBaseURL();
//check against account details endpoint
//while sandbox backend doesn't have a login endpoint
return async function testLogin(
username: string,
password: string,
): Promise<{
valid: boolean;
cause?: ErrorType;
}> {
try {
await request(baseUrl, `access-api/accounts/${username}/`, {
basicAuth: { username, password },
preventCache: true,
});
return { valid: true };
} catch (error) {
if (error instanceof RequestError) {
return { valid: false, cause: error.cause.type };
}
return { valid: false, cause: ErrorType.UNEXPECTED };
}
};
}
export function useAuthenticatedBackend(): useBackendType {
const { state } = useBackendContext();
const { request: requestHandler } = useApiContext();

View File

@ -288,9 +288,10 @@ export function useRatiosAndFeeConfig(): HttpResponse<
HttpResponseOk<SandboxBackend.Circuit.Config>,
RequestError<SandboxBackend.SandboxError>
>([`circuit-api/config`], fetcher, {
refreshInterval: 1000,
refreshInterval: 60 * 1000,
refreshWhenHidden: false,
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
errorRetryCount: 0,

View File

@ -36,9 +36,9 @@ import {
} from "../context/pageState.js";
import { useAccountDetails } from "../hooks/access.js";
import {
useAdminAccountAPI,
useBusinessAccountDetails,
useBusinessAccounts,
useAdminAccountAPI,
} from "../hooks/circuit.js";
import {
buildRequestErrorMessage,
@ -50,7 +50,6 @@ import {
} from "../utils.js";
import { ErrorBannerFloat } from "./BankFrame.js";
import { ShowCashoutDetails } from "./BusinessAccount.js";
import { PaymentOptions } from "./PaymentOptions.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@ -581,7 +580,6 @@ function CreateNewAccount({
template={undefined}
purpose="create"
onChange={(a) => {
console.log(a);
setSubmitAccount(a);
}}
/>
@ -831,6 +829,7 @@ function RemoveAccount({
title: i18n.str`Can't delete the account`,
description: i18n.str`Balance is not empty`,
}}
onClear={() => saveError(undefined)}
/>
)}
{error && (

View File

@ -237,6 +237,7 @@ function CreateCashout({
if (!result.ok) return onLoadNotOk(result);
if (!ratiosResult.ok) return onLoadNotOk(ratiosResult);
const config = ratiosResult.data;
const balance = Amounts.parseOrThrow(result.data.balance.amount);
const debitThreshold = Amounts.parseOrThrow(result.data.debitThreshold);
const zero = Amounts.zeroOfCurrency(balance.currency);
@ -254,23 +255,14 @@ function CreateCashout({
if (!sellRate || sellRate < 0) return <div>error rate</div>;
const amount = Amounts.parse(`${balance.currency}:${form.amount}`);
const amount_debit = !amount
? zero
: form.isDebit
? amount
: truncate(Amounts.divide(Amounts.add(amount, sellFee).amount, sellRate));
const credit_before_fee = !amount
? zero
: form.isDebit
? truncate(Amounts.divide(amount, 1 / sellRate))
: Amounts.add(amount, sellFee).amount;
const __amount_credit = Amounts.sub(credit_before_fee, sellFee).amount;
const amount_credit = Amounts.parseOrThrow(
`${fiatCurrency}:${Amounts.stringifyValue(__amount_credit)}`,
);
const calc = !amount
? { debit: zero, credit: zero, beforeFee: zero }
: !form.isDebit
? calculateFromCredit(amount, sellFee, sellRate)
: calculateFromDebit(amount, sellFee, sellRate);
const balanceAfter = Amounts.sub(balance, amount_debit).amount;
const balanceAfter = Amounts.sub(balance, calc.debit).amount;
function updateForm(newForm: typeof form): void {
setForm(newForm);
@ -280,11 +272,11 @@ function CreateCashout({
? i18n.str`required`
: !amount
? i18n.str`could not be parsed`
: Amounts.cmp(limit, amount_debit) === -1
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`balance is not enough`
: Amounts.cmp(credit_before_fee, sellFee) === -1
: Amounts.cmp(calc.beforeFee, sellFee) === -1
? i18n.str`the total amount to transfer does not cover the fees`
: Amounts.isZero(amount_credit)
: Amounts.isZero(calc.credit)
? i18n.str`the total transfer at destination will be zero`
: undefined,
channel: !form.channel ? i18n.str`required` : undefined,
@ -408,7 +400,7 @@ function CreateCashout({
id="withdraw-amount"
disabled
name="withdraw-amount"
value={amount_debit ? Amounts.stringifyValue(amount_debit) : ""}
value={Amounts.stringifyValue(calc.debit)}
/>
</div>
</fieldset>
@ -454,7 +446,7 @@ function CreateCashout({
// type="number"
style={{ color: "black" }}
disabled
value={Amounts.stringifyValue(credit_before_fee)}
value={Amounts.stringifyValue(calc.beforeFee)}
/>
</div>
</fieldset>
@ -503,7 +495,7 @@ function CreateCashout({
id="withdraw-amount"
disabled
name="withdraw-amount"
value={amount_credit ? Amounts.stringifyValue(amount_credit) : ""}
value={Amounts.stringifyValue(calc.credit)}
/>
</div>
</fieldset>
@ -584,8 +576,10 @@ function CreateCashout({
if (errors) return;
try {
const res = await createCashout({
amount_credit: Amounts.stringify(amount_credit),
amount_debit: Amounts.stringify(amount_debit),
amount_credit: `${fiatCurrency}:${Amounts.stringifyValue(
calc.credit,
)}`,
amount_debit: Amounts.stringify(calc.debit),
subject: form.subject,
tan_channel: form.channel,
});
@ -630,25 +624,6 @@ function CreateCashout({
);
}
const MAX_AMOUNT_DIGIT = 2;
/**
* Truncate the amount of digits to display
* in the form based on the fee calculations
*
* Backend must have the same truncation
* @param a
* @returns
*/
function truncate(a: AmountJson): AmountJson {
const str = Amounts.stringify(a);
const idx = str.indexOf(".");
if (idx === -1) {
return a;
}
const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT);
return Amounts.parseOrThrow(truncated);
}
interface ShowCashoutProps {
id: string;
onCancel: () => void;
@ -836,6 +811,58 @@ export function ShowCashoutDetails({
);
}
const MAX_AMOUNT_DIGIT = 2;
/**
* Truncate the amount of digits to display
* in the form based on the fee calculations
*
* Backend must have the same truncation
* @param a
* @returns
*/
function truncate(a: AmountJson): AmountJson {
const str = Amounts.stringify(a);
const idx = str.indexOf(".");
if (idx === -1) {
return a;
}
const truncated = str.substring(0, idx + 1 + MAX_AMOUNT_DIGIT);
return Amounts.parseOrThrow(truncated);
}
type TransferCalculation = {
debit: AmountJson;
credit: AmountJson;
beforeFee: AmountJson;
};
function calculateFromDebit(
amount: AmountJson,
sellFee: AmountJson,
sellRate: number,
): TransferCalculation {
const debit = amount;
const beforeFee = truncate(Amounts.divide(debit, 1 / sellRate));
const credit = Amounts.sub(beforeFee, sellFee).amount;
return { debit, credit, beforeFee };
}
function calculateFromCredit(
amount: AmountJson,
sellFee: AmountJson,
sellRate: number,
): TransferCalculation {
const credit = amount;
const beforeFee = Amounts.add(credit, sellFee).amount;
const debit = truncate(Amounts.divide(beforeFee, sellRate));
return { debit, credit, beforeFee };
}
export function assertUnreachable(x: never): never {
throw new Error("Didn't expect to get here");
}

View File

@ -14,12 +14,18 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import {
ErrorType,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
import { ErrorMessage } from "../context/pageState.js";
import { useCredentialsChecker } from "../hooks/backend.js";
import { bankUiSettings } from "../settings.js";
import { undefinedIfEmpty } from "../utils.js";
import { ErrorBannerFloat } from "./BankFrame.js";
import { USERNAME_REGEX } from "./RegistrationPage.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
@ -31,6 +37,8 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
const [username, setUsername] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
const testLogin = useCredentialsChecker();
const [error, saveError] = useState<ErrorMessage | undefined>();
const ref = useRef<HTMLInputElement>(null);
useEffect(function focusInput() {
ref.current?.focus();
@ -48,6 +56,9 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
return (
<Fragment>
<h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
{error && (
<ErrorBannerFloat error={error} onClear={() => saveError(undefined)} />
)}
<div class="login-div">
<form
class="login-form"
@ -105,10 +116,41 @@ export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
type="submit"
class="pure-button pure-button-primary"
disabled={!!errors}
onClick={(e) => {
onClick={async (e) => {
e.preventDefault();
if (!username || !password) return;
backend.logIn({ username, password });
const { valid, cause } = await testLogin(username, password);
if (valid) {
backend.logIn({ username, password });
} else {
switch (cause) {
case ErrorType.CLIENT: {
saveError({
title: i18n.str`Wrong credentials or username`,
});
break;
}
case ErrorType.SERVER: {
saveError({
title: i18n.str`Server had a problem, try again later or report.`,
});
break;
}
case ErrorType.TIMEOUT: {
saveError({
title: i18n.str`Could not reach the server, please report.`,
});
break;
}
default: {
saveError({
title: i18n.str`Unexpected error, please report.`,
});
break;
}
}
backend.logOut();
}
setUsername(undefined);
setPassword(undefined);
}}