aboutsummaryrefslogtreecommitdiff
path: root/packages/anastasis-webui
diff options
context:
space:
mode:
Diffstat (limited to 'packages/anastasis-webui')
-rw-r--r--packages/anastasis-webui/src/components/app.tsx24
-rw-r--r--packages/anastasis-webui/src/components/header/index.tsx24
-rw-r--r--packages/anastasis-webui/src/components/header/style.css48
-rw-r--r--packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts52
-rw-r--r--packages/anastasis-webui/src/routes/home/index.tsx831
-rw-r--r--packages/anastasis-webui/src/routes/home/style.css2
6 files changed, 598 insertions, 383 deletions
diff --git a/packages/anastasis-webui/src/components/app.tsx b/packages/anastasis-webui/src/components/app.tsx
index 5abb12a3d..45c9035f0 100644
--- a/packages/anastasis-webui/src/components/app.tsx
+++ b/packages/anastasis-webui/src/components/app.tsx
@@ -1,23 +1,13 @@
-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>
- </div>
- );
+ return (
+ <div id="preact_root">
+ <AnastasisClient />
+ </div>
+ );
};
export default App;
diff --git a/packages/anastasis-webui/src/components/header/index.tsx b/packages/anastasis-webui/src/components/header/index.tsx
deleted file mode 100644
index f2b6fe8ad..000000000
--- a/packages/anastasis-webui/src/components/header/index.tsx
+++ /dev/null
@@ -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;
diff --git a/packages/anastasis-webui/src/components/header/style.css b/packages/anastasis-webui/src/components/header/style.css
deleted file mode 100644
index f08fda702..000000000
--- a/packages/anastasis-webui/src/components/header/style.css
+++ /dev/null
@@ -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);
-}
diff --git a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
index 6ca0ccfae..efa0592dd 100644
--- a/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
+++ b/packages/anastasis-webui/src/hooks/use-anastasis-reducer.ts
@@ -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/";
diff --git a/packages/anastasis-webui/src/routes/home/index.tsx b/packages/anastasis-webui/src/routes/home/index.tsx
index f0b630851..99f8febb4 100644
--- a/packages/anastasis-webui/src/routes/home/index.tsx
+++ b/packages/anastasis-webui/src/routes/home/index.tsx
@@ -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,85 +25,340 @@ import {
} from "../../hooks/use-anastasis-reducer";
import style from "./style.css";
-interface ContinentSelectionProps {
+const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
+
+function isBackup(reducer: AnastasisReducerApi) {
+ return !!reducer.currentReducerState?.backup_state;
+}
+
+interface CommonReducerProps {
reducer: AnastasisReducerApi;
reducerState: ReducerStateBackup | ReducerStateRecovery;
}
-function isBackup(reducer: AnastasisReducerApi) {
- return !!reducer.currentReducerState?.backup_state;
+function withProcessLabel(reducer: AnastasisReducerApi, text: string): string {
+ if (isBackup(reducer)) {
+ return "Backup: " + text;
+ }
+ return "Recovery: " + text;
}
-function ContinentSelection(props: ContinentSelectionProps) {
+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>
- {reducerState.continents.map((x: any) => {
- const sel = (x: string) =>
- reducer.transition("select_continent", { continent: x });
- return (
- <button onClick={() => sel(x.name)} key={x.name}>
- {x.name}
- </button>
- );
- })}
- </div>
- <div>
- <button onClick={() => reducer.back()}>Back</button>
- </div>
- </div>
+ <AnastasisClientFrame
+ hideNext
+ title={withProcessLabel(reducer, "Select Continent")}
+ >
+ {reducerState.continents.map((x: any) => {
+ const sel = (x: string) =>
+ reducer.transition("select_continent", { continent: x });
+ return (
+ <button onClick={() => sel(x.name)} key={x.name}>
+ {x.name}
+ </button>
+ );
+ })}
+ </AnastasisClientFrame>
+ );
+}
+
+function CountrySelection(props: CommonReducerProps) {
+ const { reducer, reducerState } = props;
+ return (
+ <AnastasisClientFrame
+ hideNext
+ title={withProcessLabel(reducer, "Select Country")}
+ >
+ {reducerState.countries.map((x: any) => {
+ const sel = (x: any) =>
+ reducer.transition("select_country", {
+ country_code: x.code,
+ currencies: [x.currency],
+ });
+ return (
+ <button onClick={() => sel(x)} key={x.name}>
+ {x.name} ({x.currency})
+ </button>
+ );
+ })}
+ </AnastasisClientFrame>
);
}
-interface CountrySelectionProps {
+interface SolveEntryProps {
reducer: AnastasisReducerApi;
- reducerState: ReducerStateBackup | ReducerStateRecovery;
+ challenge: ChallengeInfo;
+ feedback?: ChallengeFeedback;
}
-function CountrySelection(props: CountrySelectionProps) {
- const { reducer, reducerState } = props;
+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>Backup: Select Country</h1>
- <ErrorBanner reducer={reducer} />
+ <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>
- {reducerState.countries.map((x: any) => {
- const sel = (x: any) =>
- reducer.transition("select_country", {
- country_code: x.code,
- currencies: [x.currency],
- });
- return (
- <button onClick={() => sel(x)} key={x.name}>
- {x.name} ({x.currency})
- </button>
- );
- })}
+ <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>
- <button onClick={() => reducer.back()}>Back</button>
+ <label>
+ File Upload: <input type="file" />
+ </label>
</div>
- </div>
+ </AnastasisClientFrame>
);
}
-const Home: FunctionalComponent = () => {
+export interface BackupReducerProps {
+ reducer: AnastasisReducerApi;
+ backupState: ReducerStateBackup;
+}
+
+function ReviewPolicies(props: BackupReducerProps) {
+ const { reducer, backupState } = props;
+ const authMethods = backupState.authentication_methods!;
+ return (
+ <AnastasisClientFrame title="Backup: Review Recovery Policies">
+ {backupState.policies?.map((p, i) => {
+ const policyName = p.methods
+ .map((x) => authMethods[x.authentication_method].type)
+ .join(" + ");
+ return (
+ <div class={style.policy}>
+ <h3>
+ Policy #{i + 1}: {policyName}
+ </h3>
+ Required Authentications:
+ <ul>
+ {p.methods.map((x) => {
+ const m = authMethods[x.authentication_method];
+ return (
+ <li>
+ {m.type} ({m.instructions}) at provider {x.provider}
+ </li>
+ );
+ })}
+ </ul>
+ <div>
+ <button
+ onClick={() =>
+ reducer.transition("delete_policy", { policy_index: i })
+ }
+ >
+ Delete Policy
+ </button>
+ </div>
+ </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={() => 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>
+ );
+}
+
+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}>
+ <button onClick={() => reducer.reset()}>Reset session</button>
+ <h1>{props.title}</h1>
+ <ErrorBanner reducer={reducer} />
+ {props.children}
+ {!props.hideNav ? (
+ <div>
+ <button onClick={() => reducer.back()}>Back</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 (
- <div class={style.home}>
- <h1>Home</h1>
- <p>
- <button autoFocus onClick={() => reducer.startBackup()}>
- Backup
- </button>
- <button onClick={() => reducer.startRecover()}>Recover</button>
- </p>
- </div>
+ <AnastasisClientFrame hideNav title="Home">
+ <button autoFocus onClick={() => reducer.startBackup()}>
+ Backup
+ </button>
+ <button onClick={() => reducer.startRecover()}>Recover</button>
+ </AnastasisClientFrame>
);
}
console.log("state", reducer.currentReducerState);
@@ -122,109 +386,17 @@ const Home: FunctionalComponent = () => {
<AuthenticationEditor backupState={reducerState} reducer={reducer} />
);
}
-
if (reducerState.backup_state === BackupStates.PoliciesReviewing) {
- const backupState: ReducerStateBackup = reducerState;
- const authMethods = backupState.authentication_methods!;
- return (
- <div class={style.home}>
- <h1>Backup: Review Recovery Policies</h1>
- <ErrorBanner reducer={reducer} />
- <div>
- {backupState.policies?.map((p, i) => {
- const policyName = p.methods
- .map((x) => authMethods[x.authentication_method].type)
- .join(" + ");
- return (
- <div class={style.policy}>
- <h3>
- Policy #{i + 1}: {policyName}
- </h3>
- Required Authentications:
- <ul>
- {p.methods.map((x) => {
- const m = authMethods[x.authentication_method];
- return (
- <li>
- {m.type} ({m.instructions}) at provider {x.provider}
- </li>
- );
- })}
- </ul>
- <div>
- <button
- onClick={() =>
- reducer.transition("delete_policy", { policy_index: i })
- }
- >
- Delete Policy
- </button>
- </div>
- </div>
- );
- })}
- </div>
- <div>
- <button onClick={() => reducer.back()}>Back</button>
- <button onClick={() => reducer.transition("next", {})}>Next</button>
- </div>
- </div>
- );
+ return <ReviewPolicies reducer={reducer} backupState={reducerState} />;
}
-
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", {});
- });
- };
- return (
- <div class={style.home}>
- <h1>Backup: Provide secret</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>
- <div>
- <button onClick={() => reducer.back()}>Back</button>
- <button onClick={() => secretNext()}>Next</button>
- </div>
- </div>
- );
+ 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)
- </button>
- </div>
- </div>
+ <button onClick={() => reducer.transition("pay", {})}>
+ Check payment status now
+ </button>
+ </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)
- </button>
- </div>
- </div>
+ <button onClick={() => reducer.transition("pay", {})}>
+ 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,50 +670,48 @@ 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>
+ <AnastasisClientFrame hideNav title="Add email authentication">
+ <p>
+ 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>
- <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
- email.
- </p>
- <div>
- <label>
- Email address
- <input
- value={email}
- autoFocus
- onChange={(e) => setEmail((e.target as any).value)}
- type="text"
- />
- </label>
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button
- onClick={() =>
- props.addAuthMethod({
- authentication_method: {
- type: "email",
- instructions: `Email to ${email}`,
- challenge: encodeCrock(stringToBytes(email)),
- },
- })
- }
- >
- Add
- </button>
- </div>
+ <label>
+ Email address:{" "}
+ <input
+ style={{ display: "block" }}
+ value={email}
+ autoFocus
+ onChange={(e) => setEmail((e.target as any).value)}
+ type="text"
+ />
+ </label>
</div>
- </div>
+ <div>
+ <button onClick={() => props.cancel()}>Cancel</button>
+ <button
+ onClick={() =>
+ props.addAuthMethod({
+ authentication_method: {
+ type: "email",
+ instructions: `Email to ${email}`,
+ challenge: encodeCrock(stringToBytes(email)),
+ },
+ })
+ }
+ >
+ Add
+ </button>
+ </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>
- <button onClick={() => props.cancel()}>Cancel</button>
- </div>
- </div>
+ <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>
+ </AnastasisClientFrame>
);
}
@@ -583,8 +839,10 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) {
const p = providers[provKey];
- for (const meth of p.methods) {
- authAvailableSet.add(meth.type);
+ if (p.methods) {
+ for (const meth of p.methods) {
+ authAvailableSet.add(meth.type);
+ }
}
}
if (selectedMethod) {
@@ -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,36 +952,29 @@ 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>
- {backupState.required_attributes.map((x: any, i: number) => {
- return (
- <AttributeEntryField
- isFirst={i == 0}
- setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
- spec={x}
- value={attrs[x.name]}
- />
- );
- })}
- </div>
- <div>
- <button onClick={() => reducer.back()}>Back</button>
- <button
- onClick={() =>
- reducer.transition("enter_user_attributes", {
- identity_attributes: attrs,
- })
- }
- >
- Next
- </button>
- </div>
- </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
+ isFirst={i == 0}
+ setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })}
+ spec={x}
+ value={attrs[x.name]}
+ />
+ );
+ })}
+ </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;
diff --git a/packages/anastasis-webui/src/routes/home/style.css b/packages/anastasis-webui/src/routes/home/style.css
index c9f34e6c8..b94981f10 100644
--- a/packages/anastasis-webui/src/routes/home/style.css
+++ b/packages/anastasis-webui/src/routes/home/style.css
@@ -1,5 +1,5 @@
.home {
- padding: 56px 20px;
+ padding: 1em 1em;
min-height: 100%;
width: 100%;
}