From 1e92093a50962f4702339e872caa4f82af90af70 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 12 Apr 2022 12:54:57 +0200 Subject: anastasis: discovery --- .../src/components/menu/NavigationBar.tsx | 2 +- packages/anastasis-webui/src/context/anastasis.ts | 7 +- .../src/hooks/use-anastasis-reducer.ts | 77 ++++++++++++++-- .../src/pages/home/SecretSelectionScreen.tsx | 100 ++++++++++++++++++++- 4 files changed, 172 insertions(+), 14 deletions(-) (limited to 'packages/anastasis-webui') diff --git a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx index 8d5a0473b..bc6d923d7 100644 --- a/packages/anastasis-webui/src/components/menu/NavigationBar.tsx +++ b/packages/anastasis-webui/src/components/menu/NavigationBar.tsx @@ -46,7 +46,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode { Contact us Report a bug diff --git a/packages/anastasis-webui/src/context/anastasis.ts b/packages/anastasis-webui/src/context/anastasis.ts index c2e7b2a47..40d25d144 100644 --- a/packages/anastasis-webui/src/context/anastasis.ts +++ b/packages/anastasis-webui/src/context/anastasis.ts @@ -23,11 +23,9 @@ import { createContext, h, VNode } from "preact"; import { useContext } from "preact/hooks"; import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer"; -type Type = AnastasisReducerApi | undefined; - const initial = undefined; -const Context = createContext(initial); +const Context = createContext(initial); interface Props { value: AnastasisReducerApi; @@ -38,4 +36,5 @@ export const AnastasisProvider = ({ value, children }: Props): VNode => { return h(Context.Provider, { value, children }); }; -export const useAnastasisContext = (): Type => useContext(Context); +export const useAnastasisContext = (): AnastasisReducerApi | undefined => + useContext(Context); diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts index b18610427..321cf3f0a 100644 --- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts +++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts @@ -1,8 +1,12 @@ import { TalerErrorCode } from "@gnu-taler/taler-util"; import { + AggregatedPolicyMetaInfo, BackupStates, + discoverPolicies, + DiscoveryCursor, getBackupStartState, getRecoveryStartState, + PolicyMetaInfo, RecoveryStates, reduceAction, ReducerState, @@ -15,6 +19,7 @@ const remoteReducer = false; interface AnastasisState { reducerState: ReducerState | undefined; currentError: any; + discoveryState: DiscoveryUiState; } async function getBackupStartStateRemote(): Promise { @@ -98,9 +103,21 @@ export interface ReducerTransactionHandle { transition(action: string, args: any): Promise; } +/** + * UI-relevant state of the policy discovery process. + */ +export interface DiscoveryUiState { + state: "none" | "active" | "finished"; + + aggregatedPolicies?: AggregatedPolicyMetaInfo[]; + + cursor?: DiscoveryCursor; +} + export interface AnastasisReducerApi { currentReducerState: ReducerState | undefined; currentError: any; + discoveryState: DiscoveryUiState; dismissError: () => void; startBackup: () => void; startRecover: () => void; @@ -109,6 +126,8 @@ export interface AnastasisReducerApi { transition(action: string, args: any): Promise; exportState: () => string; importState: (s: string) => void; + discoverStart(): Promise; + discoverMore(): Promise; /** * Run multiple reducer steps in a transaction without * affecting the UI-visible transition state in-between. @@ -152,6 +171,9 @@ export function useAnastasisReducer(): AnastasisReducerApi { () => ({ reducerState: getStateFromStorage(), currentError: undefined, + discoveryState: { + state: "none", + }, }), ); @@ -192,6 +214,7 @@ export function useAnastasisReducer(): AnastasisReducerApi { return { currentReducerState: anastasisState.reducerState, currentError: anastasisState.currentError, + discoveryState: anastasisState.discoveryState, async startBackup() { let s: ReducerState; if (remoteReducer) { @@ -213,17 +236,59 @@ export function useAnastasisReducer(): AnastasisReducerApi { } }, exportState() { - const state = getStateFromStorage() - return JSON.stringify(state) + const state = getStateFromStorage(); + return JSON.stringify(state); }, importState(s: string) { try { - const state = JSON.parse(s) - setAnastasisState({ reducerState: state, currentError: undefined }) + const state = JSON.parse(s); + setAnastasisState({ + reducerState: state, + currentError: undefined, + discoveryState: { + state: "none", + }, + }); } catch (e) { - throw Error('could not restore the state') + throw Error("could not restore the state"); + } + }, + async discoverStart(): Promise { + const res = await discoverPolicies(this.currentReducerState!, undefined); + const aggregatedPolicies: AggregatedPolicyMetaInfo[] = []; + const polHashToIndex: Record = {}; + for (const pol of res.policies) { + const oldIndex = polHashToIndex[pol.policy_hash]; + if (oldIndex != null) { + aggregatedPolicies[oldIndex].providers.push({ + provider_url: pol.provider_url, + version: pol.version, + }); + } else { + aggregatedPolicies.push({ + attribute_mask: pol.attribute_mask, + policy_hash: pol.policy_hash, + providers: [ + { + provider_url: pol.provider_url, + version: pol.version, + }, + ], + secret_name: pol.secret_name, + }); + polHashToIndex[pol.policy_hash] = aggregatedPolicies.length - 1; + } } + setAnastasisState({ + ...anastasisState, + discoveryState: { + state: "finished", + aggregatedPolicies, + cursor: res.cursor, + }, + }); }, + async discoverMore(): Promise {}, async startRecover() { let s: ReducerState; if (remoteReducer) { @@ -301,7 +366,7 @@ export function useAnastasisReducer(): AnastasisReducerApi { } class ReducerTxImpl implements ReducerTransactionHandle { - constructor(public transactionState: ReducerState) { } + constructor(public transactionState: ReducerState) {} async transition(action: string, args: any): Promise { let s: ReducerState; if (remoteReducer) { diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx index 076d205b6..84f0303fe 100644 --- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx @@ -1,9 +1,10 @@ import { AuthenticationProviderStatus, AuthenticationProviderStatusOk, + PolicyMetaInfo, } from "@gnu-taler/anastasis-core"; import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { AsyncButton } from "../../components/AsyncButton"; import { PhoneNumberInput } from "../../components/fields/NumberInput"; import { useAnastasisContext } from "../../context/anastasis"; @@ -13,8 +14,100 @@ import { AnastasisClientFrame } from "./index"; export function SecretSelectionScreen(): VNode { const [selectingVersion, setSelectingVersion] = useState(false); const reducer = useAnastasisContext(); + const [manageProvider, setManageProvider] = useState(false); + + useEffect(() => { + async function f() { + if (reducer) { + await reducer.discoverStart(); + } + } + f().catch((e) => console.log(e)); + }, []); + + if (!reducer) { + return
no reducer in context
; + } + + if ( + !reducer.currentReducerState || + reducer.currentReducerState.recovery_state === undefined + ) { + return
invalid state
; + } + const provs = reducer.currentReducerState.authentication_providers ?? {}; + const recoveryDocument = reducer.currentReducerState.recovery_document; + + if (manageProvider) { + return setManageProvider(false)} />; + } + + if (reducer.discoveryState.state === "none") { + // Can this even happen? + return ( + +
waiting to start discovery
+
+ ); + } + + if (reducer.discoveryState.state === "active") { + return ( + +
loading secret versions
+
+ ); + } + + const policies = reducer.discoveryState.aggregatedPolicies ?? []; + + if (policies.length === 0) { + return ( + () => {}} + > + ); + } + + return ( + +

Found versions:

+ {policies.map((x) => ( +
+ {x.policy_hash} / {x.secret_name} + +
+ ))} + +
+ ); +} + +export function OldSecretSelectionScreen(): VNode { + const [selectingVersion, setSelectingVersion] = useState(false); + const reducer = useAnastasisContext(); const [manageProvider, setManageProvider] = useState(false); + + useEffect(() => { + async function f() { + if (reducer) { + await reducer.discoverStart(); + } + } + f().catch((e) => console.log(e)); + }, []); + const currentVersion = (reducer?.currentReducerState && "recovery_document" in reducer.currentReducerState && @@ -71,15 +164,16 @@ export function SecretSelectionScreen(): VNode { return setManageProvider(false)} />; } - const provierInfo = provs[ + const providerInfo = provs[ recoveryDocument.provider_url ] as AuthenticationProviderStatusOk; + return (
-

{provierInfo.business_name}

+

{providerInfo.business_name}

{currentVersion === 0 ? (

Set to recover the latest version

-- cgit v1.2.3