common anstasis frame

This commit is contained in:
Florian Dold 2021-10-13 19:32:14 +02:00
parent fbf501e727
commit 3aad5e774d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 606 additions and 391 deletions

View File

@ -1,21 +1,11 @@
import { FunctionalComponent, h } from 'preact'; import { FunctionalComponent, h } from "preact";
import { Route, Router } from 'preact-router';
import Home from '../routes/home'; import AnastasisClient from "../routes/home";
import Profile from '../routes/profile';
import NotFoundPage from '../routes/notfound';
import Header from './header';
const App: FunctionalComponent = () => { const App: FunctionalComponent = () => {
return ( return (
<div id="preact_root"> <div id="preact_root">
<Header /> <AnastasisClient />
<Router>
<Route path="/" component={Home} />
<Route path="/profile/" component={Profile} user="me" />
<Route path="/profile/:user" component={Profile} />
<NotFoundPage default />
</Router>
</div> </div>
); );
}; };

View File

@ -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;

View File

@ -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);
}

View File

@ -11,6 +11,7 @@ export interface ReducerStateBackup {
code: undefined; code: undefined;
continents: any; continents: any;
countries: any; countries: any;
identity_attributes?: { [n: string]: string };
authentication_providers: any; authentication_providers: any;
authentication_methods?: AuthMethod[]; authentication_methods?: AuthMethod[];
required_attributes: any; required_attributes: any;
@ -39,14 +40,60 @@ export interface AuthMethod {
challenge: string; challenge: string;
} }
export interface ChallengeInfo {
cost: string;
instructions: string;
type: string;
uuid: string;
}
export interface ReducerStateRecovery { export interface ReducerStateRecovery {
backup_state: undefined; backup_state: undefined;
recovery_state: RecoveryStates; recovery_state: RecoveryStates;
code: undefined; code: undefined;
identity_attributes?: { [n: string]: string };
continents: any; continents: any;
countries: any; countries: any;
required_attributes: 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 { export interface ReducerStateError {
@ -76,6 +123,11 @@ export enum RecoveryStates {
ContinentSelecting = "CONTINENT_SELECTING", ContinentSelecting = "CONTINENT_SELECTING",
CountrySelecting = "COUNTRY_SELECTING", CountrySelecting = "COUNTRY_SELECTING",
UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING", UserAttributesCollecting = "USER_ATTRIBUTES_COLLECTING",
SecretSelecting = "SECRET_SELECTING",
ChallengeSelecting = "CHALLENGE_SELECTING",
ChallengePaying = "CHALLENGE_PAYING",
ChallengeSolving = "CHALLENGE_SOLVING",
RecoveryFinished = "RECOVERY_FINISHED",
} }
const reducerBaseUrl = "http://localhost:5000/"; const reducerBaseUrl = "http://localhost:5000/";

View File

@ -1,14 +1,23 @@
import { import {
bytesToString,
canonicalJson, canonicalJson,
decodeCrock,
encodeCrock, encodeCrock,
stringToBytes, stringToBytes,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { FunctionalComponent, h } from "preact"; import {
import { useState } from "preact/hooks"; FunctionalComponent,
ComponentChildren,
h,
createContext,
} from "preact";
import { useState, useContext, useRef, useLayoutEffect } from "preact/hooks";
import { import {
AnastasisReducerApi, AnastasisReducerApi,
AuthMethod, AuthMethod,
BackupStates, BackupStates,
ChallengeFeedback,
ChallengeInfo,
RecoveryStates, RecoveryStates,
ReducerStateBackup, ReducerStateBackup,
ReducerStateRecovery, ReducerStateRecovery,
@ -16,22 +25,31 @@ import {
} from "../../hooks/use-anastasis-reducer"; } from "../../hooks/use-anastasis-reducer";
import style from "./style.css"; import style from "./style.css";
interface ContinentSelectionProps { const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
reducer: AnastasisReducerApi;
reducerState: ReducerStateBackup | ReducerStateRecovery;
}
function isBackup(reducer: AnastasisReducerApi) { function isBackup(reducer: AnastasisReducerApi) {
return !!reducer.currentReducerState?.backup_state; 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; const { reducer, reducerState } = props;
return ( return (
<div class={style.home}> <AnastasisClientFrame
<h1>{isBackup(reducer) ? "Backup" : "Recovery"}: Select Continent</h1> hideNext
<ErrorBanner reducer={reducer} /> title={withProcessLabel(reducer, "Select Continent")}
<div> >
{reducerState.continents.map((x: any) => { {reducerState.continents.map((x: any) => {
const sel = (x: string) => const sel = (x: string) =>
reducer.transition("select_continent", { continent: x }); reducer.transition("select_continent", { continent: x });
@ -41,26 +59,17 @@ function ContinentSelection(props: ContinentSelectionProps) {
</button> </button>
); );
})} })}
</div> </AnastasisClientFrame>
<div>
<button onClick={() => reducer.back()}>Back</button>
</div>
</div>
); );
} }
interface CountrySelectionProps { function CountrySelection(props: CommonReducerProps) {
reducer: AnastasisReducerApi;
reducerState: ReducerStateBackup | ReducerStateRecovery;
}
function CountrySelection(props: CountrySelectionProps) {
const { reducer, reducerState } = props; const { reducer, reducerState } = props;
return ( return (
<div class={style.home}> <AnastasisClientFrame
<h1>Backup: Select Country</h1> hideNext
<ErrorBanner reducer={reducer} /> title={withProcessLabel(reducer, "Select Country")}
<div> >
{reducerState.countries.map((x: any) => { {reducerState.countries.map((x: any) => {
const sel = (x: any) => const sel = (x: any) =>
reducer.transition("select_country", { reducer.transition("select_country", {
@ -73,64 +82,111 @@ function CountrySelection(props: CountrySelectionProps) {
</button> </button>
); );
})} })}
</AnastasisClientFrame>
);
}
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 (
<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>
<div> <div>
<button onClick={() => reducer.back()}>Back</button> <label>
Secret value:{" "}
<input
value={secretValue}
onChange={(e) =>
setSecretValue((e.target as HTMLInputElement).value)
}
type="text"
/>
</label>
</div> </div>
or:
<div>
<label>
File Upload: <input type="file" />
</label>
</div> </div>
</AnastasisClientFrame>
); );
} }
const Home: FunctionalComponent = () => { export interface BackupReducerProps {
const reducer = useAnastasisReducer(); reducer: AnastasisReducerApi;
const reducerState = reducer.currentReducerState; backupState: ReducerStateBackup;
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>
);
}
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) { function ReviewPolicies(props: BackupReducerProps) {
const backupState: ReducerStateBackup = reducerState; const { reducer, backupState } = props;
const authMethods = backupState.authentication_methods!; const authMethods = backupState.authentication_methods!;
return ( return (
<div class={style.home}> <AnastasisClientFrame title="Backup: Review Recovery Policies">
<h1>Backup: Review Recovery Policies</h1>
<ErrorBanner reducer={reducer} />
<div>
{backupState.policies?.map((p, i) => { {backupState.policies?.map((p, i) => {
const policyName = p.methods const policyName = p.methods
.map((x) => authMethods[x.authentication_method].type) .map((x) => authMethods[x.authentication_method].type)
@ -163,68 +219,184 @@ const Home: FunctionalComponent = () => {
</div> </div>
); );
})} })}
</div> </AnastasisClientFrame>
<div>
<button onClick={() => reducer.back()}>Back</button>
<button onClick={() => reducer.transition("next", {})}>Next</button>
</div>
</div>
); );
} }
if (reducerState.backup_state === BackupStates.SecretEditing) { export interface RecoveryReducerProps {
const [secretName, setSecretName] = useState(""); reducer: AnastasisReducerApi;
const [secretValue, setSecretValue] = useState(""); recoveryState: ReducerStateRecovery;
const secretNext = () => { }
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) => { reducer.runTransaction(async (tx) => {
await tx.transition("enter_secret_name", { await tx.transition("change_version", {
name: secretName, version: n,
provider_url: p,
}); });
await tx.transition("enter_secret", { setSelectingVersion(false);
secret: { });
value: "EDJP6WK5EG50", }
mime: "text/plain", 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>;
}, },
expiration: { )}
t_ms: new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5, </select>
}, <div>
}); <input
await tx.transition("next", {}); 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 ( return (
<div class={style.home}> <div class={style.home}>
<h1>Backup: Provide secret</h1> <button onClick={() => reducer.reset()}>Reset session</button>
<h1>{props.title}</h1>
<ErrorBanner reducer={reducer} /> <ErrorBanner reducer={reducer} />
<div> {props.children}
<label> {!props.hideNav ? (
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> <div>
<button onClick={() => reducer.back()}>Back</button> <button onClick={() => reducer.back()}>Back</button>
<button onClick={() => secretNext()}>Next</button> {!props.hideNext ? (
<button onClick={() => next()}>Next</button>
) : null}
</div> </div>
) : null}
</div> </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) { if (reducerState.backup_state === BackupStates.BackupFinished) {
const backupState: ReducerStateBackup = reducerState; const backupState: ReducerStateBackup = reducerState;
return ( return (
<div class={style.home}> <AnastasisClientFrame hideNext title="Backup finished">
<h1>Backup finished</h1>
<p> <p>
Your backup of secret "{backupState.secret_name ?? "??"}" was Your backup of secret "{backupState.secret_name ?? "??"}" was
successful. successful.
@ -240,10 +412,8 @@ const Home: FunctionalComponent = () => {
); );
})} })}
</ul> </ul>
<button onClick={() => reducer.reset()}> <button onClick={() => reducer.reset()}>Back to start</button>
Start a new backup/recovery </AnastasisClientFrame>
</button>
</div>
); );
} }
@ -251,8 +421,10 @@ const Home: FunctionalComponent = () => {
const backupState: ReducerStateBackup = reducerState; const backupState: ReducerStateBackup = reducerState;
const payments = backupState.payments ?? []; const payments = backupState.payments ?? [];
return ( return (
<div class={style.home}> <AnastasisClientFrame
<h1>Backup: Authentication Storage Payments</h1> hideNext
title="Backup: Authentication Storage Payments"
>
<p> <p>
Some of the providers require a payment to store the encrypted Some of the providers require a payment to store the encrypted
authentication information. authentication information.
@ -262,22 +434,19 @@ const Home: FunctionalComponent = () => {
return <li>{x}</li>; return <li>{x}</li>;
})} })}
</ul> </ul>
<div>
<button onClick={() => reducer.back()}>Back</button>
<button onClick={() => reducer.transition("pay", {})}> <button onClick={() => reducer.transition("pay", {})}>
Check payment(s) Check payment status now
</button> </button>
</div> </AnastasisClientFrame>
</div>
); );
} }
if (reducerState.backup_state === BackupStates.PoliciesPaying) { if (reducerState.backup_state === BackupStates.PoliciesPaying) {
const backupState: ReducerStateBackup = reducerState; const backupState: ReducerStateBackup = reducerState;
const payments = backupState.policy_payment_requests ?? []; const payments = backupState.policy_payment_requests ?? [];
return ( return (
<div class={style.home}> <AnastasisClientFrame hideNext title="Backup: Recovery Document Payments">
<h1>Backup: Recovery Document Payments</h1>
<p> <p>
Some of the providers require a payment to store the encrypted Some of the providers require a payment to store the encrypted
recovery document. recovery document.
@ -291,23 +460,111 @@ const Home: FunctionalComponent = () => {
); );
})} })}
</ul> </ul>
<div>
<button onClick={() => reducer.back()}>Back</button>
<button onClick={() => reducer.transition("pay", {})}> <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> </button>
</div> </div>
);
})}
</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); console.log("unknown state", reducer.currentReducerState);
return ( return (
<div class={style.home}> <AnastasisClientFrame hideNav title="Bug">
<h1>Home</h1>
<p>Bug: Unknown state.</p> <p>Bug: Unknown state.</p>
<button onClick={() => reducer.reset()}>Reset</button> <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 ( return (
<div class={style.home}> <AnastasisClientFrame hideNav title="Add SMS authentication">
<h1>Add {props.method} authentication</h1>
<div> <div>
<p> <p>
For SMS authentication, you need to provide a mobile number. When For SMS authentication, you need to provide a mobile number. When
@ -338,9 +598,11 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
receive via SMS. receive via SMS.
</p> </p>
<label> <label>
Mobile number{" "} Mobile number:{" "}
<input <input
value={mobileNumber} value={mobileNumber}
//ref={inputRef}
style={{ display: "block" }}
autoFocus autoFocus
onChange={(e) => setMobileNumber((e.target as any).value)} onChange={(e) => setMobileNumber((e.target as any).value)}
type="text" type="text"
@ -351,7 +613,7 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
<button onClick={() => addSmsAuth()}>Add</button> <button onClick={() => addSmsAuth()}>Add</button>
</div> </div>
</div> </div>
</div> </AnastasisClientFrame>
); );
} }
@ -359,8 +621,7 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
const [questionText, setQuestionText] = useState(""); const [questionText, setQuestionText] = useState("");
const [answerText, setAnswerText] = useState(""); const [answerText, setAnswerText] = useState("");
return ( return (
<div class={style.home}> <AnastasisClientFrame hideNav title="Add Security Question">
<h1>Add {props.method} authentication</h1>
<div> <div>
<p> <p>
For security question authentication, you need to provide a question For security question authentication, you need to provide a question
@ -370,9 +631,10 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
</p> </p>
<div> <div>
<label> <label>
Security question Security question:{" "}
<input <input
value={questionText} value={questionText}
style={{ display: "block" }}
autoFocus autoFocus
onChange={(e) => setQuestionText((e.target as any).value)} onChange={(e) => setQuestionText((e.target as any).value)}
type="text" type="text"
@ -381,9 +643,10 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
</div> </div>
<div> <div>
<label> <label>
Answer Answer:{" "}
<input <input
value={answerText} value={answerText}
style={{ display: "block" }}
autoFocus autoFocus
onChange={(e) => setAnswerText((e.target as any).value)} onChange={(e) => setAnswerText((e.target as any).value)}
type="text" type="text"
@ -407,25 +670,24 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
</button> </button>
</div> </div>
</div> </div>
</div> </AnastasisClientFrame>
); );
} }
function AuthMethodEmailSetup(props: AuthMethodSetupProps) { function AuthMethodEmailSetup(props: AuthMethodSetupProps) {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
return ( return (
<div class={style.home}> <AnastasisClientFrame hideNav title="Add email authentication">
<h1>Add {props.method} authentication</h1>
<div>
<p> <p>
For email authentication, you need to provid an email address. When For email authentication, you need to provide an email address. When
recovering your secret, you need to enter the code you will receive by recovering your secret, you will need to enter the code you receive by
email. email.
</p> </p>
<div> <div>
<label> <label>
Email address Email address:{" "}
<input <input
style={{ display: "block" }}
value={email} value={email}
autoFocus autoFocus
onChange={(e) => setEmail((e.target as any).value)} onChange={(e) => setEmail((e.target as any).value)}
@ -449,8 +711,7 @@ function AuthMethodEmailSetup(props: AuthMethodSetupProps) {
Add Add
</button> </button>
</div> </div>
</div> </AnastasisClientFrame>
</div>
); );
} }
@ -460,6 +721,28 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
const [city, setCity] = useState(""); const [city, setCity] = useState("");
const [postcode, setPostcode] = useState(""); const [postcode, setPostcode] = useState("");
const [country, setCountry] = 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 ( return (
<div class={style.home}> <div class={style.home}>
<h1>Add {props.method} authentication</h1> <h1>Add {props.method} authentication</h1>
@ -526,29 +809,7 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
</div> </div>
<div> <div>
<button onClick={() => props.cancel()}>Cancel</button> <button onClick={() => props.cancel()}>Cancel</button>
<button <button onClick={() => addPostAuth()}>Add</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>
</div> </div>
</div> </div>
</div> </div>
@ -557,15 +818,10 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
function AuthMethodNotImplemented(props: AuthMethodSetupProps) { function AuthMethodNotImplemented(props: AuthMethodSetupProps) {
return ( return (
<div class={style.home}> <AnastasisClientFrame hideNav title={`Add ${props.method} authentication`}>
<h1>Add {props.method} authentication</h1> <p>This auth method is not implemented yet, please choose another one.</p>
<div>
<p>
This auth method is not implemented yet, please choose another one.
</p>
<button onClick={() => props.cancel()}>Cancel</button> <button onClick={() => props.cancel()}>Cancel</button>
</div> </AnastasisClientFrame>
</div>
); );
} }
@ -583,10 +839,12 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
const authAvailableSet = new Set<string>(); const authAvailableSet = new Set<string>();
for (const provKey of Object.keys(providers)) { for (const provKey of Object.keys(providers)) {
const p = providers[provKey]; const p = providers[provKey];
if (p.methods) {
for (const meth of p.methods) { for (const meth of p.methods) {
authAvailableSet.add(meth.type); authAvailableSet.add(meth.type);
} }
} }
}
if (selectedMethod) { if (selectedMethod) {
const cancel = () => setSelectedMethod(undefined); const cancel = () => setSelectedMethod(undefined);
const addMethod = (args: any) => { const addMethod = (args: any) => {
@ -653,10 +911,7 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
backupState.authentication_methods ?? []; backupState.authentication_methods ?? [];
const haveMethodsConfigured = configuredAuthMethods.length; const haveMethodsConfigured = configuredAuthMethods.length;
return ( return (
<div class={style.home}> <AnastasisClientFrame title="Backup: Configure Authentication Methods">
<h1>Backup: Configure Authentication Methods</h1>
<ErrorBanner reducer={reducer} />
<h2>Add authentication method</h2>
<div> <div>
<MethodButton method="sms" label="SMS" /> <MethodButton method="sms" label="SMS" />
<MethodButton method="email" label="Email" /> <MethodButton method="email" label="Email" />
@ -686,11 +941,7 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
) : ( ) : (
<p>No authentication methods configured yet.</p> <p>No authentication methods configured yet.</p>
)} )}
<div> </AnastasisClientFrame>
<button onClick={() => reducer.back()}>Back</button>
<button onClick={() => reducer.transition("next", {})}>Next</button>
</div>
</div>
); );
} }
@ -701,12 +952,18 @@ export interface AttributeEntryProps {
function AttributeEntry(props: AttributeEntryProps) { function AttributeEntry(props: AttributeEntryProps) {
const { reducer, reducerState: backupState } = props; const { reducer, reducerState: backupState } = props;
const [attrs, setAttrs] = useState<Record<string, string>>({}); const [attrs, setAttrs] = useState<Record<string, string>>(
props.reducerState.identity_attributes ?? {},
);
return ( return (
<div class={style.home}> <AnastasisClientFrame
<h1>Backup: Enter Basic User Attributes</h1> title={withProcessLabel(reducer, "Select Country")}
<ErrorBanner reducer={reducer} /> onNext={() =>
<div> reducer.transition("enter_user_attributes", {
identity_attributes: attrs,
})
}
>
{backupState.required_attributes.map((x: any, i: number) => { {backupState.required_attributes.map((x: any, i: number) => {
return ( return (
<AttributeEntryField <AttributeEntryField
@ -717,20 +974,7 @@ function AttributeEntry(props: AttributeEntryProps) {
/> />
); );
})} })}
</div> </AnastasisClientFrame>
<div>
<button onClick={() => reducer.back()}>Back</button>
<button
onClick={() =>
reducer.transition("enter_user_attributes", {
identity_attributes: attrs,
})
}
>
Next
</button>
</div>
</div>
); );
} }
@ -744,8 +988,9 @@ export interface AttributeEntryFieldProps {
function AttributeEntryField(props: AttributeEntryFieldProps) { function AttributeEntryField(props: AttributeEntryFieldProps) {
return ( return (
<div> <div>
<label>{props.spec.label}</label> <label>{props.spec.label}:</label>
<input <input
style={{ display: "block" }}
autoFocus={props.isFirst} autoFocus={props.isFirst}
type="text" type="text"
value={props.value} value={props.value}
@ -777,4 +1022,4 @@ function ErrorBanner(props: ErrorBannerProps) {
return null; return null;
} }
export default Home; export default AnastasisClient;

View File

@ -1,5 +1,5 @@
.home { .home {
padding: 56px 20px; padding: 1em 1em;
min-height: 100%; min-height: 100%;
width: 100%; width: 100%;
} }