Merge branch 'master' into age-withdraw

This commit is contained in:
Özgür Kesim 2023-09-12 13:52:55 +02:00
commit 121a6da785
Signed by: oec
GPG Key ID: 3D76A56D79EDD9D7
58 changed files with 1062 additions and 863 deletions

@ -1 +1 @@
Subproject commit 001f5dd081fc8729ff8def90c4a1c3f93eb8689a
Subproject commit 23538677f6c6be2a62f38dc6137ecdd1c76b7b15

29
ci/ci.sh Executable file
View File

@ -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

View File

@ -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

View File

@ -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 -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/**"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -241,7 +241,7 @@ export function AuthenticationEditorScreen(): VNode {
</p>
{authAvailableSet.size > 0 && (
<p class="block">
We couldn&apos;t find provider for some of the authentication
We couldn't find provider for some of the authentication
methods.
</p>
)}

View File

@ -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 --filter @gnu-taler/demobank-ui... compile
pnpm run check
pnpm run build

View File

@ -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,

View File

@ -21,7 +21,7 @@ export interface FakeDOMStringList extends Array<string> {
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();

View File

@ -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 (
<Fragment>
<NotConnectedAppMenu title="Welcome!" />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@ -91,7 +87,7 @@ function ApplicationStatusRoutes(): VNode {
return (
<Fragment>
<NotConnectedAppMenu title="Login" />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@ -109,7 +105,7 @@ function ApplicationStatusRoutes(): VNode {
description: `Check your url`,
}}
/>
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@ -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}`,
}}
/>
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>;
}
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}`,
}}
/>
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>;
}
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}`,
}}
/>
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@ -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}`,
}}
/>
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>
}

View File

@ -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 <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
<Settings />
<Settings onClose={() => setShowSettings(false)} />
</Fragment>
}
@ -100,7 +100,7 @@ export function ApplicationReadyRoutes(): VNode {
type: "ERROR",
}}
/>
<LoginPage onConfirm={updateLoginStatus} />
<ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@ -108,14 +108,13 @@ export function ApplicationReadyRoutes(): VNode {
instanceNameByBackendURL = match[1];
}
console.log(unauthorized, unauthorizedAdmin)
if (unauthorized || unauthorizedAdmin) {
return <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
<NotificationCard
notification={{
message: i18n.str`Access denied`,
description: i18n.str`Check your token is valid`,
description: i18n.str`Check your token is valid 1`,
type: "ERROR",
}}
/>
@ -132,7 +131,6 @@ export function ApplicationReadyRoutes(): VNode {
admin={admin}
onUnauthorized={() => setUnauthorized(true)}
onLoginPass={() => {
console.log("ahora si")
setUnauthorized(false)
}}
instanceNameByBackendURL={instanceNameByBackendURL}

View File

@ -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<GlobalNotifState>(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 <NotificationCard
return <Fragment>
<NotificationCard
notification={{
message: i18n.str`Access denied`,
description: i18n.str`Redirecting to login page.`,
type: "ERROR",
}}
/>
<LoginPage onConfirm={changeToken} />
</Fragment>
}
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
@ -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 <Fragment />;

View File

@ -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 <http://www.gnu.org/licenses/>
*/
/**
*
* @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 (<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header
class="modal-card-head"
style={{ border: "1px solid", borderBottom: 0 }}
>
<p class="modal-card-title">{i18n.str`Login required`}</p>
</header>
<section
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
<p>
<i18n.Translate>Need the access token for the instance.</i18n.Translate>
</p>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
<i18n.Translate>Access Token</i18n.Translate>
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="password"
placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
? onConfirm(url, normalizeToken(token))
: null
}
value={token}
onInput={(e): void => setToken(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
</section>
<footer
class="modal-card-foot "
style={{
justifyContent: "flex-end",
border: "1px solid",
borderTop: 0,
}}
>
<AsyncButton
onClick={async () => {
const secretToken = normalizeToken(token);
const { valid, cause } = await testLogin(`${url}/instances/${id}`, secretToken);
if (valid) {
onConfirm(url, secretToken);
} else {
onConfirm(url);
}
}}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</footer>
</div>
</div>
</div>)
}
return (
<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header
class="modal-card-head"
style={{ border: "1px solid", borderBottom: 0 }}
>
<p class="modal-card-title">{i18n.str`Login required`}</p>
</header>
<section
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
<i18n.Translate>Please enter your access token.</i18n.Translate>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">URL</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="text"
placeholder="set new url"
name="id"
value={url}
onKeyPress={(e) =>
e.keyCode === 13
? onConfirm(url, normalizeToken(token))
: null
}
onInput={(e): void => setURL(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
<i18n.Translate>Access Token</i18n.Translate>
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="password"
placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
? onConfirm(url, normalizeToken(token))
: null
}
value={token}
onInput={(e): void => setToken(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
</section>
<footer
class="modal-card-foot "
style={{
justifyContent: "flex-end",
border: "1px solid",
borderTop: 0,
}}
>
<AsyncButton
onClick={async () => {
const secretToken = normalizeToken(token);
const { valid, cause } = await testLogin(url, secretToken);
if (valid) {
onConfirm(url, secretToken);
} else {
onConfirm(url);
}
}}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</footer>
</div>
</div>
</div>
);
}
function AsyncButton({ onClick, children }: { onClick: () => Promise<void>, children: ComponentChildren }): VNode {
const [running, setRunning] = useState(false)
return <button class="button is-info" disabled={running} onClick={() => {
setRunning(true)
onClick().then(() => {
setRunning(false)
}).catch(() => {
setRunning(false)
})
}}>
{children}
</button>
}

View File

@ -40,13 +40,13 @@ export function DefaultInstanceFormFields({
showId: boolean;
}): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
const { url: backendURL } = useBackendContext()
return (
<Fragment>
{showId && (
<InputWithAddon<Entity>
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.`}

View File

@ -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({
<i class="mdi mdi-web" />
</span>
<span class="menu-item-label">
{new URL(backend.url).hostname}
{new URL(backendURL).hostname}
</span>
</div>
</li>

View File

@ -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 : (
<InputWithAddon<Entity>
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)`}
/>

View File

@ -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({

View File

@ -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<BackendContextType>({
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 });
};

View File

@ -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<Type>({} as any);

View File

@ -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 {

View File

@ -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<MerchantBackend.ErrorDetail>
@ -92,18 +97,33 @@ export function useBackendConfig(): HttpResponse<
const { request } = useBackendBaseRequest();
type Type = MerchantBackend.VersionResponse;
const [result, setResult] = useState<
HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>
>({ loading: true });
type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number }
const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 });
useEffect(() => {
if (result.timer) {
clearTimeout(result.timer)
}
function tryConfig(): void {
request<Type>(`/config`)
.then((data) => setResult(data))
.catch((error) => setResult(error));
.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<LoginResult> {
const data: MerchantBackend.Instances.LoginTokenRequest = {
scope: "write",
duration: {
d_us: "forever"
},
refreshable: true,
}
try {
const response = await request(instance, `/private/`, {
const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(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<LoginResult> {
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<T>(
endpoint: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
return requestHandler<T>(backend, endpoint, { token, ...options });
return requestHandler<T>(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<T>(
endpoint: string,

View File

@ -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<LoginToken>()
.property("token", codecForString())
.property("expiration", codecForTimestamp)
.build("loginToken")
const TOKENS_KEY = buildStorageKey("backend-token", codecForMap(loginTokenCodec));
export function useBackendURL(
url?: string,
): [string, boolean, StateUpdater<string>, () => void] {
const [value, setter] = useNotNullLocalStorage(
): [string, StateUpdater<string>] {
const [value, setter] = useSimpleLocalStorage(
"backend-url",
url || calculateRootPath(),
);
const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
const checkedSetter = (v: ValueOrFunction<string>) => {
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<string>] {
@ -104,10 +109,10 @@ export function useLang(initial?: string): [string, StateUpdater<string>] {
? 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<string>];
}
export function useLocalStorage(
export function useSimpleLocalStorage(
key: string,
initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
@ -137,28 +142,3 @@ export function useLocalStorage(
return [storedValue, setValue];
}
export function useNotNullLocalStorage(
key: string,
initialValue: string,
): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): 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];
}

View File

@ -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({

View File

@ -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<void>;
deleteInstance: () => Promise<void>;
clearToken: () => Promise<void>;
setNewToken: (token: string) => Promise<void>;
setNewToken: (token: AccessToken) => Promise<void>;
}
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<void> => {
const setNewToken = async (newToken: AccessToken): Promise<void> => {
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<void> => {
const setNewToken = async (newToken: AccessToken): Promise<void> => {
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);
};

View File

@ -90,10 +90,7 @@ export class ApiMockEnvironment extends MockEnvironment {
const SC: any = SWRConfig;
return (
<BackendContextProvider
defaultUrl="http://backend"
initialToken={undefined}
>
<BackendContextProvider defaultUrl="http://backend">
<InstanceContextProvider
value={{
token: undefined,

View File

@ -24,15 +24,6 @@ import {
codecForString,
} from "@gnu-taler/taler-util";
function parse_json_or_undefined<T>(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";

View File

@ -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<Partial<Paid>>(order);
const { url } = useBackendContext();
const { url: backendURL } = useBackendContext()
const refundurl = stringifyRefundUri({
merchantBaseUrl: url,
merchantBaseUrl: backendURL,
orderId: order.contract_terms.order_id
})
const refundable =

View File

@ -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 <http://www.gnu.org/licenses/>
*/
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 (
<Fragment>
<div class="field is-horizontal">

View File

@ -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<Partial<Entity>>({
@ -128,7 +124,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
>
<InputWithAddon<Entity>
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.`}
/>

View File

@ -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 (

View File

@ -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<Partial<Entity>>(template);
@ -115,7 +105,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
{backend.url}/templates/{template.id}
{backendURL}/templates/{template.id}
</span>
</div>
</div>

View File

@ -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 (

View File

@ -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 (
<Fragment>
<NotificationCard notification={notif} />
<DetailPage
instanceId={id}
currentToken={hasPrefix ? currentToken.substring(PREFIX.length) : currentToken}
currentToken={hasPrefix ? currentToken.token.substring(PREFIX.length) : currentToken?.token}
onClearToken={async (): Promise<void> => {
try {
await clearToken();
@ -72,7 +72,7 @@ export default function Token({
}}
onNewToken={async (newToken): Promise<void> => {
try {
await setNewToken(`secret-token:${newToken}`);
await setNewToken(newToken);
onChange();
} catch (error) {
if (error instanceof Error) {

View File

@ -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 <http://www.gnu.org/licenses/>
*/
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<void>,
setNewToken: (t: AccessToken) => Promise<void>,
): VNode {
const { changeToken } = useInstanceContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@ -119,11 +118,8 @@ function CommonUpdate(
d: MerchantBackend.Instances.InstanceAuthConfigurationMessage,
): Promise<void> => {
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);
}}
/>
</Fragment>

View File

@ -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)}...`;

View File

@ -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 <LoginModal onConfirm={onConfirm} />;
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 (<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header
class="modal-card-head"
style={{ border: "1px solid", borderBottom: 0 }}
>
<p class="modal-card-title">{i18n.str`Login required`}</p>
</header>
<section
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
<p>
<i18n.Translate>Need the access token for the instance.</i18n.Translate>
</p>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
<i18n.Translate>Access Token</i18n.Translate>
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="password"
placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
? doLogin()
: null
}
value={token}
onInput={(e): void => setToken(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
</section>
<footer
class="modal-card-foot "
style={{
justifyContent: "flex-end",
border: "1px solid",
borderTop: 0,
}}
>
<AsyncButton
onClick={doLogin}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</footer>
</div>
</div>
</div>)
}
return (
<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header
class="modal-card-head"
style={{ border: "1px solid", borderBottom: 0 }}
>
<p class="modal-card-title">{i18n.str`Login required`}</p>
</header>
<section
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
<i18n.Translate>Please enter your access token.</i18n.Translate>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">URL</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="text"
placeholder="set new url"
name="id"
value={backendURL}
disabled
readOnly
/>
</p>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
<i18n.Translate>Access Token</i18n.Translate>
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="password"
placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
? doLogin()
: null
}
value={token}
onInput={(e): void => setToken(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
</section>
<footer
class="modal-card-foot "
style={{
justifyContent: "space-between",
border: "1px solid",
borderTop: 0,
}}
>
<AsyncButton
onClick={changeServer}
>
<i18n.Translate>Change server</i18n.Translate>
</AsyncButton>
<AsyncButton
type="is-info"
onClick={doLogin}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</footer>
</div>
</div>
</div>
);
}
function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise<void>, children: ComponentChildren }): VNode {
const [running, setRunning] = useState(false)
return <button class={"button " + type} disabled={disabled || running} onClick={() => {
setRunning(true)
onClick().then(() => {
setRunning(false)
}).catch(() => {
setRunning(false)
})
}}>
{children}
</button>
}
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 (
<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header
class="modal-card-head"
style={{ border: "1px solid", borderBottom: 0 }}
>
<p class="modal-card-title">{i18n.str`Connect to backend`}</p>
</header>
<section
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
<i18n.Translate>Location of the backend server</i18n.Translate>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">URL</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="text"
placeholder="set new url"
name="id"
value={url ?? ""}
onKeyPress={(e) =>
e.keyCode === 13
? doConnect()
: null
}
onInput={(e): void => setURL(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
</section>
<footer
class="modal-card-foot "
style={{
justifyContent: "flex-end",
border: "1px solid",
borderTop: 0,
}}
>
<AsyncButton
disabled={backendURL === url}
onClick={doConnect}
>
<i18n.Translate>Try again</i18n.Translate>
</AsyncButton>
</footer>
</div>
</div>
</div>
);
}

View File

@ -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 {
/>
</FormProvider>
</div>
</div>
<div class="column" />
</div>
</section >
{onClose &&
<section class="section is-main-section">
<button
class="button"
onClick={onClose}
>
<i18n.Translate>Close</i18n.Translate>
</button>
</section>
}
</div >
}

View File

@ -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(
@ -1883,10 +1882,8 @@ 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
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 ---");

View File

@ -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;

View File

@ -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);
}
}

View File

@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
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<any> {

View File

@ -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`,
);
}

View File

@ -1972,42 +1972,58 @@ export interface ExchangeAgeWithdrawRevealResponse {
ev_sigs : BlindedDenominationSignature[];
}
export interface DepositSuccess {
interface DepositConfirmationSignature {
// The EdDSA signature of `TALER_DepositConfirmationPS` using a current
// `signing key of the exchange <sign-key-priv>` 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 <sign-key-pub>` 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<DepositSuccess> =>
buildCodecForObject<DepositSuccess>()
.property("exchange_pub", codecForString())
export const codecForDepositConfirmationSignature =
(): Codec<DepositConfirmationSignature> =>
buildCodecForObject<DepositConfirmationSignature>()
.property("exchange_sig", codecForString())
.build("DepositConfirmationSignature");
export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
buildCodecForObject<BatchDepositSuccess>()
.property("exchange_pub", 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.
@ -2231,6 +2247,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.
@ -2293,6 +2312,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 <merchant-pub>`, 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-priv>`.
coin_sig: EddsaSignatureString;
}
export interface WalletKycUuid {
// UUID that the wallet should use when initiating
// the KYC check.

View File

@ -57,7 +57,9 @@ import {
DenomKeyType,
DenominationPubKey,
ExchangeAuditor,
InternationalizedString,
MerchantContractTerms,
MerchantInfo,
PeerContractTerms,
UnblindedSignature,
codecForMerchantContractTerms,
@ -2667,3 +2669,49 @@ export const codecForTestingSetTimetravelRequest =
buildCodecForObject<TestingSetTimetravelRequest>()
.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;
}

View File

@ -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 run --filter @gnu-taler/taler-wallet-cli... compile
install:
$(MAKE) deps
$(MAKE) install-nodeps

View File

@ -1042,52 +1042,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.
@ -1710,6 +1664,8 @@ export interface DepositGroupRecord {
/**
* Verbatim contract terms.
*
* FIXME: Move this to the contract terms object store!
*/
contractTermsRaw: MerchantContractTerms;

View File

@ -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.

View File

@ -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<void> {
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<string> = 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,12 +965,14 @@ async function processDepositGroupPendingDeposit(
if (!dg) {
return;
}
const coinStatus = dg.statusPerCoin[i];
for (const batchIndex of batchIndexes) {
const coinStatus = dg.statusPerCoin[batchIndex];
switch (coinStatus) {
case DepositElementStatus.DepositPending:
dg.statusPerCoin[i] = DepositElementStatus.Tracking;
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,

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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.
*/

View File

@ -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",

View File

@ -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 <http://www.gnu.org/licenses/>
*/
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";

View File

@ -25,6 +25,8 @@ export enum ErrorType {
UNEXPECTED,
}
/**
*
* @param baseUrl URL where the service is located
@ -60,10 +62,27 @@ export async function defaultRequestHandler<T>(
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<T>(
} 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<T>(
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<T>(
} catch (ex) {
const info: RequestInfo = {
payload,
url: _url.href,
url: validURL.href,
hasToken: !!options.token,
status: 0,
options,
};
if (ex instanceof Error) {
if (ex.message === "HTTP_REQUEST_TIMEOUT") {
const error: HttpRequestTimeoutError = {
info,
type: ErrorType.TIMEOUT,
message: "Request timeout",
message: "request timeout",
};
throw new RequestError(error);
}
}
const error: HttpResponseUnexpectedError = {
info,
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<T>(
if (response.ok) {
const result = await buildRequestOk<T>(
response,
_url.href,
validURL.href,
payload,
!!options.token,
options,
@ -133,7 +179,7 @@ export async function defaultRequestHandler<T>(
} else {
const dataTxt = await response.text();
const error = buildRequestFailed(
_url.href,
validURL.href,
dataTxt,
response.status,
payload,
@ -377,3 +423,12 @@ export function buildRequestFailed<ErrorDetail>(
return error;
}
}
function validateURL(baseUrl: string, endpoint: string): URL | undefined {
try {
return new URL(`${baseUrl}${endpoint}`)
} catch (ex) {
return undefined
}
}