diff options
Diffstat (limited to 'packages/anastasis-webui/src/pages')
37 files changed, 1604 insertions, 414 deletions
diff --git a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx index f74dcefba..2c7f54c5b 100644 --- a/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/AttributeEntryScreen.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/camelcase */ import { UserAttributeSpec, validators } from "anastasis-core"; -import { h, VNode } from "preact"; +import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; import { AnastasisClientFrame, withProcessLabel } from "./index"; @@ -20,53 +20,38 @@ export function AttributeEntryScreen(): VNode { if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) { return <div>invalid state</div> } + const reqAttr = reducer.currentReducerState.required_attributes || [] + let hasErrors = false; + const fieldList: VNode[] = reqAttr.map((spec, i: number) => { + const value = attrs[spec.name] + const error = checkIfValid(value, spec) + hasErrors = hasErrors || error !== undefined + return ( + <AttributeEntryField + key={i} + isFirst={i == 0} + setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })} + spec={spec} + errorMessage={error} + value={value} /> + ); + }) return ( <AnastasisClientFrame title={withProcessLabel(reducer, "Who are you?")} + hideNext={hasErrors ? "Complete the form." : undefined} onNext={() => reducer.transition("enter_user_attributes", { identity_attributes: attrs, })} > <div class="columns"> <div class="column is-half"> - - {reducer.currentReducerState.required_attributes?.map((x, i: number) => { - const value = attrs[x.name] - function checkIfValid(): string | undefined { - const pattern = x['validation-regex'] - if (pattern) { - const re = new RegExp(pattern) - if (!re.test(value)) return 'The value is invalid' - } - const logic = x['validation-logic'] - if (logic) { - const func = (validators as any)[logic]; - if (func && typeof func === 'function' && !func(value)) return 'Please check the value' - } - const optional = x.optional - console.log('optiona', optional) - if (!optional && !value) { - return 'This value is required' - } - return undefined - } - - return ( - <AttributeEntryField - key={i} - isFirst={i == 0} - setValue={(v: string) => setAttrs({ ...attrs, [x.name]: v })} - spec={x} - isValid={checkIfValid} - value={value} /> - ); - })} - + {fieldList} </div> <div class="column is-half" > - <p>This personal information will help to locate your secret in the first place</p> + <p>This personal information will help to locate your secret.</p> <h1><b>This stay private</b></h1> <p>The information you have entered here: </p> @@ -92,14 +77,13 @@ interface AttributeEntryFieldProps { value: string; setValue: (newValue: string) => void; spec: UserAttributeSpec; - isValid: () => string | undefined; + errorMessage: string | undefined; } const possibleBirthdayYear: Array<number> = [] -for (let i = 0; i < 100; i++ ) { +for (let i = 0; i < 100; i++) { possibleBirthdayYear.push(2020 - i) } function AttributeEntryField(props: AttributeEntryFieldProps): VNode { - const errorMessage = props.isValid() return ( <div> @@ -108,14 +92,14 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode { grabFocus={props.isFirst} label={props.spec.label} years={possibleBirthdayYear} - error={errorMessage} + error={props.errorMessage} bind={[props.value, props.setValue]} />} {props.spec.type === 'number' && <NumberInput grabFocus={props.isFirst} label={props.spec.label} - error={errorMessage} + error={props.errorMessage} bind={[props.value, props.setValue]} /> } @@ -123,7 +107,7 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode { <TextInput grabFocus={props.isFirst} label={props.spec.label} - error={errorMessage} + error={props.errorMessage} bind={[props.value, props.setValue]} /> } @@ -136,3 +120,21 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode { </div> ); } + +function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined { + const pattern = spec['validation-regex'] + if (pattern) { + const re = new RegExp(pattern) + if (!re.test(value)) return 'The value is invalid' + } + const logic = spec['validation-logic'] + if (logic) { + const func = (validators as any)[logic]; + if (func && typeof func === 'function' && !func(value)) return 'Please check the value' + } + const optional = spec.optional + if (!optional && !value) { + return 'This value is required' + } + return undefined +} diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx deleted file mode 100644 index c3783ea6c..000000000 --- a/packages/anastasis-webui/src/pages/home/AuthMethodEmailSetup.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import { - encodeCrock, - stringToBytes -} from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; -import { AnastasisClientFrame } from "./index"; -import { TextInput } from "../../components/fields/TextInput"; - -export function AuthMethodEmailSetup(props: AuthMethodSetupProps): VNode { - const [email, setEmail] = useState(""); - return ( - <AnastasisClientFrame hideNav title="Add email authentication"> - <p> - For email authentication, you need to provide an email address. When - recovering your secret, you will need to enter the code you receive by - email. - </p> - <div> - <TextInput - label="Email address" - grabFocus - bind={[email, setEmail]} /> - </div> - <div> - <button onClick={() => props.cancel()}>Cancel</button> - <button - onClick={() => props.addAuthMethod({ - authentication_method: { - type: "email", - instructions: `Email to ${email}`, - challenge: encodeCrock(stringToBytes(email)), - }, - })} - > - Add - </button> - </div> - </AnastasisClientFrame> - ); -} diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx deleted file mode 100644 index c4ddeff91..000000000 --- a/packages/anastasis-webui/src/pages/home/AuthMethodPostSetup.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import { - canonicalJson, encodeCrock, - stringToBytes -} from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; -import { TextInput } from "../../components/fields/TextInput"; - -export function AuthMethodPostSetup(props: AuthMethodSetupProps): VNode { - const [fullName, setFullName] = useState(""); - const [street, setStreet] = useState(""); - const [city, setCity] = useState(""); - const [postcode, setPostcode] = useState(""); - const [country, setCountry] = useState(""); - - const addPostAuth = () => { - const challengeJson = { - full_name: fullName, - street, - city, - postcode, - country, - }; - props.addAuthMethod({ - authentication_method: { - type: "email", - instructions: `Letter to address in postal code ${postcode}`, - challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))), - }, - }); - }; - - return ( - <div class="home"> - <h1>Add {props.method} authentication</h1> - <div> - <p> - For postal letter authentication, you need to provide a postal - address. When recovering your secret, you will be asked to enter a - code that you will receive in a letter to that address. - </p> - <div> - <TextInput - grabFocus - label="Full Name" - bind={[fullName, setFullName]} /> - </div> - <div> - <TextInput label="Street" bind={[street, setStreet]} /> - </div> - <div> - <TextInput label="City" bind={[city, setCity]} /> - </div> - <div> - <TextInput label="Postal Code" bind={[postcode, setPostcode]} /> - </div> - <div> - <TextInput label="Country" bind={[country, setCountry]} /> - </div> - <div> - <button onClick={() => props.cancel()}>Cancel</button> - <button onClick={() => addPostAuth()}>Add</button> - </div> - </div> - </div> - ); -} diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx deleted file mode 100644 index f1bab94ab..000000000 --- a/packages/anastasis-webui/src/pages/home/AuthMethodQuestionSetup.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import { - encodeCrock, - stringToBytes -} from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; -import { AnastasisClientFrame } from "./index"; -import { TextInput } from "../../components/fields/TextInput"; - -export function AuthMethodQuestionSetup(props: AuthMethodSetupProps): VNode { - const [questionText, setQuestionText] = useState(""); - const [answerText, setAnswerText] = useState(""); - const addQuestionAuth = (): void => props.addAuthMethod({ - authentication_method: { - type: "question", - instructions: questionText, - challenge: encodeCrock(stringToBytes(answerText)), - }, - }); - return ( - <AnastasisClientFrame hideNav title="Add Security Question"> - <div> - <p> - For security question authentication, you need to provide a question - and its answer. When recovering your secret, you will be shown the - question and you will need to type the answer exactly as you typed it - here. - </p> - <div> - <TextInput - label="Security question" - grabFocus - bind={[questionText, setQuestionText]} /> - </div> - <div> - <TextInput label="Answer" bind={[answerText, setAnswerText]} /> - </div> - <div> - <button onClick={() => props.cancel()}>Cancel</button> - <button onClick={() => addQuestionAuth()}>Add</button> - </div> - </div> - </AnastasisClientFrame> - ); -} diff --git a/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx b/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx deleted file mode 100644 index 6f4797275..000000000 --- a/packages/anastasis-webui/src/pages/home/AuthMethodSmsSetup.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable @typescript-eslint/camelcase */ -import { - encodeCrock, - stringToBytes -} from "@gnu-taler/taler-util"; -import { h, VNode } from "preact"; -import { useState, useRef, useLayoutEffect } from "preact/hooks"; -import { AuthMethodSetupProps } from "./AuthenticationEditorScreen"; -import { AnastasisClientFrame } from "./index"; - -export function AuthMethodSmsSetup(props: AuthMethodSetupProps): VNode { - const [mobileNumber, setMobileNumber] = useState(""); - const addSmsAuth = (): void => { - props.addAuthMethod({ - authentication_method: { - type: "sms", - instructions: `SMS to ${mobileNumber}`, - challenge: encodeCrock(stringToBytes(mobileNumber)), - }, - }); - }; - const inputRef = useRef<HTMLInputElement>(null); - useLayoutEffect(() => { - inputRef.current?.focus(); - }, []); - return ( - <AnastasisClientFrame hideNav title="Add SMS authentication"> - <div> - <p> - For SMS authentication, you need to provide a mobile number. When - recovering your secret, you will be asked to enter the code you - receive via SMS. - </p> - <label> - Mobile number:{" "} - <input - value={mobileNumber} - ref={inputRef} - style={{ display: "block" }} - autoFocus - onChange={(e) => setMobileNumber((e.target as any).value)} - type="text" /> - </label> - <div> - <button onClick={() => props.cancel()}>Cancel</button> - <button onClick={() => addSmsAuth()}>Add</button> - </div> - </div> - </AnastasisClientFrame> - ); -} diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx index 8f86831a9..5077c3eb0 100644 --- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.stories.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/camelcase */ /* This file is part of GNU Taler (C) 2021 Taler Systems S.A. @@ -19,6 +20,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { ReducerState } from 'anastasis-core'; import { createExample, reducerStatesExample } from '../../utils'; import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen'; @@ -36,3 +38,56 @@ export default { }; export const Example = createExample(TestedComponent, reducerStatesExample.authEditing); +export const OneAuthMethodConfigured = createExample(TestedComponent, { + ...reducerStatesExample.authEditing, + authentication_methods: [{ + type: 'question', + instructions: 'what time is it?', + challenge: 'asd', + }] +} as ReducerState); + + +export const SomeMoreAuthMethodConfigured = createExample(TestedComponent, { + ...reducerStatesExample.authEditing, + authentication_methods: [{ + type: 'question', + instructions: 'what time is it?', + challenge: 'asd', + },{ + type: 'question', + instructions: 'what time is it?', + challenge: 'qwe', + },{ + type: 'sms', + instructions: 'what time is it?', + challenge: 'asd', + },{ + type: 'email', + instructions: 'what time is it?', + challenge: 'asd', + },{ + type: 'email', + instructions: 'what time is it?', + challenge: 'asd', + },{ + type: 'email', + instructions: 'what time is it?', + challenge: 'asd', + },{ + type: 'email', + instructions: 'what time is it?', + challenge: 'asd', + }] +} as ReducerState); + +export const NoAuthMethodProvided = createExample(TestedComponent, { + ...reducerStatesExample.authEditing, + authentication_providers: {}, + authentication_methods: [] +} as ReducerState); + + // type: string; + // instructions: string; + // challenge: string; + // mime_type?: string; diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx index e9ffccbac..f4d2aee58 100644 --- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx @@ -1,19 +1,19 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { AuthMethod, ReducerStateBackup } from "anastasis-core"; -import { h, VNode } from "preact"; +import { AuthMethod } from "anastasis-core"; +import { ComponentChildren, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; -import { AnastasisReducerApi } from "../../hooks/use-anastasis-reducer"; -import { AuthMethodEmailSetup } from "./AuthMethodEmailSetup"; -import { AuthMethodPostSetup } from "./AuthMethodPostSetup"; -import { AuthMethodQuestionSetup } from "./AuthMethodQuestionSetup"; -import { AuthMethodSmsSetup } from "./AuthMethodSmsSetup"; +import { authMethods, KnownAuthMethods } from "./authMethodSetup"; import { AnastasisClientFrame } from "./index"; + + +const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T> + export function AuthenticationEditorScreen(): VNode { - const [selectedMethod, setSelectedMethod] = useState<string | undefined>( - undefined - ); + const [noProvidersAck, setNoProvidersAck] = useState(false) + const [selectedMethod, setSelectedMethod] = useState<KnownAuthMethods | undefined>(undefined); + const reducer = useAnastasisContext() if (!reducer) { return <div>no reducer in context</div> @@ -21,7 +21,29 @@ export function AuthenticationEditorScreen(): VNode { if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { return <div>invalid state</div> } + const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? []; + const haveMethodsConfigured = configuredAuthMethods.length > 0; + + function removeByIndex(index: number): void { + if (reducer) reducer.transition("delete_authentication", { + authentication_method: index, + }) + } + + const camByType: { [s: string]: AuthMethodWithRemove[] } = {} + for (let index = 0; index < configuredAuthMethods.length; index++) { + const cam = { + ...configuredAuthMethods[index], + remove: () => removeByIndex(index) + } + const prevValue = camByType[cam.type] || [] + prevValue.push(cam) + camByType[cam.type] = prevValue; + } + + const providers = reducer.currentReducerState.authentication_providers!; + const authAvailableSet = new Set<string>(); for (const provKey of Object.keys(providers)) { const p = providers[provKey]; @@ -31,79 +53,106 @@ export function AuthenticationEditorScreen(): VNode { } } } + if (selectedMethod) { const cancel = (): void => setSelectedMethod(undefined); const addMethod = (args: any): void => { reducer.transition("add_authentication", args); setSelectedMethod(undefined); }; - const methodMap: Record< - string, (props: AuthMethodSetupProps) => h.JSX.Element - > = { - sms: AuthMethodSmsSetup, - question: AuthMethodQuestionSetup, - email: AuthMethodEmailSetup, - post: AuthMethodPostSetup, - }; - const AuthSetup = methodMap[selectedMethod] ?? AuthMethodNotImplemented; + + const AuthSetup = authMethods[selectedMethod].screen ?? AuthMethodNotImplemented; return ( <AuthSetup cancel={cancel} + configured={camByType[selectedMethod] || []} addAuthMethod={addMethod} method={selectedMethod} /> ); } - function MethodButton(props: { method: string; label: string }): VNode { + function MethodButton(props: { method: KnownAuthMethods }): VNode { return ( - <button - disabled={!authAvailableSet.has(props.method)} - onClick={() => { - setSelectedMethod(props.method); - if (reducer) reducer.dismissError(); - }} - > - {props.label} - </button> + <div class="block"> + <button + style={{ justifyContent: 'space-between' }} + class="button is-fullwidth" + onClick={() => { + if (!authAvailableSet.has(props.method)) { + //open add sms dialog + } else { + setSelectedMethod(props.method); + } + if (reducer) reducer.dismissError(); + }} + > + <div style={{ display: 'flex' }}> + <span class="icon "> + {authMethods[props.method].icon} + </span> + <span> + {authMethods[props.method].label} + </span> + </div> + {!authAvailableSet.has(props.method) && + <span class="icon has-text-danger" > + <i class="mdi mdi-exclamation-thick" /> + </span> + } + {camByType[props.method] && + <span class="tag is-info" > + {camByType[props.method].length} + </span> + } + </button> + </div> ); } - const configuredAuthMethods: AuthMethod[] = reducer.currentReducerState.authentication_methods ?? []; - const haveMethodsConfigured = configuredAuthMethods.length; + const errors = !haveMethodsConfigured ? "There is not enough authentication methods." : undefined; return ( - <AnastasisClientFrame title="Backup: Configure Authentication Methods"> - <div> - <MethodButton method="sms" label="SMS" /> - <MethodButton method="email" label="Email" /> - <MethodButton method="question" label="Question" /> - <MethodButton method="post" label="Physical Mail" /> - <MethodButton method="totp" label="TOTP" /> - <MethodButton method="iban" label="IBAN" /> - </div> - <h2>Configured authentication methods</h2> - {haveMethodsConfigured ? ( - configuredAuthMethods.map((x, i) => { - return ( - <p key={i}> - {x.type} ({x.instructions}){" "} - <button - onClick={() => reducer.transition("delete_authentication", { - authentication_method: i, - })} - > - Delete - </button> + <AnastasisClientFrame title="Backup: Configure Authentication Methods" hideNext={errors}> + <div class="columns"> + <div class="column is-half"> + <div> + {getKeys(authMethods).map(method => <MethodButton key={method} method={method} />)} + </div> + {authAvailableSet.size === 0 && <ConfirmModal active={!noProvidersAck} onCancel={() => setNoProvidersAck(true)} description="No providers founds" label="Add a provider manually"> + We have found no trusted cloud providers for your recovery secret. You can add a provider manually. + To add a provider you must know the provider URL (e.g. https://provider.com) + <p> + <a>More about cloud providers</a> </p> - ); - }) - ) : ( - <p>No authentication methods configured yet.</p> - )} + </ConfirmModal>} + + {/* {haveMethodsConfigured && ( + configuredAuthMethods.map((x, i) => { + return ( + <p key={i}> + {x.type} ({x.instructions}){" "} + <button class="button is-danger is-small" + onClick={() => reducer.transition("delete_authentication", { + authentication_method: i, + })} + > + Remove + </button> + </p> + ); + }) + )} */} + </div> + <div class="column is-half"> + When recovering your wallet, you will be asked to verify your identity via the methods you configure here. + </div> + </div> </AnastasisClientFrame> ); } +type AuthMethodWithRemove = AuthMethod & { remove: () => void } export interface AuthMethodSetupProps { method: string; addAuthMethod: (x: any) => void; + configured: AuthMethodWithRemove[]; cancel: () => void; } @@ -116,8 +165,36 @@ function AuthMethodNotImplemented(props: AuthMethodSetupProps): VNode { ); } -interface AuthenticationEditorProps { - reducer: AnastasisReducerApi; - backupState: ReducerStateBackup; + +function ConfirmModal({ active, description, onCancel, onConfirm, children, danger, disabled, label = 'Confirm' }: Props): VNode { + return <div class={active ? "modal is-active" : "modal"}> + <div class="modal-background " onClick={onCancel} /> + <div class="modal-card" style={{ maxWidth: 700 }}> + <header class="modal-card-head"> + {!description ? null : <p class="modal-card-title"><b>{description}</b></p>} + <button class="delete " aria-label="close" onClick={onCancel} /> + </header> + <section class="modal-card-body"> + {children} + </section> + <footer class="modal-card-foot"> + <button class="button" onClick={onCancel} >Dismiss</button> + <div class="buttons is-right" style={{ width: '100%' }}> + <button class={danger ? "button is-danger " : "button is-info "} disabled={disabled} onClick={onConfirm} >{label}</button> + </div> + </footer> + </div> + <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> + </div> } +interface Props { + active?: boolean; + description?: string; + onCancel?: () => void; + onConfirm?: () => void; + label?: string; + children?: ComponentChildren; + danger?: boolean; + disabled?: boolean; +} diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx index 0c9d007bc..b71a79727 100644 --- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.stories.tsx @@ -37,7 +37,7 @@ export default { }, }; -export const Simple = createExample(TestedComponent, reducerStatesExample.backupFinished); +export const WithoutName = createExample(TestedComponent, reducerStatesExample.backupFinished); export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished, secret_name: 'super_secret', diff --git a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx index 218f1d1fd..70ac8157d 100644 --- a/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/BackupFinishedScreen.tsx @@ -1,3 +1,4 @@ +import { format } from "date-fns"; import { h, VNode } from "preact"; import { useAnastasisContext } from "../../context/anastasis"; import { AnastasisClientFrame } from "./index"; @@ -11,23 +12,30 @@ export function BackupFinishedScreen(): VNode { return <div>invalid state</div> } const details = reducer.currentReducerState.success_details - return (<AnastasisClientFrame hideNext title="Backup finished"> - <p> - Your backup of secret "{reducer.currentReducerState.secret_name ?? "??"}" was + + return (<AnastasisClientFrame hideNav title="Backup finished"> + {reducer.currentReducerState.secret_name ? <p> + Your backup of secret <b>"{reducer.currentReducerState.secret_name}"</b> was successful. - </p> - <p>The backup is stored by the following providers:</p> + </p> : + <p> + Your secret was successfully backed up. + </p>} - {details && <ul> + {details && <div class="block"> + <p>The backup is stored by the following providers:</p> {Object.keys(details).map((x, i) => { const sd = details[x]; return ( - <li key={i}> - {x} (Policy version {sd.policy_version}) - </li> + <div key={i} class="box"> + {x} + <p> + version {sd.policy_version} + {sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd/MM/yyyy')}` : ' without expiration date'} + </p> + </div> ); })} - </ul>} - <button onClick={() => reducer.reset()}>Back to start</button> + </div>} </AnastasisClientFrame>); } diff --git a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx index 3bb3fb837..cf44d5bf4 100644 --- a/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ChallengeOverviewScreen.tsx @@ -47,8 +47,9 @@ export function ChallengeOverviewScreen(): VNode { const atLeastThereIsOnePolicySolved = policiesWithInfo.find(p => p.isPolicySolved) !== undefined + const errors = !atLeastThereIsOnePolicySolved ? "Solve one policy before proceeding" : undefined; return ( - <AnastasisClientFrame hideNext={!atLeastThereIsOnePolicySolved} title="Recovery: Solve challenges"> + <AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges"> {!policies.length ? <p> No policies found, try with another version of the secret </p> : (policies.length === 1 ? <p> diff --git a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx index d87afdf46..84896a2ec 100644 --- a/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ChallengePayingScreen.tsx @@ -13,7 +13,7 @@ export function ChallengePayingScreen(): VNode { const payments = ['']; //reducer.currentReducerState.payments ?? return ( <AnastasisClientFrame - hideNext + hideNav title="Recovery: Challenge Paying" > <p> diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx index 94c0409da..713655625 100644 --- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx @@ -1,20 +1,108 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { BackupStates, ContinentInfo, RecoveryStates } from "anastasis-core"; import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; import { useAnastasisContext } from "../../context/anastasis"; import { AnastasisClientFrame, withProcessLabel } from "./index"; export function ContinentSelectionScreen(): VNode { const reducer = useAnastasisContext() + + //FIXME: remove this when #7056 is fixed + const [countryCode, setCountryCode] = useState("") + if (!reducer || !reducer.currentReducerState || !("continents" in reducer.currentReducerState)) { return <div /> } - const select = (continent: string) => (): void => reducer.transition("select_continent", { continent }); + const selectContinent = (continent: string): void => { + reducer.transition("select_continent", { continent }) + }; + const selectCountry = (country: string): void => { + setCountryCode(country) + }; + + + const continentList = reducer.currentReducerState.continents || []; + const countryList = reducer.currentReducerState.countries || []; + const theContinent = reducer.currentReducerState.selected_continent || "" + // const cc = reducer.currentReducerState.selected_country || ""; + const theCountry = countryList.find(c => c.code === countryCode) + const selectCountryAction = () => { + //selection should be when the select box changes it value + if (!theCountry) return; + reducer.transition("select_country", { + country_code: countryCode, + currencies: [theCountry.currency], + }) + } + + const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting || + reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting; + + const errors = !theCountry ? "Select a country" : undefined + return ( - <AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Continent")}> - {reducer.currentReducerState.continents.map((x: any) => ( - <button class="button" onClick={select(x.name)} key={x.name}> - {x.name} - </button> - ))} + <AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Select location")} onNext={selectCountryAction}> + <div class="columns"> + <div class="column is-half"> + <div class="field"> + <label class="label">Continent</label> + <div class="control has-icons-left"> + <div class="select " > + <select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} disabled={!step1}> + <option key="none" disabled selected value=""> Choose a continent </option> + {continentList.map(prov => ( + <option key={prov.name} value={prov.name}> + {prov.name} + </option> + ))} + </select> + <div class="icon is-small is-left"> + <i class="mdi mdi-earth" /> + </div> + </div> + {!step1 && <span class="control"> + <a class="button is-danger" onClick={() => reducer.back()}> + X + </a> + </span>} + </div> + </div> + + <div class="field"> + <label class="label">Country</label> + <div class="control has-icons-left"> + <div class="select" > + <select onChange={(e) => selectCountry((e.target as any).value)} disabled={!theContinent} value={theCountry?.code || ""}> + <option key="none" disabled selected value=""> Choose a country </option> + {countryList.map(prov => ( + <option key={prov.name} value={prov.code}> + {prov.name} + </option> + ))} + </select> + <div class="icon is-small is-left"> + <i class="mdi mdi-earth" /> + </div> + </div> + </div> + </div> + + {theCountry && <div class="field"> + <label class="label">Available currencies:</label> + <div class="control"> + <input class="input is-small" type="text" readonly value={theCountry.currency} /> + </div> + </div>} + </div> + <div class="column is-half"> + <p> + A location will help to define a common information that will be use to locate your secret and a currency + for payments if needed. + </p> + </div> + </div> + </AnastasisClientFrame> ); } diff --git a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx index 417c08633..77329f4fa 100644 --- a/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/CountrySelectionScreen.tsx @@ -18,7 +18,7 @@ export function CountrySelectionScreen(): VNode { return ( <AnastasisClientFrame hideNext title={withProcessLabel(reducer, "Select Country")} > <div style={{ display: 'flex', flexDirection: 'column' }}> - {reducer.currentReducerState.countries.map((x: any) => ( + {reducer.currentReducerState.countries!.map((x: any) => ( <div key={x.name}> <button class="button" onClick={() => sel(x)} > {x.name} ({x.currency}) diff --git a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx index 8a39cf0e4..a470f5155 100644 --- a/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/PoliciesPayingScreen.tsx @@ -13,7 +13,7 @@ export function PoliciesPayingScreen(): VNode { const payments = reducer.currentReducerState.policy_payment_requests ?? []; return ( - <AnastasisClientFrame hideNext title="Backup: Recovery Document Payments"> + <AnastasisClientFrame hideNav title="Backup: Recovery Document Payments"> <p> Some of the providers require a payment to store the encrypted recovery document. diff --git a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx index 8c8a2c7c8..bbcaa10a5 100644 --- a/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/RecoveryFinishedScreen.tsx @@ -17,7 +17,7 @@ export function RecoveryFinishedScreen(): VNode { } const encodedSecret = reducer.currentReducerState.core_secret?.value if (!encodedSecret) { - return <AnastasisClientFrame title="Recovery Problem" hideNext> + return <AnastasisClientFrame title="Recovery Problem" hideNav> <p> Secret not found </p> @@ -25,7 +25,7 @@ export function RecoveryFinishedScreen(): VNode { } const secret = bytesToString(decodeCrock(encodedSecret)) return ( - <AnastasisClientFrame title="Recovery Finished" hideNext> + <AnastasisClientFrame title="Recovery Finished" hideNav> <p> Secret: {secret} </p> diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx index 91855b023..007011326 100644 --- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.stories.tsx @@ -43,11 +43,11 @@ export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, { methods: [{ authentication_method: 0, provider: 'asd' - },{ + }, { authentication_method: 1, provider: 'asd' }] - },{ + }, { methods: [{ authentication_method: 1, provider: 'asd' @@ -58,27 +58,191 @@ export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, { export const SomePoliciesWithMethods = createExample(TestedComponent, { ...reducerStatesExample.policyReview, - policies: [{ - methods: [{ - authentication_method: 0, - provider: 'asd' - },{ - authentication_method: 1, - provider: 'asd' - }] - },{ - methods: [{ - authentication_method: 1, - provider: 'asd' - }] - }], + policies: [ + { + methods: [ + { + authentication_method: 0, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 1, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 2, + provider: "https://kudos.demo.anastasis.lu/" + } + ] + }, + { + methods: [ + { + authentication_method: 0, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 1, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 3, + provider: "https://anastasis.demo.taler.net/" + } + ] + }, + { + methods: [ + { + authentication_method: 0, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 1, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 4, + provider: "https://anastasis.demo.taler.net/" + } + ] + }, + { + methods: [ + { + authentication_method: 0, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 2, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 3, + provider: "https://anastasis.demo.taler.net/" + } + ] + }, + { + methods: [ + { + authentication_method: 0, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 2, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 4, + provider: "https://anastasis.demo.taler.net/" + } + ] + }, + { + methods: [ + { + authentication_method: 0, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 3, + provider: "https://anastasis.demo.taler.net/" + }, + { + authentication_method: 4, + provider: "https://anastasis.demo.taler.net/" + } + ] + }, + { + methods: [ + { + authentication_method: 1, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 2, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 3, + provider: "https://anastasis.demo.taler.net/" + } + ] + }, + { + methods: [ + { + authentication_method: 1, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 2, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 4, + provider: "https://anastasis.demo.taler.net/" + } + ] + }, + { + methods: [ + { + authentication_method: 1, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 3, + provider: "https://anastasis.demo.taler.net/" + }, + { + authentication_method: 4, + provider: "https://anastasis.demo.taler.net/" + } + ] + }, + { + methods: [ + { + authentication_method: 2, + provider: "https://kudos.demo.anastasis.lu/" + }, + { + authentication_method: 3, + provider: "https://anastasis.demo.taler.net/" + }, + { + authentication_method: 4, + provider: "https://anastasis.demo.taler.net/" + } + ] + } + ], authentication_methods: [{ - challenge: 'asd', - instructions: 'ins', - type: 'type', + type: "email", + instructions: "Email to qwe@asd.com", + challenge: "E5VPA" + }, { + type: "sms", + instructions: "SMS to 555-555", + challenge: "" + }, { + type: "question", + instructions: "Does P equal NP?", + challenge: "C5SP8" },{ - challenge: 'asd2', - instructions: 'ins2', - type: 'type2', + type: "email", + instructions: "Email to qwe@asd.com", + challenge: "E5VPA" + }, { + type: "sms", + instructions: "SMS to 555-555", + challenge: "" + }, { + type: "question", + instructions: "Does P equal NP?", + challenge: "C5SP8" }] } as ReducerState); diff --git a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx index b360ccaf0..6d5220a05 100644 --- a/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/ReviewPoliciesScreen.tsx @@ -2,6 +2,7 @@ import { h, VNode } from "preact"; import { useAnastasisContext } from "../../context/anastasis"; import { AnastasisClientFrame } from "./index"; +import { authMethods, KnownAuthMethods } from "./authMethodSetup"; export function ReviewPoliciesScreen(): VNode { const reducer = useAnastasisContext() @@ -11,43 +12,50 @@ export function ReviewPoliciesScreen(): VNode { if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) { return <div>invalid state</div> } - const authMethods = reducer.currentReducerState.authentication_methods ?? []; + const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? []; const policies = reducer.currentReducerState.policies ?? []; + const errors = policies.length < 1 ? 'Need more policies' : undefined return ( - <AnastasisClientFrame title="Backup: Review Recovery Policies"> + <AnastasisClientFrame hideNext={errors} title="Backup: Review Recovery Policies"> + {policies.length > 0 && <p class="block"> + Based on your configured authentication method you have created, some policies + have been configured. In order to recover your secret you have to solve all the + challenges of at least one policy. + </p> } + {policies.length < 1 && <p class="block"> + No policies had been created. Go back and add more authentication methods. + </p> } {policies.map((p, policy_index) => { const methods = p.methods - .map(x => authMethods[x.authentication_method] && ({ ...authMethods[x.authentication_method], provider: x.provider })) + .map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider })) .filter(x => !!x) const policyName = methods.map(x => x.type).join(" + "); return ( - <div key={policy_index} class="policy"> - <h3> - Policy #{policy_index + 1}: {policyName} - </h3> - Required Authentications: - {!methods.length && <p> - No auth method found - </p>} - <ul> + <div key={policy_index} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> + <div> + <h3 class="subtitle"> + Policy #{policy_index + 1}: {policyName} + </h3> + {!methods.length && <p> + No auth method found + </p>} {methods.map((m, i) => { return ( - <li key={i}> - {m.type} ({m.instructions}) at provider {m.provider} - </li> + <p key={i} class="block" style={{display:'flex', alignItems:'center'}}> + <span class="icon"> + {authMethods[m.type as KnownAuthMethods]?.icon} + </span> + <span> + {m.instructions} recovery provided by <a href={m.provider}>{m.provider}</a> + </span> + </p> ); })} - </ul> - <div> - <button - onClick={() => reducer.transition("delete_policy", { policy_index })} - > - Delete Policy - </button> </div> + <div style={{ marginTop: 'auto', marginBottom: 'auto' }}><button class="button is-danger" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button></div> </div> ); })} diff --git a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx index 79a46761c..915465c3f 100644 --- a/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretEditorScreen.tsx @@ -6,6 +6,7 @@ import { useAnastasisContext } from "../../context/anastasis"; import { AnastasisClientFrame} from "./index"; import { TextInput } from "../../components/fields/TextInput"; +import { FileInput } from "../../components/fields/FileInput"; export function SecretEditorScreen(): VNode { const reducer = useAnastasisContext() @@ -57,6 +58,10 @@ export function SecretEditorScreen(): VNode { <TextInput label="Secret Value:" bind={[secretValue, setSecretValue]} + /> or import a file + <FileInput + label="Open file from your device" + bind={[secretValue, setSecretValue]} /> </div> </AnastasisClientFrame> diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx index 5d67ee472..d0b83bda5 100644 --- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx @@ -37,7 +37,7 @@ export function SecretSelectionScreen(): VNode { const recoveryDocument = reducer.currentReducerState.recovery_document if (!recoveryDocument) { return ( - <AnastasisClientFrame hideNext title="Recovery: Problem"> + <AnastasisClientFrame hideNext="Recovery document not found" title="Recovery: Problem"> <p>No recovery document found, try with another provider</p> <table class="table"> <tr> diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx index c05c36b07..cb6561b3f 100644 --- a/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveScreen.stories.tsx @@ -44,7 +44,7 @@ export const NotSupportedChallenge = createExample(TestedComponent, { recovery_information: { challenges: [{ cost: 'USD:1', - instructions: 'follow htis instructions', + instructions: 'does P equals NP?', type: 'chall-type', uuid: 'ASDASDSAD!1' }], @@ -58,7 +58,7 @@ export const MismatchedChallengeId = createExample(TestedComponent, { recovery_information: { challenges: [{ cost: 'USD:1', - instructions: 'follow htis instructions', + instructions: 'does P equals NP?', type: 'chall-type', uuid: 'ASDASDSAD!1' }], @@ -72,7 +72,7 @@ export const SmsChallenge = createExample(TestedComponent, { recovery_information: { challenges: [{ cost: 'USD:1', - instructions: 'follow htis instructions', + instructions: 'SMS to 555-5555', type: 'sms', uuid: 'ASDASDSAD!1' }], @@ -86,7 +86,7 @@ export const QuestionChallenge = createExample(TestedComponent, { recovery_information: { challenges: [{ cost: 'USD:1', - instructions: 'follow htis instructions', + instructions: 'does P equals NP?', type: 'question', uuid: 'ASDASDSAD!1' }], @@ -100,7 +100,7 @@ export const EmailChallenge = createExample(TestedComponent, { recovery_information: { challenges: [{ cost: 'USD:1', - instructions: 'follow htis instructions', + instructions: 'Email to sebasjm@some-domain.com', type: 'email', uuid: 'ASDASDSAD!1' }], @@ -114,7 +114,7 @@ export const PostChallenge = createExample(TestedComponent, { recovery_information: { challenges: [{ cost: 'USD:1', - instructions: 'follow htis instructions', + instructions: 'Letter to address in postal code ABC123', type: 'post', uuid: 'ASDASDSAD!1' }], diff --git a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx index 077726e02..b0cfa9bb0 100644 --- a/packages/anastasis-webui/src/pages/home/SolveScreen.tsx +++ b/packages/anastasis-webui/src/pages/home/SolveScreen.tsx @@ -8,26 +8,26 @@ import { useAnastasisContext } from "../../context/anastasis"; export function SolveScreen(): VNode { const reducer = useAnastasisContext() const [answer, setAnswer] = useState(""); - + if (!reducer) { - return <AnastasisClientFrame hideNext title="Recovery problem"> + return <AnastasisClientFrame hideNav title="Recovery problem"> <div>no reducer in context</div> </AnastasisClientFrame> } if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) { - return <AnastasisClientFrame hideNext title="Recovery problem"> + return <AnastasisClientFrame hideNav title="Recovery problem"> <div>invalid state</div> </AnastasisClientFrame> } if (!reducer.currentReducerState.recovery_information) { - return <AnastasisClientFrame hideNext title="Recovery problem"> + 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 hideNext title="Recovery problem"> - <div>no selected uuid</div> + return <AnastasisClientFrame hideNav title="Recovery problem"> + <div>invalid state</div> </AnastasisClientFrame> } @@ -55,7 +55,7 @@ export function SolveScreen(): VNode { function onCancel(): void { reducer?.back() } - + return ( <AnastasisClientFrame @@ -70,9 +70,9 @@ export function SolveScreen(): VNode { feedback={challengeFeedback[selectedUuid]} /> <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> - <button class="button" onClick={onCancel}>Cancel</button> - <button class="button is-info" onClick={onNext} >Confirm</button> - </div> + <button class="button" onClick={onCancel}>Cancel</button> + <button class="button is-info" onClick={onNext} >Confirm</button> + </div> </AnastasisClientFrame> ); } @@ -82,13 +82,13 @@ export interface SolveEntryProps { challenge: ChallengeInfo; feedback?: ChallengeFeedback; answer: string; - setAnswer: (s:string) => void; + setAnswer: (s: string) => void; } function SolveSmsEntry({ challenge, answer, setAnswer }: SolveEntryProps): VNode { return (<Fragment> - <p>An sms has been sent to "<b>{challenge.instructions}</b>". Type the code below</p> - <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> + <p>An sms has been sent to "<b>{challenge.instructions}</b>". Type the code below</p> + <TextInput label="Answer" grabFocus bind={[answer, setAnswer]} /> </Fragment> ); } diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodEmailSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodEmailSetup.stories.tsx new file mode 100644 index 000000000..e178a4955 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodEmailSetup.stories.tsx @@ -0,0 +1,66 @@ +/* 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'; + + +export default { + title: 'Pages/backup/authMethods/email', + component: TestedComponent, + args: { + order: 5, + }, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +const type: KnownAuthMethods = 'email' + +export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [] +}); + +export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'Email to sebasjm@email.com ', + remove: () => null + }] +}); + +export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'Email to sebasjm@email.com', + remove: () => null + },{ + challenge: 'qwe', + type, + instructions: 'Email to someone@sebasjm.com', + remove: () => null + }] +}); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodEmailSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodEmailSetup.tsx new file mode 100644 index 000000000..e8cee9cb4 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodEmailSetup.tsx @@ -0,0 +1,62 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { + encodeCrock, + stringToBytes +} from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; +import { AnastasisClientFrame } from "../index"; +import { TextInput } from "../../../components/fields/TextInput"; +import { EmailInput } from "../../../components/fields/EmailInput"; + +const EMAIL_PATTERN = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ + +export function AuthMethodEmailSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode { + const [email, setEmail] = useState(""); + const addEmailAuth = (): void => addAuthMethod({ + authentication_method: { + type: "email", + instructions: `Email to ${email}`, + challenge: encodeCrock(stringToBytes(email)), + }, + }); + const emailError = !EMAIL_PATTERN.test(email) ? 'Email address is not valid' : undefined + const errors = !email ? 'Add your email' : emailError + + return ( + <AnastasisClientFrame hideNav title="Add email authentication"> + <p> + For email authentication, you need to provide an email address. When + recovering your secret, you will need to enter the code you receive by + email. + </p> + <div> + <EmailInput + label="Email address" + error={emailError} + placeholder="email@domain.com" + bind={[email, setEmail]} /> + </div> + {configured.length > 0 && <section class="section"> + <div class="block"> + Your emails: + </div><div class="block"> + {configured.map((c, i) => { + return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> + <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p> + <div><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}>Canceul</button> + <span data-tooltip={errors}> + <button class="button is-info" disabled={errors !== undefined} onClick={addEmailAuth}>Add</button> + </span> + </div> + </div> + </AnastasisClientFrame> + ); +} diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodIbanSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodIbanSetup.stories.tsx new file mode 100644 index 000000000..71f618646 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodIbanSetup.stories.tsx @@ -0,0 +1,65 @@ +/* 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'; + + +export default { + title: 'Pages/backup/authMethods/IBAN', + component: TestedComponent, + args: { + order: 5, + }, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +const type: KnownAuthMethods = 'iban' + +export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [] +}); + +export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'Wire transfer from QWEASD123123 with holder Sebastian', + remove: () => null + }] +}); +export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'Wire transfer from QWEASD123123 with holder Javier', + remove: () => null + },{ + challenge: 'qwe', + type, + instructions: 'Wire transfer from QWEASD123123 with holder Sebastian', + remove: () => null + }] +},); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodIbanSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodIbanSetup.tsx new file mode 100644 index 000000000..c9edbfa07 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodIbanSetup.tsx @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { + canonicalJson, + encodeCrock, + stringToBytes +} from "@gnu-taler/taler-util"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { TextInput } from "../../../components/fields/TextInput"; +import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; +import { AnastasisClientFrame } from "../index"; + +export function AuthMethodIbanSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode { + const [name, setName] = useState(""); + const [account, setAccount] = useState(""); + const addIbanAuth = (): void => addAuthMethod({ + authentication_method: { + type: "iban", + instructions: `Wire transfer from ${account} with holder ${name}`, + challenge: encodeCrock(stringToBytes(canonicalJson({ + name, account + }))), + }, + }); + const errors = !name ? 'Add an account name' : ( + !account ? 'Add an account IBAN number' : undefined + ) + return ( + <AnastasisClientFrame hideNav title="Add bank transfer authentication"> + <p> + For bank transfer authentication, you need to provide a bank + account (account holder name and IBAN). When recovering your + secret, you will be asked to pay the recovery fee via bank + transfer from the account you provided here. + </p> + <div> + <TextInput + label="Bank account holder name" + grabFocus + placeholder="John Smith" + bind={[name, setName]} /> + <TextInput + label="IBAN" + placeholder="DE91100000000123456789" + bind={[account, setAccount]} /> + </div> + {configured.length > 0 && <section class="section"> + <div class="block"> + Your bank accounts: + </div><div class="block"> + {configured.map((c, i) => { + return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> + <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p> + <div><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> + <span data-tooltip={errors}> + <button class="button is-info" disabled={errors !== undefined} onClick={addIbanAuth}>Add</button> + </span> + </div> + </div> + </AnastasisClientFrame> + ); +} diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodPostSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodPostSetup.stories.tsx new file mode 100644 index 000000000..0f1c17495 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodPostSetup.stories.tsx @@ -0,0 +1,66 @@ +/* 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'; + + +export default { + title: 'Pages/backup/authMethods/Post', + component: TestedComponent, + args: { + order: 5, + }, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +const type: KnownAuthMethods = 'post' + +export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [] +}); + +export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'Letter to address in postal code QWE456', + remove: () => null + }] +}); + +export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'Letter to address in postal code QWE456', + remove: () => null + },{ + challenge: 'qwe', + type, + instructions: 'Letter to address in postal code ABC123', + remove: () => null + }] +}); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodPostSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodPostSetup.tsx new file mode 100644 index 000000000..bfeaaa832 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodPostSetup.tsx @@ -0,0 +1,102 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { + canonicalJson, encodeCrock, + stringToBytes +} from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; +import { TextInput } from "../../../components/fields/TextInput"; +import { AnastasisClientFrame } from ".."; + +export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode { + const [fullName, setFullName] = useState(""); + const [street, setStreet] = useState(""); + const [city, setCity] = useState(""); + const [postcode, setPostcode] = useState(""); + const [country, setCountry] = useState(""); + + const addPostAuth = () => { + const challengeJson = { + full_name: fullName, + street, + city, + postcode, + country, + }; + addAuthMethod({ + authentication_method: { + type: "post", + instructions: `Letter to address in postal code ${postcode}`, + challenge: encodeCrock(stringToBytes(canonicalJson(challengeJson))), + }, + }); + }; + + const errors = !fullName ? 'The full name is missing' : ( + !street ? 'The street is missing' : ( + !city ? 'The city is missing' : ( + !postcode ? 'The postcode is missing' : ( + !country ? 'The country is missing' : undefined + ) + ) + ) + ) + return ( + <AnastasisClientFrame hideNav title="Add postal authentication"> + <p> + For postal letter authentication, you need to provide a postal + address. When recovering your secret, you will be asked to enter a + code that you will receive in a letter to that address. + </p> + <div> + <TextInput + grabFocus + label="Full Name" + bind={[fullName, setFullName]} + /> + </div> + <div> + <TextInput + label="Street" + bind={[street, setStreet]} + /> + </div> + <div> + <TextInput + label="City" bind={[city, setCity]} + /> + </div> + <div> + <TextInput + label="Postal Code" bind={[postcode, setPostcode]} + /> + </div> + <div> + <TextInput + label="Country" + bind={[country, setCountry]} + /> + </div> + + {configured.length > 0 && <section class="section"> + <div class="block"> + Your postal code: + </div><div class="block"> + {configured.map((c, i) => { + return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> + <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p> + <div><button class="button is-danger" onClick={c.remove} >Delete</button></div> + </div> + })} + </div> + </section>} + <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> + <button class="button" onClick={cancel}>Cancel</button> + <span data-tooltip={errors}> + <button class="button is-info" disabled={errors !== undefined} onClick={addPostAuth}>Add</button> + </span> + </div> + </AnastasisClientFrame> + ); +} diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodQuestionSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodQuestionSetup.stories.tsx new file mode 100644 index 000000000..3ba4a84ca --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodQuestionSetup.stories.tsx @@ -0,0 +1,66 @@ +/* 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'; + + +export default { + title: 'Pages/backup/authMethods/Question', + component: TestedComponent, + args: { + order: 5, + }, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +const type: KnownAuthMethods = 'question' + +export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [] +}); + +export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'Is integer factorization polynomial? (non-quantum computer)', + remove: () => null + }] +}); + +export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'Does P equal NP?', + remove: () => null + },{ + challenge: 'asd', + type, + instructions: 'Are continuous groups automatically differential groups?', + remove: () => null + }] +}); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodQuestionSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodQuestionSetup.tsx new file mode 100644 index 000000000..eab800e35 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodQuestionSetup.tsx @@ -0,0 +1,70 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { + encodeCrock, + stringToBytes +} from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; +import { AnastasisClientFrame } from "../index"; +import { TextInput } from "../../../components/fields/TextInput"; + +export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode { + const [questionText, setQuestionText] = useState(""); + const [answerText, setAnswerText] = useState(""); + const addQuestionAuth = (): void => addAuthMethod({ + authentication_method: { + type: "question", + instructions: questionText, + challenge: encodeCrock(stringToBytes(answerText)), + }, + }); + + const errors = !questionText ? "Add your security question" : ( + !answerText ? 'Add the answer to your question' : undefined + ) + return ( + <AnastasisClientFrame hideNav title="Add Security Question"> + <div> + <p> + For security question authentication, you need to provide a question + and its answer. When recovering your secret, you will be shown the + question and you will need to type the answer exactly as you typed it + here. + </p> + <div> + <TextInput + label="Security question" + grabFocus + placeholder="Your question" + bind={[questionText, setQuestionText]} /> + </div> + <div> + <TextInput + label="Answer" + placeholder="Your answer" + bind={[answerText, setAnswerText]} + /> + </div> + + {configured.length > 0 && <section class="section"> + <div class="block"> + Your security questions: + </div><div class="block"> + {configured.map((c, i) => { + return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> + <p style={{ marginBottom: 'auto', marginTop: 'auto' }}>{c.instructions}</p> + <div><button class="button is-danger" onClick={c.remove} >Delete</button></div> + </div> + })} + </div></section>} + <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> + <button class="button" onClick={cancel}>Cancel</button> + <span data-tooltip={errors}> + <button class="button is-info" disabled={errors !== undefined} onClick={addQuestionAuth}>Add</button> + </span> + </div> + </div> + </AnastasisClientFrame > + ); +} diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodSmsSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodSmsSetup.stories.tsx new file mode 100644 index 000000000..ae8297ef7 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodSmsSetup.stories.tsx @@ -0,0 +1,66 @@ +/* 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'; + + +export default { + title: 'Pages/backup/authMethods/Sms', + component: TestedComponent, + args: { + order: 5, + }, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +const type: KnownAuthMethods = 'sms' + +export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [] +}); + +export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'SMS to +11-1234-2345', + remove: () => null + }] +}); + +export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'SMS to +11-1234-2345', + remove: () => null + },{ + challenge: 'qwe', + type, + instructions: 'SMS to +11-5555-2345', + remove: () => null + }] +}); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodSmsSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodSmsSetup.tsx new file mode 100644 index 000000000..9e85af2b2 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodSmsSetup.tsx @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { + encodeCrock, + stringToBytes +} from "@gnu-taler/taler-util"; +import { Fragment, h, VNode } from "preact"; +import { useLayoutEffect, useRef, useState } from "preact/hooks"; +import { NumberInput } from "../../../components/fields/NumberInput"; +import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; +import { AnastasisClientFrame } from "../index"; + +export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode { + const [mobileNumber, setMobileNumber] = useState(""); + const addSmsAuth = (): void => { + addAuthMethod({ + authentication_method: { + type: "sms", + instructions: `SMS to ${mobileNumber}`, + challenge: encodeCrock(stringToBytes(mobileNumber)), + }, + }); + }; + const inputRef = useRef<HTMLInputElement>(null); + useLayoutEffect(() => { + inputRef.current?.focus(); + }, []); + const errors = !mobileNumber ? 'Add a mobile number' : undefined + return ( + <AnastasisClientFrame hideNav title="Add SMS authentication"> + <div> + <p> + For SMS authentication, you need to provide a mobile number. When + recovering your secret, you will be asked to enter the code you + receive via SMS. + </p> + <div class="container"> + <NumberInput + label="Mobile number" + placeholder="Your mobile number" + grabFocus + bind={[mobileNumber, setMobileNumber]} /> + </div> + {configured.length > 0 && <section class="section"> + <div class="block"> + Your mobile numbers: + </div><div class="block"> + {configured.map((c, i) => { + return <div key={i} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}> + <p style={{ marginTop: 'auto', marginBottom: 'auto' }}>{c.instructions}</p> + <div><button class="button is-danger" onClick={c.remove}>Delete</button></div> + </div> + })} + </div></section>} + <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> + <button class="button" onClick={cancel}>Cancel</button> + <span data-tooltip={errors}> + <button class="button is-info" disabled={errors !== undefined} onClick={addSmsAuth}>Add</button> + </span> + </div> + </div> + </AnastasisClientFrame> + ); +} diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.stories.tsx new file mode 100644 index 000000000..3447e3d61 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.stories.tsx @@ -0,0 +1,64 @@ +/* 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'; + + +export default { + title: 'Pages/backup/authMethods/TOTP', + component: TestedComponent, + args: { + order: 5, + }, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +const type: KnownAuthMethods = 'totp' + +export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [] +}); +export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'instr', + remove: () => null + }] +}); +export const WithMoreExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: 'instr', + remove: () => null + },{ + challenge: 'qwe', + type, + instructions: 'instr', + remove: () => null + }] +}); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.tsx new file mode 100644 index 000000000..bbffedad6 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodTotpSetup.tsx @@ -0,0 +1,47 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { + encodeCrock, + stringToBytes +} from "@gnu-taler/taler-util"; +import { h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; +import { AnastasisClientFrame } from "../index"; +import { TextInput } from "../../../components/fields/TextInput"; +import { QR } from "../../../components/QR"; + +export function AuthMethodTotpSetup({addAuthMethod, cancel, configured}: AuthMethodSetupProps): VNode { + const [name, setName] = useState(""); + const addTotpAuth = (): void => addAuthMethod({ + authentication_method: { + type: "totp", + instructions: `Enter code for ${name}`, + challenge: encodeCrock(stringToBytes(name)), + }, + }); + const errors = !name ? 'The TOTP name is missing' : undefined; + return ( + <AnastasisClientFrame hideNav title="Add TOTP authentication"> + <p> + For Time-based One-Time Password (TOTP) authentication, you need to set + a name for the TOTP secret. Then, you must scan the generated QR code + with your TOTP App to import the TOTP secret into your TOTP App. + </p> + <div> + <TextInput + label="TOTP Name" + grabFocus + bind={[name, setName]} /> + </div> + <QR text={`sometext ${name}`} /> + <div> + <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> + <button class="button" onClick={cancel}>Cancel</button> + <span data-tooltip={errors}> + <button class="button is-info" disabled={errors !== undefined} onClick={addTotpAuth}>Add</button> + </span> + </div> + </div> + </AnastasisClientFrame> + ); +} diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodVideoSetup.stories.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodVideoSetup.stories.tsx new file mode 100644 index 000000000..3c4c7bf39 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodVideoSetup.stories.tsx @@ -0,0 +1,66 @@ +/* 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/authMethods/Video', + component: TestedComponent, + args: { + order: 5, + }, + argTypes: { + onUpdate: { action: 'onUpdate' }, + onBack: { action: 'onBack' }, + }, +}; + +const type: KnownAuthMethods = 'video' + +export const Empty = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [] +}); + +export const WithOneExample = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: logoImage, + remove: () => null + }] +}); + +export const WithMoreExamples = createExample(TestedComponent[type].screen, reducerStatesExample.authEditing, { + configured: [{ + challenge: 'qwe', + type, + instructions: logoImage, + remove: () => null + },{ + challenge: 'qwe', + type, + instructions: logoImage, + remove: () => null + }] +}); diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodVideoSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodVideoSetup.tsx new file mode 100644 index 000000000..d292a9d24 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/AuthMethodVideoSetup.tsx @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/camelcase */ +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 "../AuthenticationEditorScreen"; +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: image, + challenge: encodeCrock(stringToBytes(image)), + }, + }) + }; + 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 + 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> + ); +} diff --git a/packages/anastasis-webui/src/pages/home/authMethodSetup/index.tsx b/packages/anastasis-webui/src/pages/home/authMethodSetup/index.tsx new file mode 100644 index 000000000..1e1d7bc03 --- /dev/null +++ b/packages/anastasis-webui/src/pages/home/authMethodSetup/index.tsx @@ -0,0 +1,68 @@ +import { h, VNode } from "preact"; +import { AuthMethodSetupProps } from "../AuthenticationEditorScreen"; + +import { AuthMethodEmailSetup as EmailScreen } from "./AuthMethodEmailSetup"; +import { AuthMethodIbanSetup as IbanScreen } from "./AuthMethodIbanSetup"; +import { AuthMethodPostSetup as PostalScreen } from "./AuthMethodPostSetup"; +import { AuthMethodQuestionSetup as QuestionScreen } from "./AuthMethodQuestionSetup"; +import { AuthMethodSmsSetup as SmsScreen } from "./AuthMethodSmsSetup"; +import { AuthMethodTotpSetup as TotpScreen } from "./AuthMethodTotpSetup"; +import { AuthMethodVideoSetup as VideScreen } from "./AuthMethodVideoSetup"; +import postalIcon from '../../../assets/icons/auth_method/postal.svg'; +import questionIcon from '../../../assets/icons/auth_method/question.svg'; +import smsIcon from '../../../assets/icons/auth_method/sms.svg'; +import videoIcon from '../../../assets/icons/auth_method/video.svg'; + +interface AuthMethodConfiguration { + icon: VNode; + label: string; + screen: (props: AuthMethodSetupProps) => VNode; +} +export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban"; + +type KnowMethodConfig = { + [name in KnownAuthMethods]: AuthMethodConfiguration; +}; + +export const authMethods: KnowMethodConfig = { + question: { + icon: <img src={questionIcon} />, + label: "Question", + screen: QuestionScreen + }, + sms: { + icon: <img src={smsIcon} />, + label: "SMS", + screen: SmsScreen + }, + email: { + icon: <i class="mdi mdi-email" />, + label: "Email", + screen: EmailScreen + + }, + iban: { + icon: <i class="mdi mdi-bank" />, + label: "IBAN", + screen: IbanScreen + + }, + post: { + icon: <img src={postalIcon} />, + label: "Physical mail", + screen: PostalScreen + + }, + totp: { + icon: <i class="mdi mdi-devices" />, + label: "TOTP", + screen: TotpScreen + + }, + video: { + icon: <img src={videoIcon} />, + label: "Video", + screen: VideScreen + + } +}
\ No newline at end of file diff --git a/packages/anastasis-webui/src/pages/home/index.tsx b/packages/anastasis-webui/src/pages/home/index.tsx index 5cef4ee9c..fefaa184c 100644 --- a/packages/anastasis-webui/src/pages/home/index.tsx +++ b/packages/anastasis-webui/src/pages/home/index.tsx @@ -11,7 +11,8 @@ import { VNode } from "preact"; import { - useErrorBoundary} from "preact/hooks"; + useErrorBoundary +} from "preact/hooks"; import { Menu } from "../../components/menu"; import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis"; import { @@ -59,7 +60,7 @@ interface AnastasisClientFrameProps { /** * Hide only the "next" button. */ - hideNext?: boolean; + hideNext?: string; } function ErrorBoundary(props: { @@ -112,13 +113,15 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode { <Menu title="Anastasis" /> <div> <div class="home" onKeyPress={(e) => handleKeyPress(e)}> - <h1>{props.title}</h1> + <h1 class="title">{props.title}</h1> <ErrorBanner /> {props.children} {!props.hideNav ? ( - <div style={{marginTop: '2em', display:'flex', justifyContent:'space-between'}}> + <div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}> <button class="button" onClick={() => reducer.back()}>Back</button> - {!props.hideNext ? <button class="button is-info"onClick={next}>Next</button> : null} + <span data-tooltip={props.hideNext}> + <button class="button is-info" onClick={next} disabled={props.hideNext !== undefined}>Next</button> + </span> </div> ) : null} </div> @@ -151,18 +154,12 @@ const AnastasisClientImpl: FunctionalComponent = () => { if ( state.backup_state === BackupStates.ContinentSelecting || - state.recovery_state === RecoveryStates.ContinentSelecting - ) { - return ( - <ContinentSelectionScreen /> - ); - } - if ( + state.recovery_state === RecoveryStates.ContinentSelecting || state.backup_state === BackupStates.CountrySelecting || state.recovery_state === RecoveryStates.CountrySelecting ) { return ( - <CountrySelectionScreen /> + <ContinentSelectionScreen /> ); } if ( |