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

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;
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/";

View File

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

View File

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