diff options
Diffstat (limited to 'packages/challenger-ui/src/pages/AnswerChallenge.tsx')
-rw-r--r-- | packages/challenger-ui/src/pages/AnswerChallenge.tsx | 315 |
1 files changed, 216 insertions, 99 deletions
diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx index 5fe2d9743..13ae16a33 100644 --- a/packages/challenger-ui/src/pages/AnswerChallenge.tsx +++ b/packages/challenger-ui/src/pages/AnswerChallenge.tsx @@ -14,8 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - ChallengerApi, + AbsoluteTime, + EmptyObject, HttpStatusCode, + TalerError, assertUnreachable, } from "@gnu-taler/taler-util"; import { @@ -24,128 +26,269 @@ import { LocalNotificationBanner, RouteDefinition, ShowInputErrorLabel, + Time, useChallengerApiContext, useLocalNotificationHandler, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; +import { + revalidateChallengeSession, + useChallengeSession, +} from "../hooks/challenge.js"; import { useSessionState } from "../hooks/session.js"; -export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/; - type Props = { - nonce: string; focus?: boolean; onComplete: () => void; - routeAsk: RouteDefinition<{ nonce: string }>; + routeAsk: RouteDefinition<EmptyObject>; }; -export function AnswerChallenge({ - focus, - nonce, - onComplete, - routeAsk, -}: Props): VNode { - const { lib } = useChallengerApiContext(); +function useReloadOnDeadline(deadline: AbsoluteTime): void { + const [, set] = useState(false); + function toggle(): void { + set((s) => !s); + } + useEffect(() => { + if (AbsoluteTime.isExpired(deadline)) { + return; + } + const diff = AbsoluteTime.difference(AbsoluteTime.now(), deadline); + if (diff.d_ms === "forever") return; + const timer = setTimeout(toggle, diff.d_ms); + return () => { + clearTimeout(timer); + }; + }, [deadline]); +} + +export function AnswerChallenge({ focus, onComplete, routeAsk }: Props): VNode { + const { config, lib } = useChallengerApiContext(); const { i18n } = useTranslationContext(); - const { state, accepted, completed } = useSessionState(); + const { state, sent, failed, completed } = useSessionState(); const [notification, withErrorHandler] = useLocalNotificationHandler(); const [pin, setPin] = useState<string | undefined>(); - const [lastTryError, setLastTryError] = - useState<ChallengerApi.InvalidPinResponse>(); const errors = undefinedIfEmpty({ pin: !pin ? i18n.str`Can't be empty` : undefined, }); - const lastEmail = !state + const restrictionKeys = !config.restrictions + ? [] + : Object.keys(config.restrictions); + const restrictionKey = !restrictionKeys.length ? undefined - : !state.lastStatus + : restrictionKeys[0]; + + const result = useChallengeSession(state); + + const lastStatus = + result && !(result instanceof TalerError) && result.type !== "fail" + ? result.body + : undefined; + + const deadline = + lastStatus == undefined ? undefined - : !state.lastStatus.last_address - ? undefined - : state.lastStatus.last_address["email"]; + : AbsoluteTime.fromProtocolTimestamp(lastStatus.retransmission_time); + + useReloadOnDeadline(deadline ?? AbsoluteTime.never()); + + if (!restrictionKey) { + return ( + <div> + invalid server configuration, there is no restriction in /config + </div> + ); + } + + const lastAddr = !lastStatus?.last_address + ? undefined + : lastStatus.last_address[restrictionKey]; + + const unableToChangeAddr = !lastStatus || lastStatus.changes_left < 1; + const contact = lastAddr ? { [restrictionKey]: lastAddr } : undefined; const onSendAgain = - !state || lastEmail === undefined + !state?.nonce || + contact === undefined || + lastStatus == undefined || + lastStatus.pin_transmissions_left === 0 || + !deadline || + !AbsoluteTime.isExpired(deadline) ? undefined : withErrorHandler( async () => { - if (!lastEmail) return; - return await lib.challenger.challenge(nonce, { email: lastEmail }); + return await lib.challenger.challenge(state.nonce, contact); }, (ok) => { if (ok.body.type === "completed") { - completed(new URL(ok.body.redirect_url)); + completed(ok.body); } else { - accepted({ - attemptsLeft: ok.body.attempts_left, - nextSend: ok.body.next_tx_time, - transmitted: ok.body.transmitted, - }); + sent(ok.body); } - return undefined; }, (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: - return i18n.str``; - case HttpStatusCode.Forbidden: - return i18n.str``; + return i18n.str`The request was not accepted, try reloading the app.`; case HttpStatusCode.NotFound: - return i18n.str``; + return i18n.str`Challenge not found.`; case HttpStatusCode.NotAcceptable: - return i18n.str``; + return i18n.str`Server templates are missing due to misconfiguration.`; case HttpStatusCode.TooManyRequests: - return i18n.str``; + return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: - return i18n.str``; + return i18n.str`Server is not able to respond due to internal problems.`; } }, ); const onCheck = - errors !== undefined || (lastTryError && lastTryError.exhausted) + !state?.nonce || + errors !== undefined || + lastStatus == undefined || + lastStatus.auth_attempts_left === 0 ? undefined : withErrorHandler( async () => { - return lib.challenger.solve(nonce, { pin: pin! }); + return lib.challenger.solve(state.nonce, { pin: pin! }); }, (ok) => { if (ok.body.type === "completed") { - completed(new URL(ok.body.redirect_url)); + completed(ok.body); } else { - setLastTryError(ok.body); + failed(ok.body); } onComplete(); }, (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: - return i18n.str`Invalid request`; + return i18n.str`The request was not accepted, try reloading the app.`; case HttpStatusCode.Forbidden: { - return i18n.str`Too many attemps where made`; + revalidateChallengeSession(); + return i18n.str`Invalid pin.`; } case HttpStatusCode.NotFound: - return i18n.str``; + return i18n.str`Challenge not found.`; case HttpStatusCode.NotAcceptable: - return i18n.str``; - case HttpStatusCode.TooManyRequests: - return i18n.str``; + return i18n.str`Server templates are missing due to misconfiguration.`; + case HttpStatusCode.TooManyRequests: { + revalidateChallengeSession(); + return i18n.str`There have been too many attempts to request challenge transmissions.`; + } case HttpStatusCode.InternalServerError: - return i18n.str``; + return i18n.str`Server is not able to respond due to internal problems.`; default: assertUnreachable(fail); } }, ); + const cantTryAnymore = lastStatus?.auth_attempts_left === 0; + + function LastContactSent(): VNode { + return ( + <p class="mt-2 text-lg leading-8 text-gray-600"> + {!lastStatus || !deadline || AbsoluteTime.isExpired(deadline) ? ( + <i18n.Translate> + Last TAN code was sent to your address "{lastAddr} + " is not valid anymore. + </i18n.Translate> + ) : ( + <Attention + title={i18n.str`A TAN code was sent to your address "${lastAddr}"`} + > + <i18n.Translate> + You should wait until " + <Time format="dd/MM/yyyy HH:mm:ss" timestamp={deadline} /> + " to send a new one. + </i18n.Translate> + </Attention> + )} + </p> + ); + } - if (!state) { - return <div>no state</div>; + function TryAnotherCode(): VNode { + return ( + <div class="mx-auto mt-4 max-w-xl flex justify-between"> + <div> + <a + data-disabled={unableToChangeAddr} + href={unableToChangeAddr ? undefined : routeAsk.url({})} + class="relative data-[disabled=true]:bg-gray-300 data-[disabled=true]:text-white data-[disabled=true]:cursor-default inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" + > + <i18n.Translate>Try with another address</i18n.Translate> + </a> + {lastStatus === undefined ? undefined : ( + <p class="mt-2 text-sm leading-6 text-gray-400"> + {lastStatus.changes_left < 1 ? ( + <i18n.Translate> + You can't change the contact address anymore. + </i18n.Translate> + ) : lastStatus.changes_left === 1 ? ( + <i18n.Translate> + You can change the contact address one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can change the contact address {lastStatus.changes_left}{" "} + more times. + </i18n.Translate> + )} + </p> + )} + </div> + <div> + <Button + type="submit" + disabled={!onSendAgain} + class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + handler={onSendAgain} + > + <i18n.Translate>Send new code</i18n.Translate> + </Button> + {lastStatus === undefined ? undefined : ( + <p class="mt-2 text-sm leading-6 text-gray-400"> + {lastStatus.pin_transmissions_left < 1 ? ( + <i18n.Translate> + We can't send you the code anymore. + </i18n.Translate> + ) : lastStatus.pin_transmissions_left === 1 ? ( + <i18n.Translate> + We can send the code one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + We can send the code {lastStatus.pin_transmissions_left} more + times. + </i18n.Translate> + )} + </p> + )} + </div> + </div> + ); } - if (!state.lastTry) { - return <div>you should do a challenge first</div>; + if (cantTryAnymore) { + return ( + <Fragment> + <LocalNotificationBanner notification={notification} /> + <div class="isolate bg-white px-6 py-12"> + <div class="mx-auto max-w-2xl text-center"> + <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl"> + <i18n.Translate>Last TAN code can not be used.</i18n.Translate> + </h2> + + <LastContactSent /> + </div> + + <TryAnotherCode /> + </div> + </Fragment> + ); } return ( @@ -159,33 +302,31 @@ export function AnswerChallenge({ Enter the TAN you received to authenticate. </i18n.Translate> </h2> - <p class="mt-2 text-lg leading-8 text-gray-600"> - {state.lastTry.transmitted ? ( - <i18n.Translate> - A TAN was sent to your address "{lastEmail}". - </i18n.Translate> - ) : ( - <Attention title={i18n.str`Resend failed`} type="warning"> + <LastContactSent /> + + {lastStatus === undefined ? undefined : ( + <p class="mt-2 text-lg leading-8 text-gray-600"> + {lastStatus.auth_attempts_left < 1 ? ( <i18n.Translate> - We recently already sent a TAN to your address " - {lastEmail}". A new TAN will not be transmitted again - before "{state.lastTry.nextSend}". + You can't check the PIN anymore. </i18n.Translate> - </Attention> - )} - </p> - {!lastTryError ? undefined : ( - <p class="mt-2 text-lg leading-8 text-gray-600"> - <i18n.Translate> - You can try another PIN but just{" "} - {lastTryError.auth_attempts_left} times more. - </i18n.Translate> + ) : lastStatus.auth_attempts_left === 1 ? ( + <i18n.Translate> + You can check the PIN one last time. + </i18n.Translate> + ) : ( + <i18n.Translate> + You can check the PIN {lastStatus.auth_attempts_left} more + times. + </i18n.Translate> + )} </p> )} </div> + <form method="POST" - class="mx-auto mt-16 max-w-xl sm:mt-20" + class="mx-auto mt-4 max-w-xl" onSubmit={(e) => { e.preventDefault(); }} @@ -219,12 +360,6 @@ export function AnswerChallenge({ /> </div> </div> - - <p class="mt-3 text-sm leading-6 text-gray-400"> - <i18n.Translate> - You have {state.lastTry.attemptsLeft} attempts left. - </i18n.Translate> - </p> </div> <div class="mt-10"> @@ -237,27 +372,9 @@ export function AnswerChallenge({ <i18n.Translate>Check</i18n.Translate> </Button> </div> - <div class="mt-10 flex justify-between"> - <div> - <a - href={routeAsk.url({ nonce })} - class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0" - > - <i18n.Translate>Change email</i18n.Translate> - </a> - </div> - <div> - <Button - type="submit" - disabled={!onSendAgain} - class="block w-full disabled:bg-gray-300 rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - handler={onSendAgain} - > - <i18n.Translate>Send code again</i18n.Translate> - </Button> - </div> - </div> </form> + + <TryAnotherCode /> </div> </Fragment> ); |