wallet-core/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts

417 lines
11 KiB
TypeScript
Raw Normal View History

/*
2022-06-06 16:45:01 +02:00
This file is part of GNU Anastasis
(C) 2021-2022 Anastasis SARL
2022-06-06 16:45:01 +02:00
GNU Anastasis is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
2022-06-06 16:45:01 +02:00
GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
2022-06-06 16:45:01 +02:00
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
2022-06-06 16:45:01 +02:00
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
2021-10-14 17:08:41 +02:00
import { TalerErrorCode } from "@gnu-taler/taler-util";
import {
2022-04-12 12:54:57 +02:00
AggregatedPolicyMetaInfo,
BackupStates,
completeProviderStatus,
2022-04-12 12:54:57 +02:00
discoverPolicies,
DiscoveryCursor,
getBackupStartState,
getRecoveryStartState,
2022-04-12 20:55:34 +02:00
mergeDiscoveryAggregate,
RecoveryStates,
reduceAction,
ReducerState,
2022-01-24 18:39:27 +01:00
} from "@gnu-taler/anastasis-core";
2021-10-11 10:58:55 +02:00
import { useState } from "preact/hooks";
2021-10-18 19:19:20 +02:00
const reducerBaseUrl = "http://localhost:5000/";
const remoteReducer = false;
2021-10-11 10:58:55 +02:00
interface AnastasisState {
reducerState: ReducerState | undefined;
currentError: any;
2022-04-12 12:54:57 +02:00
discoveryState: DiscoveryUiState;
2021-10-11 10:58:55 +02:00
}
2021-10-18 19:19:20 +02:00
async function getBackupStartStateRemote(): Promise<ReducerState> {
2021-10-14 17:08:41 +02:00
let resp: Response;
try {
resp = await fetch(new URL("start-backup", reducerBaseUrl).href);
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Network request to remote reducer ${reducerBaseUrl} failed`,
} as any;
}
try {
return await resp.json();
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Could not parse response from reducer`,
} as any;
}
2021-10-11 10:58:55 +02:00
}
2021-10-18 19:19:20 +02:00
async function getRecoveryStartStateRemote(): Promise<ReducerState> {
2021-10-14 17:08:41 +02:00
let resp: Response;
try {
resp = await fetch(new URL("start-recovery", reducerBaseUrl).href);
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Network request to remote reducer ${reducerBaseUrl} failed`,
} as any;
}
try {
return await resp.json();
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Could not parse response from reducer`,
} as any;
}
2021-10-11 10:58:55 +02:00
}
2021-10-18 19:19:20 +02:00
async function reduceStateRemote(
2021-10-11 10:58:55 +02:00
state: any,
action: string,
args: any,
): Promise<ReducerState> {
2021-10-14 17:08:41 +02:00
let resp: Response;
try {
resp = await fetch(new URL("action", reducerBaseUrl).href, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
state,
action,
arguments: args,
}),
});
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Network request to remote reducer ${reducerBaseUrl} failed`,
} as any;
}
try {
return await resp.json();
} catch (e) {
return {
code: TalerErrorCode.ANASTASIS_REDUCER_NETWORK_FAILED,
message: `Could not parse response from reducer`,
} as any;
}
2021-10-11 10:58:55 +02:00
}
2021-10-13 10:48:25 +02:00
export interface ReducerTransactionHandle {
transactionState: ReducerState;
transition(action: string, args: any): Promise<ReducerState>;
}
2022-04-12 12:54:57 +02:00
/**
* UI-relevant state of the policy discovery process.
*/
export interface DiscoveryUiState {
state: "none" | "active" | "finished";
aggregatedPolicies?: AggregatedPolicyMetaInfo[];
cursor?: DiscoveryCursor;
}
2021-10-11 10:58:55 +02:00
export interface AnastasisReducerApi {
2021-10-13 10:48:25 +02:00
currentReducerState: ReducerState | undefined;
2021-10-11 10:58:55 +02:00
currentError: any;
2022-04-12 12:54:57 +02:00
discoveryState: DiscoveryUiState;
2021-10-13 10:48:25 +02:00
dismissError: () => void;
2021-10-11 10:58:55 +02:00
startBackup: () => void;
startRecover: () => void;
2021-10-13 10:48:25 +02:00
reset: () => void;
back: () => Promise<void>;
transition(action: string, args: any): Promise<void>;
2021-11-24 21:38:39 +01:00
exportState: () => string;
importState: (s: string) => void;
2022-04-12 12:54:57 +02:00
discoverStart(): Promise<void>;
discoverMore(): Promise<void>;
2021-10-13 10:48:25 +02:00
/**
* Run multiple reducer steps in a transaction without
* affecting the UI-visible transition state in-between.
*/
runTransaction(
f: (h: ReducerTransactionHandle) => Promise<void>,
): Promise<void>;
2021-10-13 10:48:25 +02:00
}
function storageGet(key: string): string | null {
if (typeof localStorage === "object") {
return localStorage.getItem(key);
}
return null;
}
function storageSet(key: string, value: any): void {
if (typeof localStorage === "object") {
return localStorage.setItem(key, value);
}
}
2021-11-24 21:38:39 +01:00
function getStateFromStorage(): any {
2021-10-13 10:48:25 +02:00
let state: any;
try {
2021-10-19 15:56:52 +02:00
const s = storageGet("anastasisReducerState");
2021-10-13 10:48:25 +02:00
if (s === "undefined") {
state = undefined;
} else if (s) {
state = JSON.parse(s);
}
} catch (e) {
2022-08-26 17:59:00 +02:00
console.log("ERROR: getStateFromStorage ", e);
2021-10-13 10:48:25 +02:00
}
return state ?? undefined;
2021-10-11 10:58:55 +02:00
}
export function useAnastasisReducer(): AnastasisReducerApi {
2021-10-13 10:48:25 +02:00
const [anastasisState, setAnastasisStateInternal] = useState<AnastasisState>(
() => ({
2021-11-24 21:38:39 +01:00
reducerState: getStateFromStorage(),
2021-10-13 10:48:25 +02:00
currentError: undefined,
2022-04-12 12:54:57 +02:00
discoveryState: {
state: "none",
},
2021-10-13 10:48:25 +02:00
}),
);
const setAnastasisState = (newState: AnastasisState) => {
try {
storageSet(
2021-10-13 10:48:25 +02:00
"anastasisReducerState",
JSON.stringify(newState.reducerState),
);
} catch (e) {
2022-08-26 17:59:00 +02:00
console.log("ERROR setAnastasisState", e);
2021-10-13 10:48:25 +02:00
}
setAnastasisStateInternal(newState);
const tryUpdateProviders = () => {
const reducerState = newState.reducerState;
if (
reducerState?.reducer_type !== "backup" &&
reducerState?.reducer_type !== "recovery"
) {
return;
}
const provMap = reducerState.authentication_providers;
if (!provMap) {
return;
}
const doUpdate = async () => {
const updates = await completeProviderStatus(provMap);
if (Object.keys(updates).length === 0) {
return;
}
const rs2 = reducerState;
if (rs2.reducer_type !== "backup" && rs2.reducer_type !== "recovery") {
return;
}
setAnastasisState({
...anastasisState,
reducerState: {
...rs2,
authentication_providers: {
...rs2.authentication_providers,
...updates,
},
},
});
};
2022-08-26 17:59:00 +02:00
doUpdate().catch((e) => console.log("ERROR doUpdate", e));
};
tryUpdateProviders();
2021-10-13 10:48:25 +02:00
};
2021-10-11 10:58:55 +02:00
2021-11-24 21:38:39 +01:00
async function doTransition(action: string, args: any): Promise<void> {
2021-10-18 19:19:20 +02:00
let s: ReducerState;
if (remoteReducer) {
s = await reduceStateRemote(anastasisState.reducerState, action, args);
} else {
s = await reduceAction(anastasisState.reducerState!, action, args);
}
2022-04-13 19:32:12 +02:00
if (s.reducer_type === "error") {
2021-10-11 10:58:55 +02:00
setAnastasisState({ ...anastasisState, currentError: s });
} else {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
}
}
return {
currentReducerState: anastasisState.reducerState,
currentError: anastasisState.currentError,
2022-04-12 12:54:57 +02:00
discoveryState: anastasisState.discoveryState,
2021-10-11 10:58:55 +02:00
async startBackup() {
2021-10-18 19:19:20 +02:00
let s: ReducerState;
if (remoteReducer) {
s = await getBackupStartStateRemote();
} else {
s = await getBackupStartState();
}
2022-04-13 19:32:12 +02:00
if (s.reducer_type === "error") {
2021-10-14 17:08:41 +02:00
setAnastasisState({
...anastasisState,
currentError: s,
});
} else {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
}
2021-10-11 10:58:55 +02:00
},
2021-11-24 21:38:39 +01:00
exportState() {
2022-04-12 12:54:57 +02:00
const state = getStateFromStorage();
return JSON.stringify(state);
2021-11-24 21:38:39 +01:00
},
importState(s: string) {
try {
2022-04-12 12:54:57 +02:00
const state = JSON.parse(s);
setAnastasisState({
reducerState: state,
currentError: undefined,
discoveryState: {
state: "none",
},
});
2021-11-24 21:38:39 +01:00
} catch (e) {
2022-04-12 12:54:57 +02:00
throw Error("could not restore the state");
}
},
async discoverStart(): Promise<void> {
const res = await discoverPolicies(this.currentReducerState!, undefined);
2022-04-12 20:55:34 +02:00
const aggregatedPolicies = mergeDiscoveryAggregate(res.policies, []);
2022-04-12 12:54:57 +02:00
setAnastasisState({
...anastasisState,
discoveryState: {
state: "finished",
aggregatedPolicies,
cursor: res.cursor,
},
});
2021-11-24 21:38:39 +01:00
},
2022-06-06 05:54:55 +02:00
async discoverMore(): Promise<void> {
return;
},
2021-10-11 10:58:55 +02:00
async startRecover() {
2021-10-18 19:19:20 +02:00
let s: ReducerState;
if (remoteReducer) {
s = await getRecoveryStartStateRemote();
} else {
s = await getRecoveryStartState();
}
2022-04-13 19:32:12 +02:00
if (s.reducer_type === "error") {
2021-10-14 17:08:41 +02:00
setAnastasisState({
...anastasisState,
currentError: s,
});
} else {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: s,
});
}
2021-10-11 10:58:55 +02:00
},
transition(action: string, args: any) {
return doTransition(action, args);
2021-10-11 10:58:55 +02:00
},
async back() {
2021-10-13 10:48:25 +02:00
const reducerState = anastasisState.reducerState;
if (!reducerState) {
return;
}
2021-10-11 10:58:55 +02:00
if (
2022-04-13 19:32:12 +02:00
(reducerState.reducer_type === "backup" &&
reducerState.backup_state === BackupStates.ContinentSelecting) ||
(reducerState.reducer_type === "recovery" &&
reducerState.recovery_state === RecoveryStates.ContinentSelecting)
2021-10-11 10:58:55 +02:00
) {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: undefined,
});
} else {
await doTransition("back", {});
2021-10-11 10:58:55 +02:00
}
},
2021-10-13 10:48:25 +02:00
dismissError() {
setAnastasisState({ ...anastasisState, currentError: undefined });
},
reset() {
setAnastasisState({
...anastasisState,
currentError: undefined,
reducerState: undefined,
});
},
async runTransaction(f) {
const txHandle = new ReducerTxImpl(anastasisState.reducerState!);
try {
await f(txHandle);
} catch (e) {
console.log("exception during reducer transaction", e);
}
const s = txHandle.transactionState;
2022-04-13 19:32:12 +02:00
if (s.reducer_type === "error") {
setAnastasisState({
...anastasisState,
currentError: txHandle.transactionState,
});
} else {
setAnastasisState({
...anastasisState,
reducerState: txHandle.transactionState,
currentError: undefined,
});
2021-10-13 10:48:25 +02:00
}
},
2021-10-11 10:58:55 +02:00
};
}
2021-10-13 10:48:25 +02:00
class ReducerTxImpl implements ReducerTransactionHandle {
2022-06-06 05:54:55 +02:00
constructor(public transactionState: ReducerState) { }
2021-10-13 10:48:25 +02:00
async transition(action: string, args: any): Promise<ReducerState> {
2021-10-18 19:19:20 +02:00
let s: ReducerState;
if (remoteReducer) {
s = await reduceStateRemote(this.transactionState, action, args);
} else {
s = await reduceAction(this.transactionState, action, args);
}
this.transactionState = s;
2021-10-13 10:48:25 +02:00
// Abort transaction as soon as we transition into an error state.
2022-04-13 19:32:12 +02:00
if (this.transactionState.reducer_type === "error") {
2021-10-13 10:48:25 +02:00
throw Error("transition resulted in error");
}
return this.transactionState;
}
}