aboutsummaryrefslogtreecommitdiff
path: root/packages/anastasis-webui/src/routes/home/index.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/anastasis-webui/src/routes/home/index.tsx')
-rw-r--r--packages/anastasis-webui/src/routes/home/index.tsx1025
1 files changed, 0 insertions, 1025 deletions
diff --git a/packages/anastasis-webui/src/routes/home/index.tsx b/packages/anastasis-webui/src/routes/home/index.tsx
deleted file mode 100644
index 1351775b5..000000000
--- a/packages/anastasis-webui/src/routes/home/index.tsx
+++ /dev/null
@@ -1,1025 +0,0 @@
-import {
- bytesToString,
- canonicalJson,
- decodeCrock,
- encodeCrock,
- stringToBytes,
-} from "@gnu-taler/taler-util";
-import {
- AuthMethod,
- BackupStates,
- ChallengeFeedback,
- ChallengeInfo,
- RecoveryStates,
- ReducerStateBackup,
- ReducerStateRecovery,
-} from "anastasis-core";
-import {
- FunctionalComponent,
- ComponentChildren,
- h,
- createContext,
-} from "preact";
-import { useState, useContext, useRef, useLayoutEffect } from "preact/hooks";
-import {
- AnastasisReducerApi,
- useAnastasisReducer,
-} from "../../hooks/use-anastasis-reducer";
-import style from "./style.css";
-
-const WithReducer = createContext<AnastasisReducerApi | undefined>(undefined);
-
-function isBackup(reducer: AnastasisReducerApi) {
- return !!reducer.currentReducerState?.backup_state;
-}
-
-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 sel = (x: string) =>
- reducer.transition("select_continent", { continent: x });
- return (
- <AnastasisClientFrame
- hideNext
- title={withProcessLabel(reducer, "Select Continent")}
- >
- {reducerState.continents.map((x: any) => (
- <button onClick={() => sel(x.name)} key={x.name}>
- {x.name}
- </button>
- ))}
- </AnastasisClientFrame>
- );
-}
-
-function CountrySelection(props: CommonReducerProps) {
- const { reducer, reducerState } = props;
- const sel = (x: any) =>
- reducer.transition("select_country", {
- country_code: x.code,
- currencies: [x.currency],
- });
- return (
- <AnastasisClientFrame
- hideNext
- title={withProcessLabel(reducer, "Select Country")}
- >
- {reducerState.countries.map((x: any) => (
- <button onClick={() => sel(x)} key={x.name}>
- {x.name} ({x.currency})
- </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>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
-
-function SolveSmsEntry(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>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
-
-function SolvePostEntry(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>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
-
-function SolveEmailEntry(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>{challenge.instructions}</p>
- <LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
- </AnastasisClientFrame>
- );
-}
-
-function SolveUnsupportedEntry(props: SolveEntryProps) {
- return (
- <AnastasisClientFrame hideNext title="Recovery: Solve challenge">
- <p>{JSON.stringify(props.challenge)}</p>
- <p>Challenge not supported.</p>
- </AnastasisClientFrame>
- );
-}
-
-function SecretEditor(props: BackupReducerProps) {
- const { reducer } = props;
- const [secretName, setSecretName] = useState(
- props.backupState.secret_name ?? "",
- );
- const [secretValue, setSecretValue] = useState(
- props.backupState.core_secret?.value ?? "" ?? "",
- );
- 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>
- <LabeledInput
- label="Secret Name:"
- grabFocus
- bind={[secretName, setSecretName]}
- />
- </div>
- <div>
- <LabeledInput
- label="Secret Value:"
- bind={[secretValue, setSecretValue]}
- />
- </div>
- </AnastasisClientFrame>
- );
-}
-
-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 recoveryDocument = recoveryState.recovery_document!;
- 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) => (
- <option selected={x === recoveryDocument.provider_url} 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)}>
- Use this version
- </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: {recoveryDocument.provider_url}</p>
- <p>Secret version: {recoveryDocument.version}</p>
- <p>Secret name: {recoveryDocument.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) {
- const reducer = useContext(WithReducer);
- if (!reducer) {
- return <p>Fatal: Reducer must be in context.</p>;
- }
- const next = () => {
- if (props.onNext) {
- props.onNext();
- } else {
- reducer.transition("next", {});
- }
- };
- const handleKeyPress = (e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>) => {
- console.log("Got key press", e.key);
- // FIXME: By default, "next" action should be executed here
- };
- return (
- <div class={style.home} onKeyPress={(e) => handleKeyPress(e)}>
- <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>
- );
-}
-
-function ChallengeOverview(props: RecoveryReducerProps) {
- const { recoveryState, reducer } = props;
- const policies = recoveryState.recovery_information!.policies;
- const chArr = recoveryState.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];
- const feedback = recoveryState.challenge_feedback?.[x.uuid];
- return (
- <div
- style={{
- borderLeft: "2px solid gray",
- paddingLeft: "0.5em",
- borderRadius: "0.5em",
- marginTop: "0.5em",
- marginBottom: "0.5em",
- }}
- >
- <h4>
- {ch.type} ({ch.instructions})
- </h4>
- <p>Status: {feedback?.state ?? "unknown"}</p>
- {feedback?.state !== "solved" ? (
- <button
- onClick={() =>
- reducer.transition("select_challenge", {
- uuid: x.uuid,
- })
- }
- >
- Solve
- </button>
- ) : null}
- </div>
- );
- })}
- </div>
- );
- })}
- </AnastasisClientFrame>
- );
-}
-
-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 (
- <AnastasisClientFrame hideNext title="Backup finished">
- <p>
- Your backup of secret "{backupState.secret_name ?? "??"}" was
- successful.
- </p>
- <p>The backup is stored by the following providers:</p>
- <ul>
- {Object.keys(backupState.success_details!).map((x, i) => {
- const sd = backupState.success_details![x];
- return (
- <li>
- {x} (Policy version {sd.policy_version})
- </li>
- );
- })}
- </ul>
- <button onClick={() => reducer.reset()}>Back to start</button>
- </AnastasisClientFrame>
- );
- }
-
- if (reducerState.backup_state === BackupStates.TruthsPaying) {
- const backupState: ReducerStateBackup = reducerState;
- const payments = backupState.payments ?? [];
- return (
- <AnastasisClientFrame
- hideNext
- title="Backup: Authentication Storage Payments"
- >
- <p>
- Some of the providers require a payment to store the encrypted
- authentication information.
- </p>
- <ul>
- {payments.map((x) => {
- return <li>{x}</li>;
- })}
- </ul>
- <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 (
- <AnastasisClientFrame hideNext title="Backup: Recovery Document Payments">
- <p>
- Some of the providers require a payment to store the encrypted
- recovery document.
- </p>
- <ul>
- {payments.map((x) => {
- return (
- <li>
- {x.provider}: {x.payto}
- </li>
- );
- })}
- </ul>
- <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) {
- return <ChallengeOverview reducer={reducer} recoveryState={reducerState} />;
- }
-
- 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];
- const dialogMap: Record<string, (p: SolveEntryProps) => h.JSX.Element> = {
- question: SolveQuestionEntry,
- sms: SolveSmsEntry,
- email: SolveEmailEntry,
- post: SolvePostEntry,
- };
- const SolveDialog =
- dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
- return (
- <SolveDialog
- challenge={selectedChallenge}
- reducer={reducer}
- feedback={challengeFeedback[selectedUuid]}
- />
- );
- }
-
- 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 (
- <AnastasisClientFrame hideNav title="Bug">
- <p>Bug: Unknown state.</p>
- <button onClick={() => reducer.reset()}>Reset</button>
- </AnastasisClientFrame>
- );
-};
-
-interface AuthMethodSetupProps {
- method: string;
- addAuthMethod: (x: any) => void;
- cancel: () => void;
-}
-
-function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
- const [mobileNumber, setMobileNumber] = useState("");
- const addSmsAuth = () => {
- props.addAuthMethod({
- authentication_method: {
- type: "sms",
- instructions: `SMS to ${mobileNumber}`,
- challenge: encodeCrock(stringToBytes(mobileNumber)),
- },
- });
- };
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- inputRef.current?.focus();
- }, []);
- return (
- <AnastasisClientFrame hideNav title="Add SMS authentication">
- <div>
- <p>
- For SMS authentication, you need to provide a mobile number. When
- recovering your secret, you will be asked to enter the code you
- receive via SMS.
- </p>
- <label>
- Mobile number:{" "}
- <input
- value={mobileNumber}
- ref={inputRef}
- style={{ display: "block" }}
- autoFocus
- onChange={(e) => setMobileNumber((e.target as any).value)}
- type="text"
- />
- </label>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addSmsAuth()}>Add</button>
- </div>
- </div>
- </AnastasisClientFrame>
- );
-}
-
-function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
- const [questionText, setQuestionText] = useState("");
- const [answerText, setAnswerText] = useState("");
- const addQuestionAuth = () =>
- props.addAuthMethod({
- authentication_method: {
- type: "question",
- instructions: questionText,
- challenge: encodeCrock(stringToBytes(answerText)),
- },
- });
- return (
- <AnastasisClientFrame hideNav title="Add Security Question">
- <div>
- <p>
- For security question authentication, you need to provide a question
- and its answer. When recovering your secret, you will be shown the
- question and you will need to type the answer exactly as you typed it
- here.
- </p>
- <div>
- <LabeledInput
- label="Security question"
- grabFocus
- bind={[questionText, setQuestionText]}
- />
- </div>
- <div>
- <LabeledInput label="Answer" bind={[answerText, setAnswerText]} />
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addQuestionAuth()}>Add</button>
- </div>
- </div>
- </AnastasisClientFrame>
- );
-}
-
-function AuthMethodEmailSetup(props: AuthMethodSetupProps) {
- const [email, setEmail] = useState("");
- return (
- <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>
- <LabeledInput
- label="Email address"
- grabFocus
- bind={[email, setEmail]}
- />
- </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>
- );
-}
-
-function AuthMethodPostSetup(props: AuthMethodSetupProps) {
- const [fullName, setFullName] = useState("");
- const [street, setStreet] = useState("");
- const [city, setCity] = useState("");
- const [postcode, setPostcode] = useState("");
- const [country, setCountry] = useState("");
-
- const addPostAuth = () => {
- const challengeJson = {
- full_name: fullName,
- street,
- city,
- postcode,
- country,
- };
- props.addAuthMethod({
- authentication_method: {
- type: "email",
- instructions: `Letter to address in postal code ${postcode}`,
- challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
- },
- });
- };
-
- return (
- <div class={style.home}>
- <h1>Add {props.method} authentication</h1>
- <div>
- <p>
- For postal letter authentication, you need to provide a postal
- address. When recovering your secret, you will be asked to enter a
- code that you will receive in a letter to that address.
- </p>
- <div>
- <LabeledInput
- grabFocus
- label="Full Name"
- bind={[fullName, setFullName]}
- />
- </div>
- <div>
- <LabeledInput label="Street" bind={[street, setStreet]} />
- </div>
- <div>
- <LabeledInput label="City" bind={[city, setCity]} />
- </div>
- <div>
- <LabeledInput label="Postal Code" bind={[postcode, setPostcode]} />
- </div>
- <div>
- <LabeledInput label="Country" bind={[country, setCountry]} />
- </div>
- <div>
- <button onClick={() => props.cancel()}>Cancel</button>
- <button onClick={() => addPostAuth()}>Add</button>
- </div>
- </div>
- </div>
- );
-}
-
-function AuthMethodNotImplemented(props: AuthMethodSetupProps) {
- return (
- <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>
- );
-}
-
-export interface AuthenticationEditorProps {
- reducer: AnastasisReducerApi;
- backupState: ReducerStateBackup;
-}
-
-function AuthenticationEditor(props: AuthenticationEditorProps) {
- const [selectedMethod, setSelectedMethod] = useState<string | undefined>(
- undefined,
- );
- const { reducer, backupState } = props;
- const providers = backupState.authentication_providers!;
- const authAvailableSet = new Set<string>();
- for (const provKey of Object.keys(providers)) {
- const p = providers[provKey];
- if ("http_status" in p && (!("error_code" in p)) && p.methods) {
- for (const meth of p.methods) {
- authAvailableSet.add(meth.type);
- }
- }
- }
- if (selectedMethod) {
- const cancel = () => setSelectedMethod(undefined);
- const addMethod = (args: any) => {
- reducer.transition("add_authentication", args);
- setSelectedMethod(undefined);
- };
- const methodMap: Record<
- string,
- (props: AuthMethodSetupProps) => h.JSX.Element
- > = {
- sms: AuthMethodSmsSetup,
- question: AuthMethodQuestionSetup,
- email: AuthMethodEmailSetup,
- post: AuthMethodPostSetup,
- };
- const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented;
- return (
- <AuthSetup
- cancel={cancel}
- addAuthMethod={addMethod}
- method={selectedMethod}
- />
- );
- }
- function MethodButton(props: { method: string; label: String }) {
- return (
- <button
- disabled={!authAvailableSet.has(props.method)}
- onClick={() => {
- setSelectedMethod(props.method);
- reducer.dismissError();
- }}
- >
- {props.label}
- </button>
- );
- }
- const configuredAuthMethods: AuthMethod[] =
- backupState.authentication_methods ?? [];
- const haveMethodsConfigured = configuredAuthMethods.length;
- return (
- <AnastasisClientFrame title="Backup: Configure Authentication Methods">
- <div>
- <MethodButton method="sms" label="SMS" />
- <MethodButton method="email" label="Email" />
- <MethodButton method="question" label="Question" />
- <MethodButton method="post" label="Physical Mail" />
- <MethodButton method="totp" label="TOTP" />
- <MethodButton method="iban" label="IBAN" />
- </div>
- <h2>Configured authentication methods</h2>
- {haveMethodsConfigured ? (
- configuredAuthMethods.map((x, i) => {
- return (
- <p>
- {x.type} ({x.instructions}){" "}
- <button
- onClick={() =>
- reducer.transition("delete_authentication", {
- authentication_method: i,
- })
- }
- >
- Delete
- </button>
- </p>
- );
- })
- ) : (
- <p>No authentication methods configured yet.</p>
- )}
- </AnastasisClientFrame>
- );
-}
-
-export interface AttributeEntryProps {
- reducer: AnastasisReducerApi;
- reducerState: ReducerStateRecovery | ReducerStateBackup;
-}
-
-function AttributeEntry(props: AttributeEntryProps) {
- const { reducer, reducerState: backupState } = props;
- const [attrs, setAttrs] = useState<Record<string, string>>(
- props.reducerState.identity_attributes ?? {},
- );
- return (
- <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>
- );
-}
-
-interface LabeledInputProps {
- label: string;
- grabFocus?: boolean;
- bind: [string, (x: string) => void];
-}
-
-function LabeledInput(props: LabeledInputProps) {
- const inputRef = useRef<HTMLInputElement>(null);
- useLayoutEffect(() => {
- if (props.grabFocus) {
- inputRef.current?.focus();
- }
- }, []);
- return (
- <label>
- {props.label}
- <input
- value={props.bind[0]}
- onChange={(e) => props.bind[1]((e.target as HTMLInputElement).value)}
- ref={inputRef}
- style={{ display: "block" }}
- />
- </label>
- );
-}
-
-export interface AttributeEntryFieldProps {
- isFirst: boolean;
- value: string;
- setValue: (newValue: string) => void;
- spec: any;
-}
-
-function AttributeEntryField(props: AttributeEntryFieldProps) {
- return (
- <div>
- <LabeledInput
- grabFocus={props.isFirst}
- label={props.spec.label}
- bind={[props.value, props.setValue]}
- />
- </div>
- );
-}
-
-interface ErrorBannerProps {
- reducer: AnastasisReducerApi;
-}
-
-/**
- * Show a dismissable error banner if there is a current error.
- */
-function ErrorBanner(props: ErrorBannerProps) {
- const currentError = props.reducer.currentError;
- if (currentError) {
- return (
- <div id={style.error}>
- <p>Error: {JSON.stringify(currentError)}</p>
- <button onClick={() => props.reducer.dismissError()}>
- Dismiss Error
- </button>
- </div>
- );
- }
- return null;
-}
-
-export default AnastasisClient;