Merge branch 'master' into age-withdraw
This commit is contained in:
commit
121a6da785
@ -1 +1 @@
|
|||||||
Subproject commit 001f5dd081fc8729ff8def90c4a1c3f93eb8689a
|
Subproject commit 23538677f6c6be2a62f38dc6137ecdd1c76b7b15
|
29
ci/ci.sh
Executable file
29
ci/ci.sh
Executable 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
|
@ -5,41 +5,16 @@
|
|||||||
# 'foo' and you add 'Foo' _here_, codespell will continue to complain
|
# 'foo' and you add 'Foo' _here_, codespell will continue to complain
|
||||||
# about 'Foo'.
|
# about 'Foo'.
|
||||||
#
|
#
|
||||||
BRE
|
|
||||||
ND
|
|
||||||
Nd
|
|
||||||
TE
|
|
||||||
TEH
|
|
||||||
UPDATEing
|
|
||||||
WAN
|
|
||||||
aci
|
aci
|
||||||
acn
|
|
||||||
ba
|
|
||||||
bre
|
|
||||||
cant
|
cant
|
||||||
complet
|
|
||||||
doas
|
|
||||||
ect
|
ect
|
||||||
ehr
|
|
||||||
fo
|
fo
|
||||||
hel
|
|
||||||
ifset
|
|
||||||
ist
|
|
||||||
keypair
|
|
||||||
nd
|
|
||||||
onl
|
|
||||||
openin
|
|
||||||
ot
|
|
||||||
ser
|
|
||||||
sie
|
|
||||||
som
|
som
|
||||||
sover
|
|
||||||
te
|
te
|
||||||
te
|
|
||||||
teh
|
|
||||||
tha
|
|
||||||
ths
|
ths
|
||||||
updateing
|
updateing
|
||||||
wan
|
|
||||||
wih
|
|
||||||
vie
|
vie
|
||||||
|
zar
|
||||||
|
nam
|
||||||
|
pares
|
||||||
|
kwanza
|
||||||
|
@ -3,4 +3,4 @@ set -exuo pipefail
|
|||||||
|
|
||||||
job_dir=$(dirname "${BASH_SOURCE[0]}")
|
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/**"
|
||||||
|
@ -45,7 +45,7 @@ If the above parameters have an optimal assignment, then replacing
|
|||||||
v'[x] := 0
|
v'[x] := 0
|
||||||
gives another optimal solution, as otherwise we'd get a better one for the first situation.
|
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
|
http://www.codeproject.com/Articles/31002/Coin-Change-Problem-Using-Dynamic-Programming
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
error reports about a failed payment which the wallet has to handle.
|
||||||
By submitting the payment this way, we also ensure that this
|
By submitting the payment this way, we also ensure that this
|
||||||
intermediate request does not require JavaScript and still does not
|
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.
|
wallet causes the fulfillment URL to be reloaded.
|
||||||
|
|
||||||
If the contract hash does not match a payment which the user
|
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
|
other users has the expected behavior
|
||||||
of asking the other user to pay for the resource.
|
of asking the other user to pay for the resource.
|
||||||
\item Asynchronously transmitting coins from injected JavaScript costs
|
\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.
|
proper error handling.
|
||||||
\item The different pages of the merchant have clear
|
\item The different pages of the merchant have clear
|
||||||
delineations: the shopping pages conclude by making an offer, and
|
delineations: the shopping pages conclude by making an offer, and
|
||||||
|
@ -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.
|
error reports about a failed payment which the wallet has to handle.
|
||||||
By submitting the payment this way, we also ensure that this
|
By submitting the payment this way, we also ensure that this
|
||||||
intermediate request does not require JavaScript and still does not
|
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.
|
wallet causes the fulfillment URL to be reloaded.
|
||||||
|
|
||||||
If the contract hash does not match a payment which the user
|
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
|
other users has the expected behavior
|
||||||
of asking the other user to pay for the resource.
|
of asking the other user to pay for the resource.
|
||||||
\item Asynchronously transmitting coins from injected JavaScript costs
|
\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.
|
proper error handling.
|
||||||
\item The different pages of the merchant have clear
|
\item The different pages of the merchant have clear
|
||||||
delineations: the shopping pages conclude by making an offer, and
|
delineations: the shopping pages conclude by making an offer, and
|
||||||
|
@ -241,7 +241,7 @@ export function AuthenticationEditorScreen(): VNode {
|
|||||||
</p>
|
</p>
|
||||||
{authAvailableSet.size > 0 && (
|
{authAvailableSet.size > 0 && (
|
||||||
<p class="block">
|
<p class="block">
|
||||||
We couldn't find provider for some of the authentication
|
We couldn't find provider for some of the authentication
|
||||||
methods.
|
methods.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -20,6 +20,7 @@ spa_dir=$(prefix)/share/taler/demobank-ui
|
|||||||
.PHONY: deps
|
.PHONY: deps
|
||||||
deps:
|
deps:
|
||||||
pnpm install --frozen-lockfile --filter @gnu-taler/demobank-ui...
|
pnpm install --frozen-lockfile --filter @gnu-taler/demobank-ui...
|
||||||
|
pnpm run --filter @gnu-taler/demobank-ui... compile
|
||||||
pnpm run check
|
pnpm run check
|
||||||
pnpm run build
|
pnpm run build
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ test("WPT idbcursor-reused.htm", async (t) => {
|
|||||||
case 0:
|
case 0:
|
||||||
cursor = e.target.result;
|
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;
|
cursor.custom_cursor_value = 1;
|
||||||
e.target.custom_request_value = 2;
|
e.target.custom_request_value = 2;
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ test("WPT idbcursor-reused.htm", async (t) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 1:
|
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(cursor.custom_cursor_value, 1, "custom cursor value");
|
||||||
t.deepEqual(
|
t.deepEqual(
|
||||||
e.target.custom_request_value,
|
e.target.custom_request_value,
|
||||||
|
@ -21,7 +21,7 @@ export interface FakeDOMStringList extends Array<string> {
|
|||||||
item: (i: number) => string | null;
|
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 => {
|
export const fakeDOMStringList = (arr: string[]): FakeDOMStringList => {
|
||||||
const arr2 = arr.slice();
|
const arr2 = arr.slice();
|
||||||
|
@ -41,7 +41,8 @@ import {
|
|||||||
import { ConfigContextProvider } from "./context/config.js";
|
import { ConfigContextProvider } from "./context/config.js";
|
||||||
import { useBackendConfig } from "./hooks/backend.js";
|
import { useBackendConfig } from "./hooks/backend.js";
|
||||||
import { strings } from "./i18n/strings.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 {
|
export function Application(): VNode {
|
||||||
return (
|
return (
|
||||||
@ -59,25 +60,20 @@ export function Application(): VNode {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function ApplicationStatusRoutes(): VNode {
|
function ApplicationStatusRoutes(): VNode {
|
||||||
const { url, updateLoginStatus, triedToLog } = useBackendContext();
|
const { url: backendURL, updateToken, changeBackend } = useBackendContext();
|
||||||
const result = useBackendConfig();
|
const result = useBackendConfig();
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
const updateLoginInfoAndGoToRoot = (url: string, token?: string) => {
|
|
||||||
updateLoginStatus(url, token);
|
|
||||||
route("/");
|
|
||||||
};
|
|
||||||
|
|
||||||
const { currency, version } = result.ok
|
const { currency, version } = result.ok
|
||||||
? result.data
|
? result.data
|
||||||
: { currency: "unknown", version: "unknown" };
|
: { currency: "unknown", version: "unknown" };
|
||||||
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
|
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
|
||||||
|
|
||||||
if (!triedToLog) {
|
if (!backendURL) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotConnectedAppMenu title="Welcome!" />
|
<NotConnectedAppMenu title="Welcome!" />
|
||||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
<ConnectionPage onConfirm={changeBackend} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -91,7 +87,7 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotConnectedAppMenu title="Login" />
|
<NotConnectedAppMenu title="Login" />
|
||||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
<ConnectionPage onConfirm={changeBackend} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,7 +105,7 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
description: `Check your url`,
|
description: `Check your url`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
<ConnectionPage onConfirm={changeBackend} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -120,10 +116,10 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Server response with an error code`,
|
message: i18n.str`Server response with an error code`,
|
||||||
type: "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>;
|
</Fragment>;
|
||||||
}
|
}
|
||||||
if (result.type === ErrorType.UNREADABLE) {
|
if (result.type === ErrorType.UNREADABLE) {
|
||||||
@ -133,10 +129,10 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
|
message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
|
||||||
type: "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>;
|
</Fragment>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@ -146,10 +142,10 @@ function ApplicationStatusRoutes(): VNode {
|
|||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Unexpected Error`,
|
message: i18n.str`Unexpected Error`,
|
||||||
type: "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>
|
</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}`,
|
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>
|
</Fragment>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,22 +18,23 @@
|
|||||||
*
|
*
|
||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { createHashHistory } from "history";
|
import { createHashHistory } from "history";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { Router, Route, route } from "preact-router";
|
import { Route, Router, route } from "preact-router";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
import { InstanceRoutes } from "./InstanceRoutes.js";
|
||||||
import {
|
import {
|
||||||
NotificationCard,
|
|
||||||
NotYetReadyAppMenu,
|
NotYetReadyAppMenu,
|
||||||
|
NotificationCard,
|
||||||
} from "./components/menu/index.js";
|
} from "./components/menu/index.js";
|
||||||
import { useBackendContext } from "./context/backend.js";
|
import { useBackendContext } from "./context/backend.js";
|
||||||
|
import { LoginToken } from "./declaration.js";
|
||||||
import { useBackendInstancesTestForAdmin } from "./hooks/backend.js";
|
import { useBackendInstancesTestForAdmin } from "./hooks/backend.js";
|
||||||
import { InstanceRoutes } from "./InstanceRoutes.js";
|
import { ConnectionPage, LoginPage } from "./paths/login/index.js";
|
||||||
import LoginPage from "./paths/login/index.js";
|
|
||||||
import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
|
|
||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
|
||||||
import { Settings } from "./paths/settings/index.js";
|
import { Settings } from "./paths/settings/index.js";
|
||||||
|
import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if admin against /management/instances
|
* Check if admin against /management/instances
|
||||||
@ -41,15 +42,14 @@ import { Settings } from "./paths/settings/index.js";
|
|||||||
*/
|
*/
|
||||||
export function ApplicationReadyRoutes(): VNode {
|
export function ApplicationReadyRoutes(): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
const { url: backendURL, changeBackend } = useBackendContext()
|
||||||
const [unauthorized, setUnauthorized] = useState(false)
|
const [unauthorized, setUnauthorized] = useState(false)
|
||||||
const {
|
const {
|
||||||
url: backendURL,
|
updateToken,
|
||||||
updateLoginStatus: updateLoginStatus2,
|
|
||||||
} = useBackendContext();
|
} = useBackendContext();
|
||||||
|
|
||||||
function updateLoginStatus(url: string, token: string | undefined) {
|
function updateLoginStatus(token: LoginToken | undefined) {
|
||||||
console.log("updateing", url, token)
|
updateToken(token)
|
||||||
updateLoginStatus2(url, token)
|
|
||||||
setUnauthorized(false)
|
setUnauthorized(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,15 +59,15 @@ export function ApplicationReadyRoutes(): VNode {
|
|||||||
route("/");
|
route("/");
|
||||||
};
|
};
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
// useEffect(() => {
|
const unauthorizedAdmin = !result.loading
|
||||||
// setUnauthorized(FF)
|
&& !result.ok
|
||||||
// }, [FF])
|
&& result.type === ErrorType.CLIENT
|
||||||
const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized
|
&& result.status === HttpStatusCode.Unauthorized;
|
||||||
|
|
||||||
if (showSettings) {
|
if (showSettings) {
|
||||||
return <Fragment>
|
return <Fragment>
|
||||||
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
||||||
<Settings />
|
<Settings onClose={() => setShowSettings(false)} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ export function ApplicationReadyRoutes(): VNode {
|
|||||||
type: "ERROR",
|
type: "ERROR",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<LoginPage onConfirm={updateLoginStatus} />
|
<ConnectionPage onConfirm={changeBackend} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -108,14 +108,13 @@ export function ApplicationReadyRoutes(): VNode {
|
|||||||
instanceNameByBackendURL = match[1];
|
instanceNameByBackendURL = match[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(unauthorized, unauthorizedAdmin)
|
|
||||||
if (unauthorized || unauthorizedAdmin) {
|
if (unauthorized || unauthorizedAdmin) {
|
||||||
return <Fragment>
|
return <Fragment>
|
||||||
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
||||||
<NotificationCard
|
<NotificationCard
|
||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Access denied`,
|
message: i18n.str`Access denied`,
|
||||||
description: i18n.str`Check your token is valid`,
|
description: i18n.str`Check your token is valid 1`,
|
||||||
type: "ERROR",
|
type: "ERROR",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -132,7 +131,6 @@ export function ApplicationReadyRoutes(): VNode {
|
|||||||
admin={admin}
|
admin={admin}
|
||||||
onUnauthorized={() => setUnauthorized(true)}
|
onUnauthorized={() => setUnauthorized(true)}
|
||||||
onLoginPass={() => {
|
onLoginPass={() => {
|
||||||
console.log("ahora si")
|
|
||||||
setUnauthorized(false)
|
setUnauthorized(false)
|
||||||
}}
|
}}
|
||||||
instanceNameByBackendURL={instanceNameByBackendURL}
|
instanceNameByBackendURL={instanceNameByBackendURL}
|
||||||
|
@ -35,7 +35,7 @@ import { InstanceContextProvider } from "./context/instance.js";
|
|||||||
import {
|
import {
|
||||||
useBackendDefaultToken,
|
useBackendDefaultToken,
|
||||||
useBackendInstanceToken,
|
useBackendInstanceToken,
|
||||||
useLocalStorage,
|
useSimpleLocalStorage,
|
||||||
} from "./hooks/index.js";
|
} from "./hooks/index.js";
|
||||||
import { useInstanceKYCDetails } from "./hooks/instance.js";
|
import { useInstanceKYCDetails } from "./hooks/instance.js";
|
||||||
import InstanceCreatePage from "./paths/admin/create/index.js";
|
import InstanceCreatePage from "./paths/admin/create/index.js";
|
||||||
@ -71,10 +71,10 @@ import InstanceUpdatePage, {
|
|||||||
AdminUpdate as InstanceAdminUpdatePage,
|
AdminUpdate as InstanceAdminUpdatePage,
|
||||||
Props as InstanceUpdatePageProps,
|
Props as InstanceUpdatePageProps,
|
||||||
} from "./paths/instance/update/index.js";
|
} 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 NotFoundPage from "./paths/notfound/index.js";
|
||||||
import { Notification } from "./utils/types.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 { Settings } from "./paths/settings/index.js";
|
||||||
import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
|
import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ export function InstanceRoutes({
|
|||||||
id,
|
id,
|
||||||
admin,
|
admin,
|
||||||
path,
|
path,
|
||||||
onUnauthorized,
|
// onUnauthorized,
|
||||||
onLoginPass,
|
onLoginPass,
|
||||||
setInstanceName,
|
setInstanceName,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
@ -155,7 +155,7 @@ export function InstanceRoutes({
|
|||||||
const [globalNotification, setGlobalNotification] =
|
const [globalNotification, setGlobalNotification] =
|
||||||
useState<GlobalNotifState>(undefined);
|
useState<GlobalNotifState>(undefined);
|
||||||
|
|
||||||
const changeToken = (token?: string) => {
|
const changeToken = (token?: LoginToken) => {
|
||||||
if (admin) {
|
if (admin) {
|
||||||
updateToken(token);
|
updateToken(token);
|
||||||
} else {
|
} else {
|
||||||
@ -201,14 +201,17 @@ export function InstanceRoutes({
|
|||||||
|
|
||||||
// const LoginPageAccessDeniend = onUnauthorized
|
// const LoginPageAccessDeniend = onUnauthorized
|
||||||
const LoginPageAccessDenied = () => {
|
const LoginPageAccessDenied = () => {
|
||||||
onUnauthorized()
|
return <Fragment>
|
||||||
return <NotificationCard
|
<NotificationCard
|
||||||
notification={{
|
notification={{
|
||||||
message: i18n.str`Access denied`,
|
message: i18n.str`Access denied`,
|
||||||
description: i18n.str`Redirecting to login page.`,
|
description: i18n.str`Redirecting to login page.`,
|
||||||
type: "ERROR",
|
type: "ERROR",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<LoginPage onConfirm={changeToken} />
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
|
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
|
||||||
@ -687,9 +690,7 @@ function AdminInstanceUpdatePage({
|
|||||||
...rest
|
...rest
|
||||||
}: { id: string } & InstanceUpdatePageProps): VNode {
|
}: { id: string } & InstanceUpdatePageProps): VNode {
|
||||||
const [token, changeToken] = useBackendInstanceToken(id);
|
const [token, changeToken] = useBackendInstanceToken(id);
|
||||||
const { updateLoginStatus: changeBackend } = useBackendContext();
|
const updateLoginStatus = (token?: LoginToken): void => {
|
||||||
const updateLoginStatus = (url: string, token?: string): void => {
|
|
||||||
changeBackend(url);
|
|
||||||
changeToken(token);
|
changeToken(token);
|
||||||
};
|
};
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
@ -752,7 +753,7 @@ function KycBanner(): VNode {
|
|||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const today = format(new Date(), dateFormatForSettings(settings));
|
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 hasBeenHidden = today === lastHide;
|
||||||
const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
|
const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
|
||||||
if (hasBeenHidden || !needsToBeShown) return <Fragment />;
|
if (hasBeenHidden || !needsToBeShown) return <Fragment />;
|
||||||
|
@ -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>
|
|
||||||
}
|
|
@ -40,13 +40,13 @@ export function DefaultInstanceFormFields({
|
|||||||
showId: boolean;
|
showId: boolean;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const backend = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{showId && (
|
{showId && (
|
||||||
<InputWithAddon<Entity>
|
<InputWithAddon<Entity>
|
||||||
name="id"
|
name="id"
|
||||||
addonBefore={`${backend.url}/instances/`}
|
addonBefore={`${backendURL}/instances/`}
|
||||||
readonly={readonlyId}
|
readonly={readonlyId}
|
||||||
label={i18n.str`Identifier`}
|
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.`}
|
tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`}
|
||||||
|
@ -25,7 +25,6 @@ import { useBackendContext } from "../../context/backend.js";
|
|||||||
import { useConfigContext } from "../../context/config.js";
|
import { useConfigContext } from "../../context/config.js";
|
||||||
import { useInstanceKYCDetails } from "../../hooks/instance.js";
|
import { useInstanceKYCDetails } from "../../hooks/instance.js";
|
||||||
import { LangSelector } from "./LangSelector.js";
|
import { LangSelector } from "./LangSelector.js";
|
||||||
import { useCredentialsChecker } from "../../hooks/backend.js";
|
|
||||||
|
|
||||||
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
|
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
|
||||||
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
|
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
|
||||||
@ -50,7 +49,7 @@ export function Sidebar({
|
|||||||
isPasswordOk
|
isPasswordOk
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const config = useConfigContext();
|
const config = useConfigContext();
|
||||||
const backend = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const kycStatus = useInstanceKYCDetails();
|
const kycStatus = useInstanceKYCDetails();
|
||||||
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
|
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
|
||||||
@ -230,7 +229,7 @@ export function Sidebar({
|
|||||||
<i class="mdi mdi-web" />
|
<i class="mdi mdi-web" />
|
||||||
</span>
|
</span>
|
||||||
<span class="menu-item-label">
|
<span class="menu-item-label">
|
||||||
{new URL(backend.url).hostname}
|
{new URL(backendURL).hostname}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -114,7 +114,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
|
|||||||
onSubscribe(hasErrors ? undefined : submit);
|
onSubscribe(hasErrors ? undefined : submit);
|
||||||
}, [submit, hasErrors]);
|
}, [submit, hasErrors]);
|
||||||
|
|
||||||
const backend = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -128,7 +128,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
|
|||||||
{alreadyExist ? undefined : (
|
{alreadyExist ? undefined : (
|
||||||
<InputWithAddon<Entity>
|
<InputWithAddon<Entity>
|
||||||
name="product_id"
|
name="product_id"
|
||||||
addonBefore={`${backend.url}/product/`}
|
addonBefore={`${backendURL}/product/`}
|
||||||
label={i18n.str`ID`}
|
label={i18n.str`ID`}
|
||||||
tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
|
tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
|
||||||
/>
|
/>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import * as tests from "@gnu-taler/web-util/testing";
|
import * as tests from "@gnu-taler/web-util/testing";
|
||||||
import { ComponentChildren, h, VNode } from "preact";
|
import { ComponentChildren, h, VNode } from "preact";
|
||||||
import { MerchantBackend } from "../declaration.js";
|
import { AccessToken, MerchantBackend } from "../declaration.js";
|
||||||
import {
|
import {
|
||||||
useAdminAPI,
|
useAdminAPI,
|
||||||
useInstanceAPI,
|
useInstanceAPI,
|
||||||
@ -64,7 +64,7 @@ describe("backend context api ", () => {
|
|||||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
management.setNewToken("another_token");
|
management.setNewToken("another_token" as AccessToken);
|
||||||
},
|
},
|
||||||
({ instance, management, admin }) => {
|
({ instance, management, admin }) => {
|
||||||
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
||||||
@ -113,7 +113,7 @@ describe("backend context api ", () => {
|
|||||||
name: "instance_name",
|
name: "instance_name",
|
||||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||||
});
|
});
|
||||||
instance.setNewToken("another_token");
|
instance.setNewToken("another_token" as AccessToken);
|
||||||
},
|
},
|
||||||
({ instance, management, admin }) => {
|
({ instance, management, admin }) => {
|
||||||
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
||||||
|
@ -20,90 +20,46 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createContext, h, VNode } from "preact";
|
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";
|
import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
|
||||||
|
|
||||||
interface BackendContextType {
|
interface BackendContextType {
|
||||||
url: string;
|
url: string,
|
||||||
token?: string;
|
token?: LoginToken;
|
||||||
triedToLog: boolean;
|
updateToken: (token: LoginToken | undefined) => void;
|
||||||
resetBackend: () => void;
|
changeBackend: (url: string) => void;
|
||||||
// clearAllTokens: () => void;
|
|
||||||
// addTokenCleaner: (c: () => void) => void;
|
|
||||||
updateLoginStatus: (url: string, token?: string) => void;
|
|
||||||
updateToken: (token?: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BackendContext = createContext<BackendContextType>({
|
const BackendContext = createContext<BackendContextType>({
|
||||||
url: "",
|
url: "",
|
||||||
token: undefined,
|
token: undefined,
|
||||||
triedToLog: false,
|
|
||||||
resetBackend: () => null,
|
|
||||||
// clearAllTokens: () => null,
|
|
||||||
// addTokenCleaner: () => null,
|
|
||||||
updateLoginStatus: () => null,
|
|
||||||
updateToken: () => null,
|
updateToken: () => null,
|
||||||
|
changeBackend: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
function useBackendContextState(
|
function useBackendContextState(
|
||||||
defaultUrl?: string,
|
defaultUrl?: string,
|
||||||
initialToken?: string,
|
|
||||||
): BackendContextType {
|
): BackendContextType {
|
||||||
const [url, triedToLog, changeBackend, resetBackend] =
|
const [url, changeBackend] = useBackendURL(defaultUrl);
|
||||||
useBackendURL(defaultUrl);
|
const [token, updateToken] = useBackendDefaultToken();
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
triedToLog,
|
|
||||||
updateLoginStatus,
|
|
||||||
resetBackend,
|
|
||||||
// clearAllTokens,
|
|
||||||
updateToken,
|
updateToken,
|
||||||
// addTokenCleaner: addTokenCleanerMemo,
|
changeBackend
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BackendContextProvider = ({
|
export const BackendContextProvider = ({
|
||||||
children,
|
children,
|
||||||
defaultUrl,
|
defaultUrl,
|
||||||
initialToken,
|
|
||||||
}: {
|
}: {
|
||||||
children: any;
|
children: any;
|
||||||
defaultUrl?: string;
|
defaultUrl?: string;
|
||||||
initialToken?: string;
|
|
||||||
}): VNode => {
|
}): VNode => {
|
||||||
const value = useBackendContextState(defaultUrl, initialToken);
|
const value = useBackendContextState(defaultUrl);
|
||||||
|
|
||||||
return h(BackendContext.Provider, { value, children });
|
return h(BackendContext.Provider, { value, children });
|
||||||
};
|
};
|
||||||
|
@ -21,12 +21,13 @@
|
|||||||
|
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
import { LoginToken } from "../declaration.js";
|
||||||
|
|
||||||
interface Type {
|
interface Type {
|
||||||
id: string;
|
id: string;
|
||||||
token?: string;
|
token?: LoginToken;
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
changeToken: (t?: string) => void;
|
changeToken: (t?: LoginToken) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Context = createContext<Type>({} as any);
|
const Context = createContext<Type>({} as any);
|
||||||
|
@ -107,6 +107,16 @@ interface RegexAccountRestriction {
|
|||||||
// human hints.
|
// human hints.
|
||||||
human_hint_i18n?: { [lang_tag: string]: string };
|
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 {
|
export namespace ExchangeBackend {
|
||||||
interface WireResponse {
|
interface WireResponse {
|
||||||
@ -491,6 +501,35 @@ export namespace MerchantBackend {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// DELETE /private/instances/$INSTANCE
|
// 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 {
|
namespace KYC {
|
||||||
|
@ -19,19 +19,21 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useSWRConfig } from "swr";
|
import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
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 {
|
import {
|
||||||
ErrorType,
|
ErrorType,
|
||||||
|
HttpError,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponseOk,
|
HttpResponseOk,
|
||||||
RequestError,
|
RequestError,
|
||||||
RequestOptions,
|
RequestOptions,
|
||||||
|
useApiContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} 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(): (
|
export function useMatchMutate(): (
|
||||||
@ -85,6 +87,9 @@ export function useBackendInstancesTestForAdmin(): HttpResponse<
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
|
||||||
|
const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
|
||||||
|
|
||||||
export function useBackendConfig(): HttpResponse<
|
export function useBackendConfig(): HttpResponse<
|
||||||
MerchantBackend.VersionResponse,
|
MerchantBackend.VersionResponse,
|
||||||
RequestError<MerchantBackend.ErrorDetail>
|
RequestError<MerchantBackend.ErrorDetail>
|
||||||
@ -92,18 +97,33 @@ export function useBackendConfig(): HttpResponse<
|
|||||||
const { request } = useBackendBaseRequest();
|
const { request } = useBackendBaseRequest();
|
||||||
|
|
||||||
type Type = MerchantBackend.VersionResponse;
|
type Type = MerchantBackend.VersionResponse;
|
||||||
|
type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number }
|
||||||
const [result, setResult] = useState<
|
const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 });
|
||||||
HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>
|
|
||||||
>({ loading: true });
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (result.timer) {
|
||||||
|
clearTimeout(result.timer)
|
||||||
|
}
|
||||||
|
function tryConfig(): void {
|
||||||
request<Type>(`/config`)
|
request<Type>(`/config`)
|
||||||
.then((data) => setResult(data))
|
.then((data) => {
|
||||||
.catch((error) => setResult(error));
|
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]);
|
}, [request]);
|
||||||
|
|
||||||
return result;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface useBackendInstanceRequestType {
|
interface useBackendInstanceRequestType {
|
||||||
@ -149,32 +169,86 @@ interface useBackendBaseRequestType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type YesOrNo = "yes" | "no";
|
type YesOrNo = "yes" | "no";
|
||||||
|
type LoginResult = {
|
||||||
|
valid: true;
|
||||||
|
token: string;
|
||||||
|
expiration: Timestamp;
|
||||||
|
} | {
|
||||||
|
valid: false;
|
||||||
|
cause: HttpError<{}>;
|
||||||
|
}
|
||||||
|
|
||||||
export function useCredentialsChecker() {
|
export function useCredentialsChecker() {
|
||||||
const { request } = useApiContext();
|
const { request } = useApiContext();
|
||||||
//check against instance details endpoint
|
//check against instance details endpoint
|
||||||
//while merchant backend doesn't have a login endpoint
|
//while merchant backend doesn't have a login endpoint
|
||||||
async function testLogin(
|
async function requestNewLoginToken(
|
||||||
instance: string,
|
baseUrl: string,
|
||||||
token: string,
|
token: AccessToken,
|
||||||
): Promise<{
|
): Promise<LoginResult> {
|
||||||
valid: boolean;
|
const data: MerchantBackend.Instances.LoginTokenRequest = {
|
||||||
cause?: ErrorType;
|
scope: "write",
|
||||||
}> {
|
duration: {
|
||||||
|
d_us: "forever"
|
||||||
|
},
|
||||||
|
refreshable: true,
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const response = await request(instance, `/private/`, {
|
const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, {
|
||||||
|
method: "POST",
|
||||||
token,
|
token,
|
||||||
|
data
|
||||||
});
|
});
|
||||||
return { valid: true };
|
return { valid: true, token: response.data.token, expiration: response.data.expiration };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof RequestError) {
|
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
|
* @returns request handler to
|
||||||
*/
|
*/
|
||||||
export function useBackendBaseRequest(): useBackendBaseRequestType {
|
export function useBackendBaseRequest(): useBackendBaseRequestType {
|
||||||
const { url: backend, token } = useBackendContext();
|
const { url: backend, token: loginToken } = useBackendContext();
|
||||||
const { request: requestHandler } = useApiContext();
|
const { request: requestHandler } = useApiContext();
|
||||||
|
const token = loginToken?.token;
|
||||||
|
|
||||||
const request = useCallback(
|
const request = useCallback(
|
||||||
function requestImpl<T>(
|
function requestImpl<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
options: RequestOptions = {},
|
options: RequestOptions = {},
|
||||||
): Promise<HttpResponseOk<T>> {
|
): 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],
|
[backend, token],
|
||||||
);
|
);
|
||||||
@ -204,10 +283,12 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
|||||||
const { token: instanceToken, id, admin } = useInstanceContext();
|
const { token: instanceToken, id, admin } = useInstanceContext();
|
||||||
const { request: requestHandler } = useApiContext();
|
const { request: requestHandler } = useApiContext();
|
||||||
|
|
||||||
const { baseUrl, token } = !admin
|
const { baseUrl, token: loginToken } = !admin
|
||||||
? { baseUrl: rootBackendUrl, token: rootToken }
|
? { baseUrl: rootBackendUrl, token: rootToken }
|
||||||
: { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken };
|
: { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken };
|
||||||
|
|
||||||
|
const token = loginToken?.token;
|
||||||
|
|
||||||
const request = useCallback(
|
const request = useCallback(
|
||||||
function requestImpl<T>(
|
function requestImpl<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
@ -19,9 +19,11 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @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 { ValueOrFunction } from "../utils/types.js";
|
||||||
import { useMemoryStorage } from "@gnu-taler/web-util/browser";
|
|
||||||
import { useMatchMutate } from "./backend.js";
|
import { useMatchMutate } from "./backend.js";
|
||||||
|
|
||||||
const calculateRootPath = () => {
|
const calculateRootPath = () => {
|
||||||
@ -32,53 +34,55 @@ const calculateRootPath = () => {
|
|||||||
return rootPath;
|
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(
|
export function useBackendURL(
|
||||||
url?: string,
|
url?: string,
|
||||||
): [string, boolean, StateUpdater<string>, () => void] {
|
): [string, StateUpdater<string>] {
|
||||||
const [value, setter] = useNotNullLocalStorage(
|
const [value, setter] = useSimpleLocalStorage(
|
||||||
"backend-url",
|
"backend-url",
|
||||||
url || calculateRootPath(),
|
url || calculateRootPath(),
|
||||||
);
|
);
|
||||||
const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
|
|
||||||
|
|
||||||
const checkedSetter = (v: ValueOrFunction<string>) => {
|
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 = () => {
|
return [value!, checkedSetter];
|
||||||
setTriedToLog(undefined);
|
|
||||||
};
|
|
||||||
return [value, !!triedToLog, checkedSetter, resetBackend];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBackendDefaultToken(
|
export function useBackendDefaultToken(
|
||||||
initialValue?: string,
|
): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
|
||||||
): [string | undefined, ((d: string | undefined) => void)] {
|
const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
|
||||||
// uncomment for testing
|
|
||||||
initialValue = "secret-token:secret" as string | undefined
|
const tokenOfDefaultInstance = tokenMap["default"]
|
||||||
const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue)
|
|
||||||
const clearCache = useMatchMutate()
|
const clearCache = useMatchMutate()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearCache()
|
clearCache()
|
||||||
}, [token])
|
}, [tokenOfDefaultInstance])
|
||||||
|
|
||||||
function updateToken(
|
function updateToken(
|
||||||
value: (string | undefined)
|
value: (LoginToken | undefined)
|
||||||
): void {
|
): void {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
reset()
|
reset()
|
||||||
} else {
|
} else {
|
||||||
setToken(value)
|
const res = { ...tokenMap, "default": value }
|
||||||
|
setToken(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [token, updateToken];
|
return [tokenMap["default"], updateToken];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBackendInstanceToken(
|
export function useBackendInstanceToken(
|
||||||
id: string,
|
id: string,
|
||||||
): [string | undefined, ((d: string | undefined) => void)] {
|
): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
|
||||||
const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`)
|
const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
|
||||||
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
|
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
|
||||||
|
|
||||||
// instance named 'default' use the default token
|
// instance named 'default' use the default token
|
||||||
@ -86,16 +90,17 @@ export function useBackendInstanceToken(
|
|||||||
return [defaultToken, defaultSetToken];
|
return [defaultToken, defaultSetToken];
|
||||||
}
|
}
|
||||||
function updateToken(
|
function updateToken(
|
||||||
value: (string | undefined)
|
value: (LoginToken | undefined)
|
||||||
): void {
|
): void {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
reset()
|
reset()
|
||||||
} else {
|
} 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>] {
|
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
|
? navigator.language || (navigator as any).userLanguage
|
||||||
: undefined;
|
: undefined;
|
||||||
const defaultLang = (browserLang || initial || "en").substring(0, 2);
|
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,
|
key: string,
|
||||||
initialValue?: string,
|
initialValue?: string,
|
||||||
): [string | undefined, StateUpdater<string | undefined>] {
|
): [string | undefined, StateUpdater<string | undefined>] {
|
||||||
@ -137,28 +142,3 @@ export function useLocalStorage(
|
|||||||
|
|
||||||
return [storedValue, setValue];
|
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];
|
|
||||||
}
|
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
import * as tests from "@gnu-taler/web-util/testing";
|
import * as tests from "@gnu-taler/web-util/testing";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { MerchantBackend } from "../declaration.js";
|
import { AccessToken, MerchantBackend } from "../declaration.js";
|
||||||
import {
|
import {
|
||||||
useAdminAPI,
|
useAdminAPI,
|
||||||
useBackendInstances,
|
useBackendInstances,
|
||||||
@ -158,7 +158,7 @@ describe("instance api interaction with details", () => {
|
|||||||
},
|
},
|
||||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||||
});
|
});
|
||||||
api.setNewToken("secret");
|
api.setNewToken("secret" as AccessToken);
|
||||||
},
|
},
|
||||||
({ query, api }) => {
|
({ query, api }) => {
|
||||||
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
||||||
|
@ -19,10 +19,11 @@ import {
|
|||||||
RequestError,
|
RequestError,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { useBackendContext } from "../context/backend.js";
|
import { useBackendContext } from "../context/backend.js";
|
||||||
import { MerchantBackend } from "../declaration.js";
|
import { AccessToken, MerchantBackend } from "../declaration.js";
|
||||||
import {
|
import {
|
||||||
useBackendBaseRequest,
|
useBackendBaseRequest,
|
||||||
useBackendInstanceRequest,
|
useBackendInstanceRequest,
|
||||||
|
useCredentialsChecker,
|
||||||
useMatchMutate,
|
useMatchMutate,
|
||||||
} from "./backend.js";
|
} from "./backend.js";
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ interface InstanceAPI {
|
|||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
deleteInstance: () => Promise<void>;
|
deleteInstance: () => Promise<void>;
|
||||||
clearToken: () => Promise<void>;
|
clearToken: () => Promise<void>;
|
||||||
setNewToken: (token: string) => Promise<void>;
|
setNewToken: (token: AccessToken) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAdminAPI(): AdminAPI {
|
export function useAdminAPI(): AdminAPI {
|
||||||
@ -86,8 +87,10 @@ export interface AdminAPI {
|
|||||||
|
|
||||||
export function useManagementAPI(instanceId: string): InstanceAPI {
|
export function useManagementAPI(instanceId: string): InstanceAPI {
|
||||||
const mutateAll = useMatchMutate();
|
const mutateAll = useMatchMutate();
|
||||||
|
const { url: backendURL } = useBackendContext()
|
||||||
const { updateToken } = useBackendContext();
|
const { updateToken } = useBackendContext();
|
||||||
const { request } = useBackendBaseRequest();
|
const { request } = useBackendBaseRequest();
|
||||||
|
const { requestNewLoginToken } = useCredentialsChecker()
|
||||||
|
|
||||||
const updateInstance = async (
|
const updateInstance = async (
|
||||||
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
|
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
|
||||||
@ -117,13 +120,20 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
|
|||||||
mutateAll(/\/management\/instances/);
|
mutateAll(/\/management\/instances/);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNewToken = async (newToken: string): Promise<void> => {
|
const setNewToken = async (newToken: AccessToken): Promise<void> => {
|
||||||
await request(`/management/instances/${instanceId}/auth`, {
|
await request(`/management/instances/${instanceId}/auth`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: { method: "token", token: newToken },
|
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/);
|
mutateAll(/\/management\/instances/);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,12 +142,13 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
|
|||||||
|
|
||||||
export function useInstanceAPI(): InstanceAPI {
|
export function useInstanceAPI(): InstanceAPI {
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
|
const { url: backendURL, updateToken } = useBackendContext()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
url: baseUrl,
|
|
||||||
token: adminToken,
|
token: adminToken,
|
||||||
updateLoginStatus,
|
|
||||||
} = useBackendContext();
|
} = useBackendContext();
|
||||||
const { request } = useBackendInstanceRequest();
|
const { request } = useBackendInstanceRequest();
|
||||||
|
const { requestNewLoginToken } = useCredentialsChecker()
|
||||||
|
|
||||||
const updateInstance = async (
|
const updateInstance = async (
|
||||||
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
|
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
|
||||||
@ -147,7 +158,7 @@ export function useInstanceAPI(): InstanceAPI {
|
|||||||
data: instance,
|
data: instance,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
|
if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
|
||||||
mutate([`/private/`], null);
|
mutate([`/private/`], null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,7 +168,7 @@ export function useInstanceAPI(): InstanceAPI {
|
|||||||
// token: adminToken,
|
// token: adminToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
|
if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
|
||||||
mutate([`/private/`], null);
|
mutate([`/private/`], null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -170,13 +181,20 @@ export function useInstanceAPI(): InstanceAPI {
|
|||||||
mutate([`/private/`], null);
|
mutate([`/private/`], null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNewToken = async (newToken: string): Promise<void> => {
|
const setNewToken = async (newToken: AccessToken): Promise<void> => {
|
||||||
await request(`/private/auth`, {
|
await request(`/private/auth`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: { method: "token", token: newToken },
|
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);
|
mutate([`/private/`], null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,10 +90,7 @@ export class ApiMockEnvironment extends MockEnvironment {
|
|||||||
const SC: any = SWRConfig;
|
const SC: any = SWRConfig;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BackendContextProvider
|
<BackendContextProvider defaultUrl="http://backend">
|
||||||
defaultUrl="http://backend"
|
|
||||||
initialToken={undefined}
|
|
||||||
>
|
|
||||||
<InstanceContextProvider
|
<InstanceContextProvider
|
||||||
value={{
|
value={{
|
||||||
token: undefined,
|
token: undefined,
|
||||||
|
@ -24,15 +24,6 @@ import {
|
|||||||
codecForString,
|
codecForString,
|
||||||
} from "@gnu-taler/taler-util";
|
} 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 {
|
export interface Settings {
|
||||||
advanceOrderMode: boolean;
|
advanceOrderMode: boolean;
|
||||||
dateFormat: "ymd" | "dmy" | "mdy";
|
dateFormat: "ymd" | "dmy" | "mdy";
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
|
import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { format, formatDistance } from "date-fns";
|
import { format, formatDistance } from "date-fns";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { FormProvider } from "../../../../components/form/FormProvider.js";
|
import { FormProvider } from "../../../../components/form/FormProvider.js";
|
||||||
import { Input } from "../../../../components/form/Input.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 { ProductList } from "../../../../components/product/ProductList.js";
|
||||||
import { useBackendContext } from "../../../../context/backend.js";
|
import { useBackendContext } from "../../../../context/backend.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
import { mergeRefunds } from "../../../../utils/amount.js";
|
import { mergeRefunds } from "../../../../utils/amount.js";
|
||||||
import { RefundModal } from "../list/Table.js";
|
import { RefundModal } from "../list/Table.js";
|
||||||
import { Event, Timeline } from "./Timeline.js";
|
import { Event, Timeline } from "./Timeline.js";
|
||||||
import { dateFormatForSettings, datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
|
||||||
|
|
||||||
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
|
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
|
||||||
type CT = MerchantBackend.ContractTerms;
|
type CT = MerchantBackend.ContractTerms;
|
||||||
@ -416,9 +416,9 @@ function PaidPage({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [value, valueHandler] = useState<Partial<Paid>>(order);
|
const [value, valueHandler] = useState<Partial<Paid>>(order);
|
||||||
const { url } = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
const refundurl = stringifyRefundUri({
|
const refundurl = stringifyRefundUri({
|
||||||
merchantBaseUrl: url,
|
merchantBaseUrl: backendURL,
|
||||||
orderId: order.contract_terms.order_id
|
orderId: order.contract_terms.order_id
|
||||||
})
|
})
|
||||||
const refundable =
|
const refundable =
|
||||||
|
@ -13,12 +13,12 @@
|
|||||||
You should have received a copy of the GNU General Public License along with
|
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/>
|
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 { format } from "date-fns";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useBackendContext } from "../../../../context/backend.js";
|
import { useBackendContext } from "../../../../context/backend.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||||
import { stringifyRewardUri } from "@gnu-taler/taler-util";
|
|
||||||
|
|
||||||
type Entity = MerchantBackend.Rewards.RewardDetails;
|
type Entity = MerchantBackend.Rewards.RewardDetails;
|
||||||
|
|
||||||
@ -29,9 +29,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode {
|
export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode {
|
||||||
const { url: merchantBaseUrl } = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
const [settings] = useSettings();
|
const [settings] = useSettings();
|
||||||
const rewardURL = stringifyRewardUri({ merchantBaseUrl, merchantRewardId })
|
const rewardURL = stringifyRewardUri({ merchantBaseUrl: backendURL, merchantRewardId })
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div class="field is-horizontal">
|
<div class="field is-horizontal">
|
||||||
|
@ -35,16 +35,12 @@ import { Input } from "../../../../components/form/Input.js";
|
|||||||
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
||||||
import { InputDuration } from "../../../../components/form/InputDuration.js";
|
import { InputDuration } from "../../../../components/form/InputDuration.js";
|
||||||
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
||||||
|
import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
|
||||||
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
||||||
import { useBackendContext } from "../../../../context/backend.js";
|
import { useBackendContext } from "../../../../context/backend.js";
|
||||||
import { useInstanceContext } from "../../../../context/instance.js";
|
|
||||||
import { MerchantBackend } from "../../../../declaration.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 { useInstanceOtpDevices } from "../../../../hooks/otp.js";
|
||||||
|
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.Template.TemplateAddDetails;
|
type Entity = MerchantBackend.Template.TemplateAddDetails;
|
||||||
|
|
||||||
@ -55,7 +51,7 @@ interface Props {
|
|||||||
|
|
||||||
export function CreatePage({ onCreate, onBack }: Props): VNode {
|
export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const backend = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
const devices = useInstanceOtpDevices()
|
const devices = useInstanceOtpDevices()
|
||||||
|
|
||||||
const [state, setState] = useState<Partial<Entity>>({
|
const [state, setState] = useState<Partial<Entity>>({
|
||||||
@ -128,7 +124,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
|||||||
>
|
>
|
||||||
<InputWithAddon<Entity>
|
<InputWithAddon<Entity>
|
||||||
name="template_id"
|
name="template_id"
|
||||||
help={`${backend.url}/templates/${state.template_id ?? ""}`}
|
help={`${backendURL}/templates/${state.template_id ?? ""}`}
|
||||||
label={i18n.str`Identifier`}
|
label={i18n.str`Identifier`}
|
||||||
tooltip={i18n.str`Name of the template in URLs.`}
|
tooltip={i18n.str`Name of the template in URLs.`}
|
||||||
/>
|
/>
|
||||||
|
@ -19,8 +19,9 @@
|
|||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
|
||||||
import { h, VNode } from "preact";
|
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
|
import { VNode, h } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { QR } from "../../../../components/exception/QR.js";
|
import { QR } from "../../../../components/exception/QR.js";
|
||||||
import {
|
import {
|
||||||
@ -29,14 +30,10 @@ import {
|
|||||||
} from "../../../../components/form/FormProvider.js";
|
} from "../../../../components/form/FormProvider.js";
|
||||||
import { Input } from "../../../../components/form/Input.js";
|
import { Input } from "../../../../components/form/Input.js";
|
||||||
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
||||||
import { ConfirmModal } from "../../../../components/modal/index.js";
|
|
||||||
import { useBackendContext } from "../../../../context/backend.js";
|
import { useBackendContext } from "../../../../context/backend.js";
|
||||||
import { useConfigContext } from "../../../../context/config.js";
|
import { useConfigContext } from "../../../../context/config.js";
|
||||||
import { useInstanceContext } from "../../../../context/instance.js";
|
import { useInstanceContext } from "../../../../context/instance.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.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;
|
type Entity = MerchantBackend.Template.UsingTemplateDetails;
|
||||||
|
|
||||||
@ -48,7 +45,7 @@ interface Props {
|
|||||||
|
|
||||||
export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
|
export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const { url: backendUrl } = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
const { id: instanceId } = useInstanceContext();
|
const { id: instanceId } = useInstanceContext();
|
||||||
const config = useConfigContext();
|
const config = useConfigContext();
|
||||||
|
|
||||||
@ -75,7 +72,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
|
|||||||
templateParams.summary = state.summary ?? ""
|
templateParams.summary = state.summary ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
const merchantBaseUrl = new URL(backendUrl).href;
|
const merchantBaseUrl = new URL(backendURL).href;
|
||||||
|
|
||||||
const payTemplateUri = stringifyPayTemplateUri({
|
const payTemplateUri = stringifyPayTemplateUri({
|
||||||
merchantBaseUrl,
|
merchantBaseUrl,
|
||||||
@ -84,7 +81,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const issuer = encodeURIComponent(
|
const issuer = encodeURIComponent(
|
||||||
`${new URL(backendUrl).host}/${instanceId}`,
|
`${new URL(backendURL).host}/${instanceId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -24,7 +24,7 @@ import {
|
|||||||
MerchantTemplateContractDetails,
|
MerchantTemplateContractDetails,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
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 { useState } from "preact/hooks";
|
||||||
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
|
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
|
||||||
import {
|
import {
|
||||||
@ -35,17 +35,10 @@ import { Input } from "../../../../components/form/Input.js";
|
|||||||
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
||||||
import { InputDuration } from "../../../../components/form/InputDuration.js";
|
import { InputDuration } from "../../../../components/form/InputDuration.js";
|
||||||
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
||||||
import { InputSelector } from "../../../../components/form/InputSelector.js";
|
|
||||||
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
||||||
import { useBackendContext } from "../../../../context/backend.js";
|
import { useBackendContext } from "../../../../context/backend.js";
|
||||||
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||||
import {
|
|
||||||
isBase32RFC3548Charset,
|
|
||||||
randomBase32Key,
|
|
||||||
} from "../../../../utils/crypto.js";
|
|
||||||
import { undefinedIfEmpty } from "../../../../utils/table.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;
|
type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
|
||||||
|
|
||||||
@ -55,12 +48,9 @@ interface Props {
|
|||||||
template: Entity;
|
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 {
|
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const backend = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
|
|
||||||
const [state, setState] = useState<Partial<Entity>>(template);
|
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-left">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<span class="is-size-4">
|
<span class="is-size-4">
|
||||||
{backend.url}/templates/{template.id}
|
{backendURL}/templates/{template.id}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -26,12 +26,13 @@ import { AsyncButton } from "../../../components/exception/AsyncButton.js";
|
|||||||
import { FormProvider } from "../../../components/form/FormProvider.js";
|
import { FormProvider } from "../../../components/form/FormProvider.js";
|
||||||
import { Input } from "../../../components/form/Input.js";
|
import { Input } from "../../../components/form/Input.js";
|
||||||
import { useInstanceContext } from "../../../context/instance.js";
|
import { useInstanceContext } from "../../../context/instance.js";
|
||||||
|
import { AccessToken } from "../../../declaration.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
currentToken: string | undefined;
|
currentToken: string | undefined;
|
||||||
onClearToken: () => void;
|
onClearToken: () => void;
|
||||||
onNewToken: (s: string) => void;
|
onNewToken: (s: AccessToken) => void;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +72,8 @@ export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewTo
|
|||||||
|
|
||||||
async function submitForm() {
|
async function submitForm() {
|
||||||
if (hasErrors) return;
|
if (hasErrors) return;
|
||||||
onNewToken(form.new_token as any)
|
const nt = `secret-token:${form.new_token}` as AccessToken;
|
||||||
|
onNewToken(nt)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -17,7 +17,7 @@ import { HttpStatusCode } from "@gnu-taler/taler-util";
|
|||||||
import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { Loading } from "../../../components/exception/loading.js";
|
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 { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
|
||||||
import { DetailPage } from "./DetailPage.js";
|
import { DetailPage } from "./DetailPage.js";
|
||||||
import { useInstanceContext } from "../../../context/instance.js";
|
import { useInstanceContext } from "../../../context/instance.js";
|
||||||
@ -49,13 +49,13 @@ export default function Token({
|
|||||||
const { token: instanceToken, id, admin } = useInstanceContext();
|
const { token: instanceToken, id, admin } = useInstanceContext();
|
||||||
|
|
||||||
const currentToken = !admin ? rootToken : instanceToken
|
const currentToken = !admin ? rootToken : instanceToken
|
||||||
const hasPrefix = currentToken !== undefined && currentToken.startsWith(PREFIX)
|
const hasPrefix = currentToken !== undefined && currentToken.token.startsWith(PREFIX)
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<NotificationCard notification={notif} />
|
<NotificationCard notification={notif} />
|
||||||
<DetailPage
|
<DetailPage
|
||||||
instanceId={id}
|
instanceId={id}
|
||||||
currentToken={hasPrefix ? currentToken.substring(PREFIX.length) : currentToken}
|
currentToken={hasPrefix ? currentToken.token.substring(PREFIX.length) : currentToken?.token}
|
||||||
onClearToken={async (): Promise<void> => {
|
onClearToken={async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await clearToken();
|
await clearToken();
|
||||||
@ -72,7 +72,7 @@ export default function Token({
|
|||||||
}}
|
}}
|
||||||
onNewToken={async (newToken): Promise<void> => {
|
onNewToken={async (newToken): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await setNewToken(`secret-token:${newToken}`);
|
await setNewToken(newToken);
|
||||||
onChange();
|
onChange();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
@ -13,18 +13,19 @@
|
|||||||
You should have received a copy of the GNU General Public License along with
|
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/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
ErrorType,
|
ErrorType,
|
||||||
HttpError,
|
HttpError,
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
useTranslationContext,
|
useTranslationContext,
|
||||||
} from "@gnu-taler/web-util/browser";
|
} from "@gnu-taler/web-util/browser";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { Loading } from "../../../components/exception/loading.js";
|
import { Loading } from "../../../components/exception/loading.js";
|
||||||
import { NotificationCard } from "../../../components/menu/index.js";
|
import { NotificationCard } from "../../../components/menu/index.js";
|
||||||
import { useInstanceContext } from "../../../context/instance.js";
|
import { useInstanceContext } from "../../../context/instance.js";
|
||||||
import { MerchantBackend } from "../../../declaration.js";
|
import { AccessToken, MerchantBackend } from "../../../declaration.js";
|
||||||
import {
|
import {
|
||||||
useInstanceAPI,
|
useInstanceAPI,
|
||||||
useInstanceDetails,
|
useInstanceDetails,
|
||||||
@ -33,7 +34,6 @@ import {
|
|||||||
} from "../../../hooks/instance.js";
|
} from "../../../hooks/instance.js";
|
||||||
import { Notification } from "../../../utils/types.js";
|
import { Notification } from "../../../utils/types.js";
|
||||||
import { UpdatePage } from "./UpdatePage.js";
|
import { UpdatePage } from "./UpdatePage.js";
|
||||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
@ -73,10 +73,9 @@ function CommonUpdate(
|
|||||||
MerchantBackend.ErrorDetail
|
MerchantBackend.ErrorDetail
|
||||||
>,
|
>,
|
||||||
updateInstance: any,
|
updateInstance: any,
|
||||||
clearToken: any,
|
clearToken: () => Promise<void>,
|
||||||
setNewToken: any,
|
setNewToken: (t: AccessToken) => Promise<void>,
|
||||||
): VNode {
|
): VNode {
|
||||||
const { changeToken } = useInstanceContext();
|
|
||||||
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
|
|
||||||
@ -119,11 +118,8 @@ function CommonUpdate(
|
|||||||
d: MerchantBackend.Instances.InstanceAuthConfigurationMessage,
|
d: MerchantBackend.Instances.InstanceAuthConfigurationMessage,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const apiCall =
|
const apiCall =
|
||||||
d.method === "external" ? clearToken() : setNewToken(d.token!);
|
d.method === "external" ? clearToken() : setNewToken(d.token! as AccessToken);
|
||||||
return apiCall
|
return apiCall.then(onConfirm).catch(onUpdateError);
|
||||||
.then(() => changeToken(d.token))
|
|
||||||
.then(onConfirm)
|
|
||||||
.catch(onUpdateError);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
@ -18,9 +18,9 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
|||||||
import { Fragment, VNode, h } from "preact";
|
import { Fragment, VNode, h } from "preact";
|
||||||
import { QR } from "../../../../components/exception/QR.js";
|
import { QR } from "../../../../components/exception/QR.js";
|
||||||
import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
|
import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
|
||||||
import { useBackendContext } from "../../../../context/backend.js";
|
|
||||||
import { useInstanceContext } from "../../../../context/instance.js";
|
import { useInstanceContext } from "../../../../context/instance.js";
|
||||||
import { MerchantBackend } from "../../../../declaration.js";
|
import { MerchantBackend } from "../../../../declaration.js";
|
||||||
|
import { useBackendContext } from "../../../../context/backend.js";
|
||||||
|
|
||||||
type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
|
type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
|
||||||
|
|
||||||
@ -38,9 +38,9 @@ export function CreatedSuccessfully({
|
|||||||
onConfirm,
|
onConfirm,
|
||||||
}: Props): VNode {
|
}: Props): VNode {
|
||||||
const { i18n } = useTranslationContext();
|
const { i18n } = useTranslationContext();
|
||||||
const backend = useBackendContext();
|
const { url: backendURL } = useBackendContext()
|
||||||
const { id: instanceId } = useInstanceContext();
|
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 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)}...`;
|
const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`;
|
||||||
|
|
||||||
|
@ -18,12 +18,301 @@
|
|||||||
*
|
*
|
||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @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 {
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
@ -13,7 +13,7 @@ function getBrowserLang(): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Settings(): VNode {
|
export function Settings({ onClose }: { onClose?: () => void }): VNode {
|
||||||
const { i18n } = useTranslationContext()
|
const { i18n } = useTranslationContext()
|
||||||
const borwserLang = getBrowserLang()
|
const borwserLang = getBrowserLang()
|
||||||
const { update } = useLang()
|
const { update } = useLang()
|
||||||
@ -94,11 +94,19 @@ export function Settings(): VNode {
|
|||||||
/>
|
/>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="column" />
|
<div class="column" />
|
||||||
</div>
|
</div>
|
||||||
|
</section >
|
||||||
|
{onClose &&
|
||||||
|
<section class="section is-main-section">
|
||||||
|
<button
|
||||||
|
class="button"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<i18n.Translate>Close</i18n.Translate>
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
}
|
||||||
|
</div >
|
||||||
}
|
}
|
@ -565,7 +565,7 @@ class BankServiceBase {
|
|||||||
protected globalTestState: GlobalTestState,
|
protected globalTestState: GlobalTestState,
|
||||||
protected bankConfig: BankConfig,
|
protected bankConfig: BankConfig,
|
||||||
protected configFile: string,
|
protected configFile: string,
|
||||||
) {}
|
) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HarnessExchangeBankAccount {
|
export interface HarnessExchangeBankAccount {
|
||||||
@ -580,8 +580,7 @@ export interface HarnessExchangeBankAccount {
|
|||||||
*/
|
*/
|
||||||
export class FakebankService
|
export class FakebankService
|
||||||
extends BankServiceBase
|
extends BankServiceBase
|
||||||
implements BankServiceHandle
|
implements BankServiceHandle {
|
||||||
{
|
|
||||||
proc: ProcessWrapper | undefined;
|
proc: ProcessWrapper | undefined;
|
||||||
|
|
||||||
http = createPlatformHttpLib({ enableThrottling: false });
|
http = createPlatformHttpLib({ enableThrottling: false });
|
||||||
@ -790,7 +789,7 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
|
|
||||||
async runWirewatchOnce() {
|
async runWirewatchOnce() {
|
||||||
if (useLibeufinBank) {
|
if (useLibeufinBank) {
|
||||||
// Not even 2 secods showed to be enough!
|
// Not even 2 seconds showed to be enough!
|
||||||
await waitMs(4000);
|
await waitMs(4000);
|
||||||
}
|
}
|
||||||
await runCommand(
|
await runCommand(
|
||||||
@ -1013,7 +1012,7 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
private exchangeConfig: ExchangeConfig,
|
private exchangeConfig: ExchangeConfig,
|
||||||
private configFilename: string,
|
private configFilename: string,
|
||||||
private keyPair: EddsaKeyPair,
|
private keyPair: EddsaKeyPair,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return this.exchangeConfig.name;
|
return this.exchangeConfig.name;
|
||||||
@ -1369,7 +1368,7 @@ export class MerchantService implements MerchantServiceInterface {
|
|||||||
private globalState: GlobalTestState,
|
private globalState: GlobalTestState,
|
||||||
private merchantConfig: MerchantConfig,
|
private merchantConfig: MerchantConfig,
|
||||||
private configFilename: string,
|
private configFilename: string,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
private currentTimetravelOffsetMs: number | undefined;
|
private currentTimetravelOffsetMs: number | undefined;
|
||||||
|
|
||||||
@ -1707,7 +1706,7 @@ export class WalletService {
|
|||||||
constructor(
|
constructor(
|
||||||
private globalState: GlobalTestState,
|
private globalState: GlobalTestState,
|
||||||
private opts: WalletServiceOptions,
|
private opts: WalletServiceOptions,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
get socketPath() {
|
get socketPath() {
|
||||||
const unixPath = path.join(
|
const unixPath = path.join(
|
||||||
@ -1816,7 +1815,7 @@ export class WalletClient {
|
|||||||
return client.call(operation, payload);
|
return client.call(operation, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private args: WalletClientArgs) {}
|
constructor(private args: WalletClientArgs) { }
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
const waiter = this.waiter;
|
const waiter = this.waiter;
|
||||||
@ -1883,10 +1882,8 @@ export class WalletCli {
|
|||||||
? `--crypto-worker=${cliOpts.cryptoWorkerType}`
|
? `--crypto-worker=${cliOpts.cryptoWorkerType}`
|
||||||
: "";
|
: "";
|
||||||
const logName = `wallet-${self.name}`;
|
const logName = `wallet-${self.name}`;
|
||||||
const command = `taler-wallet-cli ${
|
const command = `taler-wallet-cli ${self.timetravelArg ?? ""
|
||||||
self.timetravelArg ?? ""
|
} ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${self.dbfile
|
||||||
} ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${
|
|
||||||
self.dbfile
|
|
||||||
}' api '${op}' ${shellWrap(JSON.stringify(payload))}`;
|
}' api '${op}' ${shellWrap(JSON.stringify(payload))}`;
|
||||||
const resp = await sh(self.globalTestState, logName, command);
|
const resp = await sh(self.globalTestState, logName, command);
|
||||||
logger.info("--- wallet core response ---");
|
logger.info("--- wallet core response ---");
|
||||||
|
@ -251,7 +251,7 @@ export interface NexusTask {
|
|||||||
taskCronSpec: string;
|
taskCronSpec: string;
|
||||||
// Only meaningful for "fetch" types.
|
// Only meaningful for "fetch" types.
|
||||||
taskParams: FetchParams;
|
taskParams: FetchParams;
|
||||||
// Timestamp in secons when the next iteration will run.
|
// Timestamp in seconds when the next iteration will run.
|
||||||
nextScheduledExecutionSec: number;
|
nextScheduledExecutionSec: number;
|
||||||
// Timestamp in seconds when the previous iteration ran.
|
// Timestamp in seconds when the previous iteration ran.
|
||||||
prevScheduledExecutionSec: number;
|
prevScheduledExecutionSec: number;
|
||||||
|
@ -193,7 +193,6 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
|
|||||||
});
|
});
|
||||||
console.log(exc);
|
console.log(exc);
|
||||||
t.assertTrue(exc.errorDetail.httpStatusCode === 401);
|
t.assertTrue(exc.errorDetail.httpStatusCode === 401);
|
||||||
t.assertDeepEqual(exc.response?.status, 401);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { codecForAny } from "./codec.js";
|
||||||
import {
|
import {
|
||||||
createPlatformHttpLib,
|
createPlatformHttpLib,
|
||||||
expectSuccessResponseOrThrow,
|
expectSuccessResponseOrThrow,
|
||||||
@ -221,7 +222,7 @@ export class MerchantApiClient {
|
|||||||
const resp = await this.httpClient.fetch(url.href, {
|
const resp = await this.httpClient.fetch(url.href, {
|
||||||
headers: this.makeAuthHeader(),
|
headers: this.makeAuthHeader(),
|
||||||
});
|
});
|
||||||
return resp.json();
|
return readSuccessResponseJsonOrThrow(resp, codecForAny());
|
||||||
}
|
}
|
||||||
|
|
||||||
async getInstanceFullDetails(instanceId: string): Promise<any> {
|
async getInstanceFullDetails(instanceId: string): Promise<any> {
|
||||||
|
@ -392,7 +392,7 @@ function csKdfMod(
|
|||||||
// Newer versions of node have TextEncoder and TextDecoder as a global,
|
// Newer versions of node have TextEncoder and TextDecoder as a global,
|
||||||
// just like modern browsers.
|
// just like modern browsers.
|
||||||
// In older versions of node or environments that do not have these
|
// 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.
|
// before stringToBytes or bytesToString is called the first time.
|
||||||
|
|
||||||
let encoder: any;
|
let encoder: any;
|
||||||
@ -693,7 +693,7 @@ export async function csBlind(
|
|||||||
* Unblind operation to unblind the signature
|
* Unblind operation to unblind the signature
|
||||||
* @param bseed seed to derive secrets
|
* @param bseed seed to derive secrets
|
||||||
* @param rPub public R received from /csr
|
* @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 b returned from exchange to select c
|
||||||
* @param csSig blinded signature
|
* @param csSig blinded signature
|
||||||
* @returns unblinded signature
|
* @returns unblinded signature
|
||||||
@ -721,7 +721,7 @@ export async function csUnblind(
|
|||||||
* Verification algorithm for CS signatures
|
* Verification algorithm for CS signatures
|
||||||
* @param hm message signed
|
* @param hm message signed
|
||||||
* @param csSig unblinded signature
|
* @param csSig unblinded signature
|
||||||
* @param csPub denomination publick key
|
* @param csPub denomination public key
|
||||||
* @returns true if valid, false if invalid
|
* @returns true if valid, false if invalid
|
||||||
*/
|
*/
|
||||||
export async function csVerify(
|
export async function csVerify(
|
||||||
@ -844,8 +844,7 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
|
|||||||
return hash(uint8ArrayBuf);
|
return hash(uint8ArrayBuf);
|
||||||
} else {
|
} else {
|
||||||
throw Error(
|
throw Error(
|
||||||
`unsupported cipher (${
|
`unsupported cipher (${(pub as DenominationPubKey).cipher
|
||||||
(pub as DenominationPubKey).cipher
|
|
||||||
}), unable to hash`,
|
}), unable to hash`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1023,7 +1022,7 @@ export enum WalletAccountMergeFlags {
|
|||||||
export class SignaturePurposeBuilder {
|
export class SignaturePurposeBuilder {
|
||||||
private chunks: Uint8Array[] = [];
|
private chunks: Uint8Array[] = [];
|
||||||
|
|
||||||
constructor(private purposeNum: number) {}
|
constructor(private purposeNum: number) { }
|
||||||
|
|
||||||
put(bytes: Uint8Array): SignaturePurposeBuilder {
|
put(bytes: Uint8Array): SignaturePurposeBuilder {
|
||||||
this.chunks.push(Uint8Array.from(bytes));
|
this.chunks.push(Uint8Array.from(bytes));
|
||||||
|
@ -1972,42 +1972,58 @@ export interface ExchangeAgeWithdrawRevealResponse {
|
|||||||
ev_sigs : BlindedDenominationSignature[];
|
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
|
// Optional base URL of the exchange for looking up wire transfers
|
||||||
// associated with this transaction. If not given,
|
// associated with this transaction. If not given,
|
||||||
// the base URL is the same as the one used for this request.
|
// 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
|
// Can be used if the base URL for ``/transactions/`` differs from that
|
||||||
// for /coins/, i.e. for load balancing. Clients SHOULD
|
// for ``/coins/``, i.e. for load balancing. Clients SHOULD
|
||||||
// respect the transaction_base_url if provided. Any HTTP server
|
// respect the ``transaction_base_url`` if provided. Any HTTP server
|
||||||
// belonging to an exchange MUST generate a 307 or 308 redirection
|
// belonging to an exchange MUST generate a 307 or 308 redirection
|
||||||
// to the correct base URL should a client uses the wrong base
|
// to the correct base URL should a client uses the wrong base
|
||||||
// URL, or if the base URL has changed since the deposit.
|
// URL, or if the base URL has changed since the deposit.
|
||||||
transaction_base_url?: string;
|
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;
|
exchange_timestamp: TalerProtocolTimestamp;
|
||||||
|
|
||||||
// the EdDSA signature of TALER_DepositConfirmationPS using a current
|
// `Public EdDSA key of the exchange <sign-key-pub>` that was used to
|
||||||
// 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
|
|
||||||
// generate the signature.
|
// 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
|
// explicitly as the client might otherwise be confused by clock skew as to
|
||||||
// which signing key was used.
|
// 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> =>
|
export const codecForDepositConfirmationSignature =
|
||||||
buildCodecForObject<DepositSuccess>()
|
(): Codec<DepositConfirmationSignature> =>
|
||||||
.property("exchange_pub", codecForString())
|
buildCodecForObject<DepositConfirmationSignature>()
|
||||||
.property("exchange_sig", codecForString())
|
.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("exchange_timestamp", codecForTimestamp)
|
||||||
.property("transaction_base_url", codecOptional(codecForString()))
|
.property("transaction_base_url", codecOptional(codecForString()))
|
||||||
.build("DepositSuccess");
|
.build("BatchDepositSuccess");
|
||||||
|
|
||||||
export interface TrackTransactionWired {
|
export interface TrackTransactionWired {
|
||||||
// Raw wire transfer identifier of the deposit.
|
// Raw wire transfer identifier of the deposit.
|
||||||
@ -2231,6 +2247,9 @@ export interface ExchangePurseDeposits {
|
|||||||
deposits: PurseDeposit[];
|
deposits: PurseDeposit[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated batch deposit should be used.
|
||||||
|
*/
|
||||||
export interface ExchangeDepositRequest {
|
export interface ExchangeDepositRequest {
|
||||||
// Amount to be deposited, can be a fraction of the
|
// Amount to be deposited, can be a fraction of the
|
||||||
// coin's total value.
|
// coin's total value.
|
||||||
@ -2293,6 +2312,67 @@ export interface ExchangeDepositRequest {
|
|||||||
h_age_commitment?: string;
|
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 {
|
export interface WalletKycUuid {
|
||||||
// UUID that the wallet should use when initiating
|
// UUID that the wallet should use when initiating
|
||||||
// the KYC check.
|
// the KYC check.
|
||||||
|
@ -57,7 +57,9 @@ import {
|
|||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
ExchangeAuditor,
|
ExchangeAuditor,
|
||||||
|
InternationalizedString,
|
||||||
MerchantContractTerms,
|
MerchantContractTerms,
|
||||||
|
MerchantInfo,
|
||||||
PeerContractTerms,
|
PeerContractTerms,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
codecForMerchantContractTerms,
|
codecForMerchantContractTerms,
|
||||||
@ -2667,3 +2669,49 @@ export const codecForTestingSetTimetravelRequest =
|
|||||||
buildCodecForObject<TestingSetTimetravelRequest>()
|
buildCodecForObject<TestingSetTimetravelRequest>()
|
||||||
.property("offsetMs", codecForNumber())
|
.property("offsetMs", codecForNumber())
|
||||||
.build("TestingSetTimetravelRequest");
|
.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;
|
||||||
|
}
|
||||||
|
@ -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
|
ln -sf $(install_target)/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs $(prefix)/bin/taler-wallet-cli
|
||||||
deps:
|
deps:
|
||||||
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli...
|
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli...
|
||||||
|
pnpm run --filter @gnu-taler/taler-wallet-cli... compile
|
||||||
install:
|
install:
|
||||||
$(MAKE) deps
|
$(MAKE) deps
|
||||||
$(MAKE) install-nodeps
|
$(MAKE) install-nodeps
|
||||||
|
@ -1042,52 +1042,6 @@ export enum RefundReason {
|
|||||||
AbortRefund = "abort-pay-refund",
|
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 {
|
export enum PurchaseStatus {
|
||||||
/**
|
/**
|
||||||
* Not downloaded yet.
|
* Not downloaded yet.
|
||||||
@ -1710,6 +1664,8 @@ export interface DepositGroupRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Verbatim contract terms.
|
* Verbatim contract terms.
|
||||||
|
*
|
||||||
|
* FIXME: Move this to the contract terms object store!
|
||||||
*/
|
*/
|
||||||
contractTermsRaw: MerchantContractTerms;
|
contractTermsRaw: MerchantContractTerms;
|
||||||
|
|
||||||
|
@ -50,6 +50,8 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
AllowedAuditorInfo,
|
||||||
|
AllowedExchangeInfo,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
@ -60,17 +62,15 @@ import {
|
|||||||
ScopeType,
|
ScopeType,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
AllowedAuditorInfo,
|
|
||||||
AllowedExchangeInfo,
|
|
||||||
RefreshGroupRecord,
|
RefreshGroupRecord,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
WithdrawalGroupStatus,
|
WithdrawalGroupStatus,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
import { checkLogicInvariant } from "../util/invariants.js";
|
import { checkLogicInvariant } from "../util/invariants.js";
|
||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
|
@ -21,70 +21,69 @@ import {
|
|||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
|
BatchDepositRequestCoin,
|
||||||
CancellationToken,
|
CancellationToken,
|
||||||
canonicalJson,
|
|
||||||
codecForDepositSuccess,
|
|
||||||
codecForTackTransactionAccepted,
|
|
||||||
codecForTackTransactionWired,
|
|
||||||
CoinRefreshRequest,
|
CoinRefreshRequest,
|
||||||
CreateDepositGroupRequest,
|
CreateDepositGroupRequest,
|
||||||
CreateDepositGroupResponse,
|
CreateDepositGroupResponse,
|
||||||
DepositGroupFees,
|
DepositGroupFees,
|
||||||
durationFromSpec,
|
Duration,
|
||||||
encodeCrock,
|
ExchangeBatchDepositRequest,
|
||||||
ExchangeDepositRequest,
|
|
||||||
ExchangeRefundRequest,
|
ExchangeRefundRequest,
|
||||||
getRandomBytes,
|
|
||||||
hashTruncate32,
|
|
||||||
hashWire,
|
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
j2s,
|
|
||||||
Logger,
|
Logger,
|
||||||
MerchantContractTerms,
|
MerchantContractTerms,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
parsePaytoUri,
|
|
||||||
PayCoinSelection,
|
PayCoinSelection,
|
||||||
PrepareDepositRequest,
|
PrepareDepositRequest,
|
||||||
PrepareDepositResponse,
|
PrepareDepositResponse,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
stringToBytes,
|
TalerError,
|
||||||
TalerErrorCode,
|
TalerErrorCode,
|
||||||
TalerProtocolTimestamp,
|
|
||||||
TalerPreciseTimestamp,
|
TalerPreciseTimestamp,
|
||||||
|
TalerProtocolTimestamp,
|
||||||
TrackTransaction,
|
TrackTransaction,
|
||||||
|
TransactionAction,
|
||||||
TransactionMajorState,
|
TransactionMajorState,
|
||||||
TransactionMinorState,
|
TransactionMinorState,
|
||||||
TransactionState,
|
TransactionState,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
WireFee,
|
WireFee,
|
||||||
TransactionAction,
|
canonicalJson,
|
||||||
Duration,
|
codecForBatchDepositSuccess,
|
||||||
|
codecForTackTransactionAccepted,
|
||||||
|
codecForTackTransactionWired,
|
||||||
|
durationFromSpec,
|
||||||
|
encodeCrock,
|
||||||
|
getRandomBytes,
|
||||||
|
hashTruncate32,
|
||||||
|
hashWire,
|
||||||
|
j2s,
|
||||||
|
parsePaytoUri,
|
||||||
|
stringToBytes,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
|
||||||
|
import { DepositElementStatus, DepositGroupRecord } from "../db.js";
|
||||||
import {
|
import {
|
||||||
DenominationRecord,
|
|
||||||
DepositGroupRecord,
|
|
||||||
DepositElementStatus,
|
|
||||||
} from "../db.js";
|
|
||||||
import { TalerError } from "@gnu-taler/taler-util";
|
|
||||||
import {
|
|
||||||
createRefreshGroup,
|
|
||||||
DepositOperationStatus,
|
DepositOperationStatus,
|
||||||
DepositTrackingInfo,
|
DepositTrackingInfo,
|
||||||
getTotalRefreshCost,
|
|
||||||
KycPendingInfo,
|
KycPendingInfo,
|
||||||
KycUserType,
|
|
||||||
PendingTaskType,
|
PendingTaskType,
|
||||||
RefreshOperationStatus,
|
RefreshOperationStatus,
|
||||||
|
createRefreshGroup,
|
||||||
|
getTotalRefreshCost,
|
||||||
} from "../index.js";
|
} from "../index.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.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 {
|
import {
|
||||||
constructTaskIdentifier,
|
|
||||||
TaskRunResult,
|
TaskRunResult,
|
||||||
|
TombstoneTag,
|
||||||
|
constructTaskIdentifier,
|
||||||
runLongpollAsync,
|
runLongpollAsync,
|
||||||
spendCoins,
|
spendCoins,
|
||||||
TombstoneTag,
|
|
||||||
} from "./common.js";
|
} from "./common.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import {
|
import {
|
||||||
@ -92,15 +91,12 @@ import {
|
|||||||
generateDepositPermissions,
|
generateDepositPermissions,
|
||||||
getTotalPaymentCost,
|
getTotalPaymentCost,
|
||||||
} from "./pay-merchant.js";
|
} from "./pay-merchant.js";
|
||||||
import { selectPayCoinsNew } from "../util/coinSelection.js";
|
|
||||||
import {
|
import {
|
||||||
constructTransactionIdentifier,
|
constructTransactionIdentifier,
|
||||||
notifyTransition,
|
notifyTransition,
|
||||||
parseTransactionIdentifier,
|
parseTransactionIdentifier,
|
||||||
stopLongpolling,
|
stopLongpolling,
|
||||||
} from "./transactions.js";
|
} from "./transactions.js";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
|
||||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* 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(
|
export function computeDepositTransactionActions(
|
||||||
dg: DepositGroupRecord,
|
dg: DepositGroupRecord,
|
||||||
): TransactionAction[] {
|
): 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(
|
export async function suspendDepositGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
depositGroupId: string,
|
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
|
* Check whether the refresh associated with the
|
||||||
* aborting deposit group is done.
|
* aborting deposit group is done.
|
||||||
@ -940,38 +905,58 @@ async function processDepositGroupPendingDeposit(
|
|||||||
contractData,
|
contractData,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (let i = 0; i < depositPermissions.length; i++) {
|
// Exchanges involved in the deposit
|
||||||
const perm = depositPermissions[i];
|
const exchanges: Set<string> = new Set();
|
||||||
|
|
||||||
if (depositGroup.statusPerCoin[i] !== DepositElementStatus.DepositPending) {
|
for (const dp of depositPermissions) {
|
||||||
continue;
|
exchanges.add(dp.exchange_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestBody: ExchangeDepositRequest = {
|
// We need to do one batch per exchange.
|
||||||
contribution: Amounts.stringify(perm.contribution),
|
for (const exchangeUrl of exchanges.values()) {
|
||||||
merchant_payto_uri: depositGroup.wire.payto_uri,
|
const coins: BatchDepositRequestCoin[] = [];
|
||||||
wire_salt: depositGroup.wire.salt,
|
const batchIndexes: number[] = [];
|
||||||
|
|
||||||
|
const batchReq: ExchangeBatchDepositRequest = {
|
||||||
|
coins,
|
||||||
h_contract_terms: depositGroup.contractTermsHash,
|
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,
|
timestamp: depositGroup.contractTermsRaw.timestamp,
|
||||||
|
wire_salt: depositGroup.wire.salt,
|
||||||
wire_transfer_deadline:
|
wire_transfer_deadline:
|
||||||
depositGroup.contractTermsRaw.wire_transfer_deadline,
|
depositGroup.contractTermsRaw.wire_transfer_deadline,
|
||||||
refund_deadline: depositGroup.contractTermsRaw.refund_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.
|
// Check for cancellation before making network request.
|
||||||
cancellationToken?.throwIfCancelled();
|
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}`);
|
logger.info(`depositing to ${url}`);
|
||||||
const httpResp = await ws.http.fetch(url.href, {
|
const httpResp = await ws.http.fetch(url.href, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: requestBody,
|
body: batchReq,
|
||||||
cancellationToken: cancellationToken,
|
cancellationToken: cancellationToken,
|
||||||
});
|
});
|
||||||
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
await readSuccessResponseJsonOrThrow(
|
||||||
|
httpResp,
|
||||||
|
codecForBatchDepositSuccess(),
|
||||||
|
);
|
||||||
|
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => [x.depositGroups])
|
.mktx((x) => [x.depositGroups])
|
||||||
@ -980,12 +965,14 @@ async function processDepositGroupPendingDeposit(
|
|||||||
if (!dg) {
|
if (!dg) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const coinStatus = dg.statusPerCoin[i];
|
for (const batchIndex of batchIndexes) {
|
||||||
|
const coinStatus = dg.statusPerCoin[batchIndex];
|
||||||
switch (coinStatus) {
|
switch (coinStatus) {
|
||||||
case DepositElementStatus.DepositPending:
|
case DepositElementStatus.DepositPending:
|
||||||
dg.statusPerCoin[i] = DepositElementStatus.Tracking;
|
dg.statusPerCoin[batchIndex] = DepositElementStatus.Tracking;
|
||||||
await tx.depositGroups.put(dg);
|
await tx.depositGroups.put(dg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1538,10 +1525,7 @@ async function getTotalFeesForDepositAmount(
|
|||||||
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iter(coin.exchangeBaseUrl)
|
.iter(coin.exchangeBaseUrl)
|
||||||
.filter((x) =>
|
.filter((x) =>
|
||||||
Amounts.isSameCurrency(
|
Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
|
||||||
x.value,
|
|
||||||
pcs.coinContributions[i],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
const amountLeft = Amounts.sub(
|
const amountLeft = Amounts.sub(
|
||||||
denom.value,
|
denom.value,
|
||||||
|
@ -63,7 +63,6 @@ import {
|
|||||||
RefreshReason,
|
RefreshReason,
|
||||||
SharePaymentResult,
|
SharePaymentResult,
|
||||||
StartRefundQueryForUriResponse,
|
StartRefundQueryForUriResponse,
|
||||||
stringifyPaytoUri,
|
|
||||||
stringifyPayUri,
|
stringifyPayUri,
|
||||||
stringifyTalerUri,
|
stringifyTalerUri,
|
||||||
TalerError,
|
TalerError,
|
||||||
@ -78,6 +77,7 @@ import {
|
|||||||
TransactionState,
|
TransactionState,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
URL,
|
URL,
|
||||||
|
WalletContractData,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
getHttpResponseErrorDetails,
|
getHttpResponseErrorDetails,
|
||||||
@ -95,7 +95,6 @@ import {
|
|||||||
PurchaseRecord,
|
PurchaseRecord,
|
||||||
PurchaseStatus,
|
PurchaseStatus,
|
||||||
RefundReason,
|
RefundReason,
|
||||||
WalletContractData,
|
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import {
|
||||||
@ -115,15 +114,13 @@ import { checkDbInvariant } from "../util/invariants.js";
|
|||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
import { GetReadOnlyAccess } from "../util/query.js";
|
||||||
import {
|
import {
|
||||||
constructTaskIdentifier,
|
constructTaskIdentifier,
|
||||||
TaskRunResult,
|
|
||||||
TaskRunResultType,
|
|
||||||
RetryInfo,
|
RetryInfo,
|
||||||
TaskIdentifiers,
|
|
||||||
} from "./common.js";
|
|
||||||
import {
|
|
||||||
runLongpollAsync,
|
runLongpollAsync,
|
||||||
runTaskWithErrorReporting,
|
runTaskWithErrorReporting,
|
||||||
spendCoins,
|
spendCoins,
|
||||||
|
TaskIdentifiers,
|
||||||
|
TaskRunResult,
|
||||||
|
TaskRunResultType,
|
||||||
} from "./common.js";
|
} from "./common.js";
|
||||||
import {
|
import {
|
||||||
calculateRefreshOutput,
|
calculateRefreshOutput,
|
||||||
@ -173,10 +170,7 @@ export async function getTotalPaymentCost(
|
|||||||
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.iter(coin.exchangeBaseUrl)
|
.iter(coin.exchangeBaseUrl)
|
||||||
.filter((x) =>
|
.filter((x) =>
|
||||||
Amounts.isSameCurrency(
|
Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
|
||||||
x.value,
|
|
||||||
pcs.coinContributions[i],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
const amountLeft = Amounts.sub(
|
const amountLeft = Amounts.sub(
|
||||||
denom.value,
|
denom.value,
|
||||||
|
@ -41,6 +41,7 @@ import {
|
|||||||
TransactionsResponse,
|
TransactionsResponse,
|
||||||
TransactionState,
|
TransactionState,
|
||||||
TransactionType,
|
TransactionType,
|
||||||
|
WalletContractData,
|
||||||
WithdrawalType,
|
WithdrawalType,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
@ -60,7 +61,6 @@ import {
|
|||||||
RefreshOperationStatus,
|
RefreshOperationStatus,
|
||||||
RefundGroupRecord,
|
RefundGroupRecord,
|
||||||
RewardRecord,
|
RewardRecord,
|
||||||
WalletContractData,
|
|
||||||
WithdrawalGroupRecord,
|
WithdrawalGroupRecord,
|
||||||
WithdrawalGroupStatus,
|
WithdrawalGroupStatus,
|
||||||
WithdrawalRecordType,
|
WithdrawalRecordType,
|
||||||
|
@ -28,21 +28,20 @@ import {
|
|||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
AgeCommitmentProof,
|
AgeCommitmentProof,
|
||||||
AgeRestriction,
|
AgeRestriction,
|
||||||
|
AllowedAuditorInfo,
|
||||||
|
AllowedExchangeInfo,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
AmountLike,
|
AmountLike,
|
||||||
AmountResponse,
|
|
||||||
Amounts,
|
Amounts,
|
||||||
AmountString,
|
AmountString,
|
||||||
CoinPublicKeyString,
|
CoinPublicKeyString,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
ConvertAmountRequest,
|
|
||||||
DenominationInfo,
|
DenominationInfo,
|
||||||
DenominationPubKey,
|
DenominationPubKey,
|
||||||
DenomSelectionState,
|
DenomSelectionState,
|
||||||
Duration,
|
Duration,
|
||||||
ForcedCoinSel,
|
ForcedCoinSel,
|
||||||
ForcedDenomSel,
|
ForcedDenomSel,
|
||||||
GetAmountRequest,
|
|
||||||
j2s,
|
j2s,
|
||||||
Logger,
|
Logger,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
@ -50,24 +49,13 @@ import {
|
|||||||
PayMerchantInsufficientBalanceDetails,
|
PayMerchantInsufficientBalanceDetails,
|
||||||
PayPeerInsufficientBalanceDetails,
|
PayPeerInsufficientBalanceDetails,
|
||||||
strcmp,
|
strcmp,
|
||||||
TransactionAmountMode,
|
|
||||||
TransactionType,
|
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { DenominationRecord } from "../db.js";
|
||||||
import {
|
import {
|
||||||
AllowedAuditorInfo,
|
|
||||||
AllowedExchangeInfo,
|
|
||||||
DenominationRecord,
|
|
||||||
} from "../db.js";
|
|
||||||
import {
|
|
||||||
DbReadOnlyTransaction,
|
|
||||||
getExchangeDetails,
|
getExchangeDetails,
|
||||||
GetReadOnlyAccess,
|
|
||||||
GetReadWriteAccess,
|
|
||||||
isWithdrawableDenom,
|
isWithdrawableDenom,
|
||||||
StoreNames,
|
|
||||||
WalletDbReadOnlyTransaction,
|
WalletDbReadOnlyTransaction,
|
||||||
WalletStoresV1,
|
|
||||||
} from "../index.js";
|
} from "../index.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import {
|
import {
|
||||||
|
@ -38,7 +38,6 @@ import {
|
|||||||
ApplyDevExperimentRequest,
|
ApplyDevExperimentRequest,
|
||||||
BackupRecovery,
|
BackupRecovery,
|
||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
FailTransactionRequest,
|
|
||||||
CheckPeerPullCreditRequest,
|
CheckPeerPullCreditRequest,
|
||||||
CheckPeerPullCreditResponse,
|
CheckPeerPullCreditResponse,
|
||||||
CheckPeerPushDebitRequest,
|
CheckPeerPushDebitRequest,
|
||||||
@ -51,14 +50,19 @@ import {
|
|||||||
ConvertAmountRequest,
|
ConvertAmountRequest,
|
||||||
CreateDepositGroupRequest,
|
CreateDepositGroupRequest,
|
||||||
CreateDepositGroupResponse,
|
CreateDepositGroupResponse,
|
||||||
|
CreateStoredBackupResponse,
|
||||||
|
DeleteStoredBackupRequest,
|
||||||
DeleteTransactionRequest,
|
DeleteTransactionRequest,
|
||||||
ExchangeDetailedResponse,
|
ExchangeDetailedResponse,
|
||||||
ExchangesListResponse,
|
ExchangesListResponse,
|
||||||
|
FailTransactionRequest,
|
||||||
ForceRefreshRequest,
|
ForceRefreshRequest,
|
||||||
ForgetKnownBankAccountsRequest,
|
ForgetKnownBankAccountsRequest,
|
||||||
GetAmountRequest,
|
GetAmountRequest,
|
||||||
GetBalanceDetailRequest,
|
GetBalanceDetailRequest,
|
||||||
GetContractTermsDetailsRequest,
|
GetContractTermsDetailsRequest,
|
||||||
|
GetCurrencyInfoRequest,
|
||||||
|
GetCurrencyInfoResponse,
|
||||||
GetExchangeTosRequest,
|
GetExchangeTosRequest,
|
||||||
GetExchangeTosResult,
|
GetExchangeTosResult,
|
||||||
GetPlanForOperationRequest,
|
GetPlanForOperationRequest,
|
||||||
@ -85,16 +89,21 @@ import {
|
|||||||
PreparePeerPushCreditRequest,
|
PreparePeerPushCreditRequest,
|
||||||
PreparePeerPushCreditResponse,
|
PreparePeerPushCreditResponse,
|
||||||
PrepareRefundRequest,
|
PrepareRefundRequest,
|
||||||
PrepareRewardRequest as PrepareRewardRequest,
|
PrepareRewardRequest,
|
||||||
PrepareTipResult as PrepareRewardResult,
|
PrepareTipResult as PrepareRewardResult,
|
||||||
|
RecoverStoredBackupRequest,
|
||||||
RecoveryLoadRequest,
|
RecoveryLoadRequest,
|
||||||
RetryTransactionRequest,
|
RetryTransactionRequest,
|
||||||
SetCoinSuspendedRequest,
|
SetCoinSuspendedRequest,
|
||||||
SetWalletDeviceIdRequest,
|
SetWalletDeviceIdRequest,
|
||||||
|
SharePaymentRequest,
|
||||||
|
SharePaymentResult,
|
||||||
StartRefundQueryForUriResponse,
|
StartRefundQueryForUriResponse,
|
||||||
StartRefundQueryRequest,
|
StartRefundQueryRequest,
|
||||||
|
StoredBackupList,
|
||||||
TestPayArgs,
|
TestPayArgs,
|
||||||
TestPayResult,
|
TestPayResult,
|
||||||
|
TestingSetTimetravelRequest,
|
||||||
Transaction,
|
Transaction,
|
||||||
TransactionByIdRequest,
|
TransactionByIdRequest,
|
||||||
TransactionsRequest,
|
TransactionsRequest,
|
||||||
@ -106,22 +115,13 @@ import {
|
|||||||
UserAttentionsResponse,
|
UserAttentionsResponse,
|
||||||
ValidateIbanRequest,
|
ValidateIbanRequest,
|
||||||
ValidateIbanResponse,
|
ValidateIbanResponse,
|
||||||
|
WalletContractData,
|
||||||
WalletCoreVersion,
|
WalletCoreVersion,
|
||||||
WalletCurrencyInfo,
|
WalletCurrencyInfo,
|
||||||
WithdrawFakebankRequest,
|
WithdrawFakebankRequest,
|
||||||
WithdrawTestBalanceRequest,
|
WithdrawTestBalanceRequest,
|
||||||
WithdrawUriInfoResponse,
|
WithdrawUriInfoResponse,
|
||||||
SharePaymentRequest,
|
|
||||||
SharePaymentResult,
|
|
||||||
GetCurrencyInfoRequest,
|
|
||||||
GetCurrencyInfoResponse,
|
|
||||||
StoredBackupList,
|
|
||||||
CreateStoredBackupResponse,
|
|
||||||
RecoverStoredBackupRequest,
|
|
||||||
DeleteStoredBackupRequest,
|
|
||||||
TestingSetTimetravelRequest,
|
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { WalletContractData } from "./db.js";
|
|
||||||
import {
|
import {
|
||||||
AddBackupProviderRequest,
|
AddBackupProviderRequest,
|
||||||
AddBackupProviderResponse,
|
AddBackupProviderResponse,
|
||||||
|
@ -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.
|
* browser extension interface.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -18,8 +18,6 @@
|
|||||||
*
|
*
|
||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WalletContractData } from "@gnu-taler/taler-wallet-core";
|
|
||||||
import * as tests from "@gnu-taler/web-util/testing";
|
import * as tests from "@gnu-taler/web-util/testing";
|
||||||
import {
|
import {
|
||||||
ErrorView,
|
ErrorView,
|
||||||
@ -27,6 +25,7 @@ import {
|
|||||||
LoadingView,
|
LoadingView,
|
||||||
ShowView,
|
ShowView,
|
||||||
} from "./ShowFullContractTermPopup.js";
|
} from "./ShowFullContractTermPopup.js";
|
||||||
|
import { WalletContractData } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "ShowFullContractTermPopup",
|
title: "ShowFullContractTermPopup",
|
||||||
|
@ -13,11 +13,13 @@
|
|||||||
You should have received a copy of the GNU General Public License along with
|
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/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
import { AbsoluteTime, Duration, Location } from "@gnu-taler/taler-util";
|
|
||||||
import {
|
import {
|
||||||
WalletApiOperation,
|
AbsoluteTime,
|
||||||
|
Duration,
|
||||||
|
Location,
|
||||||
WalletContractData,
|
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 { styled } from "@linaria/react";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { Fragment, h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
|
@ -25,6 +25,8 @@ export enum ErrorType {
|
|||||||
UNEXPECTED,
|
UNEXPECTED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param baseUrl URL where the service is located
|
* @param baseUrl URL where the service is located
|
||||||
@ -60,10 +62,27 @@ export async function defaultRequestHandler<T>(
|
|||||||
const requestPreventCache = options.preventCache ?? false;
|
const requestPreventCache = options.preventCache ?? false;
|
||||||
const requestPreventCors = options.preventCors ?? 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]) => {
|
Object.entries(requestParams).forEach(([key, value]) => {
|
||||||
_url.searchParams.set(key, String(value));
|
validURL.searchParams.set(key, String(value));
|
||||||
});
|
});
|
||||||
|
|
||||||
let payload: BodyInit | undefined = undefined;
|
let payload: BodyInit | undefined = undefined;
|
||||||
@ -77,7 +96,20 @@ export async function defaultRequestHandler<T>(
|
|||||||
} else if (typeof requestBody === "object") {
|
} else if (typeof requestBody === "object") {
|
||||||
payload = JSON.stringify(requestBody);
|
payload = JSON.stringify(requestBody);
|
||||||
} else {
|
} 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;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(_url.href, {
|
response = await fetch(validURL.href, {
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
method: requestMethod,
|
method: requestMethod,
|
||||||
credentials: "omit",
|
credentials: "omit",
|
||||||
@ -100,15 +132,29 @@ export async function defaultRequestHandler<T>(
|
|||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const info: RequestInfo = {
|
const info: RequestInfo = {
|
||||||
payload,
|
payload,
|
||||||
url: _url.href,
|
url: validURL.href,
|
||||||
hasToken: !!options.token,
|
hasToken: !!options.token,
|
||||||
status: 0,
|
status: 0,
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (ex instanceof Error) {
|
||||||
|
if (ex.message === "HTTP_REQUEST_TIMEOUT") {
|
||||||
const error: HttpRequestTimeoutError = {
|
const error: HttpRequestTimeoutError = {
|
||||||
info,
|
info,
|
||||||
type: ErrorType.TIMEOUT,
|
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);
|
throw new RequestError(error);
|
||||||
}
|
}
|
||||||
@ -124,7 +170,7 @@ export async function defaultRequestHandler<T>(
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await buildRequestOk<T>(
|
const result = await buildRequestOk<T>(
|
||||||
response,
|
response,
|
||||||
_url.href,
|
validURL.href,
|
||||||
payload,
|
payload,
|
||||||
!!options.token,
|
!!options.token,
|
||||||
options,
|
options,
|
||||||
@ -133,7 +179,7 @@ export async function defaultRequestHandler<T>(
|
|||||||
} else {
|
} else {
|
||||||
const dataTxt = await response.text();
|
const dataTxt = await response.text();
|
||||||
const error = buildRequestFailed(
|
const error = buildRequestFailed(
|
||||||
_url.href,
|
validURL.href,
|
||||||
dataTxt,
|
dataTxt,
|
||||||
response.status,
|
response.status,
|
||||||
payload,
|
payload,
|
||||||
@ -377,3 +423,12 @@ export function buildRequestFailed<ErrorDetail>(
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateURL(baseUrl: string, endpoint: string): URL | undefined {
|
||||||
|
try {
|
||||||
|
return new URL(`${baseUrl}${endpoint}`)
|
||||||
|
} catch (ex) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user