From 905be0124a1b013d802db33265879d4b80501362 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 10 Sep 2023 15:38:43 -0300 Subject: [PATCH 01/11] more codespell --- contrib/articles/spending.txt | 2 +- contrib/articles/ui/ui-cameraready.tex | 4 +- contrib/articles/ui/ui.tex | 4 +- .../pages/home/AuthenticationEditorScreen.tsx | 2 +- .../idb-wpt-ported/idbcursor-reused.test.ts | 4 +- .../idb-bridge/src/util/fakeDOMStringList.ts | 2 +- packages/taler-harness/src/harness/harness.ts | 23 +++--- .../taler-harness/src/harness/libeufin.ts | 74 +++++++++---------- packages/taler-util/src/taler-crypto.ts | 11 ++- packages/taler-wallet-core/src/wallet.ts | 8 +- 10 files changed, 65 insertions(+), 69 deletions(-) diff --git a/contrib/articles/spending.txt b/contrib/articles/spending.txt index 7f2c716f8..887a80910 100644 --- a/contrib/articles/spending.txt +++ b/contrib/articles/spending.txt @@ -45,7 +45,7 @@ If the above parameters have an optimal assignment, then replacing v'[x] := 0 gives another optimal solution, as otherwise we'd get a better one for the first situation. -There is however no assurence that t[x] = price mod v[x] for some x in D, so nievely such solutions give you running times like O(price * |D|), which kinda sucks actually. Just one simplified example : +There is however no assurence that t[x] = price mod v[x] for some x in D, so naively such solutions give you running times like O(price * |D|), which kinda sucks actually. Just one simplified example : http://www.codeproject.com/Articles/31002/Coin-Change-Problem-Using-Dynamic-Programming diff --git a/contrib/articles/ui/ui-cameraready.tex b/contrib/articles/ui/ui-cameraready.tex index ef79d91ba..305b77258 100644 --- a/contrib/articles/ui/ui-cameraready.tex +++ b/contrib/articles/ui/ui-cameraready.tex @@ -881,7 +881,7 @@ the page. Then the wallet inspects the response as it may contain error reports about a failed payment which the wallet has to handle. By submitting the payment this way, we also ensure that this intermediate request does not require JavaScript and still does not -interfer with navigation. Once the Web shop confirms the payment, the +interfere with navigation. Once the Web shop confirms the payment, the wallet causes the fulfillment URL to be reloaded. If the contract hash does not match a payment which the user @@ -937,7 +937,7 @@ it has the following key advantages: other users has the expected behavior of asking the other user to pay for the resource. \item Asynchronously transmitting coins from injected JavaScript costs - one roundtrip, but does not interfer with navigation and allows + one roundtrip, but does not interfere with navigation and allows proper error handling. \item The different pages of the merchant have clear delineations: the shopping pages conclude by making an offer, and diff --git a/contrib/articles/ui/ui.tex b/contrib/articles/ui/ui.tex index 37cd8b076..620dd5ecd 100644 --- a/contrib/articles/ui/ui.tex +++ b/contrib/articles/ui/ui.tex @@ -933,7 +933,7 @@ the page. Then the wallet inspects the response as it may contain error reports about a failed payment which the wallet has to handle. By submitting the payment this way, we also ensure that this intermediate request does not require JavaScript and still does not -interfer with navigation. Once the Web shop confirms the payment, the +interfere with navigation. Once the Web shop confirms the payment, the wallet causes the fulfillment URL to be reloaded. If the contract hash does not match a payment which the user @@ -989,7 +989,7 @@ it has the following key advantages: other users has the expected behavior of asking the other user to pay for the resource. \item Asynchronously transmitting coins from injected JavaScript costs - one roundtrip, but does not interfer with navigation and allows + one roundtrip, but does not interfere with navigation and allows proper error handling. \item The different pages of the merchant have clear delineations: the shopping pages conclude by making an offer, and diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx index 3018f88dd..54bbc626d 100644 --- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx @@ -241,7 +241,7 @@ export function AuthenticationEditorScreen(): VNode {

{authAvailableSet.size > 0 && (

- We couldn't find provider for some of the authentication + We couldn't find provider for some of the authentication methods.

)} diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts index b13bd1fc3..e0e6c2bf8 100644 --- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts +++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts @@ -26,7 +26,7 @@ test("WPT idbcursor-reused.htm", async (t) => { case 0: cursor = e.target.result; - t.deepEqual(cursor.value, "data", "prequisite cursor.value"); + t.deepEqual(cursor.value, "data", "prerequisite cursor.value"); cursor.custom_cursor_value = 1; e.target.custom_request_value = 2; @@ -34,7 +34,7 @@ test("WPT idbcursor-reused.htm", async (t) => { break; case 1: - t.deepEqual(cursor.value, "data2", "prequisite cursor.value"); + t.deepEqual(cursor.value, "data2", "prerequisite cursor.value"); t.deepEqual(cursor.custom_cursor_value, 1, "custom cursor value"); t.deepEqual( e.target.custom_request_value, diff --git a/packages/idb-bridge/src/util/fakeDOMStringList.ts b/packages/idb-bridge/src/util/fakeDOMStringList.ts index 92785f9e1..24f5c96f4 100644 --- a/packages/idb-bridge/src/util/fakeDOMStringList.ts +++ b/packages/idb-bridge/src/util/fakeDOMStringList.ts @@ -21,7 +21,7 @@ export interface FakeDOMStringList extends Array { item: (i: number) => string | null; } -// Would be nicer to sublcass Array, but I'd have to sacrifice Node 4 support to do that. +// Would be nicer to subclass Array, but I'd have to sacrifice Node 4 support to do that. export const fakeDOMStringList = (arr: string[]): FakeDOMStringList => { const arr2 = arr.slice(); diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 24e42099e..0c3c367af 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -565,7 +565,7 @@ class BankServiceBase { protected globalTestState: GlobalTestState, protected bankConfig: BankConfig, protected configFile: string, - ) {} + ) { } } export interface HarnessExchangeBankAccount { @@ -580,8 +580,7 @@ export interface HarnessExchangeBankAccount { */ export class FakebankService extends BankServiceBase - implements BankServiceHandle -{ + implements BankServiceHandle { proc: ProcessWrapper | undefined; http = createPlatformHttpLib({ enableThrottling: false }); @@ -790,7 +789,7 @@ export class ExchangeService implements ExchangeServiceInterface { async runWirewatchOnce() { if (useLibeufinBank) { - // Not even 2 secods showed to be enough! + // Not even 2 seconds showed to be enough! await waitMs(4000); } await runCommand( @@ -1013,7 +1012,7 @@ export class ExchangeService implements ExchangeServiceInterface { private exchangeConfig: ExchangeConfig, private configFilename: string, private keyPair: EddsaKeyPair, - ) {} + ) { } get name() { return this.exchangeConfig.name; @@ -1369,7 +1368,7 @@ export class MerchantService implements MerchantServiceInterface { private globalState: GlobalTestState, private merchantConfig: MerchantConfig, private configFilename: string, - ) {} + ) { } private currentTimetravelOffsetMs: number | undefined; @@ -1707,7 +1706,7 @@ export class WalletService { constructor( private globalState: GlobalTestState, private opts: WalletServiceOptions, - ) {} + ) { } get socketPath() { const unixPath = path.join( @@ -1816,7 +1815,7 @@ export class WalletClient { return client.call(operation, payload); } - constructor(private args: WalletClientArgs) {} + constructor(private args: WalletClientArgs) { } async connect(): Promise { const waiter = this.waiter; @@ -1883,11 +1882,9 @@ export class WalletCli { ? `--crypto-worker=${cliOpts.cryptoWorkerType}` : ""; const logName = `wallet-${self.name}`; - const command = `taler-wallet-cli ${ - self.timetravelArg ?? "" - } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${ - self.dbfile - }' api '${op}' ${shellWrap(JSON.stringify(payload))}`; + const command = `taler-wallet-cli ${self.timetravelArg ?? "" + } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${self.dbfile + }' api '${op}' ${shellWrap(JSON.stringify(payload))}`; const resp = await sh(self.globalTestState, logName, command); logger.info("--- wallet core response ---"); logger.info(resp); diff --git a/packages/taler-harness/src/harness/libeufin.ts b/packages/taler-harness/src/harness/libeufin.ts index 9f3e7a5a0..caeea85ae 100644 --- a/packages/taler-harness/src/harness/libeufin.ts +++ b/packages/taler-harness/src/harness/libeufin.ts @@ -251,7 +251,7 @@ export interface NexusTask { taskCronSpec: string; // Only meaningful for "fetch" types. taskParams: FetchParams; - // Timestamp in secons when the next iteration will run. + // Timestamp in seconds when the next iteration will run. nextScheduledExecutionSec: number; // Timestamp in seconds when the previous iteration ran. prevScheduledExecutionSec: number; @@ -618,9 +618,9 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-createebicssubscriber", "libeufin-cli sandbox ebicssubscriber create" + - ` --host-id=${details.hostId}` + - ` --partner-id=${details.partnerId}` + - ` --user-id=${details.userId}`, + ` --host-id=${details.hostId}` + + ` --partner-id=${details.partnerId}` + + ` --user-id=${details.userId}`, this.env(), ); console.log(stdout); @@ -634,13 +634,13 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-createebicsbankaccount", "libeufin-cli sandbox ebicsbankaccount create" + - ` --iban=${bankAccountDetails.iban}` + - ` --bic=${bankAccountDetails.bic}` + - ` --person-name='${bankAccountDetails.personName}'` + - ` --account-name=${bankAccountDetails.accountName}` + - ` --ebics-host-id=${sd.hostId}` + - ` --ebics-partner-id=${sd.partnerId}` + - ` --ebics-user-id=${sd.userId}`, + ` --iban=${bankAccountDetails.iban}` + + ` --bic=${bankAccountDetails.bic}` + + ` --person-name='${bankAccountDetails.personName}'` + + ` --account-name=${bankAccountDetails.accountName}` + + ` --ebics-host-id=${sd.hostId}` + + ` --ebics-partner-id=${sd.partnerId}` + + ` --ebics-user-id=${sd.userId}`, this.env(), ); console.log(stdout); @@ -673,11 +673,11 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-createebicsconnection", `libeufin-cli connections new-ebics-connection` + - ` --ebics-url=${connectionDetails.ebicsUrl}` + - ` --host-id=${connectionDetails.subscriberDetails.hostId}` + - ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` + - ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` + - ` ${connectionDetails.connectionName}`, + ` --ebics-url=${connectionDetails.ebicsUrl}` + + ` --host-id=${connectionDetails.subscriberDetails.hostId}` + + ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` + + ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` + + ` ${connectionDetails.connectionName}`, { ...process.env, LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, @@ -693,9 +693,9 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-createbackupfile", `libeufin-cli connections export-backup` + - ` --passphrase=${details.passphrase}` + - ` --output-file=${details.outputFile}` + - ` ${details.connectionName}`, + ` --passphrase=${details.passphrase}` + + ` --output-file=${details.outputFile}` + + ` ${details.connectionName}`, { ...process.env, LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, @@ -711,7 +711,7 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-createkeyletter", `libeufin-cli connections get-key-letter` + - ` ${details.connectionName} ${details.outputFile}`, + ` ${details.connectionName} ${details.outputFile}`, { ...process.env, LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, @@ -774,9 +774,9 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-importbankaccount", "libeufin-cli connections import-bank-account" + - ` --offered-account-id=${importDetails.offeredBankAccountName}` + - ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` + - ` ${importDetails.connectionName}`, + ` --offered-account-id=${importDetails.offeredBankAccountName}` + + ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` + + ` ${importDetails.connectionName}`, { ...process.env, LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, @@ -822,12 +822,12 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-preparepayment", `libeufin-cli accounts prepare-payment` + - ` --creditor-iban=${details.creditorIban}` + - ` --creditor-bic=${details.creditorBic}` + - ` --creditor-name='${details.creditorName}'` + - ` --payment-subject='${details.subject}'` + - ` --payment-amount=${details.currency}:${details.amount}` + - ` ${details.nexusBankAccountName}`, + ` --creditor-iban=${details.creditorIban}` + + ` --creditor-bic=${details.creditorBic}` + + ` --creditor-name='${details.creditorName}'` + + ` --payment-subject='${details.subject}'` + + ` --payment-amount=${details.currency}:${details.amount}` + + ` ${details.nexusBankAccountName}`, { ...process.env, LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, @@ -846,8 +846,8 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-submitpayments", `libeufin-cli accounts submit-payments` + - ` --payment-uuid=${paymentUuid}` + - ` ${details.nexusBankAccountName}`, + ` --payment-uuid=${paymentUuid}` + + ` ${details.nexusBankAccountName}`, { ...process.env, LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, @@ -863,9 +863,9 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-new-anastasis-facade", `libeufin-cli facades new-anastasis-facade` + - ` --currency ${req.currency}` + - ` --facade-name ${req.facadeName}` + - ` ${req.connectionName} ${req.accountName}`, + ` --currency ${req.currency}` + + ` --facade-name ${req.facadeName}` + + ` ${req.connectionName} ${req.accountName}`, { ...process.env, LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, @@ -881,9 +881,9 @@ export class LibeufinCli { this.globalTestState, "libeufin-cli-new-taler-wire-gateway-facade", `libeufin-cli facades new-taler-wire-gateway-facade` + - ` --currency ${req.currency}` + - ` --facade-name ${req.facadeName}` + - ` ${req.connectionName} ${req.accountName}`, + ` --currency ${req.currency}` + + ` --facade-name ${req.facadeName}` + + ` ${req.connectionName} ${req.accountName}`, { ...process.env, LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts index 9425a9320..3ad388794 100644 --- a/packages/taler-util/src/taler-crypto.ts +++ b/packages/taler-util/src/taler-crypto.ts @@ -392,7 +392,7 @@ function csKdfMod( // Newer versions of node have TextEncoder and TextDecoder as a global, // just like modern browsers. // In older versions of node or environments that do not have these -// globals, they must be polyfilled (by adding them to globa/globalThis) +// globals, they must be polyfilled (by adding them to global/globalThis) // before stringToBytes or bytesToString is called the first time. let encoder: any; @@ -693,7 +693,7 @@ export async function csBlind( * Unblind operation to unblind the signature * @param bseed seed to derive secrets * @param rPub public R received from /csr - * @param csPub denomination publick key + * @param csPub denomination public key * @param b returned from exchange to select c * @param csSig blinded signature * @returns unblinded signature @@ -721,7 +721,7 @@ export async function csUnblind( * Verification algorithm for CS signatures * @param hm message signed * @param csSig unblinded signature - * @param csPub denomination publick key + * @param csPub denomination public key * @returns true if valid, false if invalid */ export async function csVerify( @@ -844,8 +844,7 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array { return hash(uint8ArrayBuf); } else { throw Error( - `unsupported cipher (${ - (pub as DenominationPubKey).cipher + `unsupported cipher (${(pub as DenominationPubKey).cipher }), unable to hash`, ); } @@ -1023,7 +1022,7 @@ export enum WalletAccountMergeFlags { export class SignaturePurposeBuilder { private chunks: Uint8Array[] = []; - constructor(private purposeNum: number) {} + constructor(private purposeNum: number) { } put(bytes: Uint8Array): SignaturePurposeBuilder { this.chunks.push(Uint8Array.from(bytes)); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 1a60b148c..2d0878afc 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -15,7 +15,7 @@ */ /** - * High-level wallet operations that should be indepentent from the underlying + * High-level wallet operations that should be independent from the underlying * browser extension interface. */ @@ -923,9 +923,9 @@ async function dumpCoins(ws: InternalWalletState): Promise { ageCommitmentProof: c.ageCommitmentProof, spend_allocation: c.spendAllocation ? { - amount: c.spendAllocation.amount, - id: c.spendAllocation.id, - } + amount: c.spendAllocation.amount, + id: c.spendAllocation.id, + } : undefined, }); } From cc27750a79d8d4dbbedaa0dfcf21fe21a7464936 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 11 Sep 2023 18:19:45 +0200 Subject: [PATCH 02/11] -WalletContractData doesn't belong to the DB --- build-system/taler-build-scripts | 2 +- packages/taler-util/src/wallet-types.ts | 48 +++++++++++++++++++ packages/taler-wallet-core/src/db.ts | 46 ------------------ .../src/operations/balance.ts | 6 +-- .../src/operations/pay-merchant.ts | 16 ++----- .../src/operations/transactions.ts | 2 +- .../src/util/coinSelection.ts | 18 ++----- .../taler-wallet-core/src/wallet-api-types.ts | 24 +++++----- .../ShowFullContractTermPopup.stories.tsx | 3 +- .../components/ShowFullContractTermPopup.tsx | 12 +++-- 10 files changed, 81 insertions(+), 96 deletions(-) diff --git a/build-system/taler-build-scripts b/build-system/taler-build-scripts index 001f5dd08..23538677f 160000 --- a/build-system/taler-build-scripts +++ b/build-system/taler-build-scripts @@ -1 +1 @@ -Subproject commit 001f5dd081fc8729ff8def90c4a1c3f93eb8689a +Subproject commit 23538677f6c6be2a62f38dc6137ecdd1c76b7b15 diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index c6f19c73f..f7bd3d120 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -57,7 +57,9 @@ import { DenomKeyType, DenominationPubKey, ExchangeAuditor, + InternationalizedString, MerchantContractTerms, + MerchantInfo, PeerContractTerms, UnblindedSignature, codecForMerchantContractTerms, @@ -2667,3 +2669,49 @@ export const codecForTestingSetTimetravelRequest = buildCodecForObject() .property("offsetMs", codecForNumber()) .build("TestingSetTimetravelRequest"); + +export interface AllowedAuditorInfo { + auditorBaseUrl: string; + auditorPub: string; +} + +export interface AllowedExchangeInfo { + exchangeBaseUrl: string; + exchangePub: string; +} + +/** + * Data extracted from the contract terms that is relevant for payment + * processing in the wallet. + */ +export interface WalletContractData { + /** + * Fulfillment URL, or the empty string if the order has no fulfillment URL. + * + * Stored as a non-nullable string as we use this field for IndexedDB indexing. + */ + fulfillmentUrl: string; + + contractTermsHash: string; + fulfillmentMessage?: string; + fulfillmentMessageI18n?: InternationalizedString; + merchantSig: string; + merchantPub: string; + merchant: MerchantInfo; + amount: AmountString; + orderId: string; + merchantBaseUrl: string; + summary: string; + summaryI18n: { [lang_tag: string]: string } | undefined; + autoRefund: TalerProtocolDuration | undefined; + maxWireFee: AmountString; + wireFeeAmortization: number; + payDeadline: TalerProtocolTimestamp; + refundDeadline: TalerProtocolTimestamp; + allowedExchanges: AllowedExchangeInfo[]; + timestamp: TalerProtocolTimestamp; + wireMethod: string; + wireInfoHash: string; + maxDepositFee: AmountString; + minimumAge?: number; +} diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 3a8109320..ba1f5b8c0 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1035,52 +1035,6 @@ export enum RefundReason { AbortRefund = "abort-pay-refund", } -export interface AllowedAuditorInfo { - auditorBaseUrl: string; - auditorPub: string; -} - -export interface AllowedExchangeInfo { - exchangeBaseUrl: string; - exchangePub: string; -} - -/** - * Data extracted from the contract terms that is relevant for payment - * processing in the wallet. - */ -export interface WalletContractData { - /** - * Fulfillment URL, or the empty string if the order has no fulfillment URL. - * - * Stored as a non-nullable string as we use this field for IndexedDB indexing. - */ - fulfillmentUrl: string; - - contractTermsHash: string; - fulfillmentMessage?: string; - fulfillmentMessageI18n?: InternationalizedString; - merchantSig: string; - merchantPub: string; - merchant: MerchantInfo; - amount: AmountString; - orderId: string; - merchantBaseUrl: string; - summary: string; - summaryI18n: { [lang_tag: string]: string } | undefined; - autoRefund: TalerProtocolDuration | undefined; - maxWireFee: AmountString; - wireFeeAmortization: number; - payDeadline: TalerProtocolTimestamp; - refundDeadline: TalerProtocolTimestamp; - allowedExchanges: AllowedExchangeInfo[]; - timestamp: TalerProtocolTimestamp; - wireMethod: string; - wireInfoHash: string; - maxDepositFee: AmountString; - minimumAge?: number; -} - export enum PurchaseStatus { /** * Not downloaded yet. diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts index a20ded2af..8034f78ea 100644 --- a/packages/taler-wallet-core/src/operations/balance.ts +++ b/packages/taler-wallet-core/src/operations/balance.ts @@ -50,6 +50,8 @@ * Imports. */ import { + AllowedAuditorInfo, + AllowedExchangeInfo, AmountJson, Amounts, BalancesResponse, @@ -60,17 +62,15 @@ import { ScopeType, } from "@gnu-taler/taler-util"; import { - AllowedAuditorInfo, - AllowedExchangeInfo, RefreshGroupRecord, WalletStoresV1, WithdrawalGroupStatus, } from "../db.js"; import { InternalWalletState } from "../internal-wallet-state.js"; +import { assertUnreachable } from "../util/assertUnreachable.js"; import { checkLogicInvariant } from "../util/invariants.js"; import { GetReadOnlyAccess } from "../util/query.js"; import { getExchangeDetails } from "./exchanges.js"; -import { assertUnreachable } from "../util/assertUnreachable.js"; /** * Logger. diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 57367bb20..fe0cbeda0 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -63,7 +63,6 @@ import { RefreshReason, SharePaymentResult, StartRefundQueryForUriResponse, - stringifyPaytoUri, stringifyPayUri, stringifyTalerUri, TalerError, @@ -78,6 +77,7 @@ import { TransactionState, TransactionType, URL, + WalletContractData, } from "@gnu-taler/taler-util"; import { getHttpResponseErrorDetails, @@ -95,7 +95,6 @@ import { PurchaseRecord, PurchaseStatus, RefundReason, - WalletContractData, WalletStoresV1, } from "../db.js"; import { @@ -115,15 +114,13 @@ import { checkDbInvariant } from "../util/invariants.js"; import { GetReadOnlyAccess } from "../util/query.js"; import { constructTaskIdentifier, - TaskRunResult, - TaskRunResultType, RetryInfo, - TaskIdentifiers, -} from "./common.js"; -import { runLongpollAsync, runTaskWithErrorReporting, spendCoins, + TaskIdentifiers, + TaskRunResult, + TaskRunResultType, } from "./common.js"; import { calculateRefreshOutput, @@ -173,10 +170,7 @@ export async function getTotalPaymentCost( const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl .iter(coin.exchangeBaseUrl) .filter((x) => - Amounts.isSameCurrency( - x.value, - pcs.coinContributions[i], - ), + Amounts.isSameCurrency(x.value, pcs.coinContributions[i]), ); const amountLeft = Amounts.sub( denom.value, diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 7bdb9af5b..31655ad71 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -41,6 +41,7 @@ import { TransactionsResponse, TransactionState, TransactionType, + WalletContractData, WithdrawalType, } from "@gnu-taler/taler-util"; import { @@ -60,7 +61,6 @@ import { RefreshOperationStatus, RefundGroupRecord, RewardRecord, - WalletContractData, WithdrawalGroupRecord, WithdrawalGroupStatus, WithdrawalRecordType, diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts index ef2f85789..6fd0f1b86 100644 --- a/packages/taler-wallet-core/src/util/coinSelection.ts +++ b/packages/taler-wallet-core/src/util/coinSelection.ts @@ -28,21 +28,20 @@ import { AbsoluteTime, AgeCommitmentProof, AgeRestriction, + AllowedAuditorInfo, + AllowedExchangeInfo, AmountJson, AmountLike, - AmountResponse, Amounts, AmountString, CoinPublicKeyString, CoinStatus, - ConvertAmountRequest, DenominationInfo, DenominationPubKey, DenomSelectionState, Duration, ForcedCoinSel, ForcedDenomSel, - GetAmountRequest, j2s, Logger, parsePaytoUri, @@ -50,24 +49,13 @@ import { PayMerchantInsufficientBalanceDetails, PayPeerInsufficientBalanceDetails, strcmp, - TransactionAmountMode, - TransactionType, UnblindedSignature, } from "@gnu-taler/taler-util"; +import { DenominationRecord } from "../db.js"; import { - AllowedAuditorInfo, - AllowedExchangeInfo, - DenominationRecord, -} from "../db.js"; -import { - DbReadOnlyTransaction, getExchangeDetails, - GetReadOnlyAccess, - GetReadWriteAccess, isWithdrawableDenom, - StoreNames, WalletDbReadOnlyTransaction, - WalletStoresV1, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index d89ad257a..67c05a42f 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -38,7 +38,6 @@ import { ApplyDevExperimentRequest, BackupRecovery, BalancesResponse, - FailTransactionRequest, CheckPeerPullCreditRequest, CheckPeerPullCreditResponse, CheckPeerPushDebitRequest, @@ -51,14 +50,19 @@ import { ConvertAmountRequest, CreateDepositGroupRequest, CreateDepositGroupResponse, + CreateStoredBackupResponse, + DeleteStoredBackupRequest, DeleteTransactionRequest, ExchangeDetailedResponse, ExchangesListResponse, + FailTransactionRequest, ForceRefreshRequest, ForgetKnownBankAccountsRequest, GetAmountRequest, GetBalanceDetailRequest, GetContractTermsDetailsRequest, + GetCurrencyInfoRequest, + GetCurrencyInfoResponse, GetExchangeTosRequest, GetExchangeTosResult, GetPlanForOperationRequest, @@ -85,16 +89,21 @@ import { PreparePeerPushCreditRequest, PreparePeerPushCreditResponse, PrepareRefundRequest, - PrepareRewardRequest as PrepareRewardRequest, + PrepareRewardRequest, PrepareTipResult as PrepareRewardResult, + RecoverStoredBackupRequest, RecoveryLoadRequest, RetryTransactionRequest, SetCoinSuspendedRequest, SetWalletDeviceIdRequest, + SharePaymentRequest, + SharePaymentResult, StartRefundQueryForUriResponse, StartRefundQueryRequest, + StoredBackupList, TestPayArgs, TestPayResult, + TestingSetTimetravelRequest, Transaction, TransactionByIdRequest, TransactionsRequest, @@ -106,22 +115,13 @@ import { UserAttentionsResponse, ValidateIbanRequest, ValidateIbanResponse, + WalletContractData, WalletCoreVersion, WalletCurrencyInfo, WithdrawFakebankRequest, WithdrawTestBalanceRequest, WithdrawUriInfoResponse, - SharePaymentRequest, - SharePaymentResult, - GetCurrencyInfoRequest, - GetCurrencyInfoResponse, - StoredBackupList, - CreateStoredBackupResponse, - RecoverStoredBackupRequest, - DeleteStoredBackupRequest, - TestingSetTimetravelRequest, } from "@gnu-taler/taler-util"; -import { WalletContractData } from "./db.js"; import { AddBackupProviderRequest, AddBackupProviderResponse, diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx index 74c92cbc6..1b1802b8c 100644 --- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx +++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx @@ -18,8 +18,6 @@ * * @author Sebastian Javier Marchano (sebasjm) */ - -import { WalletContractData } from "@gnu-taler/taler-wallet-core"; import * as tests from "@gnu-taler/web-util/testing"; import { ErrorView, @@ -27,6 +25,7 @@ import { LoadingView, ShowView, } from "./ShowFullContractTermPopup.js"; +import { WalletContractData } from "@gnu-taler/taler-util"; export default { title: "ShowFullContractTermPopup", diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx index db9b6ebcd..0b3cca0b2 100644 --- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx +++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx @@ -13,11 +13,13 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ -import { AbsoluteTime, Duration, Location } from "@gnu-taler/taler-util"; import { - WalletApiOperation, + AbsoluteTime, + Duration, + Location, WalletContractData, -} from "@gnu-taler/taler-wallet-core"; +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { styled } from "@linaria/react"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -334,8 +336,8 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode { !contractTerms.autoRefund ? Duration.getZero() : Duration.fromTalerProtocolDuration( - contractTerms.autoRefund, - ), + contractTerms.autoRefund, + ), )} format="dd MMMM yyyy, HH:mm" /> From d118ffb88b393f0ab2c2d534486d5651186fcd92 Mon Sep 17 00:00:00 2001 From: Devan Carpenter Date: Mon, 11 Sep 2023 11:09:48 -0400 Subject: [PATCH 03/11] ci: add script for running CI locally --- ci/ci.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100755 ci/ci.sh diff --git a/ci/ci.sh b/ci/ci.sh new file mode 100755 index 000000000..fc523d8f5 --- /dev/null +++ b/ci/ci.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -evuo pipefail + +# Use podman if available, otherwise use docker. +# Fails if neither is found in PATH +OCI_RUNTIME=$(which podman || which docker) +REPO_NAME=$(basename "${PWD}") +JOB_NAME="${1}" +JOB_CONTAINER=$((grep CONTAINER_NAME ci/jobs/${JOB_NAME}/config.ini | cut -d' ' -f 3) || echo "${REPO_NAME}") + +echo "${JOB_CONTAINER}" + +if [ "${JOB_CONTAINER}" = "${REPO_NAME}" ] ; then + "${OCI_RUNTIME}" build \ + -t "${JOB_CONTAINER}" \ + -f ci/Containerfile . +fi + +"${OCI_RUNTIME}" run \ + --rm \ + -ti \ + --volume "${PWD}":/workdir \ + --workdir /workdir \ + "${JOB_CONTAINER}" \ + ci/jobs/"${JOB_NAME}"/job.sh + +top_dir=$(dirname "${BASH_SOURCE[0]}") + +#"${top_dir}"/build.sh From f444a497589ddf8815b04008210c69fdea0d4803 Mon Sep 17 00:00:00 2001 From: Devan Carpenter Date: Mon, 11 Sep 2023 11:17:33 -0400 Subject: [PATCH 04/11] ci: add more generated dirs to codespell skip --- ci/jobs/0-codespell/job.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/jobs/0-codespell/job.sh b/ci/jobs/0-codespell/job.sh index cb41388c3..d27d0888b 100755 --- a/ci/jobs/0-codespell/job.sh +++ b/ci/jobs/0-codespell/job.sh @@ -3,4 +3,4 @@ set -exuo pipefail job_dir=$(dirname "${BASH_SOURCE[0]}") -codespell -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex,*/vendor/**" +codespell -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex,*/vendor/**,*/node_modules/**,*.pnpm-store/**" From 7e55b25c37c78614eb5012c6aade8b054aa59dcb Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 11 Sep 2023 19:35:50 +0200 Subject: [PATCH 05/11] build system: missing compile step --- build-system/taler-build-scripts | 2 +- packages/demobank-ui/Makefile | 1 + packages/taler-wallet-cli/Makefile | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build-system/taler-build-scripts b/build-system/taler-build-scripts index 23538677f..001f5dd08 160000 --- a/build-system/taler-build-scripts +++ b/build-system/taler-build-scripts @@ -1 +1 @@ -Subproject commit 23538677f6c6be2a62f38dc6137ecdd1c76b7b15 +Subproject commit 001f5dd081fc8729ff8def90c4a1c3f93eb8689a diff --git a/packages/demobank-ui/Makefile b/packages/demobank-ui/Makefile index fc570b270..7abb3d101 100644 --- a/packages/demobank-ui/Makefile +++ b/packages/demobank-ui/Makefile @@ -20,6 +20,7 @@ spa_dir=$(prefix)/share/taler/demobank-ui .PHONY: deps deps: pnpm install --frozen-lockfile --filter @gnu-taler/demobank-ui... + pnpm run compile --frozen-lockfile --filter @gnu-taler/demobank-ui... pnpm run check pnpm run build diff --git a/packages/taler-wallet-cli/Makefile b/packages/taler-wallet-cli/Makefile index 388401eae..9bd8767dc 100644 --- a/packages/taler-wallet-cli/Makefile +++ b/packages/taler-wallet-cli/Makefile @@ -39,6 +39,7 @@ install-nodeps: ln -sf $(install_target)/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs $(prefix)/bin/taler-wallet-cli deps: pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... + pnpm compile --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... install: $(MAKE) deps $(MAKE) install-nodeps From e2422b68ebb2a29fb2e4d86f8a8cf9ec2a33e099 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 11 Sep 2023 19:56:32 +0200 Subject: [PATCH 06/11] -fix pnpm invocation --- packages/demobank-ui/Makefile | 2 +- packages/taler-wallet-cli/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/demobank-ui/Makefile b/packages/demobank-ui/Makefile index 7abb3d101..8e41cc7c6 100644 --- a/packages/demobank-ui/Makefile +++ b/packages/demobank-ui/Makefile @@ -20,7 +20,7 @@ spa_dir=$(prefix)/share/taler/demobank-ui .PHONY: deps deps: pnpm install --frozen-lockfile --filter @gnu-taler/demobank-ui... - pnpm run compile --frozen-lockfile --filter @gnu-taler/demobank-ui... + pnpm run --filter @gnu-taler/demobank-ui... compile pnpm run check pnpm run build diff --git a/packages/taler-wallet-cli/Makefile b/packages/taler-wallet-cli/Makefile index 9bd8767dc..6d695e9c1 100644 --- a/packages/taler-wallet-cli/Makefile +++ b/packages/taler-wallet-cli/Makefile @@ -39,7 +39,7 @@ install-nodeps: ln -sf $(install_target)/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs $(prefix)/bin/taler-wallet-cli deps: pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... - pnpm compile --frozen-lockfile --filter @gnu-taler/taler-wallet-cli... + pnpm run --filter @gnu-taler/taler-wallet-cli... compile install: $(MAKE) deps $(MAKE) install-nodeps From 8c20f4b27946679267bb44255721a9f14ae1077a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 11 Sep 2023 15:07:55 -0300 Subject: [PATCH 07/11] new login token --- .../src/Application.tsx | 32 +- .../src/ApplicationReadyRoutes.tsx | 42 ++- .../src/InstanceRoutes.tsx | 35 +- .../src/components/exception/login.tsx | 244 -------------- .../instance/DefaultInstanceFormFields.tsx | 4 +- .../src/components/menu/SideBar.tsx | 5 +- .../src/components/product/ProductForm.tsx | 4 +- .../src/context/backend.test.ts | 6 +- .../src/context/backend.ts | 66 +--- .../src/context/instance.ts | 5 +- .../src/declaration.d.ts | 39 +++ .../src/hooks/backend.ts | 139 ++++++-- .../merchant-backoffice-ui/src/hooks/index.ts | 84 ++--- .../src/hooks/instance.test.ts | 4 +- .../src/hooks/instance.ts | 38 ++- .../src/hooks/testing.tsx | 5 +- .../src/hooks/useSettings.ts | 9 - .../instance/orders/details/DetailPage.tsx | 8 +- .../instance/reserves/details/RewardInfo.tsx | 6 +- .../instance/templates/create/CreatePage.tsx | 12 +- .../paths/instance/templates/qr/QrPage.tsx | 15 +- .../instance/templates/update/UpdatePage.tsx | 16 +- .../src/paths/instance/token/DetailPage.tsx | 6 +- .../src/paths/instance/token/index.tsx | 8 +- .../src/paths/instance/update/index.tsx | 18 +- .../validators/create/CreatedSuccessfully.tsx | 6 +- .../src/paths/login/index.tsx | 299 +++++++++++++++++- .../src/paths/settings/index.tsx | 18 +- packages/web-util/src/utils/request.ts | 75 ++++- 29 files changed, 697 insertions(+), 551 deletions(-) delete mode 100644 packages/merchant-backoffice-ui/src/components/exception/login.tsx diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index 5e82821ae..1a7617643 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -41,7 +41,8 @@ import { import { ConfigContextProvider } from "./context/config.js"; import { useBackendConfig } from "./hooks/backend.js"; import { strings } from "./i18n/strings.js"; -import LoginPage from "./paths/login/index.js"; +import { ConnectionPage, LoginPage } from "./paths/login/index.js"; +import { LoginToken } from "./declaration.js"; export function Application(): VNode { return ( @@ -59,25 +60,20 @@ export function Application(): VNode { * @returns */ function ApplicationStatusRoutes(): VNode { - const { url, updateLoginStatus, triedToLog } = useBackendContext(); + const { url: backendURL, updateToken, changeBackend } = useBackendContext(); const result = useBackendConfig(); const { i18n } = useTranslationContext(); - const updateLoginInfoAndGoToRoot = (url: string, token?: string) => { - updateLoginStatus(url, token); - route("/"); - }; - const { currency, version } = result.ok ? result.data : { currency: "unknown", version: "unknown" }; const ctx = useMemo(() => ({ currency, version }), [currency, version]); - if (!triedToLog) { + if (!backendURL) { return ( - + ); } @@ -91,7 +87,7 @@ function ApplicationStatusRoutes(): VNode { return ( - + ); } @@ -109,7 +105,7 @@ function ApplicationStatusRoutes(): VNode { description: `Check your url`, }} /> - + ); } @@ -120,10 +116,10 @@ function ApplicationStatusRoutes(): VNode { notification={{ message: i18n.str`Server response with an error code`, type: "ERROR", - description: i18n.str`Got message ${result.message} from ${result.info?.url}`, + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, }} /> - + ; } if (result.type === ErrorType.UNREADABLE) { @@ -133,10 +129,10 @@ function ApplicationStatusRoutes(): VNode { notification={{ message: i18n.str`Response from server is unreadable, http status: ${result.status}`, type: "ERROR", - description: i18n.str`Got message ${result.message} from ${result.info?.url}`, + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, }} /> - + ; } return ( @@ -146,10 +142,10 @@ function ApplicationStatusRoutes(): VNode { notification={{ message: i18n.str`Unexpected Error`, type: "ERROR", - description: i18n.str`Got message ${result.message} from ${result.info?.url}`, + description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, }} /> - + ); } @@ -168,7 +164,7 @@ function ApplicationStatusRoutes(): VNode { description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`, }} /> - + } diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx index 46dea98e3..8bfbdb076 100644 --- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx @@ -18,22 +18,23 @@ * * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode } from "@gnu-taler/taler-util"; import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; -import { Fragment, h, VNode } from "preact"; -import { Router, Route, route } from "preact-router"; -import { useEffect, useState } from "preact/hooks"; +import { Fragment, VNode, h } from "preact"; +import { Route, Router, route } from "preact-router"; +import { useState } from "preact/hooks"; +import { InstanceRoutes } from "./InstanceRoutes.js"; import { - NotificationCard, NotYetReadyAppMenu, + NotificationCard, } from "./components/menu/index.js"; import { useBackendContext } from "./context/backend.js"; +import { LoginToken } from "./declaration.js"; import { useBackendInstancesTestForAdmin } from "./hooks/backend.js"; -import { InstanceRoutes } from "./InstanceRoutes.js"; -import LoginPage from "./paths/login/index.js"; -import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { ConnectionPage, LoginPage } from "./paths/login/index.js"; import { Settings } from "./paths/settings/index.js"; +import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; /** * Check if admin against /management/instances @@ -41,15 +42,14 @@ import { Settings } from "./paths/settings/index.js"; */ export function ApplicationReadyRoutes(): VNode { const { i18n } = useTranslationContext(); + const { url: backendURL, changeBackend } = useBackendContext() const [unauthorized, setUnauthorized] = useState(false) const { - url: backendURL, - updateLoginStatus: updateLoginStatus2, + updateToken, } = useBackendContext(); - function updateLoginStatus(url: string, token: string | undefined) { - console.log("updateing", url, token) - updateLoginStatus2(url, token) + function updateLoginStatus(token: LoginToken | undefined) { + updateToken(token) setUnauthorized(false) } @@ -59,15 +59,15 @@ export function ApplicationReadyRoutes(): VNode { route("/"); }; const [showSettings, setShowSettings] = useState(false) - // useEffect(() => { - // setUnauthorized(FF) - // }, [FF]) - const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized + const unauthorizedAdmin = !result.loading + && !result.ok + && result.type === ErrorType.CLIENT + && result.status === HttpStatusCode.Unauthorized; if (showSettings) { return setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> - + setShowSettings(false)} /> } @@ -100,7 +100,7 @@ export function ApplicationReadyRoutes(): VNode { type: "ERROR", }} /> - + ); } @@ -108,14 +108,13 @@ export function ApplicationReadyRoutes(): VNode { instanceNameByBackendURL = match[1]; } - console.log(unauthorized, unauthorizedAdmin) if (unauthorized || unauthorizedAdmin) { return setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> @@ -132,7 +131,6 @@ export function ApplicationReadyRoutes(): VNode { admin={admin} onUnauthorized={() => setUnauthorized(true)} onLoginPass={() => { - console.log("ahora si") setUnauthorized(false) }} instanceNameByBackendURL={instanceNameByBackendURL} diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index ee8db9a9f..c2a9d3b18 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -35,7 +35,7 @@ import { InstanceContextProvider } from "./context/instance.js"; import { useBackendDefaultToken, useBackendInstanceToken, - useLocalStorage, + useSimpleLocalStorage, } from "./hooks/index.js"; import { useInstanceKYCDetails } from "./hooks/instance.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; @@ -71,10 +71,10 @@ import InstanceUpdatePage, { AdminUpdate as InstanceAdminUpdatePage, Props as InstanceUpdatePageProps, } from "./paths/instance/update/index.js"; -import LoginPage from "./paths/login/index.js"; +import { LoginPage } from "./paths/login/index.js"; import NotFoundPage from "./paths/notfound/index.js"; import { Notification } from "./utils/types.js"; -import { MerchantBackend } from "./declaration.js"; +import { LoginToken, MerchantBackend } from "./declaration.js"; import { Settings } from "./paths/settings/index.js"; import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; @@ -143,7 +143,7 @@ export function InstanceRoutes({ id, admin, path, - onUnauthorized, + // onUnauthorized, onLoginPass, setInstanceName, }: Props): VNode { @@ -155,7 +155,7 @@ export function InstanceRoutes({ const [globalNotification, setGlobalNotification] = useState(undefined); - const changeToken = (token?: string) => { + const changeToken = (token?: LoginToken) => { if (admin) { updateToken(token); } else { @@ -201,14 +201,17 @@ export function InstanceRoutes({ // const LoginPageAccessDeniend = onUnauthorized const LoginPageAccessDenied = () => { - onUnauthorized() - return + return + + + + } function IfAdminCreateDefaultOr(Next: FunctionComponent) { @@ -687,9 +690,7 @@ function AdminInstanceUpdatePage({ ...rest }: { id: string } & InstanceUpdatePageProps): VNode { const [token, changeToken] = useBackendInstanceToken(id); - const { updateLoginStatus: changeBackend } = useBackendContext(); - const updateLoginStatus = (url: string, token?: string): void => { - changeBackend(url); + const updateLoginStatus = (token?: LoginToken): void => { changeToken(token); }; const value = useMemo( @@ -752,7 +753,7 @@ function KycBanner(): VNode { const { i18n } = useTranslationContext(); const [settings] = useSettings(); const today = format(new Date(), dateFormatForSettings(settings)); - const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide"); + const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide"); const hasBeenHidden = today === lastHide; const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; if (hasBeenHidden || !needsToBeShown) return ; diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx deleted file mode 100644 index 4fa440fc7..000000000 --- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx +++ /dev/null @@ -1,244 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { useBackendContext } from "../../context/backend.js"; -import { useInstanceContext } from "../../context/instance.js"; -import { useCredentialsChecker } from "../../hooks/backend.js"; -import { Notification } from "../../utils/types.js"; - -interface Props { - withMessage?: Notification; - onConfirm: (backend: string, token?: string) => void; -} - -function getTokenValuePart(t: string): string { - if (!t) return t; - const match = /secret-token:(.*)/.exec(t); - if (!match || !match[1]) return ""; - return match[1]; -} - -function normalizeToken(r: string): string { - return `secret-token:${r}`; -} - -function cleanUp(s: string): string { - let result = s; - if (result.indexOf("webui/") !== -1) { - result = result.substring(0, result.indexOf("webui/")); - } - return result; -} - -export function LoginModal({ onConfirm, withMessage }: Props): VNode { - const { url: backendUrl, token: baseToken } = useBackendContext(); - const { admin, token: instanceToken, id } = useInstanceContext(); - const testLogin = useCredentialsChecker(); - const currentToken = getTokenValuePart( - (!admin ? baseToken : instanceToken) ?? "", - ); - const [token, setToken] = useState(currentToken); - - const [url, setURL] = useState(cleanUp(backendUrl)); - const { i18n } = useTranslationContext(); - - if (admin && id !== "default") { - //admin trying to access another instance - return (
-
- -
-
) - } - - return ( -
-
- -
-
- ); -} - -function AsyncButton({ onClick, children }: { onClick: () => Promise, children: ComponentChildren }): VNode { - const [running, setRunning] = useState(false) - return -} diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx index b75dc83b3..6f5881fc0 100644 --- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx +++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx @@ -40,13 +40,13 @@ export function DefaultInstanceFormFields({ showId: boolean; }): VNode { const { i18n } = useTranslationContext(); - const backend = useBackendContext(); + const { url: backendURL } = useBackendContext() return ( {showId && ( name="id" - addonBefore={`${backend.url}/instances/`} + addonBefore={`${backendURL}/instances/`} readonly={readonlyId} label={i18n.str`Identifier`} tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`} diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx index be2f8dde5..3d5f20c85 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -25,7 +25,6 @@ import { useBackendContext } from "../../context/backend.js"; import { useConfigContext } from "../../context/config.js"; import { useInstanceKYCDetails } from "../../hooks/instance.js"; import { LangSelector } from "./LangSelector.js"; -import { useCredentialsChecker } from "../../hooks/backend.js"; const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -50,7 +49,7 @@ export function Sidebar({ isPasswordOk }: Props): VNode { const config = useConfigContext(); - const backend = useBackendContext(); + const { url: backendURL } = useBackendContext() const { i18n } = useTranslationContext(); const kycStatus = useInstanceKYCDetails(); const needKYC = kycStatus.ok && kycStatus.data.type === "redirect"; @@ -230,7 +229,7 @@ export function Sidebar({ - {new URL(backend.url).hostname} + {new URL(backendURL).hostname} diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx index 726a94f5e..8bebbd298 100644 --- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -114,7 +114,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { onSubscribe(hasErrors ? undefined : submit); }, [submit, hasErrors]); - const backend = useBackendContext(); + const { url: backendURL } = useBackendContext() const { i18n } = useTranslationContext(); return ( @@ -128,7 +128,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { {alreadyExist ? undefined : ( name="product_id" - addonBefore={`${backend.url}/product/`} + addonBefore={`${backendURL}/product/`} label={i18n.str`ID`} tooltip={i18n.str`product identification to use in URLs (for internal use only)`} /> diff --git a/packages/merchant-backoffice-ui/src/context/backend.test.ts b/packages/merchant-backoffice-ui/src/context/backend.test.ts index cb0010c4b..b042d5a25 100644 --- a/packages/merchant-backoffice-ui/src/context/backend.test.ts +++ b/packages/merchant-backoffice-ui/src/context/backend.test.ts @@ -21,7 +21,7 @@ import * as tests from "@gnu-taler/web-util/testing"; import { ComponentChildren, h, VNode } from "preact"; -import { MerchantBackend } from "../declaration.js"; +import { AccessToken, MerchantBackend } from "../declaration.js"; import { useAdminAPI, useInstanceAPI, @@ -64,7 +64,7 @@ describe("backend context api ", () => { } as MerchantBackend.Instances.QueryInstancesResponse, }); - management.setNewToken("another_token"); + management.setNewToken("another_token" as AccessToken); }, ({ instance, management, admin }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ @@ -113,7 +113,7 @@ describe("backend context api ", () => { name: "instance_name", } as MerchantBackend.Instances.QueryInstancesResponse, }); - instance.setNewToken("another_token"); + instance.setNewToken("another_token" as AccessToken); }, ({ instance, management, admin }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts index 43e9e4d27..056f9a192 100644 --- a/packages/merchant-backoffice-ui/src/context/backend.ts +++ b/packages/merchant-backoffice-ui/src/context/backend.ts @@ -20,90 +20,46 @@ */ import { createContext, h, VNode } from "preact"; -import { useCallback, useContext, useState } from "preact/hooks"; +import { useContext } from "preact/hooks"; +import { LoginToken } from "../declaration.js"; import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js"; interface BackendContextType { - url: string; - token?: string; - triedToLog: boolean; - resetBackend: () => void; - // clearAllTokens: () => void; - // addTokenCleaner: (c: () => void) => void; - updateLoginStatus: (url: string, token?: string) => void; - updateToken: (token?: string) => void; + url: string, + token?: LoginToken; + updateToken: (token: LoginToken | undefined) => void; + changeBackend: (url: string) => void; } const BackendContext = createContext({ url: "", token: undefined, - triedToLog: false, - resetBackend: () => null, - // clearAllTokens: () => null, - // addTokenCleaner: () => null, - updateLoginStatus: () => null, updateToken: () => null, + changeBackend: () => null, }); function useBackendContextState( defaultUrl?: string, - initialToken?: string, ): BackendContextType { - const [url, triedToLog, changeBackend, resetBackend] = - useBackendURL(defaultUrl); - const [token, _updateToken] = useBackendDefaultToken(initialToken); - const updateToken = (t?: string) => { - _updateToken(t); - }; - - // const tokenCleaner = useCallback(() => { - // updateToken(undefined); - // }, []); - // const [cleaners, setCleaners] = useState([tokenCleaner]); - // const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]); - // const addTokenCleanerMemo = useCallback( - // (c: () => void) => { - // addTokenCleaner(c); - // }, - // [tokenCleaner], - // ); - - // const clearAllTokens = () => { - // cleaners.forEach((c) => c()); - // for (let i = 0; i < localStorage.length; i++) { - // const k = localStorage.key(i); - // if (k && /^backend-token/.test(k)) localStorage.removeItem(k); - // } - // resetBackend(); - // }; - - const updateLoginStatus = (url: string, token?: string) => { - changeBackend(url); - updateToken(token); - }; + const [url, changeBackend] = useBackendURL(defaultUrl); + const [token, updateToken] = useBackendDefaultToken(); return { url, token, - triedToLog, - updateLoginStatus, - resetBackend, - // clearAllTokens, updateToken, - // addTokenCleaner: addTokenCleanerMemo, + changeBackend }; } export const BackendContextProvider = ({ children, defaultUrl, - initialToken, }: { children: any; defaultUrl?: string; - initialToken?: string; }): VNode => { - const value = useBackendContextState(defaultUrl, initialToken); + const value = useBackendContextState(defaultUrl); return h(BackendContext.Provider, { value, children }); }; diff --git a/packages/merchant-backoffice-ui/src/context/instance.ts b/packages/merchant-backoffice-ui/src/context/instance.ts index 9a25fe80c..3c6cc2b63 100644 --- a/packages/merchant-backoffice-ui/src/context/instance.ts +++ b/packages/merchant-backoffice-ui/src/context/instance.ts @@ -21,12 +21,13 @@ import { createContext } from "preact"; import { useContext } from "preact/hooks"; +import { LoginToken } from "../declaration.js"; interface Type { id: string; - token?: string; + token?: LoginToken; admin?: boolean; - changeToken: (t?: string) => void; + changeToken: (t?: LoginToken) => void; } const Context = createContext({} as any); diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts index 5ca9c1e09..c3e6ea3da 100644 --- a/packages/merchant-backoffice-ui/src/declaration.d.ts +++ b/packages/merchant-backoffice-ui/src/declaration.d.ts @@ -107,6 +107,16 @@ interface RegexAccountRestriction { // human hints. human_hint_i18n?: { [lang_tag: string]: string }; } +interface LoginToken { + token: string, + expiration: Timestamp, +} +// token used to get loginToken +// must forget after used +declare const __ac_token: unique symbol; +type AccessToken = string & { + [__ac_token]: true; +}; export namespace ExchangeBackend { interface WireResponse { @@ -491,6 +501,35 @@ export namespace MerchantBackend { }; } // DELETE /private/instances/$INSTANCE + interface LoginTokenRequest { + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + duration?: RelativeTime; + + // Can this token be refreshed? + // Defaults to false. + refreshable?: boolean; + } + interface LoginTokenSuccessResponse { + // The login token that can be used to access resources + // that are in scope for some time. Must be prefixed + // with "Bearer " when used in the "Authorization" HTTP header. + // Will already begin with the RFC 8959 prefix. + token: string; + + // Scope of the token (which kinds of operations it will allow) + scope: "readonly" | "write"; + + // Server may impose its own upper bound + // on the token validity duration + expiration: Timestamp; + + // Can this token be refreshed? + refreshable: boolean; + } } namespace KYC { diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts index ecd34df6d..fe4155788 100644 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts @@ -19,19 +19,21 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useSWRConfig } from "swr"; -import { MerchantBackend } from "../declaration.js"; -import { useBackendContext } from "../context/backend.js"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { useInstanceContext } from "../context/instance.js"; +import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; import { ErrorType, + HttpError, HttpResponse, HttpResponseOk, RequestError, RequestOptions, + useApiContext, } from "@gnu-taler/web-util/browser"; -import { useApiContext } from "@gnu-taler/web-util/browser"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useSWRConfig } from "swr"; +import { useBackendContext } from "../context/backend.js"; +import { useInstanceContext } from "../context/instance.js"; +import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js"; export function useMatchMutate(): ( @@ -85,6 +87,9 @@ export function useBackendInstancesTestForAdmin(): HttpResponse< return result; } +const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; +const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; + export function useBackendConfig(): HttpResponse< MerchantBackend.VersionResponse, RequestError @@ -92,18 +97,33 @@ export function useBackendConfig(): HttpResponse< const { request } = useBackendBaseRequest(); type Type = MerchantBackend.VersionResponse; - - const [result, setResult] = useState< - HttpResponse> - >({ loading: true }); + type State = { data: HttpResponse>, timer: number } + const [result, setResult] = useState({ data: { loading: true }, timer: 0 }); useEffect(() => { - request(`/config`) - .then((data) => setResult(data)) - .catch((error) => setResult(error)); + if (result.timer) { + clearTimeout(result.timer) + } + function tryConfig(): void { + request(`/config`) + .then((data) => { + const timer: any = setTimeout(() => { + tryConfig() + }, CHECK_CONFIG_INTERVAL_OK) + setResult({ data, timer }) + }) + .catch((error) => { + const timer: any = setTimeout(() => { + tryConfig() + }, CHECK_CONFIG_INTERVAL_FAIL) + const data = error.cause + setResult({ data, timer }) + }); + } + tryConfig() }, [request]); - return result; + return result.data; } interface useBackendInstanceRequestType { @@ -149,32 +169,86 @@ interface useBackendBaseRequestType { } type YesOrNo = "yes" | "no"; +type LoginResult = { + valid: true; + token: string; + expiration: Timestamp; +} | { + valid: false; + cause: HttpError<{}>; +} export function useCredentialsChecker() { const { request } = useApiContext(); //check against instance details endpoint //while merchant backend doesn't have a login endpoint - async function testLogin( - instance: string, - token: string, - ): Promise<{ - valid: boolean; - cause?: ErrorType; - }> { + async function requestNewLoginToken( + baseUrl: string, + token: AccessToken, + ): Promise { + const data: MerchantBackend.Instances.LoginTokenRequest = { + scope: "write", + duration: { + d_us: "forever" + }, + refreshable: true, + } try { - const response = await request(instance, `/private/`, { + const response = await request(baseUrl, `/private/token`, { + method: "POST", token, + data }); - return { valid: true }; + return { valid: true, token: response.data.token, expiration: response.data.expiration }; } catch (error) { if (error instanceof RequestError) { - return { valid: false, cause: error.cause.type }; + return { valid: false, cause: error.cause }; } - return { valid: false, cause: ErrorType.UNEXPECTED }; + return { + valid: false, cause: { + type: ErrorType.UNEXPECTED, + loading: false, + info: { + hasToken: true, + status: 0, + options: {}, + url: `/private/token`, + payload: {} + }, + exception: error, + message: (error instanceof Error ? error.message : "unpexepected error") + } + }; } }; - return testLogin + + async function refreshLoginToken( + baseUrl: string, + token: LoginToken + ): Promise { + + if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { + return { + valid: false, cause: { + type: ErrorType.CLIENT, + status: HttpStatusCode.Unauthorized, + message: "login token expired, login again.", + info: { + hasToken: true, + status: 401, + options: {}, + url: `/private/token`, + payload: {} + }, + payload: {} + }, + } + } + + return requestNewLoginToken(baseUrl, token.token as AccessToken) + } + return { requestNewLoginToken, refreshLoginToken } } /** @@ -183,15 +257,20 @@ export function useCredentialsChecker() { * @returns request handler to */ export function useBackendBaseRequest(): useBackendBaseRequestType { - const { url: backend, token } = useBackendContext(); + const { url: backend, token: loginToken } = useBackendContext(); const { request: requestHandler } = useApiContext(); + const token = loginToken?.token; const request = useCallback( function requestImpl( endpoint: string, options: RequestOptions = {}, ): Promise> { - return requestHandler(backend, endpoint, { token, ...options }); + return requestHandler(backend, endpoint, { token, ...options }).then(res => { + return res + }).catch(err => { + throw err + }); }, [backend, token], ); @@ -204,10 +283,12 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType { const { token: instanceToken, id, admin } = useInstanceContext(); const { request: requestHandler } = useApiContext(); - const { baseUrl, token } = !admin + const { baseUrl, token: loginToken } = !admin ? { baseUrl: rootBackendUrl, token: rootToken } : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken }; + const token = loginToken?.token; + const request = useCallback( function requestImpl( endpoint: string, diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index 79b22304a..ee696779f 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -19,9 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { StateUpdater, useCallback, useEffect, useState } from "preact/hooks"; +import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util"; +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { StateUpdater, useEffect, useState } from "preact/hooks"; +import { LoginToken } from "../declaration.js"; import { ValueOrFunction } from "../utils/types.js"; -import { useMemoryStorage } from "@gnu-taler/web-util/browser"; import { useMatchMutate } from "./backend.js"; const calculateRootPath = () => { @@ -32,53 +34,55 @@ const calculateRootPath = () => { return rootPath; }; +const loginTokenCodec = buildCodecForObject() + .property("token", codecForString()) + .property("expiration", codecForTimestamp) + .build("loginToken") +const TOKENS_KEY = buildStorageKey("backend-token", codecForMap(loginTokenCodec)); + + export function useBackendURL( url?: string, -): [string, boolean, StateUpdater, () => void] { - const [value, setter] = useNotNullLocalStorage( +): [string, StateUpdater] { + const [value, setter] = useSimpleLocalStorage( "backend-url", url || calculateRootPath(), ); - const [triedToLog, setTriedToLog] = useLocalStorage("tried-login"); const checkedSetter = (v: ValueOrFunction) => { - setTriedToLog("yes"); - return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, "")); + return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); }; - const resetBackend = () => { - setTriedToLog(undefined); - }; - return [value, !!triedToLog, checkedSetter, resetBackend]; + return [value!, checkedSetter]; } export function useBackendDefaultToken( - initialValue?: string, -): [string | undefined, ((d: string | undefined) => void)] { - // uncomment for testing - initialValue = "secret-token:secret" as string | undefined - const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue) +): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { + const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) + + const tokenOfDefaultInstance = tokenMap["default"] const clearCache = useMatchMutate() useEffect(() => { clearCache() - }, [token]) + }, [tokenOfDefaultInstance]) function updateToken( - value: (string | undefined) + value: (LoginToken | undefined) ): void { if (value === undefined) { reset() } else { - setToken(value) + const res = { ...tokenMap, "default": value } + setToken(res) } } - return [token, updateToken]; + return [tokenMap["default"], updateToken]; } export function useBackendInstanceToken( id: string, -): [string | undefined, ((d: string | undefined) => void)] { - const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`) +): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { + const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) const [defaultToken, defaultSetToken] = useBackendDefaultToken(); // instance named 'default' use the default token @@ -86,16 +90,17 @@ export function useBackendInstanceToken( return [defaultToken, defaultSetToken]; } function updateToken( - value: (string | undefined) + value: (LoginToken | undefined) ): void { if (value === undefined) { reset() } else { - setToken(value) + const res = { ...tokenMap, [id]: value } + setToken(res) } } - return [token, updateToken]; + return [tokenMap[id], updateToken]; } export function useLang(initial?: string): [string, StateUpdater] { @@ -104,10 +109,10 @@ export function useLang(initial?: string): [string, StateUpdater] { ? navigator.language || (navigator as any).userLanguage : undefined; const defaultLang = (browserLang || initial || "en").substring(0, 2); - return useNotNullLocalStorage("lang-preference", defaultLang); + return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater]; } -export function useLocalStorage( +export function useSimpleLocalStorage( key: string, initialValue?: string, ): [string | undefined, StateUpdater] { @@ -137,28 +142,3 @@ export function useLocalStorage( return [storedValue, setValue]; } - -export function useNotNullLocalStorage( - key: string, - initialValue: string, -): [string, StateUpdater] { - const [storedValue, setStoredValue] = useState((): string => { - return typeof window !== "undefined" - ? window.localStorage.getItem(key) || initialValue - : initialValue; - }); - - const setValue = (value: string | ((val: string) => string)) => { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setStoredValue(valueToStore); - if (typeof window !== "undefined") { - if (!valueToStore) { - window.localStorage.removeItem(key); - } else { - window.localStorage.setItem(key, valueToStore); - } - } - }; - - return [storedValue, setValue]; -} diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts index d15b3f6d7..a7b8d047c 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts @@ -21,7 +21,7 @@ import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; +import { AccessToken, MerchantBackend } from "../declaration.js"; import { useAdminAPI, useBackendInstances, @@ -158,7 +158,7 @@ describe("instance api interaction with details", () => { }, } as MerchantBackend.Instances.QueryInstancesResponse, }); - api.setNewToken("secret"); + api.setNewToken("secret" as AccessToken); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts index 32ed30c6f..50f9487a3 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -19,10 +19,11 @@ import { RequestError, } from "@gnu-taler/web-util/browser"; import { useBackendContext } from "../context/backend.js"; -import { MerchantBackend } from "../declaration.js"; +import { AccessToken, MerchantBackend } from "../declaration.js"; import { useBackendBaseRequest, useBackendInstanceRequest, + useCredentialsChecker, useMatchMutate, } from "./backend.js"; @@ -36,7 +37,7 @@ interface InstanceAPI { ) => Promise; deleteInstance: () => Promise; clearToken: () => Promise; - setNewToken: (token: string) => Promise; + setNewToken: (token: AccessToken) => Promise; } export function useAdminAPI(): AdminAPI { @@ -86,8 +87,10 @@ export interface AdminAPI { export function useManagementAPI(instanceId: string): InstanceAPI { const mutateAll = useMatchMutate(); + const { url: backendURL } = useBackendContext() const { updateToken } = useBackendContext(); const { request } = useBackendBaseRequest(); + const { requestNewLoginToken } = useCredentialsChecker() const updateInstance = async ( instance: MerchantBackend.Instances.InstanceReconfigurationMessage, @@ -117,13 +120,20 @@ export function useManagementAPI(instanceId: string): InstanceAPI { mutateAll(/\/management\/instances/); }; - const setNewToken = async (newToken: string): Promise => { + const setNewToken = async (newToken: AccessToken): Promise => { await request(`/management/instances/${instanceId}/auth`, { method: "POST", data: { method: "token", token: newToken }, }); - updateToken(newToken); + const resp = await requestNewLoginToken(backendURL, newToken) + if (resp.valid) { + const { token, expiration } = resp + updateToken({ token, expiration }); + } else { + updateToken(undefined) + } + mutateAll(/\/management\/instances/); }; @@ -132,12 +142,13 @@ export function useManagementAPI(instanceId: string): InstanceAPI { export function useInstanceAPI(): InstanceAPI { const { mutate } = useSWRConfig(); + const { url: backendURL, updateToken } = useBackendContext() + const { - url: baseUrl, token: adminToken, - updateLoginStatus, } = useBackendContext(); const { request } = useBackendInstanceRequest(); + const { requestNewLoginToken } = useCredentialsChecker() const updateInstance = async ( instance: MerchantBackend.Instances.InstanceReconfigurationMessage, @@ -147,7 +158,7 @@ export function useInstanceAPI(): InstanceAPI { data: instance, }); - if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null); + if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); mutate([`/private/`], null); }; @@ -157,7 +168,7 @@ export function useInstanceAPI(): InstanceAPI { // token: adminToken, }); - if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null); + if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); mutate([`/private/`], null); }; @@ -170,13 +181,20 @@ export function useInstanceAPI(): InstanceAPI { mutate([`/private/`], null); }; - const setNewToken = async (newToken: string): Promise => { + const setNewToken = async (newToken: AccessToken): Promise => { await request(`/private/auth`, { method: "POST", data: { method: "token", token: newToken }, }); - updateLoginStatus(baseUrl, newToken); + const resp = await requestNewLoginToken(backendURL, newToken) + if (resp.valid) { + const { token, expiration } = resp + updateToken({ token, expiration }); + } else { + updateToken(undefined) + } + mutate([`/private/`], null); }; diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx index ebbc6f64a..847d512b0 100644 --- a/packages/merchant-backoffice-ui/src/hooks/testing.tsx +++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx @@ -90,10 +90,7 @@ export class ApiMockEnvironment extends MockEnvironment { const SC: any = SWRConfig; return ( - + (str: string | undefined): T | undefined { - if (str === undefined) return undefined; - try { - return JSON.parse(str); - } catch { - return undefined; - } -} - export interface Settings { advanceOrderMode: boolean; dateFormat: "ymd" | "dmy" | "mdy"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx index e42adc2ff..1cfbec29b 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -22,7 +22,7 @@ import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format, formatDistance } from "date-fns"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; @@ -35,10 +35,10 @@ import { TextField } from "../../../../components/form/TextField.js"; import { ProductList } from "../../../../components/product/ProductList.js"; import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend } from "../../../../declaration.js"; +import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; import { mergeRefunds } from "../../../../utils/amount.js"; import { RefundModal } from "../list/Table.js"; import { Event, Timeline } from "./Timeline.js"; -import { dateFormatForSettings, datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; type CT = MerchantBackend.ContractTerms; @@ -416,9 +416,9 @@ function PaidPage({ }) const [value, valueHandler] = useState>(order); - const { url } = useBackendContext(); + const { url: backendURL } = useBackendContext() const refundurl = stringifyRefundUri({ - merchantBaseUrl: url, + merchantBaseUrl: backendURL, orderId: order.contract_terms.order_id }) const refundable = diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx index 57a051ed7..780068a91 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx @@ -13,12 +13,12 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ +import { stringifyRewardUri } from "@gnu-taler/taler-util"; import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend } from "../../../../declaration.js"; import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; -import { stringifyRewardUri } from "@gnu-taler/taler-util"; type Entity = MerchantBackend.Rewards.RewardDetails; @@ -29,9 +29,9 @@ interface Props { } export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode { - const { url: merchantBaseUrl } = useBackendContext(); + const { url: backendURL } = useBackendContext() const [settings] = useSettings(); - const rewardURL = stringifyRewardUri({ merchantBaseUrl, merchantRewardId }) + const rewardURL = stringifyRewardUri({ merchantBaseUrl: backendURL, merchantRewardId }) return (
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx index 8629d8dee..78ea07477 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -35,16 +35,12 @@ import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { useBackendContext } from "../../../../context/backend.js"; -import { useInstanceContext } from "../../../../context/instance.js"; import { MerchantBackend } from "../../../../declaration.js"; -import { - isBase32RFC3548Charset -} from "../../../../utils/crypto.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = MerchantBackend.Template.TemplateAddDetails; @@ -55,7 +51,7 @@ interface Props { export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const backend = useBackendContext(); + const { url: backendURL } = useBackendContext() const devices = useInstanceOtpDevices() const [state, setState] = useState>({ @@ -128,7 +124,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { > name="template_id" - help={`${backend.url}/templates/${state.template_id ?? ""}`} + help={`${backendURL}/templates/${state.template_id ?? ""}`} label={i18n.str`Identifier`} tooltip={i18n.str`Name of the template in URLs.`} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx index c65cf6a19..5140aae3a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -19,8 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { QR } from "../../../../components/exception/QR.js"; import { @@ -29,14 +30,10 @@ import { } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { ConfirmModal } from "../../../../components/modal/index.js"; import { useBackendContext } from "../../../../context/backend.js"; import { useConfigContext } from "../../../../context/config.js"; import { useInstanceContext } from "../../../../context/instance.js"; import { MerchantBackend } from "../../../../declaration.js"; -import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; -import { useOtpDeviceDetails } from "../../../../hooks/otp.js"; -import { Loading } from "../../../../components/exception/loading.js"; type Entity = MerchantBackend.Template.UsingTemplateDetails; @@ -48,7 +45,7 @@ interface Props { export function QrPage({ contract, id: templateId, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendUrl } = useBackendContext(); + const { url: backendURL } = useBackendContext() const { id: instanceId } = useInstanceContext(); const config = useConfigContext(); @@ -75,7 +72,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode { templateParams.summary = state.summary ?? "" } - const merchantBaseUrl = new URL(backendUrl).href; + const merchantBaseUrl = new URL(backendURL).href; const payTemplateUri = stringifyPayTemplateUri({ merchantBaseUrl, @@ -84,7 +81,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode { }) const issuer = encodeURIComponent( - `${new URL(backendUrl).host}/${instanceId}`, + `${new URL(backendURL).host}/${instanceId}`, ); return ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx index 30d47385c..82b74e1fa 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -24,7 +24,7 @@ import { MerchantTemplateContractDetails, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -35,17 +35,10 @@ import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { useBackendContext } from "../../../../context/backend.js"; import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { - isBase32RFC3548Charset, - randomBase32Key, -} from "../../../../utils/crypto.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { QR } from "../../../../components/exception/QR.js"; -import { useInstanceContext } from "../../../../context/instance.js"; type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; @@ -55,12 +48,9 @@ interface Props { template: Entity; } -const algorithms = [0, 1, 2]; -const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; - export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const backend = useBackendContext(); + const { url: backendURL } = useBackendContext() const [state, setState] = useState>(template); @@ -115,7 +105,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
- {backend.url}/templates/{template.id} + {backendURL}/templates/{template.id}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx index 984880752..4b0db200a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -26,12 +26,13 @@ import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormProvider } from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; import { useInstanceContext } from "../../../context/instance.js"; +import { AccessToken } from "../../../declaration.js"; interface Props { instanceId: string; currentToken: string | undefined; onClearToken: () => void; - onNewToken: (s: string) => void; + onNewToken: (s: AccessToken) => void; onBack?: () => void; } @@ -71,7 +72,8 @@ export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewTo async function submitForm() { if (hasErrors) return; - onNewToken(form.new_token as any) + const nt = `secret-token:${form.new_token}` as AccessToken; + onNewToken(nt) } return ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx index d5910361b..0a49448f8 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -17,7 +17,7 @@ import { HttpStatusCode } from "@gnu-taler/taler-util"; import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { Loading } from "../../../components/exception/loading.js"; -import { MerchantBackend } from "../../../declaration.js"; +import { AccessToken, MerchantBackend } from "../../../declaration.js"; import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; import { DetailPage } from "./DetailPage.js"; import { useInstanceContext } from "../../../context/instance.js"; @@ -49,13 +49,13 @@ export default function Token({ const { token: instanceToken, id, admin } = useInstanceContext(); const currentToken = !admin ? rootToken : instanceToken - const hasPrefix = currentToken !== undefined && currentToken.startsWith(PREFIX) + const hasPrefix = currentToken !== undefined && currentToken.token.startsWith(PREFIX) return ( => { try { await clearToken(); @@ -72,7 +72,7 @@ export default function Token({ }} onNewToken={async (newToken): Promise => { try { - await setNewToken(`secret-token:${newToken}`); + await setNewToken(newToken); onChange(); } catch (error) { if (error instanceof Error) { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx index 4a8162611..6c5e7a514 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -13,18 +13,19 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ +import { HttpStatusCode } from "@gnu-taler/taler-util"; import { ErrorType, HttpError, HttpResponse, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { Loading } from "../../../components/exception/loading.js"; import { NotificationCard } from "../../../components/menu/index.js"; import { useInstanceContext } from "../../../context/instance.js"; -import { MerchantBackend } from "../../../declaration.js"; +import { AccessToken, MerchantBackend } from "../../../declaration.js"; import { useInstanceAPI, useInstanceDetails, @@ -33,7 +34,6 @@ import { } from "../../../hooks/instance.js"; import { Notification } from "../../../utils/types.js"; import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; export interface Props { onBack: () => void; @@ -73,10 +73,9 @@ function CommonUpdate( MerchantBackend.ErrorDetail >, updateInstance: any, - clearToken: any, - setNewToken: any, + clearToken: () => Promise, + setNewToken: (t: AccessToken) => Promise, ): VNode { - const { changeToken } = useInstanceContext(); const [notif, setNotif] = useState(undefined); const { i18n } = useTranslationContext(); @@ -119,11 +118,8 @@ function CommonUpdate( d: MerchantBackend.Instances.InstanceAuthConfigurationMessage, ): Promise => { const apiCall = - d.method === "external" ? clearToken() : setNewToken(d.token!); - return apiCall - .then(() => changeToken(d.token)) - .then(onConfirm) - .catch(onUpdateError); + d.method === "external" ? clearToken() : setNewToken(d.token! as AccessToken); + return apiCall.then(onConfirm).catch(onUpdateError); }} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx index 3ad3cb3a3..22ae55677 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx @@ -18,9 +18,9 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { QR } from "../../../../components/exception/QR.js"; import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { useBackendContext } from "../../../../context/backend.js"; import { useInstanceContext } from "../../../../context/instance.js"; import { MerchantBackend } from "../../../../declaration.js"; +import { useBackendContext } from "../../../../context/backend.js"; type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; @@ -38,9 +38,9 @@ export function CreatedSuccessfully({ onConfirm, }: Props): VNode { const { i18n } = useTranslationContext(); - const backend = useBackendContext(); + const { url: backendURL } = useBackendContext() const { id: instanceId } = useInstanceContext(); - const issuer = new URL(backend.url).hostname; + const issuer = new URL(backendURL).hostname; const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`; diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx index caa63c714..9948307e4 100644 --- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -18,12 +18,301 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode } from "preact"; -import { LoginModal } from "../../components/exception/login.js"; + +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ComponentChildren, h, VNode } from "preact"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useBackendContext } from "../../context/backend.js"; +import { useInstanceContext } from "../../context/instance.js"; +import { AccessToken, LoginToken } from "../../declaration.js"; +import { useCredentialsChecker } from "../../hooks/backend.js"; +import { useBackendURL } from "../../hooks/index.js"; interface Props { - onConfirm: (url: string, token?: string) => void; + onConfirm: (token: LoginToken | undefined) => void; } -export default function LoginPage({ onConfirm }: Props): VNode { - return ; + +function getTokenValuePart(t: string): string { + if (!t) return t; + const match = /secret-token:(.*)/.exec(t); + if (!match || !match[1]) return ""; + return match[1]; } + +function normalizeToken(r: string): AccessToken { + return `secret-token:${r}` as AccessToken; +} + +function cleanUp(s: string): string { + let result = s; + if (result.indexOf("webui/") !== -1) { + result = result.substring(0, result.indexOf("webui/")); + } + return result; +} + +export function LoginPage({ onConfirm }: Props): VNode { + const { url: backendURL, changeBackend } = useBackendContext(); + const { admin, id } = useInstanceContext(); + const { requestNewLoginToken } = useCredentialsChecker(); + const [token, setToken] = useState(""); + + const { i18n } = useTranslationContext(); + + + const doLogin = useCallback(async function doLoginImpl() { + const secretToken = normalizeToken(token); + const baseUrl = id === undefined ? backendURL : `${backendURL}/instances/${id}` + const result = await requestNewLoginToken(baseUrl, secretToken); + if (result.valid) { + const { token, expiration } = result + onConfirm({ token, expiration }); + } else { + onConfirm(undefined); + } + }, [backendURL, id, token]) + + async function changeServer() { + changeBackend("") + } + + console.log(admin, id) + if (admin && id !== "default") { + //admin trying to access another instance + return (
+
+ +
+
) + } + + return ( +
+
+ +
+
+ ); +} + +function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise, children: ComponentChildren }): VNode { + const [running, setRunning] = useState(false) + return +} + + +export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }): VNode { + const { url: backendURL } = useBackendContext() + + const [url, setURL] = useState(cleanUp(backendURL)); + const { i18n } = useTranslationContext(); + + async function doConnect() { + onConfirm(url) + } + + return ( +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx index 0d514f2df..87bd2fa39 100644 --- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx @@ -13,7 +13,7 @@ function getBrowserLang(): string | undefined { return undefined; } -export function Settings(): VNode { +export function Settings({ onClose }: { onClose?: () => void }): VNode { const { i18n } = useTranslationContext() const borwserLang = getBrowserLang() const { update } = useLang() @@ -94,11 +94,19 @@ export function Settings(): VNode { />
- -
- - + + {onClose && +
+ +
+ } + } \ No newline at end of file diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts index 1464eca98..8ce21b0e1 100644 --- a/packages/web-util/src/utils/request.ts +++ b/packages/web-util/src/utils/request.ts @@ -25,6 +25,8 @@ export enum ErrorType { UNEXPECTED, } + + /** * * @param baseUrl URL where the service is located @@ -60,10 +62,27 @@ export async function defaultRequestHandler( const requestPreventCache = options.preventCache ?? false; const requestPreventCors = options.preventCors ?? false; - const _url = new URL(`${baseUrl}${endpoint}`); + const validURL = validateURL(baseUrl, endpoint); + + if (!validURL) { + const error: HttpResponseUnexpectedError = { + info: { + url: `${baseUrl}${endpoint}`, + payload: {}, + hasToken: !!options.token, + status: 0, + options, + }, + type: ErrorType.UNEXPECTED, + exception: undefined, + loading: false, + message: `invalid URL: "${validURL}"`, + }; + throw new RequestError(error) + } Object.entries(requestParams).forEach(([key, value]) => { - _url.searchParams.set(key, String(value)); + validURL.searchParams.set(key, String(value)); }); let payload: BodyInit | undefined = undefined; @@ -77,7 +96,20 @@ export async function defaultRequestHandler( } else if (typeof requestBody === "object") { payload = JSON.stringify(requestBody); } else { - throw Error("unsupported request body type"); + const error: HttpResponseUnexpectedError = { + info: { + url: validURL.href, + payload: {}, + hasToken: !!options.token, + status: 0, + options, + }, + type: ErrorType.UNEXPECTED, + exception: undefined, + loading: false, + message: `unsupported request body type: "${typeof requestBody}"`, + }; + throw new RequestError(error) } } @@ -88,7 +120,7 @@ export async function defaultRequestHandler( let response; try { - response = await fetch(_url.href, { + response = await fetch(validURL.href, { headers: requestHeaders, method: requestMethod, credentials: "omit", @@ -100,15 +132,29 @@ export async function defaultRequestHandler( } catch (ex) { const info: RequestInfo = { payload, - url: _url.href, + url: validURL.href, hasToken: !!options.token, status: 0, options, }; - const error: HttpRequestTimeoutError = { + + if (ex instanceof Error) { + if (ex.message === "HTTP_REQUEST_TIMEOUT") { + const error: HttpRequestTimeoutError = { + info, + type: ErrorType.TIMEOUT, + message: "request timeout", + }; + throw new RequestError(error); + } + } + + const error: HttpResponseUnexpectedError = { info, - type: ErrorType.TIMEOUT, - message: "Request timeout", + type: ErrorType.UNEXPECTED, + exception: ex, + loading: false, + message: (ex instanceof Error ? ex.message : ""), }; throw new RequestError(error); } @@ -124,7 +170,7 @@ export async function defaultRequestHandler( if (response.ok) { const result = await buildRequestOk( response, - _url.href, + validURL.href, payload, !!options.token, options, @@ -133,7 +179,7 @@ export async function defaultRequestHandler( } else { const dataTxt = await response.text(); const error = buildRequestFailed( - _url.href, + validURL.href, dataTxt, response.status, payload, @@ -377,3 +423,12 @@ export function buildRequestFailed( return error; } } + +function validateURL(baseUrl: string, endpoint: string): URL | undefined { + try { + return new URL(`${baseUrl}${endpoint}`) + } catch (ex) { + return undefined + } + +} \ No newline at end of file From 56f7e33daec4b3c6d1a76470c33e346f128e6361 Mon Sep 17 00:00:00 2001 From: Devan Carpenter Date: Mon, 11 Sep 2023 21:53:14 -0400 Subject: [PATCH 08/11] ci: cleanup codespell ignore word list --- ci/jobs/0-codespell/dictionary.txt | 33 ++++-------------------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/ci/jobs/0-codespell/dictionary.txt b/ci/jobs/0-codespell/dictionary.txt index 95f860504..aeace0e9e 100644 --- a/ci/jobs/0-codespell/dictionary.txt +++ b/ci/jobs/0-codespell/dictionary.txt @@ -5,41 +5,16 @@ # 'foo' and you add 'Foo' _here_, codespell will continue to complain # about 'Foo'. # -BRE -ND -Nd -TE -TEH -UPDATEing -WAN aci -acn -ba -bre cant -complet -doas ect -ehr fo -hel -ifset -ist -keypair -nd -onl -openin -ot -ser -sie som -sover te -te -teh -tha ths updateing -wan -wih vie +zar +nam +pares +kwanza From dceae7e6885832f42596bc477e7d0af28522ce53 Mon Sep 17 00:00:00 2001 From: Devan Carpenter Date: Mon, 11 Sep 2023 21:53:54 -0400 Subject: [PATCH 09/11] ci: make codespell output more verbose --- ci/jobs/0-codespell/job.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/jobs/0-codespell/job.sh b/ci/jobs/0-codespell/job.sh index d27d0888b..9271343e6 100755 --- a/ci/jobs/0-codespell/job.sh +++ b/ci/jobs/0-codespell/job.sh @@ -3,4 +3,4 @@ set -exuo pipefail job_dir=$(dirname "${BASH_SOURCE[0]}") -codespell -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex,*/vendor/**,*/node_modules/**,*.pnpm-store/**" +codespell -q 0 -I "${job_dir}"/dictionary.txt -S "*.bib,*.bst,*.cls,*.json,*.png,*.svg,*.wav,*.gz,*/templating/test?/**,**/auditor/*.sql,**/templating/mustach**,*.fees,*key,*.tag,*.info,*.latexmkrc,*.ecc,*.jpg,*.zkey,*.sqlite,*/contrib/hellos/**,*/vpn/tests/**,*.priv,*.file,*.tgz,*.woff,*.gif,*.odt,*.fee,*.deflate,*.dat,*.jpeg,*.eps,*.odg,*/m4/ax_lib_postgresql.m4,*/m4/libgcrypt.m4,*.rpath,config.status,ABOUT-NLS,*/doc/texinfo.tex,*.PNG,*.??.json,*.docx,*.ods,*.doc,*.docx,*.xcf,*.xlsx,*.ecc,*.ttf,*.woff2,*.eot,*.ttf,*.eot,*.mp4,*.pptx,*.epgz,*.min.js,**/*.map,**/fonts/**,*.pack.js,*.po,*.bbl,*/afl-tests/*,*/.git/**,*.pdf,*.epub,**/signing-key.asc,**/pnpm-lock.yaml,**/*.svg,**/*.cls,**/rfc.bib,**/*.bst,*/cbdc-es.tex,*/cbdc-it.tex,**/ExchangeSelection/example.ts,*/testcurl/test_tricky.c,*/i18n/strings.ts,*/src/anastasis-data.ts,**/doc/flows/main.de.tex,*/vendor/**,*/node_modules/**,*.pnpm-store/**" From a437605ebaf4267e60ae26f50ac107cdf11a8876 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 12 Sep 2023 08:52:14 +0200 Subject: [PATCH 10/11] taler-util: check status of merchant instances list response --- build-system/taler-build-scripts | 2 +- .../src/integrationtests/test-merchant-instances.ts | 1 - packages/taler-util/src/MerchantApiClient.ts | 3 ++- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build-system/taler-build-scripts b/build-system/taler-build-scripts index 001f5dd08..23538677f 160000 --- a/build-system/taler-build-scripts +++ b/build-system/taler-build-scripts @@ -1 +1 @@ -Subproject commit 001f5dd081fc8729ff8def90c4a1c3f93eb8689a +Subproject commit 23538677f6c6be2a62f38dc6137ecdd1c76b7b15 diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts index fd7a8ca3a..27de8a0a0 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts @@ -193,7 +193,6 @@ export async function runMerchantInstancesTest(t: GlobalTestState) { }); console.log(exc); t.assertTrue(exc.errorDetail.httpStatusCode === 401); - t.assertDeepEqual(exc.response?.status, 401); } } diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts index cbdcb9fdf..ccbbf79b3 100644 --- a/packages/taler-util/src/MerchantApiClient.ts +++ b/packages/taler-util/src/MerchantApiClient.ts @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see */ +import { codecForAny } from "./codec.js"; import { createPlatformHttpLib, expectSuccessResponseOrThrow, @@ -221,7 +222,7 @@ export class MerchantApiClient { const resp = await this.httpClient.fetch(url.href, { headers: this.makeAuthHeader(), }); - return resp.json(); + return readSuccessResponseJsonOrThrow(resp, codecForAny()); } async getInstanceFullDetails(instanceId: string): Promise { From ee8993f11cf81721cc30b4473e40124c2fee0dff Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 12 Sep 2023 12:24:42 +0200 Subject: [PATCH 11/11] wallet-core: use batch deposit API --- packages/taler-util/src/taler-types.ts | 116 ++++++++++-- packages/taler-wallet-core/src/db.ts | 2 + .../src/operations/deposits.ts | 178 ++++++++---------- 3 files changed, 181 insertions(+), 115 deletions(-) diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index a78df7452..eaba1ae3d 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -1889,42 +1889,58 @@ export interface ExchangeRefreshRevealRequest { old_age_commitment?: Edx25519PublicKeyEnc[]; } -export interface DepositSuccess { +interface DepositConfirmationSignature { + // The EdDSA signature of `TALER_DepositConfirmationPS` using a current + // `signing key of the exchange ` affirming the successful + // deposit and that the exchange will transfer the funds after the refund + // deadline, or as soon as possible if the refund deadline is zero. + exchange_sig: EddsaSignatureString; +} + +export interface BatchDepositSuccess { // Optional base URL of the exchange for looking up wire transfers // associated with this transaction. If not given, // the base URL is the same as the one used for this request. - // Can be used if the base URL for /transactions/ differs from that - // for /coins/, i.e. for load balancing. Clients SHOULD - // respect the transaction_base_url if provided. Any HTTP server + // Can be used if the base URL for ``/transactions/`` differs from that + // for ``/coins/``, i.e. for load balancing. Clients SHOULD + // respect the ``transaction_base_url`` if provided. Any HTTP server // belonging to an exchange MUST generate a 307 or 308 redirection // to the correct base URL should a client uses the wrong base // URL, or if the base URL has changed since the deposit. transaction_base_url?: string; - // timestamp when the deposit was received by the exchange. + // Timestamp when the deposit was received by the exchange. exchange_timestamp: TalerProtocolTimestamp; - // the EdDSA signature of TALER_DepositConfirmationPS using a current - // signing key of the exchange affirming the successful - // deposit and that the exchange will transfer the funds after the refund - // deadline, or as soon as possible if the refund deadline is zero. - exchange_sig: string; - - // public EdDSA key of the exchange that was used to + // `Public EdDSA key of the exchange ` that was used to // generate the signature. - // Should match one of the exchange's signing keys from /keys. It is given + // Should match one of the exchange's signing keys from ``/keys``. It is given // explicitly as the client might otherwise be confused by clock skew as to // which signing key was used. - exchange_pub: string; + exchange_pub: EddsaPublicKeyString; + + // Array of deposit confirmation signatures from the exchange + // Entries must be in the same order the coins were given + // in the batch deposit request. + exchange_sigs: DepositConfirmationSignature[]; } -export const codecForDepositSuccess = (): Codec => - buildCodecForObject() +export const codecForDepositConfirmationSignature = + (): Codec => + buildCodecForObject() + .property("exchange_sig", codecForString()) + .build("DepositConfirmationSignature"); + +export const codecForBatchDepositSuccess = (): Codec => + buildCodecForObject() .property("exchange_pub", codecForString()) - .property("exchange_sig", codecForString()) + .property( + "exchange_sigs", + codecForList(codecForDepositConfirmationSignature()), + ) .property("exchange_timestamp", codecForTimestamp) .property("transaction_base_url", codecOptional(codecForString())) - .build("DepositSuccess"); + .build("BatchDepositSuccess"); export interface TrackTransactionWired { // Raw wire transfer identifier of the deposit. @@ -2148,6 +2164,9 @@ export interface ExchangePurseDeposits { deposits: PurseDeposit[]; } +/** + * @deprecated batch deposit should be used. + */ export interface ExchangeDepositRequest { // Amount to be deposited, can be a fraction of the // coin's total value. @@ -2210,6 +2229,67 @@ export interface ExchangeDepositRequest { h_age_commitment?: string; } +export type WireSalt = string; + +export interface ExchangeBatchDepositRequest { + // The merchant's account details. + merchant_payto_uri: string; + + // The salt is used to hide the ``payto_uri`` from customers + // when computing the ``h_wire`` of the merchant. + wire_salt: WireSalt; + + // SHA-512 hash of the contract of the merchant with the customer. Further + // details are never disclosed to the exchange. + h_contract_terms: HashCodeString; + + // The list of coins that are going to be deposited with this Request. + coins: BatchDepositRequestCoin[]; + + // Timestamp when the contract was finalized. + timestamp: TalerProtocolTimestamp; + + // Indicative time by which the exchange undertakes to transfer the funds to + // the merchant, in case of successful payment. A wire transfer deadline of 'never' + // is not allowed. + wire_transfer_deadline: TalerProtocolTimestamp; + + // EdDSA `public key of the merchant `, so that the client can identify the + // merchant for refund requests. + merchant_pub: EddsaPublicKeyString; + + // Date until which the merchant can issue a refund to the customer via the + // exchange, to be omitted if refunds are not allowed. + // + // THIS FIELD WILL BE DEPRICATED, once the refund mechanism becomes a + // policy via extension. + refund_deadline?: TalerProtocolTimestamp; + + // CAVEAT: THIS IS WORK IN PROGRESS + // (Optional) policy for the batch-deposit. + // This might be a refund, auction or escrow policy. + policy?: any; +} + +export interface BatchDepositRequestCoin { + // EdDSA public key of the coin being deposited. + coin_pub: EddsaPublicKeyString; + + // Hash of denomination RSA key with which the coin is signed. + denom_pub_hash: HashCodeString; + + // Exchange's unblinded RSA signature of the coin. + ub_sig: UnblindedSignature; + + // Amount to be deposited, can be a fraction of the + // coin's total value. + contribution: Amounts; + + // Signature over `TALER_DepositRequestPS`, made by the customer with the + // `coin's private key `. + coin_sig: EddsaSignatureString; +} + export interface WalletKycUuid { // UUID that the wallet should use when initiating // the KYC check. diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index ba1f5b8c0..04c3ce723 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -1657,6 +1657,8 @@ export interface DepositGroupRecord { /** * Verbatim contract terms. + * + * FIXME: Move this to the contract terms object store! */ contractTermsRaw: MerchantContractTerms; diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 8ea792d91..a3483a332 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -21,70 +21,69 @@ import { AbsoluteTime, AmountJson, Amounts, + BatchDepositRequestCoin, CancellationToken, - canonicalJson, - codecForDepositSuccess, - codecForTackTransactionAccepted, - codecForTackTransactionWired, CoinRefreshRequest, CreateDepositGroupRequest, CreateDepositGroupResponse, DepositGroupFees, - durationFromSpec, - encodeCrock, - ExchangeDepositRequest, + Duration, + ExchangeBatchDepositRequest, ExchangeRefundRequest, - getRandomBytes, - hashTruncate32, - hashWire, HttpStatusCode, - j2s, Logger, MerchantContractTerms, NotificationType, - parsePaytoUri, PayCoinSelection, PrepareDepositRequest, PrepareDepositResponse, RefreshReason, - stringToBytes, + TalerError, TalerErrorCode, - TalerProtocolTimestamp, TalerPreciseTimestamp, + TalerProtocolTimestamp, TrackTransaction, + TransactionAction, TransactionMajorState, TransactionMinorState, TransactionState, TransactionType, URL, WireFee, - TransactionAction, - Duration, + canonicalJson, + codecForBatchDepositSuccess, + codecForTackTransactionAccepted, + codecForTackTransactionWired, + durationFromSpec, + encodeCrock, + getRandomBytes, + hashTruncate32, + hashWire, + j2s, + parsePaytoUri, + stringToBytes, } from "@gnu-taler/taler-util"; +import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { DepositElementStatus, DepositGroupRecord } from "../db.js"; import { - DenominationRecord, - DepositGroupRecord, - DepositElementStatus, -} from "../db.js"; -import { TalerError } from "@gnu-taler/taler-util"; -import { - createRefreshGroup, DepositOperationStatus, DepositTrackingInfo, - getTotalRefreshCost, KycPendingInfo, - KycUserType, PendingTaskType, RefreshOperationStatus, + createRefreshGroup, + getTotalRefreshCost, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; -import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { assertUnreachable } from "../util/assertUnreachable.js"; +import { selectPayCoinsNew } from "../util/coinSelection.js"; +import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { - constructTaskIdentifier, TaskRunResult, + TombstoneTag, + constructTaskIdentifier, runLongpollAsync, spendCoins, - TombstoneTag, } from "./common.js"; import { getExchangeDetails } from "./exchanges.js"; import { @@ -92,15 +91,12 @@ import { generateDepositPermissions, getTotalPaymentCost, } from "./pay-merchant.js"; -import { selectPayCoinsNew } from "../util/coinSelection.js"; import { constructTransactionIdentifier, notifyTransition, parseTransactionIdentifier, stopLongpolling, } from "./transactions.js"; -import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; -import { assertUnreachable } from "../util/assertUnreachable.js"; /** * Logger. @@ -169,6 +165,10 @@ export function computeDepositTransactionStatus( } } +/** + * Compute the possible actions possible on a deposit transaction + * based on the current transaction state. + */ export function computeDepositTransactionActions( dg: DepositGroupRecord, ): TransactionAction[] { @@ -200,6 +200,11 @@ export function computeDepositTransactionActions( } } +/** + * Put a deposit group in a suspended state. + * While the deposit group is suspended, no network requests + * will be made to advance the transaction status. + */ export async function suspendDepositGroup( ws: InternalWalletState, depositGroupId: string, @@ -406,46 +411,6 @@ export async function deleteDepositGroup( }); } -/** - * Check KYC status with the exchange, throw an appropriate exception when KYC - * is required. - * - * FIXME: Why does this throw an exception when KYC is required? - * Should we not return some proper result record here? - */ -async function checkDepositKycStatus( - ws: InternalWalletState, - exchangeUrl: string, - kycInfo: KycPendingInfo, - userType: KycUserType, -): Promise { - const url = new URL( - `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`, - exchangeUrl, - ); - logger.info(`kyc url ${url.href}`); - const kycStatusReq = await ws.http.fetch(url.href, { - method: "GET", - }); - if (kycStatusReq.status === HttpStatusCode.Ok) { - logger.warn("kyc requested, but already fulfilled"); - return; - } else if (kycStatusReq.status === HttpStatusCode.Accepted) { - const kycStatus = await kycStatusReq.json(); - logger.info(`kyc status: ${j2s(kycStatus)}`); - // FIXME: This error code is totally wrong - throw TalerError.fromDetail( - TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, - { - kycUrl: kycStatus.kyc_url, - }, - `KYC check required for deposit`, - ); - } else { - throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`); - } -} - /** * Check whether the refresh associated with the * aborting deposit group is done. @@ -940,38 +905,58 @@ async function processDepositGroupPendingDeposit( contractData, ); - for (let i = 0; i < depositPermissions.length; i++) { - const perm = depositPermissions[i]; + // Exchanges involved in the deposit + const exchanges: Set = new Set(); - if (depositGroup.statusPerCoin[i] !== DepositElementStatus.DepositPending) { - continue; - } + for (const dp of depositPermissions) { + exchanges.add(dp.exchange_url); + } - const requestBody: ExchangeDepositRequest = { - contribution: Amounts.stringify(perm.contribution), - merchant_payto_uri: depositGroup.wire.payto_uri, - wire_salt: depositGroup.wire.salt, + // We need to do one batch per exchange. + for (const exchangeUrl of exchanges.values()) { + const coins: BatchDepositRequestCoin[] = []; + const batchIndexes: number[] = []; + + const batchReq: ExchangeBatchDepositRequest = { + coins, h_contract_terms: depositGroup.contractTermsHash, - ub_sig: perm.ub_sig, + merchant_payto_uri: depositGroup.wire.payto_uri, + merchant_pub: depositGroup.contractTermsRaw.merchant_pub, timestamp: depositGroup.contractTermsRaw.timestamp, + wire_salt: depositGroup.wire.salt, wire_transfer_deadline: depositGroup.contractTermsRaw.wire_transfer_deadline, refund_deadline: depositGroup.contractTermsRaw.refund_deadline, - coin_sig: perm.coin_sig, - denom_pub_hash: perm.h_denom, - merchant_pub: depositGroup.merchantPub, - h_age_commitment: perm.h_age_commitment, }; + + for (let i = 0; i < depositPermissions.length; i++) { + const perm = depositPermissions[i]; + if (perm.exchange_url != exchangeUrl) { + continue; + } + coins.push({ + coin_pub: perm.coin_pub, + coin_sig: perm.coin_sig, + contribution: Amounts.stringify(perm.contribution), + denom_pub_hash: perm.h_denom, + ub_sig: perm.ub_sig, + }); + batchIndexes.push(i); + } + // Check for cancellation before making network request. cancellationToken?.throwIfCancelled(); - const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url); + const url = new URL(`batch-deposit`, exchangeUrl); logger.info(`depositing to ${url}`); const httpResp = await ws.http.fetch(url.href, { method: "POST", - body: requestBody, + body: batchReq, cancellationToken: cancellationToken, }); - await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); + await readSuccessResponseJsonOrThrow( + httpResp, + codecForBatchDepositSuccess(), + ); await ws.db .mktx((x) => [x.depositGroups]) @@ -980,11 +965,13 @@ async function processDepositGroupPendingDeposit( if (!dg) { return; } - const coinStatus = dg.statusPerCoin[i]; - switch (coinStatus) { - case DepositElementStatus.DepositPending: - dg.statusPerCoin[i] = DepositElementStatus.Tracking; - await tx.depositGroups.put(dg); + for (const batchIndex of batchIndexes) { + const coinStatus = dg.statusPerCoin[batchIndex]; + switch (coinStatus) { + case DepositElementStatus.DepositPending: + dg.statusPerCoin[batchIndex] = DepositElementStatus.Tracking; + await tx.depositGroups.put(dg); + } } }); } @@ -1538,10 +1525,7 @@ async function getTotalFeesForDepositAmount( const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl .iter(coin.exchangeBaseUrl) .filter((x) => - Amounts.isSameCurrency( - x.value, - pcs.coinContributions[i], - ), + Amounts.isSameCurrency(x.value, pcs.coinContributions[i]), ); const amountLeft = Amounts.sub( denom.value,