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">
<div class="control">
<div class="field has-addons">
<p class="control">
<input
type="text"
class={showError ? 'input is-danger' : 'input'}
readonly
onFocus={() => { setOpened(true) } }
value={value}
onChange={(e) => {
const text = e.currentTarget.value
setDirty(true)
props.bind[1](text);
}}
ref={inputRef} />
<span class="control icon is-right">
</p>
<p class="control">
<a class="button" onClick={() => { setOpened(true) }}>
<span class="icon"><i class="mdi mdi-calendar" /></span>
</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

@ -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}>
<AnastasisClientFrame hideNext={errors} title={withProcessLabel(reducer, "Where do you live?")} onNext={selectCountryAction}>
<div class="columns" >
<div class="column is-half">
<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,9 +16,30 @@ 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">
@ -26,6 +51,9 @@ export function ReviewPoliciesScreen(): VNode {
{policies.length < 1 && <p class="block">
No policies had been created. Go back and add more authentication methods.
</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 }))
@ -55,7 +83,10 @@ export function ReviewPoliciesScreen(): VNode {
);
})}
</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,8 +10,6 @@ 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">
@ -36,8 +34,6 @@ export function StartScreen(): VNode {
</div>
<div class="column" />
</div>
</section>
</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 => {
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>
<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

@ -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,