commit f3aea323f63199f6726583bf3f458a6134b0d9f8 parent 90e4e00bcfac3940475473f3a44978b89060b81e Author: Sebastian <sebasjm@gmail.com> Date: Wed, 3 Mar 2021 14:43:40 -0300 refactor backend mutate api to use token from instance id, implemented new passwod endpoint Diffstat:
15 files changed, 216 insertions(+), 221 deletions(-)
diff --git a/packages/frontend/src/context/backend.ts b/packages/frontend/src/context/backend.ts @@ -20,6 +20,7 @@ export interface BackendContextType { url: string; token?: string; changeBackend: (url: string) => void; + resetBackend: () => void; // clearTokens: () => void; // addTokenCleaner: (c: StateUpdater<string | undefined>) => void; updateToken: (token?:string) => void; @@ -35,25 +36,29 @@ export interface ConfigContextType { export interface InstanceContextType { id: string; token?: string; + admin?: boolean; } -export const BackendContext = createContext<BackendContextType>({ +const BackendContext = createContext<BackendContextType>({ url: '', lang: 'en', token: undefined, changeBackend: () => null, + resetBackend: () => null, // clearTokens: () => null, // addTokenCleaner: () => null, updateToken: () => null, setLang: () => null, }) -export const ConfigContext = createContext<ConfigContextType>(null!) +const ConfigContext = createContext<ConfigContextType>(null!) -export const useConfigContext = () => useContext(ConfigContext); +const InstanceContext = createContext<InstanceContextType>({} as any) +export const ConfigContextProvider = ConfigContext.Provider +export const useConfigContext = () => useContext(ConfigContext); +export const BackendContextProvider = BackendContext.Provider +export const useBackendContext = () => useContext(BackendContext); +export const InstanceContextProvider = InstanceContext.Provider +export const useInstanceContext = () => useContext(InstanceContext); -export const InstanceContext = createContext<InstanceContextType>({ - id: '', - token: undefined, -}) -\ No newline at end of file diff --git a/packages/frontend/src/declaration.d.ts b/packages/frontend/src/declaration.d.ts @@ -14,10 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ - /** - * - * @author Sebastian Javier Marchano (sebasjm) - */ +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ @@ -101,6 +101,24 @@ export namespace MerchantBackend { } namespace Instances { + //POST /private/instances/$INSTANCE/auth + interface InstanceAuthConfigurationMessage { + // Type of authentication. + // "external": The mechant backend does not do + // any authentication checks. Instead an API + // gateway must do the authentication. + // "token": The merchant checks an auth token. + // See "token" for details. + method: "external" | "token"; + + // For method "external", this field is mandatory. + // The token MUST begin with the string "secret-token:". + // After the auth token has been set (with method "token"), + // the value must be provided in a "Authorization: Bearer $token" + // header. + token?: string; + + } //POST /private/instances interface InstanceConfigurationMessage { // The URI where the wallet will send coins. A merchant may have @@ -121,7 +139,7 @@ export namespace MerchantBackend { // Optional, if not given authentication will be disabled for // this instance (hopefully authentication checks are still // done by some reverse proxy). - auth_token?: string; + auth: InstanceAuthConfigurationMessage; // The merchant's physical address (to be put into contracts). address: Location; @@ -164,12 +182,6 @@ export namespace MerchantBackend { // Merchant name corresponding to this instance. name: string; - // "Authentication" header required to authorize management access the instance. - // Optional, if not given authentication will be disabled for - // this instance (hopefully authentication checks are still - // done by some reverse proxy). - auth_token?: string; - // The merchant's physical address (to be put into contracts). address: Location; @@ -265,6 +277,11 @@ export namespace MerchantBackend { // offers we make be valid by default? default_pay_deadline: RelativeTime; + // Authentication configuration. + // Does not contain the token when token auth is configured. + auth: { + method: "external" | "token"; + }; } interface MerchantAccount { diff --git a/packages/frontend/src/hooks/backend.ts b/packages/frontend/src/hooks/backend.ts @@ -23,7 +23,7 @@ import useSWR, { mutate } from 'swr'; import axios from 'axios' import { MerchantBackend } from '../declaration'; import { useContext } from 'preact/hooks'; -import { BackendContext, InstanceContext } from '../context/backend'; +import { useBackendContext, useInstanceContext } from '../context/backend'; type HttpResponse<T> = HttpResponseOk<T> | HttpResponseError; @@ -41,6 +41,7 @@ export interface SwrError { interface HttpResponseError { data: undefined; unauthorized: boolean; + notfound: boolean; error?: SwrError; } @@ -56,9 +57,17 @@ interface RequestOptions { async function request(url: string, options: RequestOptions = {}): Promise<any> { - const headers = options.token ? { Authorization: `${options.token}` } : undefined + const headers = options.token ? { Authorization: `Bearer ${options.token}` } : undefined try { + // http://localhost:9966/instances/blog/private/instances + // Hack, endpoint should respond 404 + if (/^\/instances\/[^/]*\/private\/instances$/.test(new URL(url).pathname)) { + console.warn(`HACK: Not going to query ${url}, instead return 404`) + throw ({ response: { status: 404 }, message: 'not found' }) + } + + const res = await axios({ method: options.method || 'get', url, @@ -79,18 +88,12 @@ function fetcher(url: string, token: string, backend: string) { return request(`${backend}${url}`, { token }) } -interface BackendMutateAPI { +interface AdminMutateAPI { createInstance: (data: MerchantBackend.Instances.InstanceConfigurationMessage) => Promise<void>; + deleteInstance: (id: string) => Promise<void>; } -interface BackendInstaceMutateAPI { - updateInstance: (data: MerchantBackend.Instances.InstanceReconfigurationMessage) => Promise<void>; - deleteInstance: () => Promise<void>; - clearToken: () => Promise<void>; - setNewToken: (token: string) => Promise<void>; -} - -export function useBackendMutateAPI(): BackendMutateAPI { - const { url, token } = useContext(BackendContext) +export function useAdminMutateAPI(): AdminMutateAPI { + const { url, token } = useBackendContext() const createInstance = async (instance: MerchantBackend.Instances.InstanceConfigurationMessage): Promise<void> => { await request(`${url}/private/instances`, { @@ -99,77 +102,105 @@ export function useBackendMutateAPI(): BackendMutateAPI { data: instance }) - mutate('/private/instances') + mutate(['/private/instances', token, url], null) + } + + const deleteInstance = async (id: string): Promise<void> => { + await request(`${url}/private/`, { + method: 'delete', + token, + }) + + mutate(['/private/instances', token, url], null) } - return { createInstance } + + return { createInstance, deleteInstance } +} + +interface InstaceMutateAPI { + updateInstance: (data: MerchantBackend.Instances.InstanceReconfigurationMessage, a?: MerchantBackend.Instances.InstanceAuthConfigurationMessage) => Promise<void>; + deleteInstance: () => Promise<void>; + clearToken: () => Promise<void>; + setNewToken: (token: string) => Promise<void>; } -export function useBackendInstanceMutateAPI(): BackendInstaceMutateAPI { - const { url } = useContext(BackendContext) - const { id, token } = useContext(InstanceContext) +export function useInstanceMutateAPI(): InstaceMutateAPI { + const { url: baseUrl, token: adminToken } = useBackendContext() + const { token, id, admin } = useInstanceContext() + + const url = !admin ? baseUrl: `${baseUrl}/instances/${id}` - const updateInstance = async (instance: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => { - await request(`${url}/private/instances/${id}`, { + const updateInstance = async (instance: MerchantBackend.Instances.InstanceReconfigurationMessage, auth?: MerchantBackend.Instances.InstanceAuthConfigurationMessage): Promise<void> => { + await request(`${url}/private/`, { method: 'patch', token, data: instance }) - mutate('/private/instances', null) - mutate(`/private/instances/${id}`, null) + if (auth) await request(`${url}/private/auth`, { + method: 'post', + token, + data: auth + }) + + if (adminToken) mutate(['/private/instances', adminToken, baseUrl], null) + mutate([`/private/`, token, url], null) }; - + const deleteInstance = async (): Promise<void> => { - await request(`${url}/private/instances/${id}`, { + await request(`${url}/private/`, { method: 'delete', - token, + token: adminToken, }) - mutate('/private/instances', null) - mutate(`/private/instances/${id}`, null) + if (adminToken) mutate(['/private/instances', adminToken, baseUrl], null) + mutate([`/private/`, token, url], null) } const clearToken = async (): Promise<void> => { - await request(`${url}/private/instances/${id}`, { - method: 'patch', + await request(`${url}/private/auth`, { + method: 'post', token, - data: { auth_token: null } + data: { method: 'external' } }) - mutate(`/private/instances/${id}`, null) + mutate([`/private/`, token, url], null) } - const setNewToken = async (token: string): Promise<void> => { - await request(`${url}/private/instances/${id}`, { - method: 'patch', + const setNewToken = async (newToken: string): Promise<void> => { + await request(`${url}/private/auth`, { + method: 'post', token, - data: { auth_token: token } + data: { method: 'token', token: newToken } }) - mutate(`/private/instances/${id}`, null) + mutate([`/private/`, token, url], null) } return { updateInstance, deleteInstance, setNewToken, clearToken } } export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { - const { url, token } = useContext(BackendContext) + const { url, token } = useBackendContext() const { data, error } = useSWR<MerchantBackend.Instances.InstancesResponse, SwrError>(['/private/instances', token, url], fetcher) - return { data, unauthorized: error?.status === 401, error } + return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } } export function useBackendInstance(): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> { - const { url } = useContext(BackendContext); - const { id, token } = useContext(InstanceContext); - const { data, error } = useSWR<MerchantBackend.Instances.QueryInstancesResponse, SwrError>([`/private/instances/${id}`, token, url], fetcher) + const { url: baseUrl } = useBackendContext(); + const { token, id, admin } = useInstanceContext(); + + const url = !admin ? baseUrl: `${baseUrl}/instances/${id}` + + const { data, error } = useSWR<MerchantBackend.Instances.QueryInstancesResponse, SwrError>([`/private/`, token, url], fetcher) - return { data, unauthorized: error?.status === 401, error } + return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } } export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> { - const { url, token } = useContext(BackendContext) + const { url, token } = useBackendContext() const { data, error } = useSWR<MerchantBackend.VersionResponse, SwrError>(['/config', token, url], fetcher) - return { data, unauthorized: error?.status === 401, error } + return { data, unauthorized: error?.status === 401, notfound: error?.status === 404, error } } diff --git a/packages/frontend/src/hooks/index.ts b/packages/frontend/src/hooks/index.ts @@ -25,16 +25,17 @@ import { ValueOrFunction } from '../utils/types'; export function useBackendContextState() { const [lang, setLang] = useLang() - const [url, changeBackend] = useBackendURL(); + const [url, changeBackend, resetBackend] = useBackendURL(); const [token, updateToken] = useBackendDefaultToken(); - return { url, token, changeBackend, updateToken, lang, setLang } + return { url, token, changeBackend, updateToken, lang, setLang, resetBackend } } -export function useBackendURL(): [string, StateUpdater<string>] { +export function useBackendURL(): [string, StateUpdater<string>, () => void] { const [value, setter] = useNotNullLocalStorage('backend-url', typeof window !== 'undefined' ? window.location.origin : '') const checkedSetter = (v: ValueOrFunction<string>) => setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, '')) - return [value, checkedSetter] + const reset = () => checkedSetter(typeof window !== 'undefined' ? window.location.origin : '') + return [value, checkedSetter, reset] } export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] { return useLocalStorage('backend-token') diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx @@ -30,7 +30,7 @@ import { Notifications } from './components/notifications'; import * as messages from './messages' import { useBackendContextState } from './hooks'; import { useNotifications } from "./hooks/notifications"; -import { BackendContext, ConfigContext } from './context/backend'; +import { BackendContextProvider, ConfigContextProvider, useBackendContext } from './context/backend'; import { useBackendConfig } from "./hooks/backend"; import { hasKey, onTranslationError } from "./utils/functions"; @@ -46,7 +46,8 @@ export enum RootPaths { } export enum InstancePaths { - update = '/instance/:id/update', + details = '/', + update = '/update', } export function Redirect({ to }: { to: string }): null { @@ -60,25 +61,29 @@ export default function Application(): VNode { const state = useBackendContextState() return ( - <BackendContext.Provider value={state}> + <BackendContextProvider value={state}> <MessageProvider locale={state.lang} onError={onTranslationError} messages={hasKey(messages, state.lang) ? messages[state.lang] : messages.en} pathSep={null as any} > <ApplicationStatusRoutes /> </MessageProvider > - </BackendContext.Provider> + </BackendContextProvider> ); } function ApplicationStatusRoutes(): VNode { const { notifications, pushNotification, removeNotification } = useNotifications() - const { changeBackend, updateToken, token } = useContext(BackendContext) + const { changeBackend, updateToken, resetBackend } = useBackendContext() const backendConfig = useBackendConfig(); const i18n = useMessageTemplate() - const cleaner = useCallback(() => { updateToken(undefined) }, []) - const [cleaners, setCleaners] = useState([cleaner]) + const tokenCleaner = useCallback(() => { updateToken(undefined) }, []) + const [cleaners, setCleaners] = useState([tokenCleaner]) const addTokenCleaner = (c: () => void) => setCleaners(cs => [...cs, c]) - const addTokenCleanerNemo = useCallback((c: () => void) => { addTokenCleaner(c) }, [cleaner]) + const addTokenCleanerMemo = useCallback((c: () => void) => { addTokenCleaner(c) }, [tokenCleaner]) + const clearAllTokens = () => { + cleaners.forEach(c => c()) + resetBackend() + } const v = `${backendConfig.data?.currency} ${backendConfig.data?.version}` const ctx = useMemo(() => ({ currency: backendConfig.data?.currency || '', version: backendConfig.data?.version || '' }), [v]) @@ -89,10 +94,7 @@ function ApplicationStatusRoutes(): VNode { if (backendConfig.unauthorized) { return <div id="app"> - <Menu onLogout={() => { - cleaners.forEach(c => c()) - route(RootPaths.list_instances) - }} /> + <Menu /> <LoginPage onConfirm={(url: string, token?: string) => { changeBackend(url) @@ -104,15 +106,12 @@ function ApplicationStatusRoutes(): VNode { } return <div id="app"> - <Menu onLogout={() => { - cleaners.forEach(c => c()) - route(RootPaths.list_instances) - }} /> + <Menu /> <LoginPage withMessage={{ message: i18n`Couldnt access the server`, type: 'ERROR', - description: !backendConfig.data && backendConfig.error ? i18n`Got message: ${backendConfig.error.message} from: ${backendConfig.error.backend} (hasToken: ${backendConfig.error.hasToken})` : undefined, + description: i18n`Got message: ${backendConfig.error.message} from: ${backendConfig.error.backend} (hasToken: ${backendConfig.error.hasToken})`, }} onConfirm={(url: string, token?: string) => { changeBackend(url) @@ -124,14 +123,9 @@ function ApplicationStatusRoutes(): VNode { } return <div id="app" class="has-navbar-fixed-top"> - <ConfigContext.Provider value={ctx}> - <Menu sidebar onLogout={() => { - cleaners.forEach(c => c()) - route(RootPaths.list_instances) - }} /> + <ConfigContextProvider value={ctx}> <Notifications notifications={notifications} removeNotification={removeNotification} /> - <Route default component={ApplicationReadyRoutes} pushNotification={pushNotification} addTokenCleaner={addTokenCleanerNemo} /> : - {/* <Route default component={LoginWithError} /> */} - </ConfigContext.Provider> + <Route default component={ApplicationReadyRoutes} pushNotification={pushNotification} addTokenCleaner={addTokenCleanerMemo} clearAllTokens={clearAllTokens}/> : + </ConfigContextProvider> </div> } diff --git a/packages/frontend/src/messages/en.po b/packages/frontend/src/messages/en.po @@ -73,6 +73,9 @@ msgstr "Wire fee Amortization" msgid "fields.instance.address.label" msgstr "Address" +msgid "Could not infer instance id from url %s" +msgstr "Could not infer instance id from url %s" + msgid "fields.instance.address.country.label" msgstr "Country" diff --git a/packages/frontend/src/routes/instances/create/CreatePage.tsx b/packages/frontend/src/routes/instances/create/CreatePage.tsx @@ -20,7 +20,7 @@ */ import { h, VNode } from "preact"; -import { useContext, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { MerchantBackend } from "../../../declaration"; import * as yup from 'yup'; import { FormErrors, FormProvider } from "../../../components/form/Field" @@ -30,12 +30,12 @@ import { Input } from "../../../components/form/Input"; import { InputSecured } from "../../../components/form/InputSecured"; import { InputWithAddon } from "../../../components/form/InputWithAddon"; import { InputGroup } from "../../../components/form/InputGroup"; -import { BackendContext, ConfigContext } from "../../../context/backend"; +import { useConfigContext, useBackendContext } from "../../../context/backend"; import { InputDuration } from "../../../components/form/InputDuration"; import { InputCurrency } from "../../../components/form/InputCurrency"; import { InputPayto } from "../../../components/form/InputPayto"; -type Entity = MerchantBackend.Instances.InstanceConfigurationMessage +type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {auth_token?: string} interface Props { onCreate: (d: Entity) => void; @@ -61,6 +61,11 @@ export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { const submit = (): void => { try { + // use conversion instead of this + const newToken = value.auth_token; + value.auth_token = undefined; + value.auth = newToken === null || newToken === undefined ? { method: "external" } : { method: "token", token: `secret-token:${newToken}` }; + // remove above use conversion schema.validateSync(value, { abortEarly: false }) onCreate(schema.cast(value) as Entity); } catch (err) { @@ -69,40 +74,10 @@ export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { setErrors(pathMessages) } } - const backend = useContext(BackendContext) - const config = useContext(ConfigContext) + const backend = useBackendContext() + const config = useConfigContext() return <div> - <section class="section is-title-bar"> - - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <ul> - <li><Message id="Merchant" /></li> - <li><Message id="Instances" /></li> - </ul> - </div> - </div> - </div> - </section> - - <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> - <div class="hero-body"> - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <h1 class="title"> - <Message id="Create new instances" /> - </h1> - </div> - </div> - <div class="level-right" style="display: none;"> - <div class="level-item" /> - </div> - </div> - </div> - </section> <section class="section is-main-section"> <div class="columns"> @@ -137,10 +112,12 @@ export function CreatePage({ onCreate, isLoading, onBack }: Props): VNode { <InputDuration<Entity> name="default_wire_transfer_delay" /> </FormProvider> - <div class="buttons is-right"> + + <div class="buttons is-right mt-5"> <button class="button" onClick={onBack} ><Message id="Cancel" /></button> <button class="button is-success" onClick={submit} ><Message id="Confirm" /></button> </div> + </div> <div class="column" /> </div> diff --git a/packages/frontend/src/routes/instances/create/index.tsx b/packages/frontend/src/routes/instances/create/index.tsx @@ -15,7 +15,7 @@ */ import { h, VNode } from "preact"; import { MerchantBackend } from "../../../declaration"; -import { useBackendMutateAPI } from "../../../hooks/backend"; +import { useAdminMutateAPI } from "../../../hooks/backend"; import { CreatePage } from "./CreatePage"; interface Props { @@ -25,7 +25,7 @@ interface Props { } export default function Create({ onBack, onConfirm, onError }: Props): VNode { - const { createInstance } = useBackendMutateAPI(); + const { createInstance } = useAdminMutateAPI(); return <CreatePage onBack={onBack} diff --git a/packages/frontend/src/routes/instances/details/DetailPage.tsx b/packages/frontend/src/routes/instances/details/DetailPage.tsx @@ -32,7 +32,6 @@ interface Props { onUpdate: () => void; onDelete: () => void; selected: MerchantBackend.Instances.QueryInstancesResponse; - isLoading: boolean; } interface KeyValue { @@ -50,32 +49,18 @@ function convert(from: MerchantBackend.Instances.QueryInstancesResponse): Entity return { ...defaults, ...rest, payto_uris }; } -export function DetailPage({ onUpdate, isLoading, selected, onDelete }: Props): VNode { +export function DetailPage({ onUpdate, selected, onDelete }: Props): VNode { const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)) const [errors, setErrors] = useState<KeyValue>({}) return <div> - <section class="section is-title-bar"> - - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <ul> - <li><Message id="Merchant" /></li> - <li><Message id="Instances" /></li> - </ul> - </div> - </div> - </div> - </section> - - <section class={isLoading ? "hero is-hero-bar" : "hero is-hero-bar is-loading"}> + <section class="hero is-hero-bar"> <div class="hero-body"> <div class="level"> <div class="level-left"> <div class="level-item"> <h1 class="title"> - <Message id="Instance details" /> + Here goes the instance description </h1> </div> </div> @@ -96,16 +81,6 @@ export function DetailPage({ onUpdate, isLoading, selected, onDelete }: Props): <Input<Entity> name="payto_uris" readonly /> </FormProvider> - <div class="buttons is-right"> - <button class="button is-danger" onClick={() => onDelete()} > - <span class="icon"><i class="mdi mdi-delete" /></span> - <span><Message id="delete" /></span> - </button> - <button class="button is-success" onClick={() => onUpdate()} > - <span class="icon"><i class="mdi mdi-pen" /></span> - <span><Message id="update" /></span> - </button> - </div> </div> <div class="column" /> </div> diff --git a/packages/frontend/src/routes/instances/details/index.tsx b/packages/frontend/src/routes/instances/details/index.tsx @@ -14,10 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { Fragment, h, VNode } from "preact"; -import { useContext, useState } from "preact/hooks"; -import { InstanceContext } from "../../../context/backend"; +import { useState } from "preact/hooks"; +import { useInstanceContext } from "../../../context/backend"; import { Notification } from "../../../utils/types"; -import { useBackendInstance, useBackendInstanceMutateAPI, SwrError } from "../../../hooks/backend"; +import { useBackendInstance, useInstanceMutateAPI, SwrError } from "../../../hooks/backend"; import { DetailPage } from "./DetailPage"; import { DeleteModal } from "../../../components/modal"; @@ -30,11 +30,11 @@ interface Props { } export default function Detail({ onUpdate, onLoadError, onUnauthorized, pushNotification, onDelete }: Props): VNode { - const { id } = useContext(InstanceContext) + const { id } = useInstanceContext() const details = useBackendInstance() const [deleting, setDeleting] = useState<boolean>(false) - const { deleteInstance } = useBackendInstanceMutateAPI() + const { deleteInstance } = useInstanceMutateAPI() if (!details.data) { if (details.unauthorized) return onUnauthorized() @@ -46,7 +46,6 @@ export default function Detail({ onUpdate, onLoadError, onUnauthorized, pushNoti return <Fragment> <DetailPage - isLoading={false} selected={details.data} onUpdate={onUpdate} onDelete={() => setDeleting(true)} diff --git a/packages/frontend/src/routes/instances/list/index.tsx b/packages/frontend/src/routes/instances/list/index.tsx @@ -21,7 +21,7 @@ import { Fragment, h, VNode } from 'preact'; import { View } from './View'; -import { useBackendInstances, useBackendInstanceMutateAPI, SwrError } from '../../../hooks/backend'; +import { useAdminMutateAPI } from '../../../hooks/backend'; import { useState } from 'preact/hooks'; import { MerchantBackend } from '../../../declaration'; import { Notification } from '../../../utils/types'; @@ -29,42 +29,18 @@ import { DeleteModal } from '../../../components/modal'; interface Props { pushNotification: (n: Notification) => void; - onUnauthorized: () => VNode; - onQueryError: (e: SwrError) => VNode; onCreate: () => void; onUpdate: (id: string) => void; + instances: MerchantBackend.Instances.Instance[] } -export default function Instances({ pushNotification, onUnauthorized, onQueryError, onCreate, onUpdate }: Props): VNode { - const list = useBackendInstances() +export default function Instances({ pushNotification, instances, onCreate, onUpdate }: Props): VNode { const [deleting, setDeleting] = useState<MerchantBackend.Instances.Instance | null>(null) - const { deleteInstance } = useBackendInstanceMutateAPI() - - const isLoadingTheList = (!list.data && !list.error) - const error = !list.data && list.error - - if (!list.data) { - if (list.unauthorized) return onUnauthorized() - if (list.error) return onQueryError(list.error) - return <div id="app"> - <section class="section is-title-bar"> - - <div class="level"> - <div class="level-left"> - <div class="level-item"> - <ul> - <li>loading</li> - </ul> - </div> - </div> - </div> - </section> - </div> - } + const { deleteInstance } = useAdminMutateAPI() return <Fragment> - <View instances={list.data.instances} - isLoading={isLoadingTheList} + <View instances={instances} + isLoading={false} onDelete={setDeleting} onCreate={onCreate} onUpdate={onUpdate} @@ -75,7 +51,7 @@ export default function Instances({ pushNotification, onUnauthorized, onQueryErr onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { try { - await deleteInstance() + await deleteInstance(deleting.id) pushNotification({ message: 'delete_success', type: 'SUCCESS' }) } catch (e) { pushNotification({ message: 'delete_error', type: 'ERROR' }) diff --git a/packages/frontend/src/routes/instances/update/UpdatePage.tsx b/packages/frontend/src/routes/instances/update/UpdatePage.tsx @@ -30,21 +30,21 @@ import { InstanceUpdateSchema as schema } from '../../../schemas' import { Message } from "preact-messages"; import { Input } from "../../../components/form/Input"; import { InputSecured } from "../../../components/form/InputSecured"; -import { ConfigContext } from "../../../context/backend"; +import { useConfigContext, useInstanceContext } from "../../../context/backend"; import { InputDuration } from "../../../components/form/InputDuration"; import { InputCurrency } from "../../../components/form/InputCurrency"; import { InputPayto } from "../../../components/form/InputPayto"; -type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage +type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {auth_token?: string} interface Props { - onUpdate: (d: Entity) => void; + onUpdate: (d: Entity, auth?: MerchantBackend.Instances.InstanceAuthConfigurationMessage) => void; selected: MerchantBackend.Instances.QueryInstancesResponse; isLoading: boolean; onBack: () => void; } -function convert(from: MerchantBackend.Instances.QueryInstancesResponse): Entity { +function convert(from: MerchantBackend.Instances.QueryInstancesResponse, token?: string): Entity { const { accounts, ...rest } = from const payto_uris = accounts.filter(a => a.active).map(a => a.payto_uri) const defaults = { @@ -52,19 +52,35 @@ function convert(from: MerchantBackend.Instances.QueryInstancesResponse): Entity default_pay_delay: { d_ms: 1000 * 60 * 60 }, //one hour default_wire_transfer_delay: { d_ms: 1000 * 60 * 60 * 2 }, //two hours } - return { ...defaults, ...rest, payto_uris }; + return { ...defaults, ...rest, payto_uris, auth_token: from.auth.method === "external" ? undefined : token }; } - +function getTokenValuePart(t?: string): string | undefined { + if (!t) return t + const match = /secret-token:(.*)/.exec(t); + if (!match || !match[1]) return undefined; + return match[1] +} export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VNode { - const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)) + const { token } = useInstanceContext() + const currentTokenValue = getTokenValuePart(token) + const [value, valueHandler] = useState<Partial<Entity>>(convert(selected, currentTokenValue)) const [errors, setErrors] = useState<FormErrors<Entity>>({}) const submit = (): void => { try { + // use conversion instead of this + const newToken = value.auth_token; + value.auth_token = undefined; + const auth: MerchantBackend.Instances.InstanceAuthConfigurationMessage | undefined = + newToken === currentTokenValue ? undefined : (newToken === null ? + { method: "external" } : + { method: "token", token: `secret-token:${newToken}` }); + + // remove above use conversion schema.validateSync(value, { abortEarly: false }) - onUpdate(schema.cast(value)); + onUpdate(schema.cast(value), auth); onBack() } catch (err) { const errors = err.inner as yup.ValidationError[] @@ -72,10 +88,9 @@ export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VN setErrors(pathMessages) } } - const config = useContext(ConfigContext) + const config = useConfigContext() return <div> - <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -108,7 +123,7 @@ export function UpdatePage({ onUpdate, isLoading, selected, onBack }: Props): VN </FormProvider> - <div class="buttons is-right"> + <div class="buttons is-right mt-4"> <button class="button" onClick={onBack} ><Message id="Cancel" /></button> <button class="button is-success" onClick={submit} ><Message id="Confirm" /></button> </div> diff --git a/packages/frontend/src/routes/instances/update/index.tsx b/packages/frontend/src/routes/instances/update/index.tsx @@ -14,11 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { Fragment, h, VNode } from "preact"; -import { useContext, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; import { UpdateTokenModal } from "../../../components/modal"; -import { InstanceContext } from "../../../context/backend"; +import { useInstanceContext } from "../../../context/backend"; import { MerchantBackend } from "../../../declaration"; -import { SwrError, useBackendInstance, useBackendInstanceMutateAPI } from "../../../hooks/backend"; +import { SwrError, useBackendInstance, useInstanceMutateAPI } from "../../../hooks/backend"; import { UpdatePage } from "./UpdatePage"; interface Props { @@ -33,10 +33,10 @@ interface Props { } export default function Update({ onBack, onConfirm, onLoadError, onUpdateError, onUnauthorized }: Props): VNode { - const { updateInstance, setNewToken, clearToken } = useBackendInstanceMutateAPI(); - const [updatingToken, setUpdatingToken] = useState<string | null>(null) + const { updateInstance, setNewToken, clearToken } = useInstanceMutateAPI(); + const [updatingToken, setUpdatingToken] = useState<boolean>(false) const details = useBackendInstance() - const { id } = useContext(InstanceContext) + const { id, token } = useInstanceContext() if (!details.data) { if (details.unauthorized) return onUnauthorized() @@ -51,15 +51,16 @@ export default function Update({ onBack, onConfirm, onLoadError, onUpdateError, onBack={onBack} isLoading={false} selected={details.data} - onUpdate={(d: MerchantBackend.Instances.InstanceReconfigurationMessage): Promise<void> => { - return updateInstance(d).then(onConfirm).catch(onUpdateError) + onUpdate={(d: MerchantBackend.Instances.InstanceReconfigurationMessage, t?: MerchantBackend.Instances.InstanceAuthConfigurationMessage): Promise<void> => { + return updateInstance(d,t).then(onConfirm).catch(onUpdateError) }} /> + <button class="button" onClick={() => setUpdatingToken(true)}>auth</button> {updatingToken && <UpdateTokenModal - oldToken={updatingToken} + oldToken={token} element={{ id, name: details.data.name }} - onCancel={() => setUpdatingToken(null)} + onCancel={() => setUpdatingToken(false)} onClear={() => clearToken()} - onConfirm={(newToken) => setNewToken(newToken) } + onConfirm={(newToken) => setNewToken(newToken)} />} </Fragment> } \ No newline at end of file diff --git a/packages/frontend/src/schemas/index.ts b/packages/frontend/src/schemas/index.ts @@ -48,11 +48,10 @@ function currencyWithAmountIsValid(value?: string): boolean { export const InstanceSchema = yup.object().shape({ id: yup.string().required().meta({type: 'url'}), name: yup.string().required(), - auth_token: yup.string() - .min(8).max(20) - .optional() - .nullable() - .meta({type: 'secured'}), + auth: yup.object().shape({ + method: yup.string().matches(/^(external|token)$/), + token: yup.string().optional().nullable(), + }), payto_uris: yup.array().of(yup.string()) .min(1) .meta({type: 'array'}) diff --git a/packages/frontend/src/utils/constants.ts b/packages/frontend/src/utils/constants.ts @@ -21,4 +21,7 @@ //https://tools.ietf.org/html/rfc8905 export const PAYTO_REGEX=/^payto:\/\/[a-zA-Z][a-zA-Z0-9-.]+(\/[a-zA-Z0-9\-\.\~\(\)@_%:!$&'*+,;=]*)*\??((amount|receiver-name|sender-name|instruction|message)=[a-zA-Z0-9\-\.\~\(\)@_%:!$'*+,;=]*&?)*$/ + export const AMOUNT_REGEX=/^[a-zA-Z][a-zA-Z]*:[0-9][0-9,]*\.?[0-9,]*$/ + +export const INSTANCE_ID_LOOKUP = /^\/instances\/([^/]*)\/?$/