anastasis-webui: updated challenge feedback
This commit is contained in:
parent
bd76b5d76f
commit
4e1fe5eb10
@ -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;
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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");
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
{
|
{
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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 />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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",
|
|
||||||
},
|
|
||||||
);
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
@ -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,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user