diff options
Diffstat (limited to 'packages/anastasis-webui')
4 files changed, 172 insertions, 14 deletions
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 </a> <a - href="https://bugs.anastasis.li/" + href="https://bugs.anastasis.lu/" style={{ alignSelf: "center", padding: "0.5em" }} > 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<Type>(initial); +const Context = createContext<AnastasisReducerApi | undefined>(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<ReducerState> { @@ -98,9 +103,21 @@ export interface ReducerTransactionHandle { transition(action: string, args: any): Promise<ReducerState>; } +/** + * 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<void>; exportState: () => string; importState: (s: string) => void; + discoverStart(): Promise<void>; + discoverMore(): Promise<void>; /** * 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<void> { + const res = await discoverPolicies(this.currentReducerState!, undefined); + const aggregatedPolicies: AggregatedPolicyMetaInfo[] = []; + const polHashToIndex: Record<string, number> = {}; + 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<void> {}, 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<ReducerState> { 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<boolean>(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 <div>no reducer in context</div>; + } + + if ( + !reducer.currentReducerState || + reducer.currentReducerState.recovery_state === undefined + ) { + return <div>invalid state</div>; + } + const provs = reducer.currentReducerState.authentication_providers ?? {}; + const recoveryDocument = reducer.currentReducerState.recovery_document; + + if (manageProvider) { + return <AddingProviderScreen onCancel={() => setManageProvider(false)} />; + } + + if (reducer.discoveryState.state === "none") { + // Can this even happen? + return ( + <AnastasisClientFrame title="Recovery: Select secret"> + <div>waiting to start discovery</div> + </AnastasisClientFrame> + ); + } + + if (reducer.discoveryState.state === "active") { + return ( + <AnastasisClientFrame title="Recovery: Select secret"> + <div>loading secret versions</div> + </AnastasisClientFrame> + ); + } + + const policies = reducer.discoveryState.aggregatedPolicies ?? []; + + if (policies.length === 0) { + return ( + <ChooseAnotherProviderScreen + providers={provs} + selected="" + onChange={(newProv) => () => {}} + ></ChooseAnotherProviderScreen> + ); + } + + return ( + <AnastasisClientFrame title="Recovery: Select secret" hideNext="Please select version to recover"> + <p>Found versions:</p> + {policies.map((x) => ( + <div> + {x.policy_hash} / {x.secret_name} + <button + onClick={async () => { + await reducer.transition("change_version", { + selection: x, + }); + }} + > + Recover + </button> + </div> + ))} + <button>Load older versions</button> + </AnastasisClientFrame> + ); +} + +export function OldSecretSelectionScreen(): VNode { + const [selectingVersion, setSelectingVersion] = useState<boolean>(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 <AddingProviderScreen onCancel={() => setManageProvider(false)} />; } - const provierInfo = provs[ + const providerInfo = provs[ recoveryDocument.provider_url ] as AuthenticationProviderStatusOk; + return ( <AnastasisClientFrame title="Recovery: Select secret"> <div class="columns"> <div class="column"> <div class="box" style={{ border: "2px solid green" }}> - <h1 class="subtitle">{provierInfo.business_name}</h1> + <h1 class="subtitle">{providerInfo.business_name}</h1> <div class="block"> {currentVersion === 0 ? ( <p>Set to recover the latest version</p> |