anastasis-webui: updated challenge feedback

This commit is contained in:
Florian Dold 2022-04-13 21:40:56 +02:00
parent bd76b5d76f
commit 4e1fe5eb10
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
26 changed files with 372 additions and 685 deletions

View File

@ -1,29 +1,48 @@
/*
This file is part of GNU Anastasis
(C) 2021-2022 Anastasis SARL
GNU Anastasis is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import { AmountString, HttpStatusCode } from "@gnu-taler/taler-util"; import { AmountString, HttpStatusCode } from "@gnu-taler/taler-util";
export enum ChallengeFeedbackStatus { export enum ChallengeFeedbackStatus {
Solved = "solved", Solved = "solved",
CodeInFile = "code-in-file",
CodeSent = "code-sent",
ServerFailure = "server-failure", ServerFailure = "server-failure",
TruthUnknown = "truth-unknown", TruthUnknown = "truth-unknown",
Redirect = "redirect", TalerPayment = "taler-payment",
Payment = "payment",
Pending = "pending",
Message = "message",
Unsupported = "unsupported", Unsupported = "unsupported",
RateLimitExceeded = "rate-limit-exceeded", RateLimitExceeded = "rate-limit-exceeded",
AuthIban = "auth-iban", AuthIban = "auth-iban",
IncorrectAnswer = "incorrect-answer",
} }
export type ChallengeFeedback = export type ChallengeFeedback =
| ChallengeFeedbackSolved | ChallengeFeedbackSolved
| ChallengeFeedbackPending | ChallengeFeedbackCodeInFile
| ChallengeFeedbackPayment | ChallengeFeedbackCodeSent
| ChallengeFeedbackIncorrectAnswer
| ChallengeFeedbackTalerPaymentRequired
| ChallengeFeedbackServerFailure | ChallengeFeedbackServerFailure
| ChallengeFeedbackRateLimitExceeded | ChallengeFeedbackRateLimitExceeded
| ChallengeFeedbackTruthUnknown | ChallengeFeedbackTruthUnknown
| ChallengeFeedbackRedirect
| ChallengeFeedbackMessage
| ChallengeFeedbackUnsupported | ChallengeFeedbackUnsupported
| ChallengeFeedbackAuthIban; | ChallengeFeedbackBankTransferRequired;
/** /**
* Challenge has been solved and the key share has * Challenge has been solved and the key share has
@ -33,13 +52,29 @@ export interface ChallengeFeedbackSolved {
state: ChallengeFeedbackStatus.Solved; state: ChallengeFeedbackStatus.Solved;
} }
export interface ChallengeFeedbackIncorrectAnswer {
state: ChallengeFeedbackStatus.IncorrectAnswer;
}
export interface ChallengeFeedbackCodeInFile {
state: ChallengeFeedbackStatus.CodeInFile;
filename: string;
display_hint: string;
}
export interface ChallengeFeedbackCodeSent {
state: ChallengeFeedbackStatus.CodeSent;
display_hint: string;
address_hint: string;
}
/** /**
* The challenge given by the server is unsupported * The challenge given by the server is unsupported
* by the current anastasis client. * by the current anastasis client.
*/ */
export interface ChallengeFeedbackUnsupported { export interface ChallengeFeedbackUnsupported {
state: ChallengeFeedbackStatus.Unsupported; state: ChallengeFeedbackStatus.Unsupported;
http_status: HttpStatusCode;
/** /**
* Human-readable identifier of the unsupported method. * Human-readable identifier of the unsupported method.
*/ */
@ -57,7 +92,7 @@ export interface ChallengeFeedbackRateLimitExceeded {
* Instructions for performing authentication via an * Instructions for performing authentication via an
* IBAN bank transfer. * IBAN bank transfer.
*/ */
export interface ChallengeFeedbackAuthIban { export interface ChallengeFeedbackBankTransferRequired {
state: ChallengeFeedbackStatus.AuthIban; state: ChallengeFeedbackStatus.AuthIban;
/** /**
@ -68,12 +103,12 @@ export interface ChallengeFeedbackAuthIban {
/** /**
* Account that should be credited. * Account that should be credited.
*/ */
credit_iban: string; target_iban: string;
/** /**
* Creditor name. * Creditor name.
*/ */
business_name: string; target_business_name: string;
/** /**
* Unstructured remittance information that should * Unstructured remittance information that should
@ -81,41 +116,7 @@ export interface ChallengeFeedbackAuthIban {
*/ */
wire_transfer_subject: string; wire_transfer_subject: string;
/**
* FIXME: This field is only present for compatibility with
* the C reducer test suite.
*/
method: "iban";
answer_code: number; answer_code: number;
/**
* FIXME: This field is only present for compatibility with
* the C reducer test suite.
*/
details: {
challenge_amount: AmountString;
credit_iban: string;
business_name: string;
wire_transfer_subject: string;
};
}
/**
* Challenge still needs to be solved.
*/
export interface ChallengeFeedbackPending {
state: ChallengeFeedbackStatus.Pending;
}
/**
* Human-readable response from the provider
* after the user failed to solve the challenge
* correctly.
*/
export interface ChallengeFeedbackMessage {
state: ChallengeFeedbackStatus.Message;
message: string;
} }
/** /**
@ -140,22 +141,12 @@ export interface ChallengeFeedbackTruthUnknown {
state: ChallengeFeedbackStatus.TruthUnknown; state: ChallengeFeedbackStatus.TruthUnknown;
} }
/**
* The user should be asked to go to a URL
* to complete the authentication there.
*/
export interface ChallengeFeedbackRedirect {
state: ChallengeFeedbackStatus.Redirect;
http_status: number;
redirect_url: string;
}
/** /**
* A payment is required before the user can * A payment is required before the user can
* even attempt to solve the challenge. * even attempt to solve the challenge.
*/ */
export interface ChallengeFeedbackPayment { export interface ChallengeFeedbackTalerPaymentRequired {
state: ChallengeFeedbackStatus.Payment; state: ChallengeFeedbackStatus.TalerPayment;
taler_pay_uri: string; taler_pay_uri: string;

View File

@ -25,6 +25,7 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { anastasisData } from "./anastasis-data.js"; import { anastasisData } from "./anastasis-data.js";
import { import {
codecForChallengeInstructionMessage,
EscrowConfigurationResponse, EscrowConfigurationResponse,
RecoveryMetaResponse, RecoveryMetaResponse,
TruthUploadRequest, TruthUploadRequest,
@ -363,9 +364,10 @@ async function getTruthValue(
case "email": case "email":
case "totp": case "totp":
case "iban": case "iban":
case "post":
return authMethod.challenge; return authMethod.challenge;
default: default:
throw Error("unknown auth type"); throw Error(`unknown auth type '${authMethod.type}'`);
} }
} }
@ -947,17 +949,27 @@ async function requestTruth(
const hresp = await getResponseHash(truth, solveRequest); const hresp = await getResponseHash(truth, solveRequest);
const resp = await fetch(url.href, { let resp: Response;
method: "POST",
headers: { try {
Accept: "application/json", resp = await fetch(url.href, {
"Content-Type": "application/json", method: "POST",
}, headers: {
body: JSON.stringify({ Accept: "application/json",
truth_decryption_key: truth.truth_key, "Content-Type": "application/json",
h_response: hresp, },
}), body: JSON.stringify({
}); truth_decryption_key: truth.truth_key,
h_response: hresp,
}),
});
} catch (e) {
return {
reducer_type: "error",
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
hint: "network error",
} as ReducerStateError;
}
logger.info( logger.info(
`got POST /truth/.../solve response from ${truth.url}, http status ${resp.status}`, `got POST /truth/.../solve response from ${truth.url}, http status ${resp.status}`,
@ -1007,6 +1019,19 @@ async function requestTruth(
return tryRecoverSecret(newState); return tryRecoverSecret(newState);
} }
if (resp.status === HttpStatusCode.Forbidden) {
const challengeFeedback: { [x: string]: ChallengeFeedback } = {
...state.challenge_feedback,
[truth.uuid]: {
state: ChallengeFeedbackStatus.IncorrectAnswer,
},
};
return {
...state,
challenge_feedback: challengeFeedback,
};
}
return { return {
reducer_type: "error", reducer_type: "error",
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
@ -1072,6 +1097,9 @@ async function selectChallenge(
const url = new URL(`/truth/${truth.uuid}/challenge`, truth.url); const url = new URL(`/truth/${truth.uuid}/challenge`, truth.url);
const newFeedback = { ...state.challenge_feedback };
delete newFeedback[truth.uuid];
switch (truth.escrow_type) { switch (truth.escrow_type) {
case ChallengeType.Question: case ChallengeType.Question:
case ChallengeType.Totp: { case ChallengeType.Totp: {
@ -1079,51 +1107,93 @@ async function selectChallenge(
...state, ...state,
recovery_state: RecoveryStates.ChallengeSolving, recovery_state: RecoveryStates.ChallengeSolving,
selected_challenge_uuid: truth.uuid, selected_challenge_uuid: truth.uuid,
challenge_feedback: { challenge_feedback: newFeedback,
...state.challenge_feedback,
[truth.uuid]: {
state: ChallengeFeedbackStatus.Pending,
},
},
}; };
} }
} }
const resp = await fetch(url.href, { let resp: Response;
method: "POST",
headers: { try {
Accept: "application/json", resp = await fetch(url.href, {
"Content-Type": "application/json", method: "POST",
}, headers: {
body: JSON.stringify({ Accept: "application/json",
truth_decryption_key: truth.truth_key, "Content-Type": "application/json",
}), },
}); body: JSON.stringify({
truth_decryption_key: truth.truth_key,
}),
});
} catch (e) {
const feedback: ChallengeFeedback = {
state: ChallengeFeedbackStatus.ServerFailure,
http_status: 0,
};
return {
...state,
recovery_state: RecoveryStates.ChallengeSelecting,
selected_challenge_uuid: truth.uuid,
challenge_feedback: {
...state.challenge_feedback,
[truth.uuid]: feedback,
},
};
}
logger.info( logger.info(
`got GET /truth/.../challenge response from ${truth.url}, http status ${resp.status}`, `got GET /truth/.../challenge response from ${truth.url}, http status ${resp.status}`,
); );
if (resp.status === HttpStatusCode.Ok) { if (resp.status === HttpStatusCode.Ok) {
const respBodyJson = await resp.json();
const instr = codecForChallengeInstructionMessage().decode(respBodyJson);
let feedback: ChallengeFeedback;
switch (instr.method) {
case "FILE_WRITTEN": {
feedback = {
state: ChallengeFeedbackStatus.CodeInFile,
display_hint: "TAN code is in file (for debugging)",
filename: instr.filename,
};
break;
}
case "IBAN_WIRE": {
feedback = {
state: ChallengeFeedbackStatus.AuthIban,
answer_code: instr.answer_code,
target_business_name: instr.business_name,
challenge_amount: instr.amount,
target_iban: instr.credit_iban,
wire_transfer_subject: instr.wire_transfer_subject,
};
break;
}
case "TAN_SENT": {
feedback = {
state: ChallengeFeedbackStatus.CodeSent,
address_hint: instr.tan_address_hint,
display_hint: "Code sent to address",
};
}
}
return { return {
...state, ...state,
recovery_state: RecoveryStates.ChallengeSolving, recovery_state: RecoveryStates.ChallengeSolving,
selected_challenge_uuid: truth.uuid, selected_challenge_uuid: truth.uuid,
challenge_feedback: { challenge_feedback: {
...state.challenge_feedback, ...state.challenge_feedback,
[truth.uuid]: { [truth.uuid]: feedback,
state: ChallengeFeedbackStatus.Pending,
},
}, },
}; };
} }
// FIXME: look at response, include in challenge_feedback! // FIXME: look at more error codes in response
return { return {
reducer_type: "error", reducer_type: "error",
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED, code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
hint: "got unexpected /truth/.../challenge response status", hint: `got unexpected /truth/.../challenge response status (${resp.status})`,
http_status: resp.status, http_status: resp.status,
} as ReducerStateError; } as ReducerStateError;
} }
@ -1727,8 +1797,9 @@ export async function reduceAction(
} }
try { try {
return await h.handler(state, parsedArgs); return await h.handler(state, parsedArgs);
} catch (e) { } catch (e: any) {
logger.error("action handler failed"); logger.error("action handler failed");
logger.error(`${e?.stack ?? e}`);
if (e instanceof ReducerError) { if (e instanceof ReducerError) {
return { return {
reducer_type: "error", reducer_type: "error",

View File

@ -16,6 +16,13 @@
import { import {
AmountString, AmountString,
buildCodecForObject,
buildCodecForUnion,
Codec,
codecForAmountString,
codecForConstString,
codecForNumber,
codecForString,
TalerProtocolTimestamp, TalerProtocolTimestamp,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -122,3 +129,86 @@ export interface RecoveryMetaDataItem {
// document was uploaded. // document was uploaded.
upload_time: TalerProtocolTimestamp; upload_time: TalerProtocolTimestamp;
} }
export type ChallengeInstructionMessage =
| FileChallengeInstructionMessage
| IbanChallengeInstructionMessage
| PinChallengeInstructionMessage;
export interface IbanChallengeInstructionMessage {
// What kind of challenge is this?
method: "IBAN_WIRE";
// How much should be wired?
amount: AmountString;
// What is the target IBAN?
credit_iban: string;
// What is the receiver name?
business_name: string;
// What is the expected wire transfer subject?
wire_transfer_subject: string;
// What is the numeric code (also part of the
// wire transfer subject) to be hashed when
// solving the challenge?
answer_code: number;
// Hint about the origin account that must be used.
debit_account_hint: string;
}
export interface PinChallengeInstructionMessage {
// What kind of challenge is this?
method: "TAN_SENT";
// Where was the PIN code sent? Note that this
// address will most likely have been obscured
// to improve privacy.
tan_address_hint: string;
}
export interface FileChallengeInstructionMessage {
// What kind of challenge is this?
method: "FILE_WRITTEN";
// Name of the file where the PIN code was written.
filename: string;
}
export const codecForFileChallengeInstructionMessage =
(): Codec<FileChallengeInstructionMessage> =>
buildCodecForObject<FileChallengeInstructionMessage>()
.property("method", codecForConstString("FILE_WRITTEN"))
.property("filename", codecForString())
.build("FileChallengeInstructionMessage");
export const codecForPinChallengeInstructionMessage =
(): Codec<PinChallengeInstructionMessage> =>
buildCodecForObject<PinChallengeInstructionMessage>()
.property("method", codecForConstString("TAN_SENT"))
.property("tan_address_hint", codecForString())
.build("PinChallengeInstructionMessage");
export const codecForIbanChallengeInstructionMessage =
(): Codec<IbanChallengeInstructionMessage> =>
buildCodecForObject<IbanChallengeInstructionMessage>()
.property("method", codecForConstString("IBAN_WIRE"))
.property("amount", codecForAmountString())
.property("business_name", codecForString())
.property("credit_iban", codecForString())
.property("wire_transfer_subject", codecForString())
.property("answer_code", codecForNumber())
.property("debit_account_hint", codecForString())
.build("IbanChallengeInstructionMessage");
export const codecForChallengeInstructionMessage =
(): Codec<ChallengeInstructionMessage> =>
buildCodecForUnion<ChallengeInstructionMessage>()
.discriminateOn("method")
.alternative("FILE_WRITTEN", codecForFileChallengeInstructionMessage())
.alternative("IBAN_WIRE", codecForIbanChallengeInstructionMessage())
.alternative("TAN_SENT", codecForPinChallengeInstructionMessage())
.build("ChallengeInstructionMessage");

View File

@ -220,8 +220,6 @@ export interface ReducerStateRecovery {
/** /**
* Explicitly selected version by the user. * Explicitly selected version by the user.
* FIXME: In the C reducer this is called "version".
* FIXME: rename to selected_secret / selected_policy?
*/ */
selected_version?: AggregatedPolicyMetaInfo; selected_version?: AggregatedPolicyMetaInfo;

View File

@ -15,7 +15,6 @@
"pretty": "prettier --write src", "pretty": "prettier --write src",
"storybook": "start-storybook -p 6006" "storybook": "start-storybook -p 6006"
}, },
"type": "module",
"eslintConfig": { "eslintConfig": {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"extends": [ "extends": [
@ -62,4 +61,4 @@
"sirv-cli": "^1.0.14", "sirv-cli": "^1.0.14",
"typescript": "^4.5.4" "typescript": "^4.5.4"
} }
} }

View File

@ -1,3 +1,22 @@
/*
This file is part of GNU Anastasis
(C) 2021-2022 Anastasis SARL
GNU Anastasis is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with
GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import { TalerErrorCode } from "@gnu-taler/taler-util"; import { TalerErrorCode } from "@gnu-taler/taler-util";
import { import {
AggregatedPolicyMetaInfo, AggregatedPolicyMetaInfo,
@ -7,7 +26,6 @@ import {
getBackupStartState, getBackupStartState,
getRecoveryStartState, getRecoveryStartState,
mergeDiscoveryAggregate, mergeDiscoveryAggregate,
PolicyMetaInfo,
RecoveryStates, RecoveryStates,
reduceAction, reduceAction,
ReducerState, ReducerState,

View File

@ -44,14 +44,6 @@ export const NewProviderWithoutProviderList = createExample(TestedComponent, {
authentication_providers: {}, authentication_providers: {},
} as ReducerState); } as ReducerState);
export const NewVideoProvider = createExample(
TestedComponent,
{
...reducerStatesExample.authEditing,
} as ReducerState,
{ providerType: "video" },
);
export const NewSmsProvider = createExample( export const NewSmsProvider = createExample(
TestedComponent, TestedComponent,
{ {

View File

@ -249,19 +249,15 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
}, },
challenge_feedback: { challenge_feedback: {
"uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() }, "uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() },
"uuid-2": {
state: ChallengeFeedbackStatus.Message.toString(),
message: "Challenge should be solved",
},
"uuid-3": { "uuid-3": {
state: ChallengeFeedbackStatus.AuthIban.toString(), state: ChallengeFeedbackStatus.AuthIban.toString(),
challenge_amount: "EUR:1", challenge_amount: "EUR:1",
credit_iban: "DE12345789000", target_iban: "DE12345789000",
business_name: "Data Loss Incorporated", target_business_name: "Data Loss Incorporated",
wire_transfer_subject: "Anastasis 987654321", wire_transfer_subject: "Anastasis 987654321",
}, },
"uuid-4": { "uuid-4": {
state: ChallengeFeedbackStatus.Payment.toString(), state: ChallengeFeedbackStatus.TalerPayment.toString(),
taler_pay_uri: "taler://pay/...", taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/", provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG", payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
@ -270,11 +266,6 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
state: ChallengeFeedbackStatus.RateLimitExceeded.toString(), state: ChallengeFeedbackStatus.RateLimitExceeded.toString(),
// "error_code": 8121 // "error_code": 8121
}, },
"uuid-6": {
state: ChallengeFeedbackStatus.Redirect.toString(),
redirect_url: "https://videoconf.example.com/",
http_status: 303,
},
"uuid-7": { "uuid-7": {
state: ChallengeFeedbackStatus.ServerFailure.toString(), state: ChallengeFeedbackStatus.ServerFailure.toString(),
http_status: 500, http_status: 500,

View File

@ -14,11 +14,8 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
return null; return null;
} }
switch (feedback.state) { switch (feedback.state) {
case ChallengeFeedbackStatus.Message:
return <div class="block has-text-danger">{feedback.message}</div>;
case ChallengeFeedbackStatus.Solved: case ChallengeFeedbackStatus.Solved:
return <div />; return <div />;
case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.AuthIban: case ChallengeFeedbackStatus.AuthIban:
return null; return null;
case ChallengeFeedbackStatus.ServerFailure: case ChallengeFeedbackStatus.ServerFailure:
@ -43,7 +40,6 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
provider for further information. provider for further information.
</div> </div>
); );
case ChallengeFeedbackStatus.Redirect:
default: default:
return <div />; return <div />;
} }
@ -178,7 +174,7 @@ export function ChallengeOverviewScreen(): VNode {
case ChallengeFeedbackStatus.RateLimitExceeded: case ChallengeFeedbackStatus.RateLimitExceeded:
return <div />; return <div />;
case ChallengeFeedbackStatus.AuthIban: case ChallengeFeedbackStatus.AuthIban:
case ChallengeFeedbackStatus.Payment: case ChallengeFeedbackStatus.TalerPayment:
return ( return (
<div> <div>
<AsyncButton <AsyncButton
@ -192,20 +188,6 @@ export function ChallengeOverviewScreen(): VNode {
</AsyncButton> </AsyncButton>
</div> </div>
); );
case ChallengeFeedbackStatus.Redirect:
return (
<div>
<AsyncButton
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Go to {feedback.redirect_url}
</AsyncButton>
</div>
);
case ChallengeFeedbackStatus.Solved: case ChallengeFeedbackStatus.Solved:
return ( return (
<div> <div>

View File

@ -16,19 +16,7 @@ export function SolveOverviewFeedbackDisplay(props: {
return <div />; return <div />;
} }
switch (feedback.state) { switch (feedback.state) {
case ChallengeFeedbackStatus.Message: case ChallengeFeedbackStatus.TalerPayment:
return (
<Notifications
notifications={[
{
type: "INFO",
message: `Message from provider`,
description: feedback.message,
},
]}
/>
);
case ChallengeFeedbackStatus.Payment:
return ( return (
<Notifications <Notifications
notifications={[ notifications={[
@ -51,7 +39,7 @@ export function SolveOverviewFeedbackDisplay(props: {
{ {
type: "INFO", type: "INFO",
message: `Message from provider`, message: `Message from provider`,
description: `Need to send a wire transfer to "${feedback.business_name}"`, description: `Need to send a wire transfer to "${feedback.target_business_name}"`,
}, },
]} ]}
/> />
@ -80,22 +68,6 @@ export function SolveOverviewFeedbackDisplay(props: {
]} ]}
/> />
); );
case ChallengeFeedbackStatus.Redirect:
return (
<Notifications
notifications={[
{
type: "INFO",
message: `Message from provider`,
description: (
<span>
Please visit this link: <a>{feedback.redirect_url}</a>
</span>
),
},
]}
/>
);
case ChallengeFeedbackStatus.Unsupported: case ChallengeFeedbackStatus.Unsupported:
return ( return (
<Notifications <Notifications
@ -121,6 +93,9 @@ export function SolveOverviewFeedbackDisplay(props: {
/> />
); );
default: default:
console.warn(
`unknown challenge feedback status ${JSON.stringify(feedback)}`,
);
return <div />; return <div />;
} }
} }

View File

@ -80,7 +80,7 @@ export const PaymentFeedback = createExample(
selected_challenge_uuid: "uuid-1", selected_challenge_uuid: "uuid-1",
challenge_feedback: { challenge_feedback: {
"uuid-1": { "uuid-1": {
state: ChallengeFeedbackStatus.Payment, state: ChallengeFeedbackStatus.TalerPayment,
taler_pay_uri: "taler://pay/...", taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/", provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG", payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",

View File

@ -9,6 +9,7 @@ import { TextInput } from "../../../components/fields/TextInput";
import { useAnastasisContext } from "../../../context/anastasis"; import { useAnastasisContext } from "../../../context/anastasis";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
import { shouldHideConfirm } from "./helpers";
import { AuthMethodSolveProps } from "./index"; import { AuthMethodSolveProps } from "./index";
export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode { export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
@ -103,12 +104,6 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Email challenge"> <AnastasisClientFrame hideNav title="Email challenge">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
@ -160,7 +155,7 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && ( {!shouldHideConfirm(feedback) && (
<AsyncButton class="button is-info" onClick={onNext}> <AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton> </AsyncButton>

View File

@ -5,10 +5,10 @@ import {
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { AsyncButton } from "../../../components/AsyncButton"; import { AsyncButton } from "../../../components/AsyncButton";
import { TextInput } from "../../../components/fields/TextInput";
import { useAnastasisContext } from "../../../context/anastasis"; import { useAnastasisContext } from "../../../context/anastasis";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
import { shouldHideConfirm } from "./helpers";
import { AuthMethodSolveProps } from "./index"; import { AuthMethodSolveProps } from "./index";
export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode { export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
@ -79,12 +79,6 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="IBAN Challenge"> <AnastasisClientFrame hideNav title="IBAN Challenge">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
@ -101,7 +95,7 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && ( {!shouldHideConfirm(feedback) && (
<AsyncButton class="button is-info" onClick={onNext}> <AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton> </AsyncButton>

View File

@ -9,6 +9,7 @@ import { TextInput } from "../../../components/fields/TextInput";
import { useAnastasisContext } from "../../../context/anastasis"; import { useAnastasisContext } from "../../../context/anastasis";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
import { shouldHideConfirm } from "./helpers";
import { AuthMethodSolveProps } from "./index"; import { AuthMethodSolveProps } from "./index";
export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode { export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
@ -102,12 +103,6 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Postal Challenge"> <AnastasisClientFrame hideNav title="Postal Challenge">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
@ -130,7 +125,7 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && ( {!shouldHideConfirm(feedback) && (
<AsyncButton class="button is-info" onClick={onNext}> <AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton> </AsyncButton>

View File

@ -20,6 +20,7 @@
*/ */
import { import {
ChallengeFeedbackBankTransferRequired,
ChallengeFeedbackStatus, ChallengeFeedbackStatus,
ReducerState, ReducerState,
} from "@gnu-taler/anastasis-core"; } from "@gnu-taler/anastasis-core";
@ -62,28 +63,6 @@ export const WithoutFeedback = createExample(
}, },
); );
export const MessageFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Message,
message: "Challenge should be solved",
},
},
} as ReducerState);
export const ServerFailureFeedback = createExample( export const ServerFailureFeedback = createExample(
TestedComponent[type].solve, TestedComponent[type].solve,
{ {
@ -92,7 +71,7 @@ export const ServerFailureFeedback = createExample(
challenges: [ challenges: [
{ {
cost: "USD:1", cost: "USD:1",
instructions: "does P equals NP?", instructions: "does P equal NP?",
type: "question", type: "question",
uuid: "ASDASDSAD!1", uuid: "ASDASDSAD!1",
}, },
@ -110,29 +89,6 @@ export const ServerFailureFeedback = createExample(
} as ReducerState, } as ReducerState,
); );
export const RedirectFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Redirect,
http_status: 302,
redirect_url: "http://video.taler.net",
},
},
} as ReducerState);
export const MessageRateLimitExceededFeedback = createExample( export const MessageRateLimitExceededFeedback = createExample(
TestedComponent[type].solve, TestedComponent[type].solve,
{ {
@ -201,6 +157,15 @@ export const TruthUnknownFeedback = createExample(TestedComponent[type].solve, {
}, },
} as ReducerState); } as ReducerState);
const ibanFeedback: ChallengeFeedbackBankTransferRequired = {
state: ChallengeFeedbackStatus.AuthIban,
challenge_amount: "EUR:1",
target_iban: "DE12345789000",
target_business_name: "Data Loss Incorporated",
wire_transfer_subject: "Anastasis 987654321",
answer_code: 987654321,
};
export const AuthIbanFeedback = createExample(TestedComponent[type].solve, { export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving, ...reducerStatesExample.challengeSolving,
recovery_information: { recovery_information: {
@ -216,23 +181,7 @@ export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
}, },
selected_challenge_uuid: "ASDASDSAD!1", selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
"ASDASDSAD!1": { "ASDASDSAD!1": ibanFeedback,
state: ChallengeFeedbackStatus.AuthIban,
challenge_amount: "EUR:1",
credit_iban: "DE12345789000",
business_name: "Data Loss Incorporated",
wire_transfer_subject: "Anastasis 987654321",
answer_code: 987654321,
// Fields that follow are only for compatibility with C reducer,
// will be removed eventually,
details: {
business_name: "foo",
challenge_amount: "foo",
credit_iban: "foo",
wire_transfer_subject: "foo",
},
method: "iban",
},
}, },
} as ReducerState); } as ReducerState);
@ -252,7 +201,7 @@ export const PaymentFeedback = createExample(TestedComponent[type].solve, {
selected_challenge_uuid: "ASDASDSAD!1", selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: { challenge_feedback: {
"ASDASDSAD!1": { "ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Payment, state: ChallengeFeedbackStatus.TalerPayment,
taler_pay_uri: "taler://pay/...", taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/", provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG", payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",

View File

@ -9,6 +9,7 @@ import { TextInput } from "../../../components/fields/TextInput";
import { useAnastasisContext } from "../../../context/anastasis"; import { useAnastasisContext } from "../../../context/anastasis";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
import { shouldHideConfirm } from "./helpers";
import { AuthMethodSolveProps } from "./index"; import { AuthMethodSolveProps } from "./index";
export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode { export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
@ -79,12 +80,6 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="Question challenge"> <AnastasisClientFrame hideNav title="Question challenge">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
@ -110,7 +105,7 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && ( {!shouldHideConfirm(feedback) && (
<AsyncButton class="button is-info" onClick={onNext}> <AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton> </AsyncButton>

View File

@ -9,6 +9,7 @@ import { TextInput } from "../../../components/fields/TextInput";
import { useAnastasisContext } from "../../../context/anastasis"; import { useAnastasisContext } from "../../../context/anastasis";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
import { shouldHideConfirm } from "./helpers";
import { AuthMethodSolveProps } from "./index"; import { AuthMethodSolveProps } from "./index";
export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode { export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
@ -103,12 +104,6 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="SMS Challenge"> <AnastasisClientFrame hideNav title="SMS Challenge">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
@ -160,7 +155,7 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && ( {!shouldHideConfirm(feedback) && (
<AsyncButton class="button is-info" onClick={onNext}> <AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton> </AsyncButton>

View File

@ -9,6 +9,7 @@ import { TextInput } from "../../../components/fields/TextInput";
import { useAnastasisContext } from "../../../context/anastasis"; import { useAnastasisContext } from "../../../context/anastasis";
import { AnastasisClientFrame } from "../index"; import { AnastasisClientFrame } from "../index";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen"; import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
import { shouldHideConfirm } from "./helpers";
import { AuthMethodSolveProps } from "./index"; import { AuthMethodSolveProps } from "./index";
export function AuthMethodTotpSolve(props: AuthMethodSolveProps): VNode { export function AuthMethodTotpSolve(props: AuthMethodSolveProps): VNode {
@ -81,12 +82,6 @@ export function AuthMethodTotpSolve(props: AuthMethodSolveProps): VNode {
reducer?.back(); reducer?.back();
} }
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return ( return (
<AnastasisClientFrame hideNav title="TOTP Challenge"> <AnastasisClientFrame hideNav title="TOTP Challenge">
<SolveOverviewFeedbackDisplay feedback={feedback} /> <SolveOverviewFeedbackDisplay feedback={feedback} />
@ -108,7 +103,7 @@ export function AuthMethodTotpSolve(props: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}> <button class="button" onClick={onCancel}>
Cancel Cancel
</button> </button>
{!shouldHideConfirm && ( {!shouldHideConfirm(feedback) && (
<AsyncButton class="button is-info" onClick={onNext}> <AsyncButton class="button is-info" onClick={onNext}>
Confirm Confirm
</AsyncButton> </AsyncButton>

View File

@ -1,83 +0,0 @@
/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
import logoImage from "../../../assets/logo.jpeg";
export default {
title: "Pages/backup/AuthorizationMethod/AuthMethods/Video",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = "video";
export const Empty = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: logoImage,
remove: () => null,
},
],
},
);
export const WithMoreExamples = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: logoImage,
remove: () => null,
},
{
challenge: "qwe",
type,
instructions: logoImage,
remove: () => null,
},
],
},
);

View File

@ -1,92 +0,0 @@
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ImageInput } from "../../../components/fields/ImageInput";
import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
export function AuthMethodVideoSetup({
cancel,
addAuthMethod,
configured,
}: AuthMethodSetupProps): VNode {
const [image, setImage] = useState("");
const addVideoAuth = (): void => {
addAuthMethod({
authentication_method: {
type: "video",
instructions: "Join a video call",
challenge: encodeCrock(stringToBytes(image)),
},
});
};
function goNextIfNoErrors(): void {
addVideoAuth();
}
return (
<AnastasisClientFrame hideNav title="Add video authentication">
<p>
For video identification, you need to provide a passport-style
photograph. When recovering your secret, you will be asked to join a
video call. During that call, a human will use the photograph to verify
your identity.
</p>
<div style={{ textAlign: "center" }}>
<ImageInput
label="Choose photograph"
grabFocus
onConfirm={goNextIfNoErrors}
bind={[image, setImage]}
/>
</div>
{configured.length > 0 && (
<section class="section">
<div class="block">Your photographs:</div>
<div class="block">
{configured.map((c, i) => {
return (
<div
key={i}
class="box"
style={{ display: "flex", justifyContent: "space-between" }}
>
<img
style={{
marginTop: "auto",
marginBottom: "auto",
width: 100,
height: 100,
border: "solid 1px black",
}}
src={c.instructions}
/>
<div style={{ marginTop: "auto", marginBottom: "auto" }}>
<button class="button is-danger" onClick={c.remove}>
Delete
</button>
</div>
</div>
);
})}
</div>
</section>
)}
<div>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={cancel}>
Cancel
</button>
<button class="button is-info" onClick={addVideoAuth}>
Add
</button>
</div>
</div>
</AnastasisClientFrame>
);
}

View File

@ -1,63 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import {
ChallengeFeedbackStatus,
ReducerState,
} from "@gnu-taler/anastasis-core";
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: "Pages/recovery/SolveChallenge/AuthMethods/video",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = "video";
export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
{
id: "uuid-1",
},
);

View File

@ -1,114 +0,0 @@
import {
ChallengeFeedbackStatus,
ChallengeInfo,
} from "@gnu-taler/anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../components/AsyncButton";
import { TextInput } from "../../../components/fields/TextInput";
import { useAnastasisContext } from "../../../context/anastasis";
import { AnastasisClientFrame } from "../index";
import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
import { AuthMethodSolveProps } from "./index";
export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
const [answer, setAnswer] = useState("");
const reducer = useAnastasisContext();
if (!reducer) {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>no reducer in context</div>
</AnastasisClientFrame>
);
}
if (
reducer.currentReducerState?.reducer_type !== "recovery"
) {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
</AnastasisClientFrame>
);
}
if (!reducer.currentReducerState.recovery_information) {
return (
<AnastasisClientFrame
hideNext="Recovery document not found"
title="Recovery problem"
>
<div>no recovery information found</div>
</AnastasisClientFrame>
);
}
if (!reducer.currentReducerState.selected_challenge_uuid) {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
}
const chArr = reducer.currentReducerState.recovery_information.challenges;
const challengeFeedback =
reducer.currentReducerState.challenge_feedback ?? {};
const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
const challenges: {
[uuid: string]: ChallengeInfo;
} = {};
for (const ch of chArr) {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer });
}
function onCancel(): void {
reducer?.back();
}
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} />
<p>You are gonna be called to check your identity</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={onCancel}>
Cancel
</button>
{!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>
)}
</div>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,12 @@
import {
ChallengeFeedback,
ChallengeFeedbackStatus,
} from "@gnu-taler/anastasis-core";
export function shouldHideConfirm(feedback: ChallengeFeedback): boolean {
return (
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown
);
}

View File

@ -3,22 +3,18 @@ import { h, VNode } from "preact";
import postalIcon from "../../../assets/icons/auth_method/postal.svg"; import postalIcon from "../../../assets/icons/auth_method/postal.svg";
import questionIcon from "../../../assets/icons/auth_method/question.svg"; import questionIcon from "../../../assets/icons/auth_method/question.svg";
import smsIcon from "../../../assets/icons/auth_method/sms.svg"; import smsIcon from "../../../assets/icons/auth_method/sms.svg";
import videoIcon from "../../../assets/icons/auth_method/video.svg";
import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup"; import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup";
import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve"; import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve";
import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup"; import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup";
import { AuthMethodPostSetup as PostalSetup } from "./AuthMethodPostSetup";
import { AuthMethodQuestionSetup as QuestionSetup } from "./AuthMethodQuestionSetup";
import { AuthMethodSmsSetup as SmsSetup } from "./AuthMethodSmsSetup";
import { AuthMethodTotpSetup as TotpSetup } from "./AuthMethodTotpSetup";
import { AuthMethodVideoSetup as VideoSetup } from "./AuthMethodVideoSetup";
import { AuthMethodIbanSolve as IbanSolve } from "./AuthMethodIbanSolve"; import { AuthMethodIbanSolve as IbanSolve } from "./AuthMethodIbanSolve";
import { AuthMethodPostSetup as PostalSetup } from "./AuthMethodPostSetup";
import { AuthMethodPostSolve as PostalSolve } from "./AuthMethodPostSolve"; import { AuthMethodPostSolve as PostalSolve } from "./AuthMethodPostSolve";
import { AuthMethodQuestionSetup as QuestionSetup } from "./AuthMethodQuestionSetup";
import { AuthMethodQuestionSolve as QuestionSolve } from "./AuthMethodQuestionSolve"; import { AuthMethodQuestionSolve as QuestionSolve } from "./AuthMethodQuestionSolve";
import { AuthMethodSmsSetup as SmsSetup } from "./AuthMethodSmsSetup";
import { AuthMethodSmsSolve as SmsSolve } from "./AuthMethodSmsSolve"; import { AuthMethodSmsSolve as SmsSolve } from "./AuthMethodSmsSolve";
import { AuthMethodTotpSetup as TotpSetup } from "./AuthMethodTotpSetup";
import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve"; import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve";
import { AuthMethodVideoSolve as VideoSolve } from "./AuthMethodVideoSolve";
export type AuthMethodWithRemove = AuthMethod & { remove: () => void }; export type AuthMethodWithRemove = AuthMethod & { remove: () => void };
@ -40,14 +36,12 @@ interface AuthMethodConfiguration {
solve: (props: AuthMethodSolveProps) => VNode; solve: (props: AuthMethodSolveProps) => VNode;
skip?: boolean; skip?: boolean;
} }
// export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
const ALL_METHODS = [ const ALL_METHODS = [
"sms", "sms",
"email", "email",
"post", "post",
"question", "question",
"video",
"totp", "totp",
"iban", "iban",
] as const; ] as const;
@ -97,11 +91,4 @@ export const authMethods: KnowMethodConfig = {
setup: TotpSetup, setup: TotpSetup,
solve: TotpSolve, solve: TotpSolve,
}, },
video: {
icon: <img src={videoIcon} />,
label: "Video",
setup: VideoSetup,
solve: VideoSolve,
skip: true,
},
}; };

View File

@ -14,10 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { generateFakeSegwitAddress } from "./index.js"; import { generateFakeSegwitAddress } from "./bitcoin.js";
import { URLSearchParams } from "./url.js"; import { URLSearchParams } from "./url.js";
export type PaytoUri = PaytoUriUnknown | PaytoUriIBAN | PaytoUriTalerBank | PaytoUriBitcoin; export type PaytoUri =
| PaytoUriUnknown
| PaytoUriIBAN
| PaytoUriTalerBank
| PaytoUriBitcoin;
interface PaytoUriGeneric { interface PaytoUriGeneric {
targetType: string; targetType: string;
@ -31,38 +35,41 @@ interface PaytoUriUnknown extends PaytoUriGeneric {
interface PaytoUriIBAN extends PaytoUriGeneric { interface PaytoUriIBAN extends PaytoUriGeneric {
isKnown: true; isKnown: true;
targetType: 'iban', targetType: "iban";
iban: string; iban: string;
} }
interface PaytoUriTalerBank extends PaytoUriGeneric { interface PaytoUriTalerBank extends PaytoUriGeneric {
isKnown: true; isKnown: true;
targetType: 'x-taler-bank', targetType: "x-taler-bank";
host: string; host: string;
account: string; account: string;
} }
interface PaytoUriBitcoin extends PaytoUriGeneric { interface PaytoUriBitcoin extends PaytoUriGeneric {
isKnown: true; isKnown: true;
targetType: 'bitcoin', targetType: "bitcoin";
generateSegwitAddress: (r: string) => { addr1: string, addr2: string }; generateSegwitAddress: (r: string) => { addr1: string; addr2: string };
addr1?: string, addr2?: string, addr1?: string;
addr2?: string;
} }
const paytoPfx = "payto://"; const paytoPfx = "payto://";
function buildSegwitGenerator(result: PaytoUriBitcoin, targetPath: string) { function buildSegwitGenerator(result: PaytoUriBitcoin, targetPath: string) {
//generate segwit address just once, save addr in payto object //generate segwit address just once, save addr in payto object
//and use it as cache //and use it as cache
return function generateSegwitAddress(reserve: string): { addr1: string, addr2: string } { return function generateSegwitAddress(reserve: string): {
if (result.addr1 && result.addr2) return { addr1: result.addr1, addr2: result.addr2 }; addr1: string;
const { addr1, addr2 } = generateFakeSegwitAddress(reserve, targetPath) addr2: string;
result.addr1 = addr1 } {
result.addr2 = addr2 if (result.addr1 && result.addr2)
return { addr1, addr2 } return { addr1: result.addr1, addr2: result.addr2 };
} const { addr1, addr2 } = generateFakeSegwitAddress(reserve, targetPath);
result.addr1 = addr1;
result.addr2 = addr2;
return { addr1, addr2 };
};
} }
/** /**
* Add query parameters to a payto URI * Add query parameters to a payto URI
@ -81,27 +88,27 @@ export function addPaytoQueryParams(
/** /**
* Serialize a PaytoURI into a valid payto:// string * Serialize a PaytoURI into a valid payto:// string
* *
* @param p * @param p
* @returns * @returns
*/ */
export function stringifyPaytoUri(p: PaytoUri): string { export function stringifyPaytoUri(p: PaytoUri): string {
const url = `${paytoPfx}${p.targetType}//${p.targetPath}` const url = `${paytoPfx}${p.targetType}//${p.targetPath}`;
if (p.params) { if (p.params) {
const search = Object.entries(p.params) const search = Object.entries(p.params)
.map(([key, value]) => `${key}=${value}`) .map(([key, value]) => `${key}=${value}`)
.join("&"); .join("&");
return `${url}?${search}` return `${url}?${search}`;
} }
return url return url;
} }
/** /**
* Parse a valid payto:// uri into a PaytoUri object * Parse a valid payto:// uri into a PaytoUri object
* RFC 8905 * RFC 8905
* *
* @param s * @param s
* @returns * @returns
*/ */
export function parsePaytoUri(s: string): PaytoUri | undefined { export function parsePaytoUri(s: string): PaytoUri | undefined {
if (!s.startsWith(paytoPfx)) { if (!s.startsWith(paytoPfx)) {
@ -127,47 +134,44 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
params[v] = k; params[v] = k;
}); });
if (targetType === 'x-taler-bank') { if (targetType === "x-taler-bank") {
const parts = targetPath.split('/') const parts = targetPath.split("/");
const host = parts[0] const host = parts[0];
const account = parts[1] const account = parts[1];
return { return {
targetPath, targetPath,
targetType, targetType,
params, params,
isKnown: true, isKnown: true,
host, account, host,
account,
}; };
} }
if (targetType === 'iban') { if (targetType === "iban") {
return { return {
isKnown: true, isKnown: true,
targetPath, targetPath,
targetType, targetType,
params, params,
iban: targetPath iban: targetPath,
}; };
} }
if (targetType === 'bitcoin') { if (targetType === "bitcoin") {
const result: PaytoUriBitcoin = { const result: PaytoUriBitcoin = {
isKnown: true, isKnown: true,
targetPath, targetPath,
targetType, targetType,
params, params,
generateSegwitAddress: (): any => null generateSegwitAddress: (): any => null,
} };
result.generateSegwitAddress = buildSegwitGenerator(result, targetPath) result.generateSegwitAddress = buildSegwitGenerator(result, targetPath);
return result; return result;
} }
return { return {
targetPath, targetPath,
targetType, targetType,
params, params,
isKnown: false isKnown: false,
}; };
} }

View File

@ -65,6 +65,7 @@ importers:
'@types/enzyme': ^3.10.11 '@types/enzyme': ^3.10.11
'@typescript-eslint/eslint-plugin': ^5.3.0 '@typescript-eslint/eslint-plugin': ^5.3.0
'@typescript-eslint/parser': ^5.3.0 '@typescript-eslint/parser': ^5.3.0
babel-plugin-add-import-extension: ^1.6.0
base64-inline-loader: 1.1.1 base64-inline-loader: 1.1.1
bulma: ^0.9.3 bulma: ^0.9.3
bulma-checkbox: ^1.1.1 bulma-checkbox: ^1.1.1
@ -106,6 +107,7 @@ importers:
'@types/enzyme': 3.10.11 '@types/enzyme': 3.10.11
'@typescript-eslint/eslint-plugin': 5.11.0_de5a1ddccd75ca1e499b8b8491d3dcba '@typescript-eslint/eslint-plugin': 5.11.0_de5a1ddccd75ca1e499b8b8491d3dcba
'@typescript-eslint/parser': 5.11.0_eslint@8.8.0+typescript@4.5.5 '@typescript-eslint/parser': 5.11.0_eslint@8.8.0+typescript@4.5.5
babel-plugin-add-import-extension: 1.6.0_@babel+core@7.13.16
bulma: 0.9.3 bulma: 0.9.3
bulma-checkbox: 1.2.1 bulma-checkbox: 1.2.1
bulma-radio: 1.2.0 bulma-radio: 1.2.0
@ -7137,6 +7139,15 @@ packages:
schema-utils: 2.7.1 schema-utils: 2.7.1
dev: true dev: true
/babel-plugin-add-import-extension/1.6.0_@babel+core@7.13.16:
resolution: {integrity: sha512-JVSQPMzNzN/S4wPRoKQ7+u8PlkV//BPUMnfWVbr63zcE+6yHdU2Mblz10Vf7qe+6Rmu4svF5jG7JxdcPi9VvKg==}
peerDependencies:
'@babel/core': '>=7.0.0'
dependencies:
'@babel/core': 7.13.16
'@babel/helper-plugin-utils': 7.16.7
dev: true
/babel-plugin-apply-mdx-type-prop/1.6.22_@babel+core@7.12.9: /babel-plugin-apply-mdx-type-prop/1.6.22_@babel+core@7.12.9:
resolution: {integrity: sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==} resolution: {integrity: sha512-VefL+8o+F/DfK24lPZMtJctrCVOfgbqLAGZSkxwhazQv4VxPg3Za/i40fu22KR2m8eEda+IfSOlPLUSIiLcnCQ==}
peerDependencies: peerDependencies: