common anstasis frame
This commit is contained in:
parent
fbf501e727
commit
3aad5e774d
@ -1,21 +1,11 @@
|
||||
import { FunctionalComponent, h } from 'preact';
|
||||
import { Route, Router } from 'preact-router';
|
||||
import { FunctionalComponent, h } from "preact";
|
||||
|
||||
import Home from '../routes/home';
|
||||
import Profile from '../routes/profile';
|
||||
import NotFoundPage from '../routes/notfound';
|
||||
import Header from './header';
|
||||
import AnastasisClient from "../routes/home";
|
||||
|
||||
const App: FunctionalComponent = () => {
|
||||
return (
|
||||
<div id="preact_root">
|
||||
<Header />
|
||||
<Router>
|
||||
<Route path="/" component={Home} />
|
||||
<Route path="/profile/" component={Profile} user="me" />
|
||||
<Route path="/profile/:user" component={Profile} />
|
||||
<NotFoundPage default />
|
||||
</Router>
|
||||
<AnastasisClient />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { FunctionalComponent, h } from 'preact';
|
||||
import { Link } from 'preact-router/match';
|
||||
import style from './style.css';
|
||||
|
||||
const Header: FunctionalComponent = () => {
|
||||
return (
|
||||
<header class={style.header}>
|
||||
<h1>Preact App</h1>
|
||||
<nav>
|
||||
<Link activeClassName={style.active} href="/">
|
||||
Home
|
||||
</Link>
|
||||
<Link activeClassName={style.active} href="/profile">
|
||||
Me
|
||||
</Link>
|
||||
<Link activeClassName={style.active} href="/profile/john">
|
||||
John
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
@ -1,48 +0,0 @@
|
||||
.header {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
padding: 0;
|
||||
background: #673AB7;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0 15px;
|
||||
font-size: 24px;
|
||||
line-height: 56px;
|
||||
font-weight: 400;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.header nav {
|
||||
float: right;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.header nav a {
|
||||
display: inline-block;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
padding: 0 15px;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
background: rgba(255,255,255,0);
|
||||
text-decoration: none;
|
||||
color: #FFF;
|
||||
will-change: background-color;
|
||||
}
|
||||
|
||||
.header nav a:hover,
|
||||
.header nav a:active {
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.header nav a.active {
|
||||
background: rgba(0,0,0,0.4);
|
||||
}
|
@ -11,6 +11,7 @@ export interface ReducerStateBackup {
|
||||
code: undefined;
|
||||
continents: any;
|
||||
countries: any;
|
||||
identity_attributes?: { [n: string]: string };
|
||||
authentication_providers: any;
|
||||
authentication_methods?: AuthMethod[];
|
||||
required_attributes: any;
|
||||
@ -39,14 +40,60 @@ export interface AuthMethod {
|
||||
challenge: string;
|
||||
}
|
||||
|
||||
export interface ChallengeInfo {
|
||||
cost: string;
|
||||
instructions: string;
|
||||
type: string;
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export interface ReducerStateRecovery {
|
||||
backup_state: undefined;
|
||||
recovery_state: RecoveryStates;
|
||||
code: undefined;
|
||||
|
||||
identity_attributes?: { [n: string]: string };
|
||||
|
||||
continents: any;
|
||||
countries: any;
|
||||
required_attributes: any;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface ReducerStateError {
|
||||
@ -76,6 +123,11 @@ export enum RecoveryStates {
|
||||
ContinentSelecting = "CONTINENT_SELECTING",
|
||||
CountrySelecting = "COUNTRY_SELECTING",
|
||||
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
|
||||
SecretSelecting = "SECRET_SELECTING",
|
||||
ChallengeSelecting = "CHALLENGE_SELECTING",
|
||||
ChallengePaying = "CHALLENGE_PAYING",
|
||||
ChallengeSolving = "CHALLENGE_SOLVING",
|
||||
RecoveryFinished = "RECOVERY_FINISHED",
|
||||
}
|
||||
|
||||
const reducerBaseUrl = "http://localhost:5000/";
|
||||
|
@ -1,14 +1,23 @@
|
||||
import {
|
||||
bytesToString,
|
||||
canonicalJson,
|
||||
decodeCrock,
|
||||
encodeCrock,
|
||||
stringToBytes,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { FunctionalComponent, h } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import {
|
||||
FunctionalComponent,
|
||||
ComponentChildren,
|
||||
h,
|
||||
createContext,
|
||||
} from "preact";
|
||||
import { useState, useContext, useRef, useLayoutEffect } from "preact/hooks";
|
||||
import {
|
||||
AnastasisReducerApi,
|
||||
AuthMethod,
|
||||
BackupStates,
|
||||
ChallengeFeedback,
|
||||
ChallengeInfo,
|
||||
RecoveryStates,
|
||||
ReducerStateBackup,
|
||||
ReducerStateRecovery,
|
||||
@ -16,22 +25,31 @@ import {
|
||||
} from "../../hooks/use-anastasis-reducer";
|
||||
import style from "./style.css";
|
||||
|
||||
interface ContinentSelectionProps {
|
||||
reducer: AnastasisReducerApi;
|
||||
reducerState: ReducerStateBackup | ReducerStateRecovery;
|
||||
}
|
||||
const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
|
||||
|
||||
function isBackup(reducer: AnastasisReducerApi) {
|
||||
return !!reducer.currentReducerState?.backup_state;
|
||||
}
|
||||
|
||||
function ContinentSelection(props: ContinentSelectionProps) {
|
||||
interface CommonReducerProps {
|
||||
reducer: AnastasisReducerApi;
|
||||
reducerState: ReducerStateBackup | ReducerStateRecovery;
|
||||
}
|
||||
|
||||
function withProcessLabel(reducer: AnastasisReducerApi, text: string): string {
|
||||
if (isBackup(reducer)) {
|
||||
return "Backup: " + text;
|
||||
}
|
||||
return "Recovery: " + text;
|
||||
}
|
||||
|
||||
function ContinentSelection(props: CommonReducerProps) {
|
||||
const { reducer, reducerState } = props;
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>{isBackup(reducer) ? "Backup" : "Recovery"}: Select Continent</h1>
|
||||
<ErrorBanner reducer={reducer} />
|
||||
<div>
|
||||
<AnastasisClientFrame
|
||||
hideNext
|
||||
title={withProcessLabel(reducer, "Select Continent")}
|
||||
>
|
||||
{reducerState.continents.map((x: any) => {
|
||||
const sel = (x: string) =>
|
||||
reducer.transition("select_continent", { continent: x });
|
||||
@ -41,26 +59,17 @@ function ContinentSelection(props: ContinentSelectionProps) {
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => reducer.back()}>Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
interface CountrySelectionProps {
|
||||
reducer: AnastasisReducerApi;
|
||||
reducerState: ReducerStateBackup | ReducerStateRecovery;
|
||||
}
|
||||
|
||||
function CountrySelection(props: CountrySelectionProps) {
|
||||
function CountrySelection(props: CommonReducerProps) {
|
||||
const { reducer, reducerState } = props;
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Backup: Select Country</h1>
|
||||
<ErrorBanner reducer={reducer} />
|
||||
<div>
|
||||
<AnastasisClientFrame
|
||||
hideNext
|
||||
title={withProcessLabel(reducer, "Select Country")}
|
||||
>
|
||||
{reducerState.countries.map((x: any) => {
|
||||
const sel = (x: any) =>
|
||||
reducer.transition("select_country", {
|
||||
@ -73,64 +82,111 @@ function CountrySelection(props: CountrySelectionProps) {
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => reducer.back()}>Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
const Home: FunctionalComponent = () => {
|
||||
const reducer = useAnastasisReducer();
|
||||
const reducerState = reducer.currentReducerState;
|
||||
if (!reducerState) {
|
||||
interface SolveEntryProps {
|
||||
reducer: AnastasisReducerApi;
|
||||
challenge: ChallengeInfo;
|
||||
feedback?: ChallengeFeedback;
|
||||
}
|
||||
|
||||
function SolveQuestionEntry(props: SolveEntryProps) {
|
||||
const [answer, setAnswer] = useState("");
|
||||
const { reducer, challenge, feedback } = props;
|
||||
const next = () =>
|
||||
reducer.transition("solve_challenge", {
|
||||
answer,
|
||||
});
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Home</h1>
|
||||
<p>
|
||||
<button autoFocus onClick={() => reducer.startBackup()}>
|
||||
Backup
|
||||
</button>
|
||||
<button onClick={() => reducer.startRecover()}>Recover</button>
|
||||
</p>
|
||||
<AnastasisClientFrame
|
||||
title="Recovery: Solve challenge"
|
||||
onNext={() => next()}
|
||||
>
|
||||
<p>Feedback: {JSON.stringify(feedback)}</p>
|
||||
<p>Question: {challenge.instructions}</p>
|
||||
<label>
|
||||
<input
|
||||
value={answer}
|
||||
onChange={(e) => setAnswer((e.target as HTMLInputElement).value)}
|
||||
type="test"
|
||||
/>
|
||||
</label>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function SecretEditor(props: BackupReducerProps) {
|
||||
const { reducer } = props;
|
||||
const [secretName, setSecretName] = useState("");
|
||||
const [secretValue, setSecretValue] = useState("");
|
||||
const secretNext = () => {
|
||||
reducer.runTransaction(async (tx) => {
|
||||
await tx.transition("enter_secret_name", {
|
||||
name: secretName,
|
||||
});
|
||||
await tx.transition("enter_secret", {
|
||||
secret: {
|
||||
value: encodeCrock(stringToBytes(secretValue)),
|
||||
mime: "text/plain",
|
||||
},
|
||||
expiration: {
|
||||
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
|
||||
},
|
||||
});
|
||||
await tx.transition("next", {});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<AnastasisClientFrame
|
||||
title="Backup: Provide secret"
|
||||
onNext={() => secretNext()}
|
||||
>
|
||||
<div>
|
||||
<label>
|
||||
Secret name:{" "}
|
||||
<input
|
||||
value={secretName}
|
||||
onChange={(e) =>
|
||||
setSecretName((e.target as HTMLInputElement).value)
|
||||
}
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Secret value:{" "}
|
||||
<input
|
||||
value={secretValue}
|
||||
onChange={(e) =>
|
||||
setSecretValue((e.target as HTMLInputElement).value)
|
||||
}
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
or:
|
||||
<div>
|
||||
<label>
|
||||
File Upload: <input type="file" />
|
||||
</label>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
console.log("state", reducer.currentReducerState);
|
||||
}
|
||||
|
||||
if (
|
||||
reducerState.backup_state === BackupStates.ContinentSelecting ||
|
||||
reducerState.recovery_state === RecoveryStates.ContinentSelecting
|
||||
) {
|
||||
return <ContinentSelection reducer={reducer} reducerState={reducerState} />;
|
||||
}
|
||||
if (
|
||||
reducerState.backup_state === BackupStates.CountrySelecting ||
|
||||
reducerState.recovery_state === RecoveryStates.CountrySelecting
|
||||
) {
|
||||
return <CountrySelection reducer={reducer} reducerState={reducerState} />;
|
||||
}
|
||||
if (
|
||||
reducerState.backup_state === BackupStates.UserAttributesCollecting ||
|
||||
reducerState.recovery_state === RecoveryStates.UserAttributesCollecting
|
||||
) {
|
||||
return <AttributeEntry reducer={reducer} reducerState={reducerState} />;
|
||||
}
|
||||
if (reducerState.backup_state === BackupStates.AuthenticationsEditing) {
|
||||
return (
|
||||
<AuthenticationEditor backupState={reducerState} reducer={reducer} />
|
||||
);
|
||||
}
|
||||
export interface BackupReducerProps {
|
||||
reducer: AnastasisReducerApi;
|
||||
backupState: ReducerStateBackup;
|
||||
}
|
||||
|
||||
if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
|
||||
const backupState: ReducerStateBackup = reducerState;
|
||||
function ReviewPolicies(props: BackupReducerProps) {
|
||||
const { reducer, backupState } = props;
|
||||
const authMethods = backupState.authentication_methods!;
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Backup: Review Recovery Policies</h1>
|
||||
<ErrorBanner reducer={reducer} />
|
||||
<div>
|
||||
<AnastasisClientFrame title="Backup: Review Recovery Policies">
|
||||
{backupState.policies?.map((p, i) => {
|
||||
const policyName = p.methods
|
||||
.map((x) => authMethods[x.authentication_method].type)
|
||||
@ -163,68 +219,184 @@ const Home: FunctionalComponent = () => {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
export interface RecoveryReducerProps {
|
||||
reducer: AnastasisReducerApi;
|
||||
recoveryState: ReducerStateRecovery;
|
||||
}
|
||||
|
||||
function SecretSelection(props: RecoveryReducerProps) {
|
||||
const { reducer, recoveryState } = props;
|
||||
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
|
||||
const [otherVersion, setOtherVersion] = useState<number>(
|
||||
recoveryState.recovery_document?.version ?? 0,
|
||||
);
|
||||
const [otherProvider, setOtherProvider] = useState<string>("");
|
||||
function selectVersion(p: string, n: number) {
|
||||
reducer.runTransaction(async (tx) => {
|
||||
await tx.transition("change_version", {
|
||||
version: n,
|
||||
provider_url: p,
|
||||
});
|
||||
setSelectingVersion(false);
|
||||
});
|
||||
}
|
||||
if (selectingVersion) {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Recovery: Select secret">
|
||||
<p>Select a different version of the secret</p>
|
||||
<select onChange={(e) => setOtherProvider((e.target as any).value)}>
|
||||
{Object.keys(recoveryState.authentication_providers ?? {}).map(
|
||||
(x) => {
|
||||
return <option value={x}>{x}</option>;
|
||||
},
|
||||
)}
|
||||
</select>
|
||||
<div>
|
||||
<input
|
||||
value={otherVersion}
|
||||
onChange={(e) =>
|
||||
setOtherVersion(Number((e.target as HTMLInputElement).value))
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<button onClick={() => selectVersion(otherProvider, otherVersion)}>
|
||||
Select
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => reducer.back()}>Back</button>
|
||||
<button onClick={() => reducer.transition("next", {})}>Next</button>
|
||||
<button onClick={() => selectVersion(otherProvider, 0)}>
|
||||
Use latest version
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => setSelectingVersion(false)}>Cancel</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AnastasisClientFrame title="Recovery: Select secret">
|
||||
<p>Provider: {recoveryState.recovery_document!.provider_url}</p>
|
||||
<p>Secret version: {recoveryState.recovery_document!.version}</p>
|
||||
<p>Secret name: {recoveryState.recovery_document!.version}</p>
|
||||
<button onClick={() => setSelectingVersion(true)}>
|
||||
Select different secret
|
||||
</button>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
if (reducerState.backup_state === BackupStates.SecretEditing) {
|
||||
const [secretName, setSecretName] = useState("");
|
||||
const [secretValue, setSecretValue] = useState("");
|
||||
const secretNext = () => {
|
||||
reducer.runTransaction(async (tx) => {
|
||||
await tx.transition("enter_secret_name", {
|
||||
name: secretName,
|
||||
});
|
||||
await tx.transition("enter_secret", {
|
||||
secret: {
|
||||
value: "EDJP6WK5EG50",
|
||||
mime: "text/plain",
|
||||
},
|
||||
expiration: {
|
||||
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5,
|
||||
},
|
||||
});
|
||||
await tx.transition("next", {});
|
||||
});
|
||||
interface AnastasisClientFrameProps {
|
||||
onNext?(): void;
|
||||
title: string;
|
||||
children: ComponentChildren;
|
||||
/**
|
||||
* Should back/next buttons be provided?
|
||||
*/
|
||||
hideNav?: boolean;
|
||||
/**
|
||||
* Hide only the "next" button.
|
||||
*/
|
||||
hideNext?: boolean;
|
||||
}
|
||||
|
||||
function AnastasisClientFrame(props: AnastasisClientFrameProps) {
|
||||
return (
|
||||
<WithReducer.Consumer>
|
||||
{(reducer) => {
|
||||
if (!reducer) {
|
||||
return <p>Fatal: Reducer must be in context.</p>;
|
||||
}
|
||||
const next = () => {
|
||||
if (props.onNext) {
|
||||
props.onNext();
|
||||
} else {
|
||||
reducer.transition("next", {});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Backup: Provide secret</h1>
|
||||
<button onClick={() => reducer.reset()}>Reset session</button>
|
||||
<h1>{props.title}</h1>
|
||||
<ErrorBanner reducer={reducer} />
|
||||
<div>
|
||||
<label>
|
||||
Secret name: <input type="text" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Secret value: <input type="text" />
|
||||
</label>
|
||||
</div>
|
||||
or:
|
||||
<div>
|
||||
<label>
|
||||
File Upload: <input type="file" />
|
||||
</label>
|
||||
</div>
|
||||
{props.children}
|
||||
{!props.hideNav ? (
|
||||
<div>
|
||||
<button onClick={() => reducer.back()}>Back</button>
|
||||
<button onClick={() => secretNext()}>Next</button>
|
||||
{!props.hideNext ? (
|
||||
<button onClick={() => next()}>Next</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</WithReducer.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
const AnastasisClient: FunctionalComponent = () => {
|
||||
const reducer = useAnastasisReducer();
|
||||
return (
|
||||
<WithReducer.Provider value={reducer}>
|
||||
<AnastasisClientImpl />
|
||||
</WithReducer.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const AnastasisClientImpl: FunctionalComponent = () => {
|
||||
const reducer = useContext(WithReducer)!;
|
||||
const reducerState = reducer.currentReducerState;
|
||||
if (!reducerState) {
|
||||
return (
|
||||
<AnastasisClientFrame hideNav title="Home">
|
||||
<button autoFocus onClick={() => reducer.startBackup()}>
|
||||
Backup
|
||||
</button>
|
||||
<button onClick={() => reducer.startRecover()}>Recover</button>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
console.log("state", reducer.currentReducerState);
|
||||
|
||||
if (
|
||||
reducerState.backup_state === BackupStates.ContinentSelecting ||
|
||||
reducerState.recovery_state === RecoveryStates.ContinentSelecting
|
||||
) {
|
||||
return <ContinentSelection reducer={reducer} reducerState={reducerState} />;
|
||||
}
|
||||
if (
|
||||
reducerState.backup_state === BackupStates.CountrySelecting ||
|
||||
reducerState.recovery_state === RecoveryStates.CountrySelecting
|
||||
) {
|
||||
return <CountrySelection reducer={reducer} reducerState={reducerState} />;
|
||||
}
|
||||
if (
|
||||
reducerState.backup_state === BackupStates.UserAttributesCollecting ||
|
||||
reducerState.recovery_state === RecoveryStates.UserAttributesCollecting
|
||||
) {
|
||||
return <AttributeEntry reducer={reducer} reducerState={reducerState} />;
|
||||
}
|
||||
if (reducerState.backup_state === BackupStates.AuthenticationsEditing) {
|
||||
return (
|
||||
<AuthenticationEditor backupState={reducerState} reducer={reducer} />
|
||||
);
|
||||
}
|
||||
if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
|
||||
return <ReviewPolicies reducer={reducer} backupState={reducerState} />;
|
||||
}
|
||||
if (reducerState.backup_state === BackupStates.SecretEditing) {
|
||||
return <SecretEditor reducer={reducer} backupState={reducerState} />;
|
||||
}
|
||||
|
||||
if (reducerState.backup_state === BackupStates.BackupFinished) {
|
||||
const backupState: ReducerStateBackup = reducerState;
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Backup finished</h1>
|
||||
<AnastasisClientFrame hideNext title="Backup finished">
|
||||
<p>
|
||||
Your backup of secret "{backupState.secret_name ?? "??"}" was
|
||||
successful.
|
||||
@ -240,10 +412,8 @@ const Home: FunctionalComponent = () => {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<button onClick={() => reducer.reset()}>
|
||||
Start a new backup/recovery
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => reducer.reset()}>Back to start</button>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@ -251,8 +421,10 @@ const Home: FunctionalComponent = () => {
|
||||
const backupState: ReducerStateBackup = reducerState;
|
||||
const payments = backupState.payments ?? [];
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Backup: Authentication Storage Payments</h1>
|
||||
<AnastasisClientFrame
|
||||
hideNext
|
||||
title="Backup: Authentication Storage Payments"
|
||||
>
|
||||
<p>
|
||||
Some of the providers require a payment to store the encrypted
|
||||
authentication information.
|
||||
@ -262,22 +434,19 @@ const Home: FunctionalComponent = () => {
|
||||
return <li>{x}</li>;
|
||||
})}
|
||||
</ul>
|
||||
<div>
|
||||
<button onClick={() => reducer.back()}>Back</button>
|
||||
<button onClick={() => reducer.transition("pay", {})}>
|
||||
Check payment(s)
|
||||
Check payment status now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
if (reducerState.backup_state === BackupStates.PoliciesPaying) {
|
||||
const backupState: ReducerStateBackup = reducerState;
|
||||
const payments = backupState.policy_payment_requests ?? [];
|
||||
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Backup: Recovery Document Payments</h1>
|
||||
<AnastasisClientFrame hideNext title="Backup: Recovery Document Payments">
|
||||
<p>
|
||||
Some of the providers require a payment to store the encrypted
|
||||
recovery document.
|
||||
@ -291,23 +460,111 @@ const Home: FunctionalComponent = () => {
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div>
|
||||
<button onClick={() => reducer.back()}>Back</button>
|
||||
<button onClick={() => reducer.transition("pay", {})}>
|
||||
Check payment(s)
|
||||
Check payment status now
|
||||
</button>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
if (reducerState.recovery_state === RecoveryStates.SecretSelecting) {
|
||||
return <SecretSelection reducer={reducer} recoveryState={reducerState} />;
|
||||
}
|
||||
|
||||
if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) {
|
||||
const policies = reducerState.recovery_information!.policies;
|
||||
const chArr = reducerState.recovery_information!.challenges;
|
||||
const challenges: {
|
||||
[uuid: string]: {
|
||||
type: string;
|
||||
instructions: string;
|
||||
cost: string;
|
||||
};
|
||||
} = {};
|
||||
for (const ch of chArr) {
|
||||
challenges[ch.uuid] = {
|
||||
type: ch.type,
|
||||
cost: ch.cost,
|
||||
instructions: ch.instructions,
|
||||
};
|
||||
}
|
||||
return (
|
||||
<AnastasisClientFrame title="Recovery: Solve challenges">
|
||||
<h2>Policies</h2>
|
||||
{policies.map((x, i) => {
|
||||
return (
|
||||
<div>
|
||||
<h3>Policy #{i + 1}</h3>
|
||||
{x.map((x) => {
|
||||
const ch = challenges[x.uuid];
|
||||
return (
|
||||
<div>
|
||||
{ch.type} ({ch.instructions})
|
||||
<button
|
||||
onClick={() =>
|
||||
reducer.transition("select_challenge", {
|
||||
uuid: x.uuid,
|
||||
})
|
||||
}
|
||||
>
|
||||
Solve
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
if (reducerState.recovery_state === RecoveryStates.ChallengeSolving) {
|
||||
const chArr = reducerState.recovery_information!.challenges;
|
||||
const challengeFeedback = reducerState.challenge_feedback ?? {};
|
||||
const selectedUuid = reducerState.selected_challenge_uuid!;
|
||||
const challenges: {
|
||||
[uuid: string]: ChallengeInfo;
|
||||
} = {};
|
||||
for (const ch of chArr) {
|
||||
challenges[ch.uuid] = ch;
|
||||
}
|
||||
const selectedChallenge = challenges[selectedUuid];
|
||||
if (selectedChallenge.type === "question") {
|
||||
return (
|
||||
<SolveQuestionEntry
|
||||
challenge={selectedChallenge}
|
||||
reducer={reducer}
|
||||
feedback={challengeFeedback[selectedUuid]}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<AnastasisClientFrame hideNext title="Recovery: Solve challenge">
|
||||
<p>{JSON.stringify(selectedChallenge)}</p>
|
||||
<p>Challenge not supported.</p>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) {
|
||||
return (
|
||||
<AnastasisClientFrame title="Recovery Finished" hideNext>
|
||||
<h1>Recovery Finished</h1>
|
||||
<p>
|
||||
Secret: {bytesToString(decodeCrock(reducerState.core_secret?.value!))}
|
||||
</p>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
console.log("unknown state", reducer.currentReducerState);
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Home</h1>
|
||||
<AnastasisClientFrame hideNav title="Bug">
|
||||
<p>Bug: Unknown state.</p>
|
||||
<button onClick={() => reducer.reset()}>Reset</button>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
};
|
||||
|
||||
@ -328,9 +585,12 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
|
||||
},
|
||||
});
|
||||
};
|
||||
//const inputRef = useRef<HTMLInputElement>(null);
|
||||
// useLayoutEffect(() => {
|
||||
// inputRef.current?.focus();
|
||||
// }, []);
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Add {props.method} authentication</h1>
|
||||
<AnastasisClientFrame hideNav title="Add SMS authentication">
|
||||
<div>
|
||||
<p>
|
||||
For SMS authentication, you need to provide a mobile number. When
|
||||
@ -338,9 +598,11 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
|
||||
receive via SMS.
|
||||
</p>
|
||||
<label>
|
||||
Mobile number{" "}
|
||||
Mobile number:{" "}
|
||||
<input
|
||||
value={mobileNumber}
|
||||
//ref={inputRef}
|
||||
style={{ display: "block" }}
|
||||
autoFocus
|
||||
onChange={(e) => setMobileNumber((e.target as any).value)}
|
||||
type="text"
|
||||
@ -351,7 +613,7 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
|
||||
<button onClick={() => addSmsAuth()}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@ -359,8 +621,7 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
|
||||
const [questionText, setQuestionText] = useState("");
|
||||
const [answerText, setAnswerText] = useState("");
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Add {props.method} authentication</h1>
|
||||
<AnastasisClientFrame hideNav title="Add Security Question">
|
||||
<div>
|
||||
<p>
|
||||
For security question authentication, you need to provide a question
|
||||
@ -370,9 +631,10 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
|
||||
</p>
|
||||
<div>
|
||||
<label>
|
||||
Security question
|
||||
Security question:{" "}
|
||||
<input
|
||||
value={questionText}
|
||||
style={{ display: "block" }}
|
||||
autoFocus
|
||||
onChange={(e) => setQuestionText((e.target as any).value)}
|
||||
type="text"
|
||||
@ -381,9 +643,10 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Answer
|
||||
Answer:{" "}
|
||||
<input
|
||||
value={answerText}
|
||||
style={{ display: "block" }}
|
||||
autoFocus
|
||||
onChange={(e) => setAnswerText((e.target as any).value)}
|
||||
type="text"
|
||||
@ -407,25 +670,24 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthMethodEmailSetup(props: AuthMethodSetupProps) {
|
||||
const [email, setEmail] = useState("");
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Add {props.method} authentication</h1>
|
||||
<div>
|
||||
<AnastasisClientFrame hideNav title="Add email authentication">
|
||||
<p>
|
||||
For email authentication, you need to provid an email address. When
|
||||
recovering your secret, you need to enter the code you will receive by
|
||||
For email authentication, you need to provide an email address. When
|
||||
recovering your secret, you will need to enter the code you receive by
|
||||
email.
|
||||
</p>
|
||||
<div>
|
||||
<label>
|
||||
Email address
|
||||
Email address:{" "}
|
||||
<input
|
||||
style={{ display: "block" }}
|
||||
value={email}
|
||||
autoFocus
|
||||
onChange={(e) => setEmail((e.target as any).value)}
|
||||
@ -449,8 +711,7 @@ function AuthMethodEmailSetup(props: AuthMethodSetupProps) {
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@ -460,6 +721,28 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
|
||||
const [city, setCity] = useState("");
|
||||
const [postcode, setPostcode] = useState("");
|
||||
const [country, setCountry] = useState("");
|
||||
|
||||
const addPostAuth = () => {
|
||||
() =>
|
||||
props.addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "email",
|
||||
instructions: `Letter to address in postal code ${postcode}`,
|
||||
challenge: encodeCrock(
|
||||
stringToBytes(
|
||||
canonicalJson({
|
||||
full_name: fullName,
|
||||
street,
|
||||
city,
|
||||
postcode,
|
||||
country,
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Add {props.method} authentication</h1>
|
||||
@ -526,29 +809,7 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => props.cancel()}>Cancel</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
props.addAuthMethod({
|
||||
authentication_method: {
|
||||
type: "email",
|
||||
instructions: `Letter to address in postal code ${postcode}`,
|
||||
challenge: encodeCrock(
|
||||
stringToBytes(
|
||||
canonicalJson({
|
||||
full_name: fullName,
|
||||
street,
|
||||
city,
|
||||
postcode,
|
||||
country,
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
<button onClick={() => addPostAuth()}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -557,15 +818,10 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
|
||||
|
||||
function AuthMethodNotImplemented(props: AuthMethodSetupProps) {
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Add {props.method} authentication</h1>
|
||||
<div>
|
||||
<p>
|
||||
This auth method is not implemented yet, please choose another one.
|
||||
</p>
|
||||
<AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
|
||||
<p>This auth method is not implemented yet, please choose another one.</p>
|
||||
<button onClick={() => props.cancel()}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@ -583,10 +839,12 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
|
||||
const authAvailableSet = new Set<string>();
|
||||
for (const provKey of Object.keys(providers)) {
|
||||
const p = providers[provKey];
|
||||
if (p.methods) {
|
||||
for (const meth of p.methods) {
|
||||
authAvailableSet.add(meth.type);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectedMethod) {
|
||||
const cancel = () => setSelectedMethod(undefined);
|
||||
const addMethod = (args: any) => {
|
||||
@ -653,10 +911,7 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
|
||||
backupState.authentication_methods ?? [];
|
||||
const haveMethodsConfigured = configuredAuthMethods.length;
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Backup: Configure Authentication Methods</h1>
|
||||
<ErrorBanner reducer={reducer} />
|
||||
<h2>Add authentication method</h2>
|
||||
<AnastasisClientFrame title="Backup: Configure Authentication Methods">
|
||||
<div>
|
||||
<MethodButton method="sms" label="SMS" />
|
||||
<MethodButton method="email" label="Email" />
|
||||
@ -686,11 +941,7 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
|
||||
) : (
|
||||
<p>No authentication methods configured yet.</p>
|
||||
)}
|
||||
<div>
|
||||
<button onClick={() => reducer.back()}>Back</button>
|
||||
<button onClick={() => reducer.transition("next", {})}>Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@ -701,12 +952,18 @@ export interface AttributeEntryProps {
|
||||
|
||||
function AttributeEntry(props: AttributeEntryProps) {
|
||||
const { reducer, reducerState: backupState } = props;
|
||||
const [attrs, setAttrs] = useState<Record<string, string>>({});
|
||||
const [attrs, setAttrs] = useState<Record<string, string>>(
|
||||
props.reducerState.identity_attributes ?? {},
|
||||
);
|
||||
return (
|
||||
<div class={style.home}>
|
||||
<h1>Backup: Enter Basic User Attributes</h1>
|
||||
<ErrorBanner reducer={reducer} />
|
||||
<div>
|
||||
<AnastasisClientFrame
|
||||
title={withProcessLabel(reducer, "Select Country")}
|
||||
onNext={() =>
|
||||
reducer.transition("enter_user_attributes", {
|
||||
identity_attributes: attrs,
|
||||
})
|
||||
}
|
||||
>
|
||||
{backupState.required_attributes.map((x: any, i: number) => {
|
||||
return (
|
||||
<AttributeEntryField
|
||||
@ -717,20 +974,7 @@ function AttributeEntry(props: AttributeEntryProps) {
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => reducer.back()}>Back</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
reducer.transition("enter_user_attributes", {
|
||||
identity_attributes: attrs,
|
||||
})
|
||||
}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AnastasisClientFrame>
|
||||
);
|
||||
}
|
||||
|
||||
@ -744,8 +988,9 @@ export interface AttributeEntryFieldProps {
|
||||
function AttributeEntryField(props: AttributeEntryFieldProps) {
|
||||
return (
|
||||
<div>
|
||||
<label>{props.spec.label}</label>
|
||||
<label>{props.spec.label}:</label>
|
||||
<input
|
||||
style={{ display: "block" }}
|
||||
autoFocus={props.isFirst}
|
||||
type="text"
|
||||
value={props.value}
|
||||
@ -777,4 +1022,4 @@ function ErrorBanner(props: ErrorBannerProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default Home;
|
||||
export default AnastasisClient;
|
||||
|
@ -1,5 +1,5 @@
|
||||
.home {
|
||||
padding: 56px 20px;
|
||||
padding: 1em 1em;
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user