feedback from meeting and editing policy

This commit is contained in:
Sebastian 2021-11-03 17:30:11 -03:00
parent 9fb6536fbc
commit a82b5a6992
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
24 changed files with 657 additions and 221 deletions

View File

@ -4,9 +4,9 @@
"version": "0.0.0",
"license": "MIT",
"scripts": {
"build": "preact build",
"build": "preact build --no-sw --no-esm",
"serve": "sirv build --port 8080 --cors --single",
"dev": "preact watch",
"dev": "preact watch --no-sw --no-esm",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"test": "jest ./tests",
"build-storybook": "build-storybook",

View File

@ -0,0 +1,51 @@
/*
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 { ComponentChildren, h, VNode } from "preact";
// import { LoadingModal } from "../modal";
import { useAsync } from "../hooks/async";
// import { Translate } from "../../i18n";
type Props = {
children: ComponentChildren;
disabled: boolean;
onClick?: () => Promise<void>;
[rest: string]: any;
};
export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VNode {
const { isLoading, request } = useAsync(onClick);
// if (isSlow) {
// return <LoadingModal onCancel={cancel} />;
// }
console.log(isLoading)
if (isLoading) {
return <button class="button">Loading...</button>;
}
return <span {...rest}>
<button class="button is-info" onClick={request} disabled={disabled}>
{children}
</button>
</span>;
}

View File

@ -1,4 +1,4 @@
import { format } from "date-fns";
import { format, isAfter, parse, sub, subYears } from "date-fns";
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { DatePicker } from "../picker/DatePicker";
@ -19,16 +19,14 @@ export function DateInput(props: DateInputProps): VNode {
inputRef.current?.focus();
}
}, [props.grabFocus]);
const [opened, setOpened2] = useState(false)
function setOpened(v: boolean): void {
console.log('dale', v)
setOpened2(v)
}
const [opened, setOpened] = useState(false)
const value = props.bind[0] || "";
const [dirty, setDirty] = useState(false)
const showError = dirty && props.error
const calendar = subYears(new Date(), 30)
return <div class="field">
<label class="label">
{props.label}
@ -36,27 +34,37 @@ export function DateInput(props: DateInputProps): VNode {
<i class="mdi mdi-information" />
</span>}
</label>
<div class="control has-icons-right">
<input
type="text"
class={showError ? 'input is-danger' : 'input'}
readonly
onFocus={() => { setOpened(true) } }
value={value}
ref={inputRef} />
<span class="control icon is-right">
<span class="icon"><i class="mdi mdi-calendar" /></span>
</span>
<div class="control">
<div class="field has-addons">
<p class="control">
<input
type="text"
class={showError ? 'input is-danger' : 'input'}
value={value}
onChange={(e) => {
const text = e.currentTarget.value
setDirty(true)
props.bind[1](text);
}}
ref={inputRef} />
</p>
<p class="control">
<a class="button" onClick={() => { setOpened(true) }}>
<span class="icon"><i class="mdi mdi-calendar" /></span>
</a>
</p>
</div>
</div>
<p class="help">Using the format yyyy-mm-dd</p>
{showError && <p class="help is-danger">{props.error}</p>}
<DatePicker
opened={opened}
initialDate={calendar}
years={props.years}
closeFunction={() => setOpened(false)}
dateReceiver={(d) => {
setDirty(true)
const v = format(d, 'yyyy/MM/dd')
const v = format(d, 'yyyy-MM-dd')
props.bind[1](v);
}}
/>

View File

@ -49,7 +49,7 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
</a>
<div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
<LangSelector />
{/* <LangSelector /> */}
</div>
</div>
</div>

View File

@ -39,9 +39,9 @@ export function Sidebar({ mobile }: Props): VNode {
return (
<aside class="aside is-placed-left is-expanded">
{mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}>
{/* {mobile && <div class="footer" onClick={(e) => { return e.stopImmediatePropagation() }}>
<LangSelector />
</div>}
</div>} */}
<div class="aside-tools">
<div class="aside-tools-label">
<div><b>Anastasis</b> Reducer</div>
@ -68,7 +68,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li class={reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
reducer.currentReducerState.backup_state === BackupStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Location &amp; Currency</Translate></span>
<span class="menu-item-label"><Translate>Location</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}>
@ -85,7 +85,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Policies reviewing</Translate></span>
<span class="menu-item-label"><Translate>Policies</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>
@ -94,12 +94,12 @@ export function Sidebar({ mobile }: Props): VNode {
<span class="menu-item-label"><Translate>Secret input</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Payment (optional)</Translate></span>
</div>
</li>
</li> */}
<li class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>
<div class="ml-4">
@ -116,7 +116,7 @@ export function Sidebar({ mobile }: Props): VNode {
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting ||
reducer.currentReducerState.recovery_state === RecoveryStates.CountrySelecting ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Location &amp; Currency</Translate></span>
<span class="menu-item-label"><Translate>Location</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>

View File

@ -24,6 +24,7 @@ import { h, Component } from "preact";
interface Props {
closeFunction?: () => void;
dateReceiver?: (d: Date) => void;
initialDate?: Date;
years?: Array<number>;
opened?: boolean;
}
@ -213,8 +214,8 @@ export class DatePicker extends Component<Props, State> {
// }
}
constructor() {
super();
constructor(props) {
super(props);
this.closeDatePicker = this.closeDatePicker.bind(this);
this.dayClicked = this.dayClicked.bind(this);
@ -226,11 +227,12 @@ export class DatePicker extends Component<Props, State> {
this.toggleYearSelector = this.toggleYearSelector.bind(this);
this.displaySelectedMonth = this.displaySelectedMonth.bind(this);
const initial = props.initialDate || now;
this.state = {
currentDate: now,
displayedMonth: now.getMonth(),
displayedYear: now.getFullYear(),
currentDate: initial,
displayedMonth: initial.getMonth(),
displayedYear: initial.getFullYear(),
selectYearMode: false
}
}

View File

@ -0,0 +1,77 @@
/*
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 { useState } from "preact/hooks";
// import { cancelPendingRequest } from "./backend";
export interface Options {
slowTolerance: number;
}
export interface AsyncOperationApi<T> {
request: (...a: any) => void;
cancel: () => void;
data: T | undefined;
isSlow: boolean;
isLoading: boolean;
error: string | undefined;
}
export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }): AsyncOperationApi<T> {
const [data, setData] = useState<T | undefined>(undefined);
const [isLoading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(undefined);
const [isSlow, setSlow] = useState(false)
const request = async (...args: any) => {
if (!fn) return;
setLoading(true);
console.log("loading true")
const handler = setTimeout(() => {
setSlow(true)
}, tooLong)
try {
const result = await fn(...args);
console.log(result)
setData(result);
} catch (error) {
setError(error);
}
setLoading(false);
setSlow(false)
clearTimeout(handler)
};
function cancel() {
// cancelPendingRequest()
setLoading(false);
setSlow(false)
}
return {
request,
cancel,
data,
isSlow,
isLoading,
error
};
}

View File

@ -52,8 +52,8 @@ export const Backup = createExample(TestedComponent, {
uuid: 'asdasdsa2',
widget: 'wid',
}, {
name: 'date',
label: 'third',
name: 'birthdate',
label: 'birthdate',
type: 'date',
uuid: 'asdasdsa3',
widget: 'calendar',

View File

@ -7,6 +7,7 @@ import { AnastasisClientFrame, withProcessLabel } from "./index";
import { TextInput } from "../../components/fields/TextInput";
import { DateInput } from "../../components/fields/DateInput";
import { NumberInput } from "../../components/fields/NumberInput";
import { isAfter, parse } from "date-fns";
export function AttributeEntryScreen(): VNode {
const reducer = useAnastasisContext()
@ -46,15 +47,14 @@ export function AttributeEntryScreen(): VNode {
identity_attributes: attrs,
})}
>
<div class="columns">
<div class="column is-half">
<div class="columns" style={{ maxWidth: 'unset' }}>
<div class="column is-one-third">
{fieldList}
</div>
<div class="column is-half" >
<div class="column is-two-third" >
<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>
<h1 class="title">This stays private</h1>
<p>The information you have entered here:</p>
<ul>
<li>
<span class="icon is-right">
@ -111,15 +111,17 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
bind={[props.value, props.setValue]}
/>
}
<span>
<div class="block">
This stays private
<span class="icon is-right">
<i class="mdi mdi-eye-off" />
</span>
This stay private
</span>
</div>
</div>
);
}
const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/
function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined {
const pattern = spec['validation-regex']
@ -136,5 +138,22 @@ function checkIfValid(value: string, spec: UserAttributeSpec): string | undefine
if (!optional && !value) {
return 'This value is required'
}
if ("date" === spec.type) {
if (!YEAR_REGEX.test(value)) {
return "The date doesn't follow the format"
}
try {
const v = parse(value, 'yyyy-MM-dd', new Date());
if (Number.isNaN(v.getTime())) {
return "Some numeric values seems out of range for a date"
}
if ("birthdate" === spec.name && isAfter(v, new Date())) {
return "A birthdate cannot be in the future"
}
} catch (e) {
return "Could not parse the date"
}
}
return undefined
}

View File

@ -142,6 +142,10 @@ export function AuthenticationEditorScreen(): VNode {
</div>
<div class="column is-half">
When recovering your wallet, you will be asked to verify your identity via the methods you configure here.
<b>Explain the exclamation marks</b>
<a>Explain how to add providers</a>
</div>
</div>
</AnastasisClientFrame>

View File

@ -23,7 +23,7 @@ export function BackupFinishedScreen(): VNode {
</p>}
{details && <div class="block">
<p>The backup is stored by the following providers:</p>
<p>The backup is stored by the following providers:</p>
{Object.keys(details).map((x, i) => {
const sd = details[x];
return (
@ -31,11 +31,14 @@ export function BackupFinishedScreen(): VNode {
{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'}
{sd.policy_expiration.t_ms !== 'never' ? ` expires at: ${format(sd.policy_expiration.t_ms, 'dd-MM-yyyy')}` : ' without expiration date'}
</p>
</div>
);
})}
</div>}
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
</div>
</AnastasisClientFrame>);
}

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/camelcase */
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
@ -19,12 +20,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen';
export default {
title: 'Pages/ContinentSelectionScreen',
title: 'Pages/Location',
component: TestedComponent,
args: {
order: 2,
@ -35,6 +37,16 @@ export default {
},
};
export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent);
export const BackupSelectCountry = createExample(TestedComponent, {
...reducerStatesExample.backupSelectContinent,
selected_continent: 'Testcontinent',
} as ReducerState);
export const RecoverySelectContinent = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent);
export const RecoverySelectCountry = createExample(TestedComponent, {
...reducerStatesExample.recoverySelectContinent,
selected_continent: 'Testcontinent',
} as ReducerState);

View File

@ -36,20 +36,21 @@ export function ContinentSelectionScreen(): VNode {
})
}
const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
// const step1 = reducer.currentReducerState.backup_state === BackupStates.ContinentSelecting ||
// reducer.currentReducerState.recovery_state === RecoveryStates.ContinentSelecting;
const errors = !theCountry ? "Select a country" : undefined
return (
<AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Select location")} onNext={selectCountryAction}>
<div class="columns">
<div class="column is-half">
<AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Where do you live?")} onNext={selectCountryAction}>
<div class="columns" >
<div class="column is-one-third">
<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}>
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth" >
<select onChange={(e) => selectContinent(e.currentTarget.value)} value={theContinent} >
<option key="none" disabled selected value=""> Choose a continent </option>
{continentList.map(prov => (
<option key={prov.name} value={prov.name}>
@ -61,18 +62,13 @@ export function ContinentSelectionScreen(): VNode {
<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" >
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth" >
<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 => (
@ -88,17 +84,17 @@ export function ContinentSelectionScreen(): VNode {
</div>
</div>
{theCountry && <div class="field">
{/* {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>
<div class="column is-half">
<div class="column is-two-third">
<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.
Your location will help us to determine which personal information
ask you for the next step.
</p>
</div>
</div>

View File

@ -1,39 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../utils';
import { CountrySelectionScreen as TestedComponent } from './CountrySelectionScreen';
export default {
title: 'Pages/CountrySelectionScreen',
component: TestedComponent,
args: {
order: 3,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
},
};
export const Backup = createExample(TestedComponent, reducerStatesExample.backupSelectCountry);
export const Recovery = createExample(TestedComponent, reducerStatesExample.recoverySelectCountry);

View File

@ -1,31 +0,0 @@
/* eslint-disable @typescript-eslint/camelcase */
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame, withProcessLabel } from "./index";
export function CountrySelectionScreen(): VNode {
const reducer = useAnastasisContext()
if (!reducer) {
return <div>no reducer in context</div>
}
if (!reducer.currentReducerState || !("countries" in reducer.currentReducerState)) {
return <div>invalid state</div>
}
const sel = (x: any): void => reducer.transition("select_country", {
country_code: x.code,
currencies: [x.currency],
});
return (
<AnastasisClientFrame hideNext={"FIXME"} title={withProcessLabel(reducer, "Select Country")} >
<div style={{ display: 'flex', flexDirection: 'column' }}>
{reducer.currentReducerState.countries!.map((x: any) => (
<div key={x.name}>
<button class="button" onClick={() => sel(x)} >
{x.name} ({x.currency})
</button>
</div>
))}
</div>
</AnastasisClientFrame>
);
}

View File

@ -0,0 +1,109 @@
/* 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 { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { EditPoliciesScreen as TestedComponent } from './EditPoliciesScreen';
export default {
title: 'Pages/backup/ReviewPoliciesScreen/EditPoliciesScreen',
args: {
order: 6,
},
component: TestedComponent,
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
},
};
export const EditingAPolicy = createExample(TestedComponent, {
...reducerStatesExample.policyReview,
policies: [{
methods: [{
authentication_method: 1,
provider: 'https://anastasis.demo.taler.net/'
}, {
authentication_method: 2,
provider: 'http://localhost:8086/'
}]
}, {
methods: [{
authentication_method: 1,
provider: 'http://localhost:8086/'
}]
}],
authentication_methods: [{
type: "email",
instructions: "Email to qwe@asd.com",
challenge: "E5VPA"
}, {
type: "totp",
instructions: "Response code for 'Anastasis'",
challenge: "E5VPA"
}, {
type: "sms",
instructions: "SMS to 6666-6666",
challenge: ""
}, {
type: "question",
instructions: "How did the chicken cross the road?",
challenge: "C5SP8"
}]
} as ReducerState, { index : 0});
export const CreatingAPolicy = createExample(TestedComponent, {
...reducerStatesExample.policyReview,
policies: [{
methods: [{
authentication_method: 1,
provider: 'https://anastasis.demo.taler.net/'
}, {
authentication_method: 2,
provider: 'http://localhost:8086/'
}]
}, {
methods: [{
authentication_method: 1,
provider: 'http://localhost:8086/'
}]
}],
authentication_methods: [{
type: "email",
instructions: "Email to qwe@asd.com",
challenge: "E5VPA"
}, {
type: "totp",
instructions: "Response code for 'Anastasis'",
challenge: "E5VPA"
}, {
type: "sms",
instructions: "SMS to 6666-6666",
challenge: ""
}, {
type: "question",
instructions: "How did the chicken cross the road?",
challenge: "C5SP8"
}]
} as ReducerState, { index : 3});

View File

@ -0,0 +1,133 @@
/* eslint-disable @typescript-eslint/camelcase */
import { AuthMethod, Policy } from "anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod";
import { AnastasisClientFrame } from "./index";
export interface ProviderInfo {
url: string;
cost: string;
isFree: boolean;
}
export type ProviderInfoByType = {
[type in KnownAuthMethods]?: ProviderInfo[];
};
interface Props {
index: number;
cancel: () => void;
confirm: (changes: MethodProvider[]) => void;
}
export interface MethodProvider {
authentication_method: number;
provider: string;
}
export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Props): VNode {
const [changedProvider, setChangedProvider] = useState<Array<string>>([])
const reducer = useAnastasisContext()
if (!reducer) {
return <div>no reducer in context</div>
}
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div>
}
const selectableProviders: ProviderInfoByType = {}
const allProviders = Object.entries(reducer.currentReducerState.authentication_providers || {})
for (let index = 0; index < allProviders.length; index++) {
const [url, status] = allProviders[index]
if ("methods" in status) {
status.methods.map(m => {
const type: KnownAuthMethods = m.type as KnownAuthMethods
const values = selectableProviders[type] || []
const isFree = !m.usage_fee || m.usage_fee.endsWith(":0")
values.push({ url, cost: m.usage_fee, isFree })
selectableProviders[type] = values
})
}
}
const allAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
const policy = policies[policy_index]
for(let method_index = 0; method_index < allAuthMethods.length; method_index++ ) {
policy?.methods.find(m => m.authentication_method === method_index)?.provider
}
function sendChanges(): void {
const newMethods: MethodProvider[] = []
allAuthMethods.forEach((method, index) => {
const oldValue = policy?.methods.find(m => m.authentication_method === index)
if (changedProvider[index] === undefined && oldValue !== undefined) {
newMethods.push(oldValue)
}
if (changedProvider[index] !== undefined && changedProvider[index] !== "") {
newMethods.push({
authentication_method: index,
provider: changedProvider[index]
})
}
})
confirm(newMethods)
}
return <AnastasisClientFrame hideNav title={!policy ? "Backup: New Policy" : "Backup: Edit Policy"}>
<section class="section">
{!policy ? <p>
Creating a new policy #{policy_index}
</p> : <p>
Editing policy #{policy_index}
</p>}
{allAuthMethods.map((method, index) => {
//take the url from the updated change or from the policy
const providerURL = changedProvider[index] === undefined ?
policy?.methods.find(m => m.authentication_method === index)?.provider :
changedProvider[index];
const type: KnownAuthMethods = method.type as KnownAuthMethods
function changeProviderTo(url: string): void {
const copy = [...changedProvider]
copy[index] = url
setChangedProvider(copy)
}
return (
<div key={index} class="block" style={{ display: 'flex', alignItems: 'center' }}>
<span class="icon">
{authMethods[type]?.icon}
</span>
<span>
{method.instructions}
</span>
<span>
<span class="select " >
<select onChange={(e) => changeProviderTo(e.currentTarget.value)} value={providerURL ?? ""}>
<option key="none" value=""> &lt;&lt; off &gt;&gt; </option>
{selectableProviders[type]?.map(prov => (
<option key={prov.url} value={prov.url}>
{prov.url}
</option>
))}
</select>
</span>
</span>
</div>
);
})}
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={cancel}>Cancel</button>
<span class="buttons">
<button class="button" onClick={() => setChangedProvider([])}>Reset</button>
<button class="button is-info" onClick={sendChanges}>Confirm</button>
</span>
</div>
</section>
</AnastasisClientFrame>
}

View File

@ -233,16 +233,16 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, {
instructions: "Does P equal NP?",
challenge: "C5SP8"
},{
type: "email",
instructions: "Email to qwe@asd.com",
type: "totp",
instructions: "Response code for 'Anastasis'",
challenge: "E5VPA"
}, {
type: "sms",
instructions: "SMS to 555-555",
instructions: "SMS to 6666-6666",
challenge: ""
}, {
type: "question",
instructions: "Does P equal NP?",
instructions: "How did the chicken cross the road?",
challenge: "C5SP8"
}]
} as ReducerState);

View File

@ -1,10 +1,14 @@
/* eslint-disable @typescript-eslint/camelcase */
import { AuthMethod } from "anastasis-core";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
import { authMethods, KnownAuthMethods } from "./authMethod";
import { EditPoliciesScreen } from "./EditPoliciesScreen";
import { AnastasisClientFrame } from "./index";
export function ReviewPoliciesScreen(): VNode {
const [editingPolicy, setEditingPolicy] = useState<number | undefined>()
const reducer = useAnastasisContext()
if (!reducer) {
return <div>no reducer in context</div>
@ -12,20 +16,44 @@ export function ReviewPoliciesScreen(): VNode {
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div>
}
const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
if (editingPolicy !== undefined) {
return (
<EditPoliciesScreen
index={editingPolicy}
cancel={() => setEditingPolicy(undefined)}
confirm={(newMethods) => {
reducer.runTransaction(async (tx) => {
await tx.transition("delete_policy", {
policy_index: editingPolicy
});
await tx.transition("add_policy", {
policy: newMethods
});
});
setEditingPolicy(undefined)
}}
/>
)
}
const errors = policies.length < 1 ? 'Need more policies' : undefined
return (
<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
have been configured. In order to recover your secret you have to solve all the
challenges of at least one policy.
</p> }
</p>}
{policies.length < 1 && <p class="block">
No policies had been created. Go back and add more authentication methods.
</p> }
</p>}
<div class="block" onClick={() => setEditingPolicy(policies.length + 1)}>
<button class="button is-success">Add new policy</button>
</div>
{policies.map((p, policy_index) => {
const methods = p.methods
.map(x => configuredAuthMethods[x.authentication_method] && ({ ...configuredAuthMethods[x.authentication_method], provider: x.provider }))
@ -44,18 +72,21 @@ export function ReviewPoliciesScreen(): VNode {
</p>}
{methods.map((m, i) => {
return (
<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>
<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>
);
})}
</div>
<div style={{ marginTop: 'auto', marginBottom: 'auto' }}><button class="button is-danger" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button></div>
<div style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}>
<button class="button is-info block" onClick={() => setEditingPolicy(policy_index)}>Edit</button>
<button class="button is-danger block" onClick={() => reducer.transition("delete_policy", { policy_index })}>Delete</button>
</div>
</div>
);
})}

View File

@ -10,33 +10,29 @@ export function StartScreen(): VNode {
}
return (
<AnastasisClientFrame hideNav title="Home">
<div>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
<div class="buttons">
<button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
<div class="icon"><i class="mdi mdi-arrow-up" /></div>
<span>Backup a secret</span>
</button>
<div class="buttons">
<button class="button is-success" autoFocus onClick={() => reducer.startBackup()}>
<div class="icon"><i class="mdi mdi-arrow-up" /></div>
<span>Backup a secret</span>
</button>
<button class="button is-info" onClick={() => reducer.startRecover()}>
<div class="icon"><i class="mdi mdi-arrow-down" /></div>
<span>Recover a secret</span>
</button>
<button class="button is-info" onClick={() => reducer.startRecover()}>
<div class="icon"><i class="mdi mdi-arrow-down" /></div>
<span>Recover a secret</span>
</button>
<button class="button">
<div class="icon"><i class="mdi mdi-file" /></div>
<span>Restore a session</span>
</button>
</div>
</div>
<div class="column" />
<button class="button">
<div class="icon"><i class="mdi mdi-file" /></div>
<span>Restore a session</span>
</button>
</div>
</section>
</div>
<div class="column" />
</div>
</AnastasisClientFrame>
);

View File

@ -27,7 +27,7 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
<AnastasisClientFrame hideNav title="Add Security Question">
<div>
<p>
For security question authentication, you need to provide a question
For2 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.
@ -47,6 +47,13 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
/>
</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={addQuestionAuth}>Add</button>
</span>
</div>
{configured.length > 0 && <section class="section">
<div class="block">
Your security questions:
@ -58,12 +65,6 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
</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 >
);

View File

@ -13,6 +13,7 @@ import {
import {
useErrorBoundary
} from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton";
import { Menu } from "../../components/menu";
import { AnastasisProvider, useAnastasisContext } from "../../context/anastasis";
import {
@ -25,7 +26,6 @@ import { BackupFinishedScreen } from "./BackupFinishedScreen";
import { ChallengeOverviewScreen } from "./ChallengeOverviewScreen";
import { ChallengePayingScreen } from "./ChallengePayingScreen";
import { ContinentSelectionScreen } from "./ContinentSelectionScreen";
import { CountrySelectionScreen } from "./CountrySelectionScreen";
import { PoliciesPayingScreen } from "./PoliciesPayingScreen";
import { RecoveryFinishedScreen } from "./RecoveryFinishedScreen";
import { ReviewPoliciesScreen } from "./ReviewPoliciesScreen";
@ -95,12 +95,19 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
if (!reducer) {
return <p>Fatal: Reducer must be in context.</p>;
}
const next = (): void => {
if (props.onNext) {
props.onNext();
} else {
reducer.transition("next", {});
}
const next = async (): Promise<void> => {
return new Promise((res, rej) => {
try {
if (props.onNext) {
props.onNext();
} else {
reducer.transition("next", {});
}
res()
} catch {
rej()
}
})
};
const handleKeyPress = (
e: h.JSX.TargetedKeyboardEvent<HTMLDivElement>,
@ -111,20 +118,18 @@ export function AnastasisClientFrame(props: AnastasisClientFrameProps): VNode {
return (
<Fragment>
<Menu title="Anastasis" />
<div>
<div class="home" onKeyPress={(e) => handleKeyPress(e)}>
<h1 class="title">{props.title}</h1>
<div class="home" onKeyPress={(e) => handleKeyPress(e)}>
<h1 class="title">{props.title}</h1>
<section class="section is-main-section">
<ErrorBanner />
{props.children}
{!props.hideNav ? (
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<span data-tooltip={props.hideNext}>
<button class="button is-info" onClick={next} disabled={props.hideNext !== undefined}>Next</button>
</span>
<AsyncButton data-tooltip={props.hideNext} onClick={next} disabled={props.hideNext !== undefined}>Next</AsyncButton>
</div>
) : null}
</div>
</section>
</div>
</Fragment>
);

View File

@ -195,7 +195,7 @@ div[data-tooltip]::before {
padding: 1em 1em;
min-height: 100%;
width: 100%;
max-width: 40em;
// max-width: 40em;
}
// .home div {

View File

@ -86,10 +86,10 @@ const base = {
{
type: "question",
usage_fee: "COL:0"
},{
}, {
type: "sms",
usage_fee: "COL:0"
},{
}, {
type: "email",
usage_fee: "COL:0"
},
@ -98,6 +98,48 @@ const base = {
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0"
},
"https://kudos.demo.anastasis.lu/": {
http_status: 200,
annual_fee: "COL:0",
business_name: "ana",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
usage_fee: "COL:0"
}, {
type: "email",
usage_fee: "COL:0"
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0"
},
"https://anastasis.demo.taler.net/": {
http_status: 200,
annual_fee: "COL:0",
business_name: "ana",
currency: "COL",
liability_limit: "COL:10",
methods: [
{
type: "question",
usage_fee: "COL:0"
}, {
type: "sms",
usage_fee: "COL:0"
}, {
type: "totp",
usage_fee: "COL:0"
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0"
},
"http://localhost:8087/": {
code: 8414,
hint: "request to provider failed"
@ -118,55 +160,72 @@ const base = {
export const reducerStatesExample = {
initial: undefined,
recoverySelectCountry: {...base,
recoverySelectCountry: {
...base,
recovery_state: RecoveryStates.CountrySelecting
} as ReducerState,
recoverySelectContinent: {...base,
recoverySelectContinent: {
...base,
recovery_state: RecoveryStates.ContinentSelecting,
} as ReducerState,
secretSelection: {...base,
secretSelection: {
...base,
recovery_state: RecoveryStates.SecretSelecting,
} as ReducerState,
recoveryFinished: {...base,
recoveryFinished: {
...base,
recovery_state: RecoveryStates.RecoveryFinished,
} as ReducerState,
challengeSelecting: {...base,
challengeSelecting: {
...base,
recovery_state: RecoveryStates.ChallengeSelecting,
} as ReducerState,
challengeSolving: {...base,
challengeSolving: {
...base,
recovery_state: RecoveryStates.ChallengeSolving,
} as ReducerState,
challengePaying: {...base,
challengePaying: {
...base,
recovery_state: RecoveryStates.ChallengePaying,
} as ReducerState,
recoveryAttributeEditing: {...base,
recoveryAttributeEditing: {
...base,
recovery_state: RecoveryStates.UserAttributesCollecting
} as ReducerState,
backupSelectCountry: {...base,
backupSelectCountry: {
...base,
backup_state: BackupStates.CountrySelecting
} as ReducerState,
backupSelectContinent: {...base,
backupSelectContinent: {
...base,
backup_state: BackupStates.ContinentSelecting,
} as ReducerState,
secretEdition: {...base,
secretEdition: {
...base,
backup_state: BackupStates.SecretEditing,
} as ReducerState,
policyReview: {...base,
policyReview: {
...base,
backup_state: BackupStates.PoliciesReviewing,
} as ReducerState,
policyPay: {...base,
policyPay: {
...base,
backup_state: BackupStates.PoliciesPaying,
} as ReducerState,
backupFinished: {...base,
backupFinished: {
...base,
backup_state: BackupStates.BackupFinished,
} as ReducerState,
authEditing: {...base,
authEditing: {
...base,
backup_state: BackupStates.AuthenticationsEditing
} as ReducerState,
backupAttributeEditing: {...base,
backupAttributeEditing: {
...base,
backup_state: BackupStates.UserAttributesCollecting
} as ReducerState,
truthsPaying: {...base,
truthsPaying: {
...base,
backup_state: BackupStates.TruthsPaying
} as ReducerState,