2021-10-14 17:08:41 +02:00
|
|
|
import { TalerErrorCode } from "@gnu-taler/taler-util";
|
2021-10-11 10:58:55 +02:00
|
|
|
import { useState } from "preact/hooks";
|
|
|
|
|
2021-10-13 10:48:25 +02:00
|
|
|
export type ReducerState =
|
|
|
|
| ReducerStateBackup
|
|
|
|
| ReducerStateRecovery
|
|
|
|
| ReducerStateError;
|
|
|
|
|
|
|
|
export interface ReducerStateBackup {
|
|
|
|
recovery_state: undefined;
|
|
|
|
backup_state: BackupStates;
|
|
|
|
code: undefined;
|
|
|
|
continents: any;
|
|
|
|
countries: any;
|
2021-10-13 19:32:14 +02:00
|
|
|
identity_attributes?: { [n: string]: string };
|
2021-10-13 10:48:25 +02:00
|
|
|
authentication_providers: any;
|
|
|
|
authentication_methods?: AuthMethod[];
|
|
|
|
required_attributes: any;
|
|
|
|
secret_name?: string;
|
|
|
|
policies?: {
|
|
|
|
methods: {
|
|
|
|
authentication_method: number;
|
|
|
|
provider: string;
|
|
|
|
}[];
|
|
|
|
}[];
|
|
|
|
success_details: {
|
|
|
|
[provider_url: string]: {
|
|
|
|
policy_version: number;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
payments?: string[];
|
|
|
|
policy_payment_requests?: {
|
|
|
|
payto: string;
|
|
|
|
provider: string;
|
|
|
|
}[];
|
2021-10-14 15:35:34 +02:00
|
|
|
|
|
|
|
core_secret?: {
|
|
|
|
mime: string;
|
|
|
|
value: string;
|
|
|
|
};
|
2021-10-13 10:48:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface AuthMethod {
|
|
|
|
type: string;
|
|
|
|
instructions: string;
|
|
|
|
challenge: string;
|
|
|
|
}
|
|
|
|
|
2021-10-13 19:32:14 +02:00
|
|
|
export interface ChallengeInfo {
|
|
|
|
cost: string;
|
|
|
|
instructions: string;
|
|
|
|
type: string;
|
|
|
|
uuid: string;
|
|
|
|
}
|
|
|
|
|
2021-10-13 10:48:25 +02:00
|
|
|
export interface ReducerStateRecovery {
|
|
|
|
backup_state: undefined;
|
|
|
|
recovery_state: RecoveryStates;
|
|
|
|
code: undefined;
|
|
|
|
|
2021-10-13 19:32:14 +02:00
|
|
|
identity_attributes?: { [n: string]: string };
|
|
|
|
|
2021-10-13 10:48:25 +02:00
|
|
|
continents: any;
|
|
|
|
countries: any;
|
2021-10-13 11:35:24 +02:00
|
|
|
required_attributes: any;
|
2021-10-13 19:32:14 +02:00
|
|
|
|
|
|
|
recovery_information?: {
|
|
|
|
challenges: ChallengeInfo[];
|
|
|
|
policies: {
|
|
|
|
/**
|
|
|
|
* UUID of the associated challenge.
|
|
|
|
*/
|
|
|
|
uuid: string;
|
|
|
|
}[][];
|
|
|
|
};
|
|
|
|
|
|
|
|
recovery_document?: {
|
|
|
|
secret_name: string;
|
|
|
|
provider_url: string;
|
|
|
|
version: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
selected_challenge_uuid?: string;
|
|
|
|
|
|
|
|
challenge_feedback?: { [uuid: string]: ChallengeFeedback };
|
|
|
|
|
|
|
|
core_secret?: {
|
|
|
|
mime: string;
|
|
|
|
value: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
authentication_providers?: {
|
|
|
|
[url: string]: {
|
|
|
|
business_name: string;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
recovery_error: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ChallengeFeedback {
|
|
|
|
state: string;
|
2021-10-13 10:48:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface ReducerStateError {
|
|
|
|
backup_state: undefined;
|
|
|
|
recovery_state: undefined;
|
|
|
|
code: number;
|
|
|
|
}
|
2021-10-11 10:58:55 +02:00
|
|
|
|
|
|
|
interface AnastasisState {
|
|
|
|
reducerState: ReducerState | undefined;
|
|
|
|
currentError: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
export enum BackupStates {
|
|
|
|
ContinentSelecting = "CONTINENT_SELECTING",
|
|
|
|
CountrySelecting = "COUNTRY_SELECTING",
|
2021-10-13 10:48:25 +02:00
|
|
|
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
|
|
|
|
AuthenticationsEditing = "AUTHENTICATIONS_EDITING",
|
|
|
|
PoliciesReviewing = "POLICIES_REVIEWING",
|
|
|
|
SecretEditing = "SECRET_EDITING",
|
|
|
|
TruthsPaying = "TRUTHS_PAYING",
|
|
|
|
PoliciesPaying = "POLICIES_PAYING",
|
|
|
|
BackupFinished = "BACKUP_FINISHED",
|
2021-10-11 10:58:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export enum RecoveryStates {
|
|
|
|
ContinentSelecting = "CONTINENT_SELECTING",
|
|
|
|
CountrySelecting = "COUNTRY_SELECTING",
|
2021-10-13 11:35:24 +02:00
|
|
|
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
|
2021-10-13 19:32:14 +02:00
|
|
|
SecretSelecting = "SECRET_SELECTING",
|
|
|
|
ChallengeSelecting = "CHALLENGE_SELECTING",
|
|
|
|
ChallengePaying = "CHALLENGE_PAYING",
|
|
|
|
ChallengeSolving = "CHALLENGE_SOLVING",
|
|
|
|
RecoveryFinished = "RECOVERY_FINISHED",
|
2021-10-11 10:58:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const reducerBaseUrl = "http://localhost:5000/";
|
|
|
|
|
|
|
|
async function getBackupStartState(): 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
|
|
|
}
|
|
|
|
|
|
|
|
async function getRecoveryStartState(): 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
|
|
|
}
|
|
|
|
|
|
|
|
async function reduceState(
|
|
|
|
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>;
|
|
|
|
}
|
|
|
|
|
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;
|
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;
|
2021-10-11 10:58:55 +02:00
|
|
|
back: () => void;
|
|
|
|
transition(action: string, args: any): 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>): void;
|
|
|
|
}
|
|
|
|
|
2021-10-15 09:44:41 +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-10-13 10:48:25 +02:00
|
|
|
function restoreState(): any {
|
|
|
|
let state: any;
|
|
|
|
try {
|
2021-10-15 09:44:41 +02:00
|
|
|
let s = storageGet("anastasisReducerState");
|
2021-10-13 10:48:25 +02:00
|
|
|
if (s === "undefined") {
|
|
|
|
state = undefined;
|
|
|
|
} else if (s) {
|
|
|
|
console.log("restoring state from", s);
|
|
|
|
state = JSON.parse(s);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
|
|
|
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>(
|
|
|
|
() => ({
|
|
|
|
reducerState: restoreState(),
|
|
|
|
currentError: undefined,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
const setAnastasisState = (newState: AnastasisState) => {
|
|
|
|
try {
|
2021-10-15 09:44:41 +02:00
|
|
|
storageSet(
|
2021-10-13 10:48:25 +02:00
|
|
|
"anastasisReducerState",
|
|
|
|
JSON.stringify(newState.reducerState),
|
|
|
|
);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
|
|
|
setAnastasisStateInternal(newState);
|
|
|
|
};
|
2021-10-11 10:58:55 +02:00
|
|
|
|
|
|
|
async function doTransition(action: string, args: any) {
|
|
|
|
console.log("reducing with", action, args);
|
|
|
|
const s = await reduceState(anastasisState.reducerState, action, args);
|
|
|
|
console.log("got new state from reducer", s);
|
|
|
|
if (s.code) {
|
|
|
|
setAnastasisState({ ...anastasisState, currentError: s });
|
|
|
|
} else {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
currentError: undefined,
|
|
|
|
reducerState: s,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
currentReducerState: anastasisState.reducerState,
|
|
|
|
currentError: anastasisState.currentError,
|
|
|
|
async startBackup() {
|
|
|
|
const s = await getBackupStartState();
|
2021-10-14 17:08:41 +02:00
|
|
|
if (s.code !== undefined) {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
currentError: s,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
currentError: undefined,
|
|
|
|
reducerState: s,
|
|
|
|
});
|
|
|
|
}
|
2021-10-11 10:58:55 +02:00
|
|
|
},
|
|
|
|
async startRecover() {
|
|
|
|
const s = await getRecoveryStartState();
|
2021-10-14 17:08:41 +02:00
|
|
|
if (s.code !== undefined) {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
currentError: s,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
currentError: undefined,
|
|
|
|
reducerState: s,
|
|
|
|
});
|
|
|
|
}
|
2021-10-11 10:58:55 +02:00
|
|
|
},
|
|
|
|
transition(action: string, args: any) {
|
|
|
|
doTransition(action, args);
|
|
|
|
},
|
|
|
|
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 (
|
2021-10-13 10:48:25 +02:00
|
|
|
reducerState.backup_state === BackupStates.ContinentSelecting ||
|
|
|
|
reducerState.recovery_state === RecoveryStates.ContinentSelecting
|
2021-10-11 10:58:55 +02:00
|
|
|
) {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
currentError: undefined,
|
|
|
|
reducerState: undefined,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
doTransition("back", {});
|
|
|
|
}
|
|
|
|
},
|
2021-10-13 10:48:25 +02:00
|
|
|
dismissError() {
|
|
|
|
setAnastasisState({ ...anastasisState, currentError: undefined });
|
|
|
|
},
|
|
|
|
reset() {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
currentError: undefined,
|
|
|
|
reducerState: undefined,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
runTransaction(f) {
|
|
|
|
async function run() {
|
|
|
|
const txHandle = new ReducerTxImpl(anastasisState.reducerState!);
|
|
|
|
try {
|
|
|
|
await f(txHandle);
|
|
|
|
} catch (e) {
|
|
|
|
console.log("exception during reducer transaction", e);
|
|
|
|
}
|
|
|
|
const s = txHandle.transactionState;
|
|
|
|
console.log("transaction finished, new state", s);
|
|
|
|
if (s.code !== undefined) {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
currentError: txHandle.transactionState,
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
setAnastasisState({
|
|
|
|
...anastasisState,
|
|
|
|
reducerState: txHandle.transactionState,
|
|
|
|
currentError: undefined,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
run();
|
|
|
|
},
|
2021-10-11 10:58:55 +02:00
|
|
|
};
|
|
|
|
}
|
2021-10-13 10:48:25 +02:00
|
|
|
|
|
|
|
class ReducerTxImpl implements ReducerTransactionHandle {
|
|
|
|
constructor(public transactionState: ReducerState) {}
|
|
|
|
async transition(action: string, args: any): Promise<ReducerState> {
|
|
|
|
console.log("making transition in transaction", action);
|
|
|
|
this.transactionState = await reduceState(
|
|
|
|
this.transactionState,
|
|
|
|
action,
|
|
|
|
args,
|
|
|
|
);
|
|
|
|
// Abort transaction as soon as we transition into an error state.
|
|
|
|
if (this.transactionState.code !== undefined) {
|
|
|
|
throw Error("transition resulted in error");
|
|
|
|
}
|
|
|
|
return this.transactionState;
|
|
|
|
}
|
|
|
|
}
|