new login token
This commit is contained in:
parent
e2422b68eb
commit
8c20f4b279
@ -41,7 +41,8 @@ import {
|
||||
import { ConfigContextProvider } from "./context/config.js";
|
||||
import { useBackendConfig } from "./hooks/backend.js";
|
||||
import { strings } from "./i18n/strings.js";
|
||||
import LoginPage from "./paths/login/index.js";
|
||||
import { ConnectionPage, LoginPage } from "./paths/login/index.js";
|
||||
import { LoginToken } from "./declaration.js";
|
||||
|
||||
export function Application(): VNode {
|
||||
return (
|
||||
@ -59,25 +60,20 @@ export function Application(): VNode {
|
||||
* @returns
|
||||
*/
|
||||
function ApplicationStatusRoutes(): VNode {
|
||||
const { url, updateLoginStatus, triedToLog } = useBackendContext();
|
||||
const { url: backendURL, updateToken, changeBackend } = useBackendContext();
|
||||
const result = useBackendConfig();
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
const updateLoginInfoAndGoToRoot = (url: string, token?: string) => {
|
||||
updateLoginStatus(url, token);
|
||||
route("/");
|
||||
};
|
||||
|
||||
const { currency, version } = result.ok
|
||||
? result.data
|
||||
: { currency: "unknown", version: "unknown" };
|
||||
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
|
||||
|
||||
if (!triedToLog) {
|
||||
if (!backendURL) {
|
||||
return (
|
||||
<Fragment>
|
||||
<NotConnectedAppMenu title="Welcome!" />
|
||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||
<ConnectionPage onConfirm={changeBackend} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@ -91,7 +87,7 @@ function ApplicationStatusRoutes(): VNode {
|
||||
return (
|
||||
<Fragment>
|
||||
<NotConnectedAppMenu title="Login" />
|
||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||
<ConnectionPage onConfirm={changeBackend} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@ -109,7 +105,7 @@ function ApplicationStatusRoutes(): VNode {
|
||||
description: `Check your url`,
|
||||
}}
|
||||
/>
|
||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||
<ConnectionPage onConfirm={changeBackend} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@ -120,10 +116,10 @@ function ApplicationStatusRoutes(): VNode {
|
||||
notification={{
|
||||
message: i18n.str`Server response with an error code`,
|
||||
type: "ERROR",
|
||||
description: i18n.str`Got message ${result.message} from ${result.info?.url}`,
|
||||
description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
|
||||
}}
|
||||
/>
|
||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||
<ConnectionPage onConfirm={changeBackend} />
|
||||
</Fragment>;
|
||||
}
|
||||
if (result.type === ErrorType.UNREADABLE) {
|
||||
@ -133,10 +129,10 @@ function ApplicationStatusRoutes(): VNode {
|
||||
notification={{
|
||||
message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
|
||||
type: "ERROR",
|
||||
description: i18n.str`Got message ${result.message} from ${result.info?.url}`,
|
||||
description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
|
||||
}}
|
||||
/>
|
||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||
<ConnectionPage onConfirm={changeBackend} />
|
||||
</Fragment>;
|
||||
}
|
||||
return (
|
||||
@ -146,10 +142,10 @@ function ApplicationStatusRoutes(): VNode {
|
||||
notification={{
|
||||
message: i18n.str`Unexpected Error`,
|
||||
type: "ERROR",
|
||||
description: i18n.str`Got message ${result.message} from ${result.info?.url}`,
|
||||
description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
|
||||
}}
|
||||
/>
|
||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||
<ConnectionPage onConfirm={changeBackend} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@ -168,7 +164,7 @@ function ApplicationStatusRoutes(): VNode {
|
||||
description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`,
|
||||
}}
|
||||
/>
|
||||
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
|
||||
<ConnectionPage onConfirm={changeBackend} />
|
||||
</Fragment>
|
||||
|
||||
}
|
||||
|
@ -18,22 +18,23 @@
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||
import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { createHashHistory } from "history";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { Router, Route, route } from "preact-router";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { Fragment, VNode, h } from "preact";
|
||||
import { Route, Router, route } from "preact-router";
|
||||
import { useState } from "preact/hooks";
|
||||
import { InstanceRoutes } from "./InstanceRoutes.js";
|
||||
import {
|
||||
NotificationCard,
|
||||
NotYetReadyAppMenu,
|
||||
NotificationCard,
|
||||
} from "./components/menu/index.js";
|
||||
import { useBackendContext } from "./context/backend.js";
|
||||
import { LoginToken } from "./declaration.js";
|
||||
import { useBackendInstancesTestForAdmin } from "./hooks/backend.js";
|
||||
import { InstanceRoutes } from "./InstanceRoutes.js";
|
||||
import LoginPage from "./paths/login/index.js";
|
||||
import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
|
||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||
import { ConnectionPage, LoginPage } from "./paths/login/index.js";
|
||||
import { Settings } from "./paths/settings/index.js";
|
||||
import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
|
||||
|
||||
/**
|
||||
* Check if admin against /management/instances
|
||||
@ -41,15 +42,14 @@ import { Settings } from "./paths/settings/index.js";
|
||||
*/
|
||||
export function ApplicationReadyRoutes(): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const { url: backendURL, changeBackend } = useBackendContext()
|
||||
const [unauthorized, setUnauthorized] = useState(false)
|
||||
const {
|
||||
url: backendURL,
|
||||
updateLoginStatus: updateLoginStatus2,
|
||||
updateToken,
|
||||
} = useBackendContext();
|
||||
|
||||
function updateLoginStatus(url: string, token: string | undefined) {
|
||||
console.log("updateing", url, token)
|
||||
updateLoginStatus2(url, token)
|
||||
function updateLoginStatus(token: LoginToken | undefined) {
|
||||
updateToken(token)
|
||||
setUnauthorized(false)
|
||||
}
|
||||
|
||||
@ -59,15 +59,15 @@ export function ApplicationReadyRoutes(): VNode {
|
||||
route("/");
|
||||
};
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
// useEffect(() => {
|
||||
// setUnauthorized(FF)
|
||||
// }, [FF])
|
||||
const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized
|
||||
const unauthorizedAdmin = !result.loading
|
||||
&& !result.ok
|
||||
&& result.type === ErrorType.CLIENT
|
||||
&& result.status === HttpStatusCode.Unauthorized;
|
||||
|
||||
if (showSettings) {
|
||||
return <Fragment>
|
||||
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
||||
<Settings />
|
||||
<Settings onClose={() => setShowSettings(false)} />
|
||||
</Fragment>
|
||||
}
|
||||
|
||||
@ -100,7 +100,7 @@ export function ApplicationReadyRoutes(): VNode {
|
||||
type: "ERROR",
|
||||
}}
|
||||
/>
|
||||
<LoginPage onConfirm={updateLoginStatus} />
|
||||
<ConnectionPage onConfirm={changeBackend} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@ -108,14 +108,13 @@ export function ApplicationReadyRoutes(): VNode {
|
||||
instanceNameByBackendURL = match[1];
|
||||
}
|
||||
|
||||
console.log(unauthorized, unauthorizedAdmin)
|
||||
if (unauthorized || unauthorizedAdmin) {
|
||||
return <Fragment>
|
||||
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
|
||||
<NotificationCard
|
||||
notification={{
|
||||
message: i18n.str`Access denied`,
|
||||
description: i18n.str`Check your token is valid`,
|
||||
description: i18n.str`Check your token is valid 1`,
|
||||
type: "ERROR",
|
||||
}}
|
||||
/>
|
||||
@ -132,7 +131,6 @@ export function ApplicationReadyRoutes(): VNode {
|
||||
admin={admin}
|
||||
onUnauthorized={() => setUnauthorized(true)}
|
||||
onLoginPass={() => {
|
||||
console.log("ahora si")
|
||||
setUnauthorized(false)
|
||||
}}
|
||||
instanceNameByBackendURL={instanceNameByBackendURL}
|
||||
|
@ -35,7 +35,7 @@ import { InstanceContextProvider } from "./context/instance.js";
|
||||
import {
|
||||
useBackendDefaultToken,
|
||||
useBackendInstanceToken,
|
||||
useLocalStorage,
|
||||
useSimpleLocalStorage,
|
||||
} from "./hooks/index.js";
|
||||
import { useInstanceKYCDetails } from "./hooks/instance.js";
|
||||
import InstanceCreatePage from "./paths/admin/create/index.js";
|
||||
@ -71,10 +71,10 @@ import InstanceUpdatePage, {
|
||||
AdminUpdate as InstanceAdminUpdatePage,
|
||||
Props as InstanceUpdatePageProps,
|
||||
} from "./paths/instance/update/index.js";
|
||||
import LoginPage from "./paths/login/index.js";
|
||||
import { LoginPage } from "./paths/login/index.js";
|
||||
import NotFoundPage from "./paths/notfound/index.js";
|
||||
import { Notification } from "./utils/types.js";
|
||||
import { MerchantBackend } from "./declaration.js";
|
||||
import { LoginToken, MerchantBackend } from "./declaration.js";
|
||||
import { Settings } from "./paths/settings/index.js";
|
||||
import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
|
||||
|
||||
@ -143,7 +143,7 @@ export function InstanceRoutes({
|
||||
id,
|
||||
admin,
|
||||
path,
|
||||
onUnauthorized,
|
||||
// onUnauthorized,
|
||||
onLoginPass,
|
||||
setInstanceName,
|
||||
}: Props): VNode {
|
||||
@ -155,7 +155,7 @@ export function InstanceRoutes({
|
||||
const [globalNotification, setGlobalNotification] =
|
||||
useState<GlobalNotifState>(undefined);
|
||||
|
||||
const changeToken = (token?: string) => {
|
||||
const changeToken = (token?: LoginToken) => {
|
||||
if (admin) {
|
||||
updateToken(token);
|
||||
} else {
|
||||
@ -201,14 +201,17 @@ export function InstanceRoutes({
|
||||
|
||||
// const LoginPageAccessDeniend = onUnauthorized
|
||||
const LoginPageAccessDenied = () => {
|
||||
onUnauthorized()
|
||||
return <NotificationCard
|
||||
notification={{
|
||||
message: i18n.str`Access denied`,
|
||||
description: i18n.str`Redirecting to login page.`,
|
||||
type: "ERROR",
|
||||
}}
|
||||
/>
|
||||
return <Fragment>
|
||||
<NotificationCard
|
||||
notification={{
|
||||
message: i18n.str`Access denied`,
|
||||
description: i18n.str`Redirecting to login page.`,
|
||||
type: "ERROR",
|
||||
}}
|
||||
/>
|
||||
<LoginPage onConfirm={changeToken} />
|
||||
</Fragment>
|
||||
|
||||
}
|
||||
|
||||
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
|
||||
@ -687,9 +690,7 @@ function AdminInstanceUpdatePage({
|
||||
...rest
|
||||
}: { id: string } & InstanceUpdatePageProps): VNode {
|
||||
const [token, changeToken] = useBackendInstanceToken(id);
|
||||
const { updateLoginStatus: changeBackend } = useBackendContext();
|
||||
const updateLoginStatus = (url: string, token?: string): void => {
|
||||
changeBackend(url);
|
||||
const updateLoginStatus = (token?: LoginToken): void => {
|
||||
changeToken(token);
|
||||
};
|
||||
const value = useMemo(
|
||||
@ -752,7 +753,7 @@ function KycBanner(): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const [settings] = useSettings();
|
||||
const today = format(new Date(), dateFormatForSettings(settings));
|
||||
const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide");
|
||||
const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide");
|
||||
const hasBeenHidden = today === lastHide;
|
||||
const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
|
||||
if (hasBeenHidden || !needsToBeShown) return <Fragment />;
|
||||
|
@ -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;
|
||||
}): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const backend = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
return (
|
||||
<Fragment>
|
||||
{showId && (
|
||||
<InputWithAddon<Entity>
|
||||
name="id"
|
||||
addonBefore={`${backend.url}/instances/`}
|
||||
addonBefore={`${backendURL}/instances/`}
|
||||
readonly={readonlyId}
|
||||
label={i18n.str`Identifier`}
|
||||
tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`}
|
||||
|
@ -25,7 +25,6 @@ import { useBackendContext } from "../../context/backend.js";
|
||||
import { useConfigContext } from "../../context/config.js";
|
||||
import { useInstanceKYCDetails } from "../../hooks/instance.js";
|
||||
import { LangSelector } from "./LangSelector.js";
|
||||
import { useCredentialsChecker } from "../../hooks/backend.js";
|
||||
|
||||
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
|
||||
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
|
||||
@ -50,7 +49,7 @@ export function Sidebar({
|
||||
isPasswordOk
|
||||
}: Props): VNode {
|
||||
const config = useConfigContext();
|
||||
const backend = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
const { i18n } = useTranslationContext();
|
||||
const kycStatus = useInstanceKYCDetails();
|
||||
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
|
||||
@ -230,7 +229,7 @@ export function Sidebar({
|
||||
<i class="mdi mdi-web" />
|
||||
</span>
|
||||
<span class="menu-item-label">
|
||||
{new URL(backend.url).hostname}
|
||||
{new URL(backendURL).hostname}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -114,7 +114,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
|
||||
onSubscribe(hasErrors ? undefined : submit);
|
||||
}, [submit, hasErrors]);
|
||||
|
||||
const backend = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
return (
|
||||
@ -128,7 +128,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
|
||||
{alreadyExist ? undefined : (
|
||||
<InputWithAddon<Entity>
|
||||
name="product_id"
|
||||
addonBefore={`${backend.url}/product/`}
|
||||
addonBefore={`${backendURL}/product/`}
|
||||
label={i18n.str`ID`}
|
||||
tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
|
||||
/>
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
import * as tests from "@gnu-taler/web-util/testing";
|
||||
import { ComponentChildren, h, VNode } from "preact";
|
||||
import { MerchantBackend } from "../declaration.js";
|
||||
import { AccessToken, MerchantBackend } from "../declaration.js";
|
||||
import {
|
||||
useAdminAPI,
|
||||
useInstanceAPI,
|
||||
@ -64,7 +64,7 @@ describe("backend context api ", () => {
|
||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||
});
|
||||
|
||||
management.setNewToken("another_token");
|
||||
management.setNewToken("another_token" as AccessToken);
|
||||
},
|
||||
({ instance, management, admin }) => {
|
||||
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
||||
@ -113,7 +113,7 @@ describe("backend context api ", () => {
|
||||
name: "instance_name",
|
||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||
});
|
||||
instance.setNewToken("another_token");
|
||||
instance.setNewToken("another_token" as AccessToken);
|
||||
},
|
||||
({ instance, management, admin }) => {
|
||||
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
||||
|
@ -20,90 +20,46 @@
|
||||
*/
|
||||
|
||||
import { createContext, h, VNode } from "preact";
|
||||
import { useCallback, useContext, useState } from "preact/hooks";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { LoginToken } from "../declaration.js";
|
||||
import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
|
||||
|
||||
interface BackendContextType {
|
||||
url: string;
|
||||
token?: string;
|
||||
triedToLog: boolean;
|
||||
resetBackend: () => void;
|
||||
// clearAllTokens: () => void;
|
||||
// addTokenCleaner: (c: () => void) => void;
|
||||
updateLoginStatus: (url: string, token?: string) => void;
|
||||
updateToken: (token?: string) => void;
|
||||
url: string,
|
||||
token?: LoginToken;
|
||||
updateToken: (token: LoginToken | undefined) => void;
|
||||
changeBackend: (url: string) => void;
|
||||
}
|
||||
|
||||
const BackendContext = createContext<BackendContextType>({
|
||||
url: "",
|
||||
token: undefined,
|
||||
triedToLog: false,
|
||||
resetBackend: () => null,
|
||||
// clearAllTokens: () => null,
|
||||
// addTokenCleaner: () => null,
|
||||
updateLoginStatus: () => null,
|
||||
updateToken: () => null,
|
||||
changeBackend: () => null,
|
||||
});
|
||||
|
||||
function useBackendContextState(
|
||||
defaultUrl?: string,
|
||||
initialToken?: string,
|
||||
): BackendContextType {
|
||||
const [url, triedToLog, changeBackend, resetBackend] =
|
||||
useBackendURL(defaultUrl);
|
||||
const [token, _updateToken] = useBackendDefaultToken(initialToken);
|
||||
const updateToken = (t?: string) => {
|
||||
_updateToken(t);
|
||||
};
|
||||
|
||||
// const tokenCleaner = useCallback(() => {
|
||||
// updateToken(undefined);
|
||||
// }, []);
|
||||
// const [cleaners, setCleaners] = useState([tokenCleaner]);
|
||||
// const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]);
|
||||
// const addTokenCleanerMemo = useCallback(
|
||||
// (c: () => void) => {
|
||||
// addTokenCleaner(c);
|
||||
// },
|
||||
// [tokenCleaner],
|
||||
// );
|
||||
|
||||
// const clearAllTokens = () => {
|
||||
// cleaners.forEach((c) => c());
|
||||
// for (let i = 0; i < localStorage.length; i++) {
|
||||
// const k = localStorage.key(i);
|
||||
// if (k && /^backend-token/.test(k)) localStorage.removeItem(k);
|
||||
// }
|
||||
// resetBackend();
|
||||
// };
|
||||
|
||||
const updateLoginStatus = (url: string, token?: string) => {
|
||||
changeBackend(url);
|
||||
updateToken(token);
|
||||
};
|
||||
const [url, changeBackend] = useBackendURL(defaultUrl);
|
||||
const [token, updateToken] = useBackendDefaultToken();
|
||||
|
||||
return {
|
||||
url,
|
||||
token,
|
||||
triedToLog,
|
||||
updateLoginStatus,
|
||||
resetBackend,
|
||||
// clearAllTokens,
|
||||
updateToken,
|
||||
// addTokenCleaner: addTokenCleanerMemo,
|
||||
changeBackend
|
||||
};
|
||||
}
|
||||
|
||||
export const BackendContextProvider = ({
|
||||
children,
|
||||
defaultUrl,
|
||||
initialToken,
|
||||
}: {
|
||||
children: any;
|
||||
defaultUrl?: string;
|
||||
initialToken?: string;
|
||||
}): VNode => {
|
||||
const value = useBackendContextState(defaultUrl, initialToken);
|
||||
const value = useBackendContextState(defaultUrl);
|
||||
|
||||
return h(BackendContext.Provider, { value, children });
|
||||
};
|
||||
|
@ -21,12 +21,13 @@
|
||||
|
||||
import { createContext } from "preact";
|
||||
import { useContext } from "preact/hooks";
|
||||
import { LoginToken } from "../declaration.js";
|
||||
|
||||
interface Type {
|
||||
id: string;
|
||||
token?: string;
|
||||
token?: LoginToken;
|
||||
admin?: boolean;
|
||||
changeToken: (t?: string) => void;
|
||||
changeToken: (t?: LoginToken) => void;
|
||||
}
|
||||
|
||||
const Context = createContext<Type>({} as any);
|
||||
|
@ -107,6 +107,16 @@ interface RegexAccountRestriction {
|
||||
// human hints.
|
||||
human_hint_i18n?: { [lang_tag: string]: string };
|
||||
}
|
||||
interface LoginToken {
|
||||
token: string,
|
||||
expiration: Timestamp,
|
||||
}
|
||||
// token used to get loginToken
|
||||
// must forget after used
|
||||
declare const __ac_token: unique symbol;
|
||||
type AccessToken = string & {
|
||||
[__ac_token]: true;
|
||||
};
|
||||
|
||||
export namespace ExchangeBackend {
|
||||
interface WireResponse {
|
||||
@ -491,6 +501,35 @@ export namespace MerchantBackend {
|
||||
};
|
||||
}
|
||||
// DELETE /private/instances/$INSTANCE
|
||||
interface LoginTokenRequest {
|
||||
// Scope of the token (which kinds of operations it will allow)
|
||||
scope: "readonly" | "write";
|
||||
|
||||
// Server may impose its own upper bound
|
||||
// on the token validity duration
|
||||
duration?: RelativeTime;
|
||||
|
||||
// Can this token be refreshed?
|
||||
// Defaults to false.
|
||||
refreshable?: boolean;
|
||||
}
|
||||
interface LoginTokenSuccessResponse {
|
||||
// The login token that can be used to access resources
|
||||
// that are in scope for some time. Must be prefixed
|
||||
// with "Bearer " when used in the "Authorization" HTTP header.
|
||||
// Will already begin with the RFC 8959 prefix.
|
||||
token: string;
|
||||
|
||||
// Scope of the token (which kinds of operations it will allow)
|
||||
scope: "readonly" | "write";
|
||||
|
||||
// Server may impose its own upper bound
|
||||
// on the token validity duration
|
||||
expiration: Timestamp;
|
||||
|
||||
// Can this token be refreshed?
|
||||
refreshable: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
namespace KYC {
|
||||
|
@ -19,19 +19,21 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { useSWRConfig } from "swr";
|
||||
import { MerchantBackend } from "../declaration.js";
|
||||
import { useBackendContext } from "../context/backend.js";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { useInstanceContext } from "../context/instance.js";
|
||||
import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
ErrorType,
|
||||
HttpError,
|
||||
HttpResponse,
|
||||
HttpResponseOk,
|
||||
RequestError,
|
||||
RequestOptions,
|
||||
useApiContext,
|
||||
} from "@gnu-taler/web-util/browser";
|
||||
import { useApiContext } from "@gnu-taler/web-util/browser";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { useBackendContext } from "../context/backend.js";
|
||||
import { useInstanceContext } from "../context/instance.js";
|
||||
import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js";
|
||||
|
||||
|
||||
export function useMatchMutate(): (
|
||||
@ -85,6 +87,9 @@ export function useBackendInstancesTestForAdmin(): HttpResponse<
|
||||
return result;
|
||||
}
|
||||
|
||||
const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
|
||||
const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
|
||||
|
||||
export function useBackendConfig(): HttpResponse<
|
||||
MerchantBackend.VersionResponse,
|
||||
RequestError<MerchantBackend.ErrorDetail>
|
||||
@ -92,18 +97,33 @@ export function useBackendConfig(): HttpResponse<
|
||||
const { request } = useBackendBaseRequest();
|
||||
|
||||
type Type = MerchantBackend.VersionResponse;
|
||||
|
||||
const [result, setResult] = useState<
|
||||
HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>
|
||||
>({ loading: true });
|
||||
type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number }
|
||||
const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
request<Type>(`/config`)
|
||||
.then((data) => setResult(data))
|
||||
.catch((error) => setResult(error));
|
||||
if (result.timer) {
|
||||
clearTimeout(result.timer)
|
||||
}
|
||||
function tryConfig(): void {
|
||||
request<Type>(`/config`)
|
||||
.then((data) => {
|
||||
const timer: any = setTimeout(() => {
|
||||
tryConfig()
|
||||
}, CHECK_CONFIG_INTERVAL_OK)
|
||||
setResult({ data, timer })
|
||||
})
|
||||
.catch((error) => {
|
||||
const timer: any = setTimeout(() => {
|
||||
tryConfig()
|
||||
}, CHECK_CONFIG_INTERVAL_FAIL)
|
||||
const data = error.cause
|
||||
setResult({ data, timer })
|
||||
});
|
||||
}
|
||||
tryConfig()
|
||||
}, [request]);
|
||||
|
||||
return result;
|
||||
return result.data;
|
||||
}
|
||||
|
||||
interface useBackendInstanceRequestType {
|
||||
@ -149,32 +169,86 @@ interface useBackendBaseRequestType {
|
||||
}
|
||||
|
||||
type YesOrNo = "yes" | "no";
|
||||
type LoginResult = {
|
||||
valid: true;
|
||||
token: string;
|
||||
expiration: Timestamp;
|
||||
} | {
|
||||
valid: false;
|
||||
cause: HttpError<{}>;
|
||||
}
|
||||
|
||||
export function useCredentialsChecker() {
|
||||
const { request } = useApiContext();
|
||||
//check against instance details endpoint
|
||||
//while merchant backend doesn't have a login endpoint
|
||||
async function testLogin(
|
||||
instance: string,
|
||||
token: string,
|
||||
): Promise<{
|
||||
valid: boolean;
|
||||
cause?: ErrorType;
|
||||
}> {
|
||||
async function requestNewLoginToken(
|
||||
baseUrl: string,
|
||||
token: AccessToken,
|
||||
): Promise<LoginResult> {
|
||||
const data: MerchantBackend.Instances.LoginTokenRequest = {
|
||||
scope: "write",
|
||||
duration: {
|
||||
d_us: "forever"
|
||||
},
|
||||
refreshable: true,
|
||||
}
|
||||
try {
|
||||
const response = await request(instance, `/private/`, {
|
||||
const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, {
|
||||
method: "POST",
|
||||
token,
|
||||
data
|
||||
});
|
||||
return { valid: true };
|
||||
return { valid: true, token: response.data.token, expiration: response.data.expiration };
|
||||
} catch (error) {
|
||||
if (error instanceof RequestError) {
|
||||
return { valid: false, cause: error.cause.type };
|
||||
return { valid: false, cause: error.cause };
|
||||
}
|
||||
|
||||
return { valid: false, cause: ErrorType.UNEXPECTED };
|
||||
return {
|
||||
valid: false, cause: {
|
||||
type: ErrorType.UNEXPECTED,
|
||||
loading: false,
|
||||
info: {
|
||||
hasToken: true,
|
||||
status: 0,
|
||||
options: {},
|
||||
url: `/private/token`,
|
||||
payload: {}
|
||||
},
|
||||
exception: error,
|
||||
message: (error instanceof Error ? error.message : "unpexepected error")
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return testLogin
|
||||
|
||||
async function refreshLoginToken(
|
||||
baseUrl: string,
|
||||
token: LoginToken
|
||||
): Promise<LoginResult> {
|
||||
|
||||
if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) {
|
||||
return {
|
||||
valid: false, cause: {
|
||||
type: ErrorType.CLIENT,
|
||||
status: HttpStatusCode.Unauthorized,
|
||||
message: "login token expired, login again.",
|
||||
info: {
|
||||
hasToken: true,
|
||||
status: 401,
|
||||
options: {},
|
||||
url: `/private/token`,
|
||||
payload: {}
|
||||
},
|
||||
payload: {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return requestNewLoginToken(baseUrl, token.token as AccessToken)
|
||||
}
|
||||
return { requestNewLoginToken, refreshLoginToken }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -183,15 +257,20 @@ export function useCredentialsChecker() {
|
||||
* @returns request handler to
|
||||
*/
|
||||
export function useBackendBaseRequest(): useBackendBaseRequestType {
|
||||
const { url: backend, token } = useBackendContext();
|
||||
const { url: backend, token: loginToken } = useBackendContext();
|
||||
const { request: requestHandler } = useApiContext();
|
||||
const token = loginToken?.token;
|
||||
|
||||
const request = useCallback(
|
||||
function requestImpl<T>(
|
||||
endpoint: string,
|
||||
options: RequestOptions = {},
|
||||
): Promise<HttpResponseOk<T>> {
|
||||
return requestHandler<T>(backend, endpoint, { token, ...options });
|
||||
return requestHandler<T>(backend, endpoint, { token, ...options }).then(res => {
|
||||
return res
|
||||
}).catch(err => {
|
||||
throw err
|
||||
});
|
||||
},
|
||||
[backend, token],
|
||||
);
|
||||
@ -204,10 +283,12 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
|
||||
const { token: instanceToken, id, admin } = useInstanceContext();
|
||||
const { request: requestHandler } = useApiContext();
|
||||
|
||||
const { baseUrl, token } = !admin
|
||||
const { baseUrl, token: loginToken } = !admin
|
||||
? { baseUrl: rootBackendUrl, token: rootToken }
|
||||
: { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken };
|
||||
|
||||
const token = loginToken?.token;
|
||||
|
||||
const request = useCallback(
|
||||
function requestImpl<T>(
|
||||
endpoint: string,
|
||||
|
@ -19,9 +19,11 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { StateUpdater, useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util";
|
||||
import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
|
||||
import { StateUpdater, useEffect, useState } from "preact/hooks";
|
||||
import { LoginToken } from "../declaration.js";
|
||||
import { ValueOrFunction } from "../utils/types.js";
|
||||
import { useMemoryStorage } from "@gnu-taler/web-util/browser";
|
||||
import { useMatchMutate } from "./backend.js";
|
||||
|
||||
const calculateRootPath = () => {
|
||||
@ -32,53 +34,55 @@ const calculateRootPath = () => {
|
||||
return rootPath;
|
||||
};
|
||||
|
||||
const loginTokenCodec = buildCodecForObject<LoginToken>()
|
||||
.property("token", codecForString())
|
||||
.property("expiration", codecForTimestamp)
|
||||
.build("loginToken")
|
||||
const TOKENS_KEY = buildStorageKey("backend-token", codecForMap(loginTokenCodec));
|
||||
|
||||
|
||||
export function useBackendURL(
|
||||
url?: string,
|
||||
): [string, boolean, StateUpdater<string>, () => void] {
|
||||
const [value, setter] = useNotNullLocalStorage(
|
||||
): [string, StateUpdater<string>] {
|
||||
const [value, setter] = useSimpleLocalStorage(
|
||||
"backend-url",
|
||||
url || calculateRootPath(),
|
||||
);
|
||||
const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
|
||||
|
||||
const checkedSetter = (v: ValueOrFunction<string>) => {
|
||||
setTriedToLog("yes");
|
||||
return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
|
||||
return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""));
|
||||
};
|
||||
|
||||
const resetBackend = () => {
|
||||
setTriedToLog(undefined);
|
||||
};
|
||||
return [value, !!triedToLog, checkedSetter, resetBackend];
|
||||
return [value!, checkedSetter];
|
||||
}
|
||||
|
||||
export function useBackendDefaultToken(
|
||||
initialValue?: string,
|
||||
): [string | undefined, ((d: string | undefined) => void)] {
|
||||
// uncomment for testing
|
||||
initialValue = "secret-token:secret" as string | undefined
|
||||
const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue)
|
||||
): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
|
||||
const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
|
||||
|
||||
const tokenOfDefaultInstance = tokenMap["default"]
|
||||
const clearCache = useMatchMutate()
|
||||
useEffect(() => {
|
||||
clearCache()
|
||||
}, [token])
|
||||
}, [tokenOfDefaultInstance])
|
||||
|
||||
function updateToken(
|
||||
value: (string | undefined)
|
||||
value: (LoginToken | undefined)
|
||||
): void {
|
||||
if (value === undefined) {
|
||||
reset()
|
||||
} else {
|
||||
setToken(value)
|
||||
const res = { ...tokenMap, "default": value }
|
||||
setToken(res)
|
||||
}
|
||||
}
|
||||
return [token, updateToken];
|
||||
return [tokenMap["default"], updateToken];
|
||||
}
|
||||
|
||||
export function useBackendInstanceToken(
|
||||
id: string,
|
||||
): [string | undefined, ((d: string | undefined) => void)] {
|
||||
const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`)
|
||||
): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
|
||||
const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
|
||||
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
|
||||
|
||||
// instance named 'default' use the default token
|
||||
@ -86,16 +90,17 @@ export function useBackendInstanceToken(
|
||||
return [defaultToken, defaultSetToken];
|
||||
}
|
||||
function updateToken(
|
||||
value: (string | undefined)
|
||||
value: (LoginToken | undefined)
|
||||
): void {
|
||||
if (value === undefined) {
|
||||
reset()
|
||||
} else {
|
||||
setToken(value)
|
||||
const res = { ...tokenMap, [id]: value }
|
||||
setToken(res)
|
||||
}
|
||||
}
|
||||
|
||||
return [token, updateToken];
|
||||
return [tokenMap[id], updateToken];
|
||||
}
|
||||
|
||||
export function useLang(initial?: string): [string, StateUpdater<string>] {
|
||||
@ -104,10 +109,10 @@ export function useLang(initial?: string): [string, StateUpdater<string>] {
|
||||
? navigator.language || (navigator as any).userLanguage
|
||||
: undefined;
|
||||
const defaultLang = (browserLang || initial || "en").substring(0, 2);
|
||||
return useNotNullLocalStorage("lang-preference", defaultLang);
|
||||
return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>];
|
||||
}
|
||||
|
||||
export function useLocalStorage(
|
||||
export function useSimpleLocalStorage(
|
||||
key: string,
|
||||
initialValue?: string,
|
||||
): [string | undefined, StateUpdater<string | undefined>] {
|
||||
@ -137,28 +142,3 @@ export function useLocalStorage(
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
|
||||
export function useNotNullLocalStorage(
|
||||
key: string,
|
||||
initialValue: string,
|
||||
): [string, StateUpdater<string>] {
|
||||
const [storedValue, setStoredValue] = useState<string>((): string => {
|
||||
return typeof window !== "undefined"
|
||||
? window.localStorage.getItem(key) || initialValue
|
||||
: initialValue;
|
||||
});
|
||||
|
||||
const setValue = (value: string | ((val: string) => string)) => {
|
||||
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
||||
setStoredValue(valueToStore);
|
||||
if (typeof window !== "undefined") {
|
||||
if (!valueToStore) {
|
||||
window.localStorage.removeItem(key);
|
||||
} else {
|
||||
window.localStorage.setItem(key, valueToStore);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return [storedValue, setValue];
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
|
||||
import * as tests from "@gnu-taler/web-util/testing";
|
||||
import { expect } from "chai";
|
||||
import { MerchantBackend } from "../declaration.js";
|
||||
import { AccessToken, MerchantBackend } from "../declaration.js";
|
||||
import {
|
||||
useAdminAPI,
|
||||
useBackendInstances,
|
||||
@ -158,7 +158,7 @@ describe("instance api interaction with details", () => {
|
||||
},
|
||||
} as MerchantBackend.Instances.QueryInstancesResponse,
|
||||
});
|
||||
api.setNewToken("secret");
|
||||
api.setNewToken("secret" as AccessToken);
|
||||
},
|
||||
({ query, api }) => {
|
||||
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
|
||||
|
@ -19,10 +19,11 @@ import {
|
||||
RequestError,
|
||||
} from "@gnu-taler/web-util/browser";
|
||||
import { useBackendContext } from "../context/backend.js";
|
||||
import { MerchantBackend } from "../declaration.js";
|
||||
import { AccessToken, MerchantBackend } from "../declaration.js";
|
||||
import {
|
||||
useBackendBaseRequest,
|
||||
useBackendInstanceRequest,
|
||||
useCredentialsChecker,
|
||||
useMatchMutate,
|
||||
} from "./backend.js";
|
||||
|
||||
@ -36,7 +37,7 @@ interface InstanceAPI {
|
||||
) => Promise<void>;
|
||||
deleteInstance: () => Promise<void>;
|
||||
clearToken: () => Promise<void>;
|
||||
setNewToken: (token: string) => Promise<void>;
|
||||
setNewToken: (token: AccessToken) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useAdminAPI(): AdminAPI {
|
||||
@ -86,8 +87,10 @@ export interface AdminAPI {
|
||||
|
||||
export function useManagementAPI(instanceId: string): InstanceAPI {
|
||||
const mutateAll = useMatchMutate();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
const { updateToken } = useBackendContext();
|
||||
const { request } = useBackendBaseRequest();
|
||||
const { requestNewLoginToken } = useCredentialsChecker()
|
||||
|
||||
const updateInstance = async (
|
||||
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
|
||||
@ -117,13 +120,20 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
|
||||
mutateAll(/\/management\/instances/);
|
||||
};
|
||||
|
||||
const setNewToken = async (newToken: string): Promise<void> => {
|
||||
const setNewToken = async (newToken: AccessToken): Promise<void> => {
|
||||
await request(`/management/instances/${instanceId}/auth`, {
|
||||
method: "POST",
|
||||
data: { method: "token", token: newToken },
|
||||
});
|
||||
|
||||
updateToken(newToken);
|
||||
const resp = await requestNewLoginToken(backendURL, newToken)
|
||||
if (resp.valid) {
|
||||
const { token, expiration } = resp
|
||||
updateToken({ token, expiration });
|
||||
} else {
|
||||
updateToken(undefined)
|
||||
}
|
||||
|
||||
mutateAll(/\/management\/instances/);
|
||||
};
|
||||
|
||||
@ -132,12 +142,13 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
|
||||
|
||||
export function useInstanceAPI(): InstanceAPI {
|
||||
const { mutate } = useSWRConfig();
|
||||
const { url: backendURL, updateToken } = useBackendContext()
|
||||
|
||||
const {
|
||||
url: baseUrl,
|
||||
token: adminToken,
|
||||
updateLoginStatus,
|
||||
} = useBackendContext();
|
||||
const { request } = useBackendInstanceRequest();
|
||||
const { requestNewLoginToken } = useCredentialsChecker()
|
||||
|
||||
const updateInstance = async (
|
||||
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
|
||||
@ -147,7 +158,7 @@ export function useInstanceAPI(): InstanceAPI {
|
||||
data: instance,
|
||||
});
|
||||
|
||||
if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
|
||||
if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
|
||||
mutate([`/private/`], null);
|
||||
};
|
||||
|
||||
@ -157,7 +168,7 @@ export function useInstanceAPI(): InstanceAPI {
|
||||
// token: adminToken,
|
||||
});
|
||||
|
||||
if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
|
||||
if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
|
||||
mutate([`/private/`], null);
|
||||
};
|
||||
|
||||
@ -170,13 +181,20 @@ export function useInstanceAPI(): InstanceAPI {
|
||||
mutate([`/private/`], null);
|
||||
};
|
||||
|
||||
const setNewToken = async (newToken: string): Promise<void> => {
|
||||
const setNewToken = async (newToken: AccessToken): Promise<void> => {
|
||||
await request(`/private/auth`, {
|
||||
method: "POST",
|
||||
data: { method: "token", token: newToken },
|
||||
});
|
||||
|
||||
updateLoginStatus(baseUrl, newToken);
|
||||
const resp = await requestNewLoginToken(backendURL, newToken)
|
||||
if (resp.valid) {
|
||||
const { token, expiration } = resp
|
||||
updateToken({ token, expiration });
|
||||
} else {
|
||||
updateToken(undefined)
|
||||
}
|
||||
|
||||
mutate([`/private/`], null);
|
||||
};
|
||||
|
||||
|
@ -90,10 +90,7 @@ export class ApiMockEnvironment extends MockEnvironment {
|
||||
const SC: any = SWRConfig;
|
||||
|
||||
return (
|
||||
<BackendContextProvider
|
||||
defaultUrl="http://backend"
|
||||
initialToken={undefined}
|
||||
>
|
||||
<BackendContextProvider defaultUrl="http://backend">
|
||||
<InstanceContextProvider
|
||||
value={{
|
||||
token: undefined,
|
||||
|
@ -24,15 +24,6 @@ import {
|
||||
codecForString,
|
||||
} from "@gnu-taler/taler-util";
|
||||
|
||||
function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
|
||||
if (str === undefined) return undefined;
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
advanceOrderMode: boolean;
|
||||
dateFormat: "ymd" | "dmy" | "mdy";
|
||||
|
@ -22,7 +22,7 @@
|
||||
import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
|
||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { Fragment, VNode, h } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { FormProvider } from "../../../../components/form/FormProvider.js";
|
||||
import { Input } from "../../../../components/form/Input.js";
|
||||
@ -35,10 +35,10 @@ import { TextField } from "../../../../components/form/TextField.js";
|
||||
import { ProductList } from "../../../../components/product/ProductList.js";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
import { MerchantBackend } from "../../../../declaration.js";
|
||||
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||
import { mergeRefunds } from "../../../../utils/amount.js";
|
||||
import { RefundModal } from "../list/Table.js";
|
||||
import { Event, Timeline } from "./Timeline.js";
|
||||
import { dateFormatForSettings, datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||
|
||||
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
|
||||
type CT = MerchantBackend.ContractTerms;
|
||||
@ -416,9 +416,9 @@ function PaidPage({
|
||||
})
|
||||
|
||||
const [value, valueHandler] = useState<Partial<Paid>>(order);
|
||||
const { url } = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
const refundurl = stringifyRefundUri({
|
||||
merchantBaseUrl: url,
|
||||
merchantBaseUrl: backendURL,
|
||||
orderId: order.contract_terms.order_id
|
||||
})
|
||||
const refundable =
|
||||
|
@ -13,12 +13,12 @@
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
import { stringifyRewardUri } from "@gnu-taler/taler-util";
|
||||
import { format } from "date-fns";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
import { MerchantBackend } from "../../../../declaration.js";
|
||||
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
|
||||
import { stringifyRewardUri } from "@gnu-taler/taler-util";
|
||||
|
||||
type Entity = MerchantBackend.Rewards.RewardDetails;
|
||||
|
||||
@ -29,9 +29,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode {
|
||||
const { url: merchantBaseUrl } = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
const [settings] = useSettings();
|
||||
const rewardURL = stringifyRewardUri({ merchantBaseUrl, merchantRewardId })
|
||||
const rewardURL = stringifyRewardUri({ merchantBaseUrl: backendURL, merchantRewardId })
|
||||
return (
|
||||
<Fragment>
|
||||
<div class="field is-horizontal">
|
||||
|
@ -35,16 +35,12 @@ import { Input } from "../../../../components/form/Input.js";
|
||||
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
||||
import { InputDuration } from "../../../../components/form/InputDuration.js";
|
||||
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
||||
import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
|
||||
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
import { useInstanceContext } from "../../../../context/instance.js";
|
||||
import { MerchantBackend } from "../../../../declaration.js";
|
||||
import {
|
||||
isBase32RFC3548Charset
|
||||
} from "../../../../utils/crypto.js";
|
||||
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||
import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
|
||||
import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
|
||||
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||
|
||||
type Entity = MerchantBackend.Template.TemplateAddDetails;
|
||||
|
||||
@ -55,7 +51,7 @@ interface Props {
|
||||
|
||||
export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const backend = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
const devices = useInstanceOtpDevices()
|
||||
|
||||
const [state, setState] = useState<Partial<Entity>>({
|
||||
@ -128,7 +124,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||
>
|
||||
<InputWithAddon<Entity>
|
||||
name="template_id"
|
||||
help={`${backend.url}/templates/${state.template_id ?? ""}`}
|
||||
help={`${backendURL}/templates/${state.template_id ?? ""}`}
|
||||
label={i18n.str`Identifier`}
|
||||
tooltip={i18n.str`Name of the template in URLs.`}
|
||||
/>
|
||||
|
@ -19,8 +19,9 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { h, VNode } from "preact";
|
||||
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
|
||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { VNode, h } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { QR } from "../../../../components/exception/QR.js";
|
||||
import {
|
||||
@ -29,14 +30,10 @@ import {
|
||||
} from "../../../../components/form/FormProvider.js";
|
||||
import { Input } from "../../../../components/form/Input.js";
|
||||
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
||||
import { ConfirmModal } from "../../../../components/modal/index.js";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
import { useConfigContext } from "../../../../context/config.js";
|
||||
import { useInstanceContext } from "../../../../context/instance.js";
|
||||
import { MerchantBackend } from "../../../../declaration.js";
|
||||
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
|
||||
import { useOtpDeviceDetails } from "../../../../hooks/otp.js";
|
||||
import { Loading } from "../../../../components/exception/loading.js";
|
||||
|
||||
type Entity = MerchantBackend.Template.UsingTemplateDetails;
|
||||
|
||||
@ -48,7 +45,7 @@ interface Props {
|
||||
|
||||
export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const { url: backendUrl } = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
const { id: instanceId } = useInstanceContext();
|
||||
const config = useConfigContext();
|
||||
|
||||
@ -75,7 +72,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
|
||||
templateParams.summary = state.summary ?? ""
|
||||
}
|
||||
|
||||
const merchantBaseUrl = new URL(backendUrl).href;
|
||||
const merchantBaseUrl = new URL(backendURL).href;
|
||||
|
||||
const payTemplateUri = stringifyPayTemplateUri({
|
||||
merchantBaseUrl,
|
||||
@ -84,7 +81,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
|
||||
})
|
||||
|
||||
const issuer = encodeURIComponent(
|
||||
`${new URL(backendUrl).host}/${instanceId}`,
|
||||
`${new URL(backendURL).host}/${instanceId}`,
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
MerchantTemplateContractDetails,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { Fragment, VNode, h } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
|
||||
import {
|
||||
@ -35,17 +35,10 @@ import { Input } from "../../../../components/form/Input.js";
|
||||
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
|
||||
import { InputDuration } from "../../../../components/form/InputDuration.js";
|
||||
import { InputNumber } from "../../../../components/form/InputNumber.js";
|
||||
import { InputSelector } from "../../../../components/form/InputSelector.js";
|
||||
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||
import {
|
||||
isBase32RFC3548Charset,
|
||||
randomBase32Key,
|
||||
} from "../../../../utils/crypto.js";
|
||||
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||
import { QR } from "../../../../components/exception/QR.js";
|
||||
import { useInstanceContext } from "../../../../context/instance.js";
|
||||
|
||||
type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
|
||||
|
||||
@ -55,12 +48,9 @@ interface Props {
|
||||
template: Entity;
|
||||
}
|
||||
|
||||
const algorithms = [0, 1, 2];
|
||||
const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
|
||||
|
||||
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const backend = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
|
||||
const [state, setState] = useState<Partial<Entity>>(template);
|
||||
|
||||
@ -115,7 +105,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<span class="is-size-4">
|
||||
{backend.url}/templates/{template.id}
|
||||
{backendURL}/templates/{template.id}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,12 +26,13 @@ import { AsyncButton } from "../../../components/exception/AsyncButton.js";
|
||||
import { FormProvider } from "../../../components/form/FormProvider.js";
|
||||
import { Input } from "../../../components/form/Input.js";
|
||||
import { useInstanceContext } from "../../../context/instance.js";
|
||||
import { AccessToken } from "../../../declaration.js";
|
||||
|
||||
interface Props {
|
||||
instanceId: string;
|
||||
currentToken: string | undefined;
|
||||
onClearToken: () => void;
|
||||
onNewToken: (s: string) => void;
|
||||
onNewToken: (s: AccessToken) => void;
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
@ -71,7 +72,8 @@ export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewTo
|
||||
|
||||
async function submitForm() {
|
||||
if (hasErrors) return;
|
||||
onNewToken(form.new_token as any)
|
||||
const nt = `secret-token:${form.new_token}` as AccessToken;
|
||||
onNewToken(nt)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -17,7 +17,7 @@ import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||
import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { Fragment, VNode, h } from "preact";
|
||||
import { Loading } from "../../../components/exception/loading.js";
|
||||
import { MerchantBackend } from "../../../declaration.js";
|
||||
import { AccessToken, MerchantBackend } from "../../../declaration.js";
|
||||
import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
|
||||
import { DetailPage } from "./DetailPage.js";
|
||||
import { useInstanceContext } from "../../../context/instance.js";
|
||||
@ -49,13 +49,13 @@ export default function Token({
|
||||
const { token: instanceToken, id, admin } = useInstanceContext();
|
||||
|
||||
const currentToken = !admin ? rootToken : instanceToken
|
||||
const hasPrefix = currentToken !== undefined && currentToken.startsWith(PREFIX)
|
||||
const hasPrefix = currentToken !== undefined && currentToken.token.startsWith(PREFIX)
|
||||
return (
|
||||
<Fragment>
|
||||
<NotificationCard notification={notif} />
|
||||
<DetailPage
|
||||
instanceId={id}
|
||||
currentToken={hasPrefix ? currentToken.substring(PREFIX.length) : currentToken}
|
||||
currentToken={hasPrefix ? currentToken.token.substring(PREFIX.length) : currentToken?.token}
|
||||
onClearToken={async (): Promise<void> => {
|
||||
try {
|
||||
await clearToken();
|
||||
@ -72,7 +72,7 @@ export default function Token({
|
||||
}}
|
||||
onNewToken={async (newToken): Promise<void> => {
|
||||
try {
|
||||
await setNewToken(`secret-token:${newToken}`);
|
||||
await setNewToken(newToken);
|
||||
onChange();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
@ -13,18 +13,19 @@
|
||||
You should have received a copy of the GNU General Public License along with
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||
import {
|
||||
ErrorType,
|
||||
HttpError,
|
||||
HttpResponse,
|
||||
useTranslationContext,
|
||||
} from "@gnu-taler/web-util/browser";
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { Fragment, VNode, h } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { Loading } from "../../../components/exception/loading.js";
|
||||
import { NotificationCard } from "../../../components/menu/index.js";
|
||||
import { useInstanceContext } from "../../../context/instance.js";
|
||||
import { MerchantBackend } from "../../../declaration.js";
|
||||
import { AccessToken, MerchantBackend } from "../../../declaration.js";
|
||||
import {
|
||||
useInstanceAPI,
|
||||
useInstanceDetails,
|
||||
@ -33,7 +34,6 @@ import {
|
||||
} from "../../../hooks/instance.js";
|
||||
import { Notification } from "../../../utils/types.js";
|
||||
import { UpdatePage } from "./UpdatePage.js";
|
||||
import { HttpStatusCode } from "@gnu-taler/taler-util";
|
||||
|
||||
export interface Props {
|
||||
onBack: () => void;
|
||||
@ -73,10 +73,9 @@ function CommonUpdate(
|
||||
MerchantBackend.ErrorDetail
|
||||
>,
|
||||
updateInstance: any,
|
||||
clearToken: any,
|
||||
setNewToken: any,
|
||||
clearToken: () => Promise<void>,
|
||||
setNewToken: (t: AccessToken) => Promise<void>,
|
||||
): VNode {
|
||||
const { changeToken } = useInstanceContext();
|
||||
const [notif, setNotif] = useState<Notification | undefined>(undefined);
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
@ -119,11 +118,8 @@ function CommonUpdate(
|
||||
d: MerchantBackend.Instances.InstanceAuthConfigurationMessage,
|
||||
): Promise<void> => {
|
||||
const apiCall =
|
||||
d.method === "external" ? clearToken() : setNewToken(d.token!);
|
||||
return apiCall
|
||||
.then(() => changeToken(d.token))
|
||||
.then(onConfirm)
|
||||
.catch(onUpdateError);
|
||||
d.method === "external" ? clearToken() : setNewToken(d.token! as AccessToken);
|
||||
return apiCall.then(onConfirm).catch(onUpdateError);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
|
@ -18,9 +18,9 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { Fragment, VNode, h } from "preact";
|
||||
import { QR } from "../../../../components/exception/QR.js";
|
||||
import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
import { useInstanceContext } from "../../../../context/instance.js";
|
||||
import { MerchantBackend } from "../../../../declaration.js";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
|
||||
type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
|
||||
|
||||
@ -38,9 +38,9 @@ export function CreatedSuccessfully({
|
||||
onConfirm,
|
||||
}: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const backend = useBackendContext();
|
||||
const { url: backendURL } = useBackendContext()
|
||||
const { id: instanceId } = useInstanceContext();
|
||||
const issuer = new URL(backend.url).hostname;
|
||||
const issuer = new URL(backendURL).hostname;
|
||||
const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`;
|
||||
const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`;
|
||||
|
||||
|
@ -18,12 +18,301 @@
|
||||
*
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
import { h, VNode } from "preact";
|
||||
import { LoginModal } from "../../components/exception/login.js";
|
||||
|
||||
import { useTranslationContext } from "@gnu-taler/web-util/browser";
|
||||
import { ComponentChildren, h, VNode } from "preact";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { useBackendContext } from "../../context/backend.js";
|
||||
import { useInstanceContext } from "../../context/instance.js";
|
||||
import { AccessToken, LoginToken } from "../../declaration.js";
|
||||
import { useCredentialsChecker } from "../../hooks/backend.js";
|
||||
import { useBackendURL } from "../../hooks/index.js";
|
||||
|
||||
interface Props {
|
||||
onConfirm: (url: string, token?: string) => void;
|
||||
onConfirm: (token: LoginToken | undefined) => void;
|
||||
}
|
||||
export default function LoginPage({ onConfirm }: Props): VNode {
|
||||
return <LoginModal onConfirm={onConfirm} />;
|
||||
|
||||
function getTokenValuePart(t: string): string {
|
||||
if (!t) return t;
|
||||
const match = /secret-token:(.*)/.exec(t);
|
||||
if (!match || !match[1]) return "";
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function normalizeToken(r: string): AccessToken {
|
||||
return `secret-token:${r}` as AccessToken;
|
||||
}
|
||||
|
||||
function cleanUp(s: string): string {
|
||||
let result = s;
|
||||
if (result.indexOf("webui/") !== -1) {
|
||||
result = result.substring(0, result.indexOf("webui/"));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function LoginPage({ onConfirm }: Props): VNode {
|
||||
const { url: backendURL, changeBackend } = useBackendContext();
|
||||
const { admin, id } = useInstanceContext();
|
||||
const { requestNewLoginToken } = useCredentialsChecker();
|
||||
const [token, setToken] = useState("");
|
||||
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
|
||||
const doLogin = useCallback(async function doLoginImpl() {
|
||||
const secretToken = normalizeToken(token);
|
||||
const baseUrl = id === undefined ? backendURL : `${backendURL}/instances/${id}`
|
||||
const result = await requestNewLoginToken(baseUrl, secretToken);
|
||||
if (result.valid) {
|
||||
const { token, expiration } = result
|
||||
onConfirm({ token, expiration });
|
||||
} else {
|
||||
onConfirm(undefined);
|
||||
}
|
||||
}, [backendURL, id, token])
|
||||
|
||||
async function changeServer() {
|
||||
changeBackend("")
|
||||
}
|
||||
|
||||
console.log(admin, id)
|
||||
if (admin && id !== "default") {
|
||||
//admin trying to access another instance
|
||||
return (<div class="columns is-centered" style={{ margin: "auto" }}>
|
||||
<div class="column is-two-thirds ">
|
||||
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
|
||||
<header
|
||||
class="modal-card-head"
|
||||
style={{ border: "1px solid", borderBottom: 0 }}
|
||||
>
|
||||
<p class="modal-card-title">{i18n.str`Login required`}</p>
|
||||
</header>
|
||||
<section
|
||||
class="modal-card-body"
|
||||
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
|
||||
>
|
||||
<p>
|
||||
<i18n.Translate>Need the access token for the instance.</i18n.Translate>
|
||||
</p>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">
|
||||
<i18n.Translate>Access Token</i18n.Translate>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
placeholder={"current access token"}
|
||||
name="token"
|
||||
onKeyPress={(e) =>
|
||||
e.keyCode === 13
|
||||
? doLogin()
|
||||
: null
|
||||
}
|
||||
value={token}
|
||||
onInput={(e): void => setToken(e?.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer
|
||||
class="modal-card-foot "
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
border: "1px solid",
|
||||
borderTop: 0,
|
||||
}}
|
||||
>
|
||||
<AsyncButton
|
||||
onClick={doLogin}
|
||||
>
|
||||
<i18n.Translate>Confirm</i18n.Translate>
|
||||
</AsyncButton>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="columns is-centered" style={{ margin: "auto" }}>
|
||||
<div class="column is-two-thirds ">
|
||||
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
|
||||
<header
|
||||
class="modal-card-head"
|
||||
style={{ border: "1px solid", borderBottom: 0 }}
|
||||
>
|
||||
<p class="modal-card-title">{i18n.str`Login required`}</p>
|
||||
</header>
|
||||
<section
|
||||
class="modal-card-body"
|
||||
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
|
||||
>
|
||||
<i18n.Translate>Please enter your access token.</i18n.Translate>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">URL</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="set new url"
|
||||
name="id"
|
||||
value={backendURL}
|
||||
disabled
|
||||
readOnly
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">
|
||||
<i18n.Translate>Access Token</i18n.Translate>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="password"
|
||||
placeholder={"current access token"}
|
||||
name="token"
|
||||
onKeyPress={(e) =>
|
||||
e.keyCode === 13
|
||||
? doLogin()
|
||||
: null
|
||||
}
|
||||
value={token}
|
||||
onInput={(e): void => setToken(e?.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer
|
||||
class="modal-card-foot "
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
border: "1px solid",
|
||||
borderTop: 0,
|
||||
}}
|
||||
>
|
||||
<AsyncButton
|
||||
|
||||
onClick={changeServer}
|
||||
>
|
||||
<i18n.Translate>Change server</i18n.Translate>
|
||||
</AsyncButton>
|
||||
|
||||
<AsyncButton
|
||||
type="is-info"
|
||||
onClick={doLogin}
|
||||
>
|
||||
<i18n.Translate>Confirm</i18n.Translate>
|
||||
</AsyncButton>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise<void>, children: ComponentChildren }): VNode {
|
||||
const [running, setRunning] = useState(false)
|
||||
return <button class={"button " + type} disabled={disabled || running} onClick={() => {
|
||||
setRunning(true)
|
||||
onClick().then(() => {
|
||||
setRunning(false)
|
||||
}).catch(() => {
|
||||
setRunning(false)
|
||||
})
|
||||
}}>
|
||||
{children}
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }): VNode {
|
||||
const { url: backendURL } = useBackendContext()
|
||||
|
||||
const [url, setURL] = useState(cleanUp(backendURL));
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
async function doConnect() {
|
||||
onConfirm(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="columns is-centered" style={{ margin: "auto" }}>
|
||||
<div class="column is-two-thirds ">
|
||||
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
|
||||
<header
|
||||
class="modal-card-head"
|
||||
style={{ border: "1px solid", borderBottom: 0 }}
|
||||
>
|
||||
<p class="modal-card-title">{i18n.str`Connect to backend`}</p>
|
||||
</header>
|
||||
<section
|
||||
class="modal-card-body"
|
||||
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
|
||||
>
|
||||
<i18n.Translate>Location of the backend server</i18n.Translate>
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
<label class="label">URL</label>
|
||||
</div>
|
||||
<div class="field-body">
|
||||
<div class="field">
|
||||
<p class="control is-expanded">
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="set new url"
|
||||
name="id"
|
||||
value={url ?? ""}
|
||||
onKeyPress={(e) =>
|
||||
e.keyCode === 13
|
||||
? doConnect()
|
||||
: null
|
||||
}
|
||||
onInput={(e): void => setURL(e?.currentTarget.value)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer
|
||||
class="modal-card-foot "
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
border: "1px solid",
|
||||
borderTop: 0,
|
||||
}}
|
||||
>
|
||||
<AsyncButton
|
||||
disabled={backendURL === url}
|
||||
onClick={doConnect}
|
||||
>
|
||||
<i18n.Translate>Try again</i18n.Translate>
|
||||
</AsyncButton>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -13,7 +13,7 @@ function getBrowserLang(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function Settings(): VNode {
|
||||
export function Settings({ onClose }: { onClose?: () => void }): VNode {
|
||||
const { i18n } = useTranslationContext()
|
||||
const borwserLang = getBrowserLang()
|
||||
const { update } = useLang()
|
||||
@ -94,11 +94,19 @@ export function Settings(): VNode {
|
||||
/>
|
||||
</FormProvider>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="column" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section >
|
||||
{onClose &&
|
||||
<section class="section is-main-section">
|
||||
<button
|
||||
class="button"
|
||||
onClick={onClose}
|
||||
>
|
||||
<i18n.Translate>Close</i18n.Translate>
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
</div >
|
||||
}
|
@ -25,6 +25,8 @@ export enum ErrorType {
|
||||
UNEXPECTED,
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param baseUrl URL where the service is located
|
||||
@ -60,10 +62,27 @@ export async function defaultRequestHandler<T>(
|
||||
const requestPreventCache = options.preventCache ?? false;
|
||||
const requestPreventCors = options.preventCors ?? false;
|
||||
|
||||
const _url = new URL(`${baseUrl}${endpoint}`);
|
||||
const validURL = validateURL(baseUrl, endpoint);
|
||||
|
||||
if (!validURL) {
|
||||
const error: HttpResponseUnexpectedError = {
|
||||
info: {
|
||||
url: `${baseUrl}${endpoint}`,
|
||||
payload: {},
|
||||
hasToken: !!options.token,
|
||||
status: 0,
|
||||
options,
|
||||
},
|
||||
type: ErrorType.UNEXPECTED,
|
||||
exception: undefined,
|
||||
loading: false,
|
||||
message: `invalid URL: "${validURL}"`,
|
||||
};
|
||||
throw new RequestError(error)
|
||||
}
|
||||
|
||||
Object.entries(requestParams).forEach(([key, value]) => {
|
||||
_url.searchParams.set(key, String(value));
|
||||
validURL.searchParams.set(key, String(value));
|
||||
});
|
||||
|
||||
let payload: BodyInit | undefined = undefined;
|
||||
@ -77,7 +96,20 @@ export async function defaultRequestHandler<T>(
|
||||
} else if (typeof requestBody === "object") {
|
||||
payload = JSON.stringify(requestBody);
|
||||
} else {
|
||||
throw Error("unsupported request body type");
|
||||
const error: HttpResponseUnexpectedError = {
|
||||
info: {
|
||||
url: validURL.href,
|
||||
payload: {},
|
||||
hasToken: !!options.token,
|
||||
status: 0,
|
||||
options,
|
||||
},
|
||||
type: ErrorType.UNEXPECTED,
|
||||
exception: undefined,
|
||||
loading: false,
|
||||
message: `unsupported request body type: "${typeof requestBody}"`,
|
||||
};
|
||||
throw new RequestError(error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +120,7 @@ export async function defaultRequestHandler<T>(
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(_url.href, {
|
||||
response = await fetch(validURL.href, {
|
||||
headers: requestHeaders,
|
||||
method: requestMethod,
|
||||
credentials: "omit",
|
||||
@ -100,15 +132,29 @@ export async function defaultRequestHandler<T>(
|
||||
} catch (ex) {
|
||||
const info: RequestInfo = {
|
||||
payload,
|
||||
url: _url.href,
|
||||
url: validURL.href,
|
||||
hasToken: !!options.token,
|
||||
status: 0,
|
||||
options,
|
||||
};
|
||||
const error: HttpRequestTimeoutError = {
|
||||
|
||||
if (ex instanceof Error) {
|
||||
if (ex.message === "HTTP_REQUEST_TIMEOUT") {
|
||||
const error: HttpRequestTimeoutError = {
|
||||
info,
|
||||
type: ErrorType.TIMEOUT,
|
||||
message: "request timeout",
|
||||
};
|
||||
throw new RequestError(error);
|
||||
}
|
||||
}
|
||||
|
||||
const error: HttpResponseUnexpectedError = {
|
||||
info,
|
||||
type: ErrorType.TIMEOUT,
|
||||
message: "Request timeout",
|
||||
type: ErrorType.UNEXPECTED,
|
||||
exception: ex,
|
||||
loading: false,
|
||||
message: (ex instanceof Error ? ex.message : ""),
|
||||
};
|
||||
throw new RequestError(error);
|
||||
}
|
||||
@ -124,7 +170,7 @@ export async function defaultRequestHandler<T>(
|
||||
if (response.ok) {
|
||||
const result = await buildRequestOk<T>(
|
||||
response,
|
||||
_url.href,
|
||||
validURL.href,
|
||||
payload,
|
||||
!!options.token,
|
||||
options,
|
||||
@ -133,7 +179,7 @@ export async function defaultRequestHandler<T>(
|
||||
} else {
|
||||
const dataTxt = await response.text();
|
||||
const error = buildRequestFailed(
|
||||
_url.href,
|
||||
validURL.href,
|
||||
dataTxt,
|
||||
response.status,
|
||||
payload,
|
||||
@ -377,3 +423,12 @@ export function buildRequestFailed<ErrorDetail>(
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
function validateURL(baseUrl: string, endpoint: string): URL | undefined {
|
||||
try {
|
||||
return new URL(`${baseUrl}${endpoint}`)
|
||||
} catch (ex) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user