anastasis-webui: implement more challenge types
This commit is contained in:
parent
c532648694
commit
40b137b549
@ -32,6 +32,11 @@ export interface ReducerStateBackup {
|
|||||||
payto: string;
|
payto: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
core_secret?: {
|
||||||
|
mime: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthMethod {
|
export interface AuthMethod {
|
||||||
|
@ -45,43 +45,39 @@ function withProcessLabel(reducer: AnastasisReducerApi, text: string): string {
|
|||||||
|
|
||||||
function ContinentSelection(props: CommonReducerProps) {
|
function ContinentSelection(props: CommonReducerProps) {
|
||||||
const { reducer, reducerState } = props;
|
const { reducer, reducerState } = props;
|
||||||
|
const sel = (x: string) =>
|
||||||
|
reducer.transition("select_continent", { continent: x });
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame
|
<AnastasisClientFrame
|
||||||
hideNext
|
hideNext
|
||||||
title={withProcessLabel(reducer, "Select Continent")}
|
title={withProcessLabel(reducer, "Select Continent")}
|
||||||
>
|
>
|
||||||
{reducerState.continents.map((x: any) => {
|
{reducerState.continents.map((x: any) => (
|
||||||
const sel = (x: string) =>
|
<button onClick={() => sel(x.name)} key={x.name}>
|
||||||
reducer.transition("select_continent", { continent: x });
|
{x.name}
|
||||||
return (
|
</button>
|
||||||
<button onClick={() => sel(x.name)} key={x.name}>
|
))}
|
||||||
{x.name}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CountrySelection(props: CommonReducerProps) {
|
function CountrySelection(props: CommonReducerProps) {
|
||||||
const { reducer, reducerState } = props;
|
const { reducer, reducerState } = props;
|
||||||
|
const sel = (x: any) =>
|
||||||
|
reducer.transition("select_country", {
|
||||||
|
country_code: x.code,
|
||||||
|
currencies: [x.currency],
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame
|
<AnastasisClientFrame
|
||||||
hideNext
|
hideNext
|
||||||
title={withProcessLabel(reducer, "Select Country")}
|
title={withProcessLabel(reducer, "Select Country")}
|
||||||
>
|
>
|
||||||
{reducerState.countries.map((x: any) => {
|
{reducerState.countries.map((x: any) => (
|
||||||
const sel = (x: any) =>
|
<button onClick={() => sel(x)} key={x.name}>
|
||||||
reducer.transition("select_country", {
|
{x.name} ({x.currency})
|
||||||
country_code: x.code,
|
</button>
|
||||||
currencies: [x.currency],
|
))}
|
||||||
});
|
|
||||||
return (
|
|
||||||
<button onClick={() => sel(x)} key={x.name}>
|
|
||||||
{x.name} ({x.currency})
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -106,21 +102,85 @@ function SolveQuestionEntry(props: SolveEntryProps) {
|
|||||||
>
|
>
|
||||||
<p>Feedback: {JSON.stringify(feedback)}</p>
|
<p>Feedback: {JSON.stringify(feedback)}</p>
|
||||||
<p>Question: {challenge.instructions}</p>
|
<p>Question: {challenge.instructions}</p>
|
||||||
<label>
|
<LabeledInput label="Answer" grabFocus bind={[answer, setAnswer]} />
|
||||||
<input
|
</AnastasisClientFrame>
|
||||||
value={answer}
|
);
|
||||||
onChange={(e) => setAnswer((e.target as HTMLInputElement).value)}
|
}
|
||||||
type="test"
|
|
||||||
/>
|
function SolveSmsEntry(props: SolveEntryProps) {
|
||||||
</label>
|
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>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SecretEditor(props: BackupReducerProps) {
|
function SecretEditor(props: BackupReducerProps) {
|
||||||
const { reducer } = props;
|
const { reducer } = props;
|
||||||
const [secretName, setSecretName] = useState("");
|
const [secretName, setSecretName] = useState(
|
||||||
const [secretValue, setSecretValue] = useState("");
|
props.backupState.secret_name ?? "",
|
||||||
|
);
|
||||||
|
const [secretValue, setSecretValue] = useState(
|
||||||
|
props.backupState.core_secret?.value ?? "" ?? "",
|
||||||
|
);
|
||||||
const secretNext = () => {
|
const secretNext = () => {
|
||||||
reducer.runTransaction(async (tx) => {
|
reducer.runTransaction(async (tx) => {
|
||||||
await tx.transition("enter_secret_name", {
|
await tx.transition("enter_secret_name", {
|
||||||
@ -144,34 +204,17 @@ function SecretEditor(props: BackupReducerProps) {
|
|||||||
onNext={() => secretNext()}
|
onNext={() => secretNext()}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput
|
||||||
Secret name:{" "}
|
label="Secret Name:"
|
||||||
<input
|
grabFocus
|
||||||
value={secretName}
|
bind={[secretName, setSecretName]}
|
||||||
onChange={(e) =>
|
/>
|
||||||
setSecretName((e.target as HTMLInputElement).value)
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput
|
||||||
Secret value:{" "}
|
label="Secret Value:"
|
||||||
<input
|
bind={[secretValue, setSecretValue]}
|
||||||
value={secretValue}
|
/>
|
||||||
onChange={(e) =>
|
|
||||||
setSecretValue((e.target as HTMLInputElement).value)
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
or:
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
File Upload: <input type="file" />
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
@ -234,6 +277,7 @@ function SecretSelection(props: RecoveryReducerProps) {
|
|||||||
const [otherVersion, setOtherVersion] = useState<number>(
|
const [otherVersion, setOtherVersion] = useState<number>(
|
||||||
recoveryState.recovery_document?.version ?? 0,
|
recoveryState.recovery_document?.version ?? 0,
|
||||||
);
|
);
|
||||||
|
const recoveryDocument = recoveryState.recovery_document!;
|
||||||
const [otherProvider, setOtherProvider] = useState<string>("");
|
const [otherProvider, setOtherProvider] = useState<string>("");
|
||||||
function selectVersion(p: string, n: number) {
|
function selectVersion(p: string, n: number) {
|
||||||
reducer.runTransaction(async (tx) => {
|
reducer.runTransaction(async (tx) => {
|
||||||
@ -250,9 +294,11 @@ function SecretSelection(props: RecoveryReducerProps) {
|
|||||||
<p>Select a different version of the secret</p>
|
<p>Select a different version of the secret</p>
|
||||||
<select onChange={(e) => setOtherProvider((e.target as any).value)}>
|
<select onChange={(e) => setOtherProvider((e.target as any).value)}>
|
||||||
{Object.keys(recoveryState.authentication_providers ?? {}).map(
|
{Object.keys(recoveryState.authentication_providers ?? {}).map(
|
||||||
(x) => {
|
(x) => (
|
||||||
return <option value={x}>{x}</option>;
|
<option selected={x === recoveryDocument.provider_url} value={x}>
|
||||||
},
|
{x}
|
||||||
|
</option>
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
<div>
|
<div>
|
||||||
@ -264,7 +310,7 @@ function SecretSelection(props: RecoveryReducerProps) {
|
|||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
<button onClick={() => selectVersion(otherProvider, otherVersion)}>
|
<button onClick={() => selectVersion(otherProvider, otherVersion)}>
|
||||||
Select
|
Use this version
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -280,9 +326,9 @@ function SecretSelection(props: RecoveryReducerProps) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame title="Recovery: Select secret">
|
<AnastasisClientFrame title="Recovery: Select secret">
|
||||||
<p>Provider: {recoveryState.recovery_document!.provider_url}</p>
|
<p>Provider: {recoveryDocument.provider_url}</p>
|
||||||
<p>Secret version: {recoveryState.recovery_document!.version}</p>
|
<p>Secret version: {recoveryDocument.version}</p>
|
||||||
<p>Secret name: {recoveryState.recovery_document!.version}</p>
|
<p>Secret name: {recoveryDocument.version}</p>
|
||||||
<button onClick={() => setSelectingVersion(true)}>
|
<button onClick={() => setSelectingVersion(true)}>
|
||||||
Select different secret
|
Select different secret
|
||||||
</button>
|
</button>
|
||||||
@ -305,37 +351,99 @@ interface AnastasisClientFrameProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AnastasisClientFrame(props: AnastasisClientFrameProps) {
|
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 (
|
return (
|
||||||
<WithReducer.Consumer>
|
<div class={style.home} onKeyPress={(e) => handleKeyPress(e)}>
|
||||||
{(reducer) => {
|
<button onClick={() => reducer.reset()}>Reset session</button>
|
||||||
if (!reducer) {
|
<h1>{props.title}</h1>
|
||||||
return <p>Fatal: Reducer must be in context.</p>;
|
<ErrorBanner reducer={reducer} />
|
||||||
}
|
{props.children}
|
||||||
const next = () => {
|
{!props.hideNav ? (
|
||||||
if (props.onNext) {
|
<div>
|
||||||
props.onNext();
|
<button onClick={() => reducer.back()}>Back</button>
|
||||||
} else {
|
{!props.hideNext ? (
|
||||||
reducer.transition("next", {});
|
<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 (
|
return (
|
||||||
<div class={style.home}>
|
<div>
|
||||||
<button onClick={() => reducer.reset()}>Reset session</button>
|
<h3>Policy #{i + 1}</h3>
|
||||||
<h1>{props.title}</h1>
|
{x.map((x) => {
|
||||||
<ErrorBanner reducer={reducer} />
|
const ch = challenges[x.uuid];
|
||||||
{props.children}
|
const feedback = recoveryState.challenge_feedback?.[x.uuid];
|
||||||
{!props.hideNav ? (
|
return (
|
||||||
<div>
|
<div
|
||||||
<button onClick={() => reducer.back()}>Back</button>
|
style={{
|
||||||
{!props.hideNext ? (
|
borderLeft: "2px solid gray",
|
||||||
<button onClick={() => next()}>Next</button>
|
paddingLeft: "0.5em",
|
||||||
) : null}
|
borderRadius: "0.5em",
|
||||||
</div>
|
marginTop: "0.5em",
|
||||||
) : null}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
})}
|
||||||
</WithReducer.Consumer>
|
</AnastasisClientFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -472,51 +580,7 @@ const AnastasisClientImpl: FunctionalComponent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) {
|
if (reducerState.recovery_state === RecoveryStates.ChallengeSelecting) {
|
||||||
const policies = reducerState.recovery_information!.policies;
|
return <ChallengeOverview reducer={reducer} recoveryState={reducerState} />;
|
||||||
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) {
|
if (reducerState.recovery_state === RecoveryStates.ChallengeSolving) {
|
||||||
@ -530,22 +594,21 @@ const AnastasisClientImpl: FunctionalComponent = () => {
|
|||||||
challenges[ch.uuid] = ch;
|
challenges[ch.uuid] = ch;
|
||||||
}
|
}
|
||||||
const selectedChallenge = challenges[selectedUuid];
|
const selectedChallenge = challenges[selectedUuid];
|
||||||
if (selectedChallenge.type === "question") {
|
const dialogMap: Record<string, (p: SolveEntryProps) => h.JSX.Element> = {
|
||||||
return (
|
question: SolveQuestionEntry,
|
||||||
<SolveQuestionEntry
|
sms: SolveSmsEntry,
|
||||||
challenge={selectedChallenge}
|
email: SolveEmailEntry,
|
||||||
reducer={reducer}
|
post: SolvePostEntry,
|
||||||
feedback={challengeFeedback[selectedUuid]}
|
};
|
||||||
/>
|
const SolveDialog =
|
||||||
);
|
dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
|
||||||
} else {
|
return (
|
||||||
return (
|
<SolveDialog
|
||||||
<AnastasisClientFrame hideNext title="Recovery: Solve challenge">
|
challenge={selectedChallenge}
|
||||||
<p>{JSON.stringify(selectedChallenge)}</p>
|
reducer={reducer}
|
||||||
<p>Challenge not supported.</p>
|
feedback={challengeFeedback[selectedUuid]}
|
||||||
</AnastasisClientFrame>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) {
|
if (reducerState.recovery_state === RecoveryStates.RecoveryFinished) {
|
||||||
@ -620,6 +683,14 @@ function AuthMethodSmsSetup(props: AuthMethodSetupProps) {
|
|||||||
function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
|
function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
|
||||||
const [questionText, setQuestionText] = useState("");
|
const [questionText, setQuestionText] = useState("");
|
||||||
const [answerText, setAnswerText] = useState("");
|
const [answerText, setAnswerText] = useState("");
|
||||||
|
const addQuestionAuth = () =>
|
||||||
|
props.addAuthMethod({
|
||||||
|
authentication_method: {
|
||||||
|
type: "question",
|
||||||
|
instructions: questionText,
|
||||||
|
challenge: encodeCrock(stringToBytes(answerText)),
|
||||||
|
},
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<AnastasisClientFrame hideNav title="Add Security Question">
|
<AnastasisClientFrame hideNav title="Add Security Question">
|
||||||
<div>
|
<div>
|
||||||
@ -630,44 +701,18 @@ function AuthMethodQuestionSetup(props: AuthMethodSetupProps) {
|
|||||||
here.
|
here.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput
|
||||||
Security question:{" "}
|
label="Security question"
|
||||||
<input
|
grabFocus
|
||||||
value={questionText}
|
bind={[questionText, setQuestionText]}
|
||||||
style={{ display: "block" }}
|
/>
|
||||||
autoFocus
|
|
||||||
onChange={(e) => setQuestionText((e.target as any).value)}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput label="Answer" bind={[answerText, setAnswerText]} />
|
||||||
Answer:{" "}
|
|
||||||
<input
|
|
||||||
value={answerText}
|
|
||||||
style={{ display: "block" }}
|
|
||||||
autoFocus
|
|
||||||
onChange={(e) => setAnswerText((e.target as any).value)}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => props.cancel()}>Cancel</button>
|
<button onClick={() => props.cancel()}>Cancel</button>
|
||||||
<button
|
<button onClick={() => addQuestionAuth()}>Add</button>
|
||||||
onClick={() =>
|
|
||||||
props.addAuthMethod({
|
|
||||||
authentication_method: {
|
|
||||||
type: "question",
|
|
||||||
instructions: questionText,
|
|
||||||
challenge: encodeCrock(stringToBytes(answerText)),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AnastasisClientFrame>
|
</AnastasisClientFrame>
|
||||||
@ -684,16 +729,11 @@ function AuthMethodEmailSetup(props: AuthMethodSetupProps) {
|
|||||||
email.
|
email.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput
|
||||||
Email address:{" "}
|
label="Email address"
|
||||||
<input
|
grabFocus
|
||||||
style={{ display: "block" }}
|
bind={[email, setEmail]}
|
||||||
value={email}
|
/>
|
||||||
autoFocus
|
|
||||||
onChange={(e) => setEmail((e.target as any).value)}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => props.cancel()}>Cancel</button>
|
<button onClick={() => props.cancel()}>Cancel</button>
|
||||||
@ -723,24 +763,20 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
|
|||||||
const [country, setCountry] = useState("");
|
const [country, setCountry] = useState("");
|
||||||
|
|
||||||
const addPostAuth = () => {
|
const addPostAuth = () => {
|
||||||
() =>
|
const challengeJson = {
|
||||||
props.addAuthMethod({
|
full_name: fullName,
|
||||||
authentication_method: {
|
street,
|
||||||
type: "email",
|
city,
|
||||||
instructions: `Letter to address in postal code ${postcode}`,
|
postcode,
|
||||||
challenge: encodeCrock(
|
country,
|
||||||
stringToBytes(
|
};
|
||||||
canonicalJson({
|
props.addAuthMethod({
|
||||||
full_name: fullName,
|
authentication_method: {
|
||||||
street,
|
type: "email",
|
||||||
city,
|
instructions: `Letter to address in postal code ${postcode}`,
|
||||||
postcode,
|
challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))),
|
||||||
country,
|
},
|
||||||
}),
|
});
|
||||||
),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -753,59 +789,23 @@ function AuthMethodPostSetup(props: AuthMethodSetupProps) {
|
|||||||
code that you will receive in a letter to that address.
|
code that you will receive in a letter to that address.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput
|
||||||
Full Name
|
grabFocus
|
||||||
<input
|
label="Full Name"
|
||||||
value={fullName}
|
bind={[fullName, setFullName]}
|
||||||
autoFocus
|
/>
|
||||||
onChange={(e) => setFullName((e.target as any).value)}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput label="Street" bind={[street, setStreet]} />
|
||||||
Street
|
|
||||||
<input
|
|
||||||
value={street}
|
|
||||||
autoFocus
|
|
||||||
onChange={(e) => setStreet((e.target as any).value)}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput label="City" bind={[city, setCity]} />
|
||||||
City
|
|
||||||
<input
|
|
||||||
value={city}
|
|
||||||
autoFocus
|
|
||||||
onChange={(e) => setCity((e.target as any).value)}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput label="Postal Code" bind={[postcode, setPostcode]} />
|
||||||
Postal Code
|
|
||||||
<input
|
|
||||||
value={postcode}
|
|
||||||
autoFocus
|
|
||||||
onChange={(e) => setPostcode((e.target as any).value)}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<LabeledInput label="Country" bind={[country, setCountry]} />
|
||||||
Country
|
|
||||||
<input
|
|
||||||
value={country}
|
|
||||||
autoFocus
|
|
||||||
onChange={(e) => setCountry((e.target as any).value)}
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={() => props.cancel()}>Cancel</button>
|
<button onClick={() => props.cancel()}>Cancel</button>
|
||||||
@ -851,48 +851,23 @@ function AuthenticationEditor(props: AuthenticationEditorProps) {
|
|||||||
reducer.transition("add_authentication", args);
|
reducer.transition("add_authentication", args);
|
||||||
setSelectedMethod(undefined);
|
setSelectedMethod(undefined);
|
||||||
};
|
};
|
||||||
switch (selectedMethod) {
|
const methodMap: Record<
|
||||||
case "sms":
|
string,
|
||||||
return (
|
(props: AuthMethodSetupProps) => h.JSX.Element
|
||||||
<AuthMethodSmsSetup
|
> = {
|
||||||
cancel={cancel}
|
sms: AuthMethodSmsSetup,
|
||||||
addAuthMethod={addMethod}
|
question: AuthMethodQuestionSetup,
|
||||||
method="sms"
|
email: AuthMethodEmailSetup,
|
||||||
/>
|
post: AuthMethodPostSetup,
|
||||||
);
|
};
|
||||||
case "question":
|
const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented;
|
||||||
return (
|
return (
|
||||||
<AuthMethodQuestionSetup
|
<AuthSetup
|
||||||
cancel={cancel}
|
cancel={cancel}
|
||||||
addAuthMethod={addMethod}
|
addAuthMethod={addMethod}
|
||||||
method="question"
|
method={selectedMethod}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "email":
|
|
||||||
return (
|
|
||||||
<AuthMethodEmailSetup
|
|
||||||
cancel={cancel}
|
|
||||||
addAuthMethod={addMethod}
|
|
||||||
method="email"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "post":
|
|
||||||
return (
|
|
||||||
<AuthMethodPostSetup
|
|
||||||
cancel={cancel}
|
|
||||||
addAuthMethod={addMethod}
|
|
||||||
method="post"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<AuthMethodNotImplemented
|
|
||||||
cancel={cancel}
|
|
||||||
addAuthMethod={addMethod}
|
|
||||||
method={selectedMethod}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function MethodButton(props: { method: string; label: String }) {
|
function MethodButton(props: { method: string; label: String }) {
|
||||||
return (
|
return (
|
||||||
@ -978,6 +953,32 @@ function AttributeEntry(props: AttributeEntryProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export interface AttributeEntryFieldProps {
|
||||||
isFirst: boolean;
|
isFirst: boolean;
|
||||||
value: string;
|
value: string;
|
||||||
@ -988,13 +989,10 @@ export interface AttributeEntryFieldProps {
|
|||||||
function AttributeEntryField(props: AttributeEntryFieldProps) {
|
function AttributeEntryField(props: AttributeEntryFieldProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label>{props.spec.label}:</label>
|
<LabeledInput
|
||||||
<input
|
grabFocus={props.isFirst}
|
||||||
style={{ display: "block" }}
|
label={props.spec.label}
|
||||||
autoFocus={props.isFirst}
|
bind={[props.value, props.setValue]}
|
||||||
type="text"
|
|
||||||
value={props.value}
|
|
||||||
onChange={(e) => props.setValue((e as any).target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
padding: 1em 1em;
|
padding: 1em 1em;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home div {
|
.home div {
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import Header from '../src/components/header';
|
|
||||||
// See: https://github.com/preactjs/enzyme-adapter-preact-pure
|
|
||||||
import { shallow } from 'enzyme';
|
|
||||||
|
|
||||||
describe('Initial Test of the Header', () => {
|
|
||||||
test('Header renders 3 nav items', () => {
|
|
||||||
const context = shallow(<Header />);
|
|
||||||
expect(context.find('h1').text()).toBe('Preact App');
|
|
||||||
expect(context.find('Link').length).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in New Issue
Block a user