anastasis-webui: show feedback in solution screen

This commit is contained in:
Florian Dold 2021-11-03 18:56:19 +01:00
parent fefdb0d7ad
commit 9fb6536fbc
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
2 changed files with 135 additions and 46 deletions

View File

@ -5,7 +5,7 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index"; import { AnastasisClientFrame } from "./index";
import { authMethods, KnownAuthMethods } from "./authMethod"; import { authMethods, KnownAuthMethods } from "./authMethod";
function FeedbackDisplay(props: { feedback?: ChallengeFeedback }) { function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
const { feedback } = props; const { feedback } = props;
if (!feedback) { if (!feedback) {
return null; return null;
@ -130,7 +130,7 @@ export function ChallengeOverviewScreen(): VNode {
<span class="icon">{method?.icon}</span> <span class="icon">{method?.icon}</span>
<span>{info.instructions}</span> <span>{info.instructions}</span>
</div> </div>
<FeedbackDisplay feedback={info.feedback} /> <OverviewFeedbackDisplay feedback={info.feedback} />
</div> </div>
<div> <div>
{method && info.feedback?.state !== "solved" ? ( {method && info.feedback?.state !== "solved" ? (

View File

@ -1,38 +1,89 @@
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { AnastasisClientFrame } from "."; import { AnastasisClientFrame } from ".";
import { ChallengeFeedback, ChallengeInfo } from "../../../../anastasis-core/lib"; import {
ChallengeFeedback,
ChallengeFeedbackStatus,
ChallengeInfo,
} from "../../../../anastasis-core/lib";
import { TextInput } from "../../components/fields/TextInput"; import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis"; import { useAnastasisContext } from "../../context/anastasis";
function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
const { feedback } = props;
if (!feedback) {
return null;
}
switch (feedback.state) {
case ChallengeFeedbackStatus.Message:
return (
<div>
<p>{feedback.message}</p>
</div>
);
case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.AuthIban:
return null;
case ChallengeFeedbackStatus.RateLimitExceeded:
return <div>Rate limit exceeded.</div>;
case ChallengeFeedbackStatus.Redirect:
return <div>Redirect (FIXME: not supported)</div>;
case ChallengeFeedbackStatus.Unsupported:
return <div>Challenge not supported by client.</div>;
case ChallengeFeedbackStatus.TruthUnknown:
return <div>Truth unknown</div>;
default:
return (
<div>
<pre>{JSON.stringify(feedback)}</pre>
</div>
);
}
}
export function SolveScreen(): VNode { export function SolveScreen(): VNode {
const reducer = useAnastasisContext() const reducer = useAnastasisContext();
const [answer, setAnswer] = useState(""); const [answer, setAnswer] = useState("");
if (!reducer) { if (!reducer) {
return <AnastasisClientFrame hideNav title="Recovery problem"> return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>no reducer in context</div> <div>no reducer in context</div>
</AnastasisClientFrame> </AnastasisClientFrame>
);
} }
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) { if (
return <AnastasisClientFrame hideNav title="Recovery problem"> !reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
</AnastasisClientFrame> </AnastasisClientFrame>
);
} }
if (!reducer.currentReducerState.recovery_information) { if (!reducer.currentReducerState.recovery_information) {
return <AnastasisClientFrame hideNext="Recovery document not found" title="Recovery problem"> return (
<AnastasisClientFrame
hideNext="Recovery document not found"
title="Recovery problem"
>
<div>no recovery information found</div> <div>no recovery information found</div>
</AnastasisClientFrame> </AnastasisClientFrame>
);
} }
if (!reducer.currentReducerState.selected_challenge_uuid) { if (!reducer.currentReducerState.selected_challenge_uuid) {
return <AnastasisClientFrame hideNav title="Recovery problem"> return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div> <div>invalid state</div>
</AnastasisClientFrame> </AnastasisClientFrame>
);
} }
const chArr = reducer.currentReducerState.recovery_information.challenges; const chArr = reducer.currentReducerState.recovery_information.challenges;
const challengeFeedback = reducer.currentReducerState.challenge_feedback ?? {}; const challengeFeedback =
reducer.currentReducerState.challenge_feedback ?? {};
const selectedUuid = reducer.currentReducerState.selected_challenge_uuid; const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
const challenges: { const challenges: {
[uuid: string]: ChallengeInfo; [uuid: string]: ChallengeInfo;
@ -47,31 +98,44 @@ export function SolveScreen(): VNode {
email: SolveEmailEntry, email: SolveEmailEntry,
post: SolvePostEntry, post: SolvePostEntry,
}; };
const SolveDialog = selectedChallenge === undefined ? SolveUndefinedEntry : dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry; const SolveDialog =
selectedChallenge === undefined
? SolveUndefinedEntry
: dialogMap[selectedChallenge.type] ?? SolveUnsupportedEntry;
function onNext(): void { function onNext(): void {
reducer?.transition("solve_challenge", { answer }) reducer?.transition("solve_challenge", { answer });
} }
function onCancel(): void { function onCancel(): void {
reducer?.back() reducer?.back();
} }
return ( return (
<AnastasisClientFrame <AnastasisClientFrame hideNav title="Recovery: Solve challenge">
hideNav <SolveOverviewFeedbackDisplay
title="Recovery: Solve challenge" feedback={challengeFeedback[selectedUuid]}
> />
<SolveDialog <SolveDialog
id={selectedUuid} id={selectedUuid}
answer={answer} answer={answer}
setAnswer={setAnswer} setAnswer={setAnswer}
challenge={selectedChallenge} challenge={selectedChallenge}
feedback={challengeFeedback[selectedUuid]} /> feedback={challengeFeedback[selectedUuid]}
/>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <div
<button class="button" onClick={onCancel}>Cancel</button> style={{
<button class="button is-info" onClick={onNext} >Confirm</button> marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={onCancel}>
Cancel
</button>
<button class="button is-info" onClick={onNext}>
Confirm
</button>
</div> </div>
</AnastasisClientFrame> </AnastasisClientFrame>
); );
@ -85,38 +149,61 @@ export interface SolveEntryProps {
setAnswer: (s: string) => void; setAnswer: (s: string) => void;
} }
function SolveSmsEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode { function SolveSmsEntry({
return (<Fragment> challenge,
<p>An sms has been sent to "<b>{challenge.instructions}</b>". Type the code below</p> answer,
setAnswer,
}: SolveEntryProps): VNode {
return (
<Fragment>
<p>
An sms has been sent to "<b>{challenge.instructions}</b>". Type the code
below
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
</Fragment> </Fragment>
); );
} }
function SolveQuestionEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode { function SolveQuestionEntry({
challenge,
answer,
setAnswer,
}: SolveEntryProps): VNode {
return ( return (
<Fragment> <Fragment>
<p>Type the answer to the following question:</p> <p>Type the answer to the following question:</p>
<pre> <pre>{challenge.instructions}</pre>
{challenge.instructions}
</pre>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
</Fragment> </Fragment>
); );
} }
function SolvePostEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode { function SolvePostEntry({
challenge,
answer,
setAnswer,
}: SolveEntryProps): VNode {
return ( return (
<Fragment> <Fragment>
<p>instruction for post type challenge "<b>{challenge.instructions}</b>"</p> <p>
instruction for post type challenge "<b>{challenge.instructions}</b>"
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
</Fragment> </Fragment>
); );
} }
function SolveEmailEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode { function SolveEmailEntry({
challenge,
answer,
setAnswer,
}: SolveEntryProps): VNode {
return ( return (
<Fragment> <Fragment>
<p>An email has been sent to "<b>{challenge.instructions}</b>". Type the code below</p> <p>
An email has been sent to "<b>{challenge.instructions}</b>". Type the
code below
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
</Fragment> </Fragment>
); );
@ -126,7 +213,8 @@ function SolveUnsupportedEntry(props: SolveEntryProps): VNode {
return ( return (
<Fragment> <Fragment>
<p> <p>
The challenge selected is not supported for this UI. Please update this version or try using another policy. The challenge selected is not supported for this UI. Please update this
version or try using another policy.
</p> </p>
<p> <p>
<b>Challenge type:</b> {props.challenge.type} <b>Challenge type:</b> {props.challenge.type}
@ -138,7 +226,8 @@ function SolveUndefinedEntry(props: SolveEntryProps): VNode {
return ( return (
<Fragment> <Fragment>
<p> <p>
There is no challenge information for id <b>"{props.id}"</b>. Try resetting the recovery session. There is no challenge information for id <b>"{props.id}"</b>. Try
resetting the recovery session.
</p> </p>
</Fragment> </Fragment>
); );