This commit is contained in:
Sebastian 2021-11-10 10:20:52 -03:00
parent e03b0d1b9b
commit a62deeef5d
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
92 changed files with 4214 additions and 2805 deletions

View File

@ -10,6 +10,7 @@
"devDependencies": {
"@linaria/esbuild": "^3.0.0-beta.13",
"@linaria/shaker": "^3.0.0-beta.13",
"esbuild": "^0.12.29"
"esbuild": "^0.12.29",
"prettier": "^2.2.1"
}
}

View File

@ -5,11 +5,14 @@
"license": "MIT",
"scripts": {
"build": "preact build --no-sw --no-esm",
"serve": "sirv build --port 8080 --cors --single",
"dev": "preact watch --no-sw --no-esm",
"serve": "sirv build --port ${PORT:=8080} --cors --single",
"dev": "preact watch --port ${PORT:=8080} --no-sw --no-esm",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
"test": "jest ./tests",
"build-storybook": "build-storybook",
"build-single": "preact build --no-sw --no-esm -c preact.single-config.js --dest single && sh remove-link-stylesheet.sh",
"serve-single": "sirv single --port ${PORT:=8080} --cors --single",
"pretty": "prettier --write src",
"storybook": "start-storybook -p 6006"
},
"eslintConfig": {
@ -25,6 +28,7 @@
"dependencies": {
"@gnu-taler/taler-util": "workspace:^0.8.3",
"anastasis-core": "workspace:^0.0.1",
"base64-inline-loader": "1.1.1",
"date-fns": "2.25.0",
"jed": "1.1.1",
"preact": "^10.5.15",

View File

@ -1,5 +1,3 @@
{
"presets": [
"preact-cli/babel"
]
"presets": ["preact-cli/babel"]
}

View File

@ -15,9 +15,9 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ComponentChildren, h, VNode } from "preact";
// import { LoadingModal } from "../modal";
@ -31,7 +31,12 @@ type Props = {
[rest: string]: any;
};
export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VNode {
export function AsyncButton({
onClick,
disabled,
children,
...rest
}: Props): VNode {
const { isLoading, request } = useAsync(onClick);
// if (isSlow) {
@ -41,9 +46,11 @@ export function AsyncButton({ onClick, disabled, children, ...rest }: Props): VN
return <button class="button">Loading...</button>;
}
return <span data-tooltip={rest['data-tooltip']} style={{marginLeft: 5}}>
<button {...rest} onClick={request} disabled={disabled}>
{children}
</button>
</span>;
return (
<span data-tooltip={rest["data-tooltip"]} style={{ marginLeft: 5 }}>
<button {...rest} onClick={request} disabled={disabled}>
{children}
</button>
</span>
);
}

View File

@ -15,9 +15,9 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
@ -27,7 +27,7 @@ export interface Notification {
type: MessageType;
}
export type MessageType = 'INFO' | 'WARN' | 'ERROR' | 'SUCCESS'
export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS";
interface Props {
notifications: Notification[];
@ -36,24 +36,39 @@ interface Props {
function messageStyle(type: MessageType): string {
switch (type) {
case "INFO": return "message is-info";
case "WARN": return "message is-warning";
case "ERROR": return "message is-danger";
case "SUCCESS": return "message is-success";
default: return "message"
case "INFO":
return "message is-info";
case "WARN":
return "message is-warning";
case "ERROR":
return "message is-danger";
case "SUCCESS":
return "message is-success";
default:
return "message";
}
}
export function Notifications({ notifications, removeNotification }: Props): VNode {
return <div class="block">
{notifications.map((n, i) => <article key={i} class={messageStyle(n.type)}>
<div class="message-header">
<p>{n.message}</p>
{removeNotification && <button class="delete" onClick={() => removeNotification && removeNotification(n)} />}
</div>
{n.description && <div class="message-body">
{n.description}
</div>}
</article>)}
</div>
export function Notifications({
notifications,
removeNotification,
}: Props): VNode {
return (
<div class="block">
{notifications.map((n, i) => (
<article key={i} class={messageStyle(n.type)}>
<div class="message-header">
<p>{n.message}</p>
{removeNotification && (
<button
class="delete"
onClick={() => removeNotification && removeNotification(n)}
/>
)}
</div>
{n.description && <div class="message-body">{n.description}</div>}
</article>
))}
</div>
);
}

View File

@ -21,15 +21,28 @@ import qrcode from "qrcode-generator";
export function QR({ text }: { text: string }): VNode {
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const qr = qrcode(0, 'L');
const qr = qrcode(0, "L");
qr.addData(text);
qr.make();
if (divRef.current) divRef.current.innerHTML = qr.createSvgTag({
scalable: true,
});
if (divRef.current)
divRef.current.innerHTML = qr.createSvgTag({
scalable: true,
});
});
return <div style={{ width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{ width: '50%', minWidth: 200, maxWidth: 300 }} ref={divRef} />
</div>;
return (
<div
style={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<div
style={{ width: "50%", minWidth: 200, maxWidth: 300 }}
ref={divRef}
/>
</div>
);
}

View File

@ -1,6 +1,5 @@
import { FunctionalComponent, h } from "preact";
import { TranslationProvider } from "../context/translation";
import AnastasisClient from "../pages/home";
const App: FunctionalComponent = () => {

View File

@ -19,56 +19,66 @@ export function DateInput(props: DateInputProps): VNode {
inputRef.current?.focus();
}
}, [props.grabFocus]);
const [opened, setOpened] = useState(false)
const [opened, setOpened] = useState(false);
const value = props.bind[0] || "";
const [dirty, setDirty] = useState(false)
const showError = dirty && props.error
const [dirty, setDirty] = useState(false);
const showError = dirty && props.error;
const calendar = subYears(new Date(), 30)
const calendar = subYears(new Date(), 30);
return <div class="field">
<label class="label">
{props.label}
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
<div class="control">
<div class="field has-addons">
<p class="control">
<input
type="text"
class={showError ? 'input is-danger' : 'input'}
value={value}
onInput={(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>
return (
<div class="field">
<label class="label">
{props.label}
{props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>
)}
</label>
<div class="control">
<div class="field has-addons">
<p class="control">
<input
type="text"
class={showError ? "input is-danger" : "input"}
value={value}
onInput={(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");
props.bind[1](v);
}}
/>
</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')
props.bind[1](v);
}}
/>
</div>
;
);
}

View File

@ -18,27 +18,34 @@ export function EmailInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
const value = props.bind[0];
const [dirty, setDirty] = useState(false)
const showError = dirty && props.error
return (<div class="field">
<label class="label">
{props.label}
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
<div class="control has-icons-right">
<input
value={value}
required
placeholder={props.placeholder}
type="email"
class={showError ? 'input is-danger' : 'input'}
onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
ref={inputRef}
style={{ display: "block" }} />
const [dirty, setDirty] = useState(false);
const showError = dirty && props.error;
return (
<div class="field">
<label class="label">
{props.label}
{props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>
)}
</label>
<div class="control has-icons-right">
<input
value={value}
required
placeholder={props.placeholder}
type="email"
class={showError ? "input is-danger" : "input"}
onInput={(e) => {
setDirty(true);
props.bind[1]((e.target as HTMLInputElement).value);
}}
ref={inputRef}
style={{ display: "block" }}
/>
</div>
{showError && <p class="help is-danger">{props.error}</p>}
</div>
{showError && <p class="help is-danger">{props.error}</p>}
</div>
);
}

View File

@ -15,14 +15,14 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { TextInputProps } from "./TextInput";
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
export function FileInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
@ -34,48 +34,54 @@ export function FileInput(props: TextInputProps): VNode {
const value = props.bind[0];
// const [dirty, setDirty] = useState(false)
const image = useRef<HTMLInputElement>(null)
const [sizeError, setSizeError] = useState(false)
const image = useRef<HTMLInputElement>(null);
const [sizeError, setSizeError] = useState(false);
function onChange(v: string): void {
// setDirty(true);
props.bind[1](v);
}
return <div class="field">
<label class="label">
<a onClick={() => image.current?.click()}>
{props.label}
</a>
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
<div class="control">
<input
ref={image} style={{ display: 'none' }}
type="file" name={String(name)}
onChange={e => {
const f: FileList | null = e.currentTarget.files
if (!f || f.length != 1) {
return onChange("")
}
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
setSizeError(true)
return onChange("")
}
setSizeError(false)
return f[0].arrayBuffer().then(b => {
const b64 = btoa(
new Uint8Array(b)
.reduce((data, byte) => data + String.fromCharCode(byte), '')
)
return onChange(`data:${f[0].type};base64,${b64}` as any)
})
}} />
{props.error && <p class="help is-danger">{props.error}</p>}
{sizeError && <p class="help is-danger">
File should be smaller than 1 MB
</p>}
return (
<div class="field">
<label class="label">
<a onClick={() => image.current?.click()}>{props.label}</a>
{props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>
)}
</label>
<div class="control">
<input
ref={image}
style={{ display: "none" }}
type="file"
name={String(name)}
onChange={(e) => {
const f: FileList | null = e.currentTarget.files;
if (!f || f.length != 1) {
return onChange("");
}
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
setSizeError(true);
return onChange("");
}
setSizeError(false);
return f[0].arrayBuffer().then((b) => {
const b64 = btoa(
new Uint8Array(b).reduce(
(data, byte) => data + String.fromCharCode(byte),
"",
),
);
return onChange(`data:${f[0].type};base64,${b64}` as any);
});
}}
/>
{props.error && <p class="help is-danger">{props.error}</p>}
{sizeError && (
<p class="help is-danger">File should be smaller than 1 MB</p>
)}
</div>
</div>
</div>
);
}

View File

@ -15,15 +15,15 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import emptyImage from "../../assets/empty.png";
import { TextInputProps } from "./TextInput";
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024
const MAX_IMAGE_UPLOAD_SIZE = 1024 * 1024;
export function ImageInput(props: TextInputProps): VNode {
const inputRef = useRef<HTMLInputElement>(null);
@ -35,47 +35,59 @@ export function ImageInput(props: TextInputProps): VNode {
const value = props.bind[0];
// const [dirty, setDirty] = useState(false)
const image = useRef<HTMLInputElement>(null)
const [sizeError, setSizeError] = useState(false)
const image = useRef<HTMLInputElement>(null);
const [sizeError, setSizeError] = useState(false);
function onChange(v: string): void {
// setDirty(true);
props.bind[1](v);
}
return <div class="field">
<label class="label">
{props.label}
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
<div class="control">
<img src={!value ? emptyImage : value} style={{ width: 200, height: 200 }} onClick={() => image.current?.click()} />
<input
ref={image} style={{ display: 'none' }}
type="file" name={String(name)}
onChange={e => {
const f: FileList | null = e.currentTarget.files
if (!f || f.length != 1) {
return onChange(emptyImage)
}
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
setSizeError(true)
return onChange(emptyImage)
}
setSizeError(false)
return f[0].arrayBuffer().then(b => {
const b64 = btoa(
new Uint8Array(b)
.reduce((data, byte) => data + String.fromCharCode(byte), '')
)
return onChange(`data:${f[0].type};base64,${b64}` as any)
})
}} />
{props.error && <p class="help is-danger">{props.error}</p>}
{sizeError && <p class="help is-danger">
Image should be smaller than 1 MB
</p>}
return (
<div class="field">
<label class="label">
{props.label}
{props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>
)}
</label>
<div class="control">
<img
src={!value ? emptyImage : value}
style={{ width: 200, height: 200 }}
onClick={() => image.current?.click()}
/>
<input
ref={image}
style={{ display: "none" }}
type="file"
name={String(name)}
onChange={(e) => {
const f: FileList | null = e.currentTarget.files;
if (!f || f.length != 1) {
return onChange(emptyImage);
}
if (f[0].size > MAX_IMAGE_UPLOAD_SIZE) {
setSizeError(true);
return onChange(emptyImage);
}
setSizeError(false);
return f[0].arrayBuffer().then((b) => {
const b64 = btoa(
new Uint8Array(b).reduce(
(data, byte) => data + String.fromCharCode(byte),
"",
),
);
return onChange(`data:${f[0].type};base64,${b64}` as any);
});
}}
/>
{props.error && <p class="help is-danger">{props.error}</p>}
{sizeError && (
<p class="help is-danger">Image should be smaller than 1 MB</p>
)}
</div>
</div>
</div>
);
}

View File

@ -18,25 +18,32 @@ export function TextInput(props: TextInputProps): VNode {
}
}, [props.grabFocus]);
const value = props.bind[0];
const [dirty, setDirty] = useState(false)
const showError = dirty && props.error
return (<div class="field">
<label class="label">
{props.label}
{props.tooltip && <span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>}
</label>
<div class="control has-icons-right">
<input
value={value}
placeholder={props.placeholder}
class={showError ? 'input is-danger' : 'input'}
onInput={(e) => {setDirty(true); props.bind[1]((e.target as HTMLInputElement).value)}}
ref={inputRef}
style={{ display: "block" }} />
const [dirty, setDirty] = useState(false);
const showError = dirty && props.error;
return (
<div class="field">
<label class="label">
{props.label}
{props.tooltip && (
<span class="icon has-tooltip-right" data-tooltip={props.tooltip}>
<i class="mdi mdi-information" />
</span>
)}
</label>
<div class="control has-icons-right">
<input
value={value}
placeholder={props.placeholder}
class={showError ? "input is-danger" : "input"}
onInput={(e) => {
setDirty(true);
props.bind[1]((e.target as HTMLInputElement).value);
}}
ref={inputRef}
style={{ display: "block" }}
/>
</div>
{showError && <p class="help is-danger">{props.error}</p>}
</div>
{showError && <p class="help is-danger">{props.error}</p>}
</div>
);
}

View File

@ -15,59 +15,78 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import langIcon from '../../assets/icons/languageicon.svg';
import langIcon from "../../assets/icons/languageicon.svg";
import { useTranslationContext } from "../../context/translation";
import { strings as messages } from '../../i18n/strings'
import { strings as messages } from "../../i18n/strings";
type LangsNames = {
[P in keyof typeof messages]: string
}
[P in keyof typeof messages]: string;
};
const names: LangsNames = {
es: 'Español [es]',
en: 'English [en]',
fr: 'Français [fr]',
de: 'Deutsch [de]',
sv: 'Svenska [sv]',
it: 'Italiano [it]',
}
es: "Español [es]",
en: "English [en]",
fr: "Français [fr]",
de: "Deutsch [de]",
sv: "Svenska [sv]",
it: "Italiano [it]",
};
function getLangName(s: keyof LangsNames | string): string {
if (names[s]) return names[s]
return String(s)
if (names[s]) return names[s];
return String(s);
}
export function LangSelector(): VNode {
const [updatingLang, setUpdatingLang] = useState(false)
const { lang, changeLanguage } = useTranslationContext()
const [updatingLang, setUpdatingLang] = useState(false);
const { lang, changeLanguage } = useTranslationContext();
return <div class="dropdown is-active ">
<div class="dropdown-trigger">
<button class="button has-tooltip-left"
data-tooltip="change language selection"
aria-haspopup="true"
aria-controls="dropdown-menu" onClick={() => setUpdatingLang(!updatingLang)}>
<div class="icon is-small is-left">
<img src={langIcon} />
</div>
<span>{getLangName(lang)}</span>
<div class="icon is-right">
<i class="mdi mdi-chevron-down" />
</div>
</button>
</div>
{updatingLang && <div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
{Object.keys(messages)
.filter((l) => l !== lang)
.map(l => <a key={l} class="dropdown-item" value={l} onClick={() => { changeLanguage(l); setUpdatingLang(false) }}>{getLangName(l)}</a>)}
return (
<div class="dropdown is-active ">
<div class="dropdown-trigger">
<button
class="button has-tooltip-left"
data-tooltip="change language selection"
aria-haspopup="true"
aria-controls="dropdown-menu"
onClick={() => setUpdatingLang(!updatingLang)}
>
<div class="icon is-small is-left">
<img src={langIcon} />
</div>
<span>{getLangName(lang)}</span>
<div class="icon is-right">
<i class="mdi mdi-chevron-down" />
</div>
</button>
</div>
</div>}
</div>
{updatingLang && (
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
{Object.keys(messages)
.filter((l) => l !== lang)
.map((l) => (
<a
key={l}
class="dropdown-item"
value={l}
onClick={() => {
changeLanguage(l);
setUpdatingLang(false);
}}
>
{getLangName(l)}
</a>
))}
</div>
</div>
)}
</div>
);
}

View File

@ -15,16 +15,15 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { Fragment, h, VNode } from 'preact';
import { BackupStates, RecoveryStates } from '../../../../anastasis-core/lib';
import { useAnastasisContext } from '../../context/anastasis';
import { Translate } from '../../i18n';
import { LangSelector } from './LangSelector';
import { Fragment, h, VNode } from "preact";
import { BackupStates, RecoveryStates } from "../../../../anastasis-core/lib";
import { useAnastasisContext } from "../../context/anastasis";
import { Translate } from "../../i18n";
import { LangSelector } from "./LangSelector";
interface Props {
mobile?: boolean;
@ -32,10 +31,10 @@ interface Props {
export function Sidebar({ mobile }: Props): VNode {
// const config = useConfigContext();
const config = { version: 'none' }
const config = { version: "none" };
// FIXME: add replacement for __VERSION__ with the current version
const process = { env: { __VERSION__: '0.0.0' } }
const reducer = useAnastasisContext()!
const process = { env: { __VERSION__: "0.0.0" } };
const reducer = useAnastasisContext()!;
return (
<aside class="aside is-placed-left is-expanded">
@ -44,114 +43,235 @@ export function Sidebar({ mobile }: Props): VNode {
</div>} */}
<div class="aside-tools">
<div class="aside-tools-label">
<div><b>Anastasis</b></div>
<div class="is-size-7 has-text-right" style={{ lineHeight: 0, marginTop: -10 }}>
<div>
<b>Anastasis</b>
</div>
<div
class="is-size-7 has-text-right"
style={{ lineHeight: 0, marginTop: -10 }}
>
Version {process.env.__VERSION__} ({config.version})
</div>
</div>
</div>
<div class="menu is-menu-main">
{!reducer.currentReducerState &&
{!reducer.currentReducerState && (
<p class="menu-label">
<Translate>Backup or Recorver</Translate>
</p>
}
)}
<ul class="menu-list">
{!reducer.currentReducerState &&
{!reducer.currentReducerState && (
<li>
<div class="ml-4">
<span class="menu-item-label"><Translate>Select one option</Translate></span>
<span class="menu-item-label">
<Translate>Select one option</Translate>
</span>
</div>
</li>
}
{reducer.currentReducerState && reducer.currentReducerState.backup_state ? <Fragment>
<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</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.UserAttributesCollecting ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Personal information</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.AuthenticationsEditing ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Authorization methods</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesReviewing ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Policies</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.backup_state === BackupStates.SecretEditing ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Secret input</Translate></span>
</div>
</li>
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.PoliciesPaying ? 'is-active' : ''}>
)}
{reducer.currentReducerState &&
reducer.currentReducerState.backup_state ? (
<Fragment>
<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</Translate>
</span>
</div>
</li>
<li
class={
reducer.currentReducerState.backup_state ===
BackupStates.UserAttributesCollecting
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Personal information</Translate>
</span>
</div>
</li>
<li
class={
reducer.currentReducerState.backup_state ===
BackupStates.AuthenticationsEditing
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Authorization methods</Translate>
</span>
</div>
</li>
<li
class={
reducer.currentReducerState.backup_state ===
BackupStates.PoliciesReviewing
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Policies</Translate>
</span>
</div>
</li>
<li
class={
reducer.currentReducerState.backup_state ===
BackupStates.SecretEditing
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Secret input</Translate>
</span>
</div>
</li>
{/* <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 class={reducer.currentReducerState.backup_state === BackupStates.BackupFinished ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Backup completed</Translate></span>
</div>
</li>
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
<li
class={
reducer.currentReducerState.backup_state ===
BackupStates.BackupFinished
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Backup completed</Translate>
</span>
</div>
</li>
{/* <li class={reducer.currentReducerState.backup_state === BackupStates.TruthsPaying ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Truth Paying</Translate></span>
</div>
</li> */}
</Fragment> : (reducer.currentReducerState && reducer.currentReducerState?.recovery_state && <Fragment>
<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</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.UserAttributesCollecting ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Personal information</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.SecretSelecting ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Secret selection</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSelecting ||
reducer.currentReducerState.recovery_state === RecoveryStates.ChallengeSolving ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Solve Challenges</Translate></span>
</div>
</li>
<li class={reducer.currentReducerState.recovery_state === RecoveryStates.RecoveryFinished ? 'is-active' : ''}>
<div class="ml-4">
<span class="menu-item-label"><Translate>Secret recovered</Translate></span>
</div>
</li>
</Fragment>)}
{reducer.currentReducerState &&
</Fragment>
) : (
reducer.currentReducerState &&
reducer.currentReducerState?.recovery_state && (
<Fragment>
<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</Translate>
</span>
</div>
</li>
<li
class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.UserAttributesCollecting
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Personal information</Translate>
</span>
</div>
</li>
<li
class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.SecretSelecting
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Secret selection</Translate>
</span>
</div>
</li>
<li
class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.ChallengeSelecting ||
reducer.currentReducerState.recovery_state ===
RecoveryStates.ChallengeSolving
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Solve Challenges</Translate>
</span>
</div>
</li>
<li
class={
reducer.currentReducerState.recovery_state ===
RecoveryStates.RecoveryFinished
? "is-active"
: ""
}
>
<div class="ml-4">
<span class="menu-item-label">
<Translate>Secret recovered</Translate>
</span>
</div>
</li>
</Fragment>
)
)}
{reducer.currentReducerState && (
<li>
<div class="buttons ml-4">
<button class="button is-danger is-right" onClick={() => reducer.reset()}>Reset session</button>
<button
class="button is-danger is-right"
onClick={() => reducer.reset()}
>
Reset session
</button>
</div>
</li>
}
)}
{/* <li>
<div class="buttons ml-4">
<button class="button is-info is-right" >Manage providers</button>
</div>
</li> */}
</ul>
</div>
</aside>
);
}

View File

@ -85,8 +85,8 @@ export function NotificationCard({
n.type === "ERROR"
? "message is-danger"
: n.type === "WARN"
? "message is-warning"
: "message is-info"
? "message is-warning"
: "message is-info"
}
>
<div class="message-header">

View File

@ -15,9 +15,9 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, Component } from "preact";
@ -34,83 +34,71 @@ interface State {
selectYearMode: boolean;
currentDate: Date;
}
const now = new Date()
const now = new Date();
const monthArrShortFull = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
]
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const monthArrShort = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
const dayArr = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat'
]
const yearArr: number[] = []
const dayArr = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const yearArr: number[] = [];
// inspired by https://codepen.io/m4r1vs/pen/MOOxyE
export class DatePicker extends Component<Props, State> {
closeDatePicker() {
this.props.closeFunction && this.props.closeFunction(); // Function gets passed by parent
}
/**
* Gets fired when a day gets clicked.
* @param {object} e The event thrown by the <span /> element clicked
*/
* Gets fired when a day gets clicked.
* @param {object} e The event thrown by the <span /> element clicked
*/
dayClicked(e: any) {
const element = e.target; // the actual element clicked
if (element.innerHTML === '') return false; // don't continue if <span /> empty
if (element.innerHTML === "") return false; // don't continue if <span /> empty
// get date from clicked element (gets attached when rendered)
const date = new Date(element.getAttribute('data-value'));
const date = new Date(element.getAttribute("data-value"));
// update the state
this.setState({ currentDate: date });
this.passDateToParent(date)
this.passDateToParent(date);
}
/**
* returns days in month as array
* @param {number} month the month to display
* @param {number} year the year to display
*/
* returns days in month as array
* @param {number} month the month to display
* @param {number} year the year to display
*/
getDaysByMonth(month: number, year: number) {
const calendar = [];
const date = new Date(year, month, 1); // month to display
@ -122,15 +110,17 @@ export class DatePicker extends Component<Props, State> {
// the calendar is 7*6 fields big, so 42 loops
for (let i = 0; i < 42; i++) {
if (i >= firstDay && day !== null) day = day + 1;
if (day !== null && day > lastDate) day = null;
// append the calendar Array
calendar.push({
day: (day === 0 || day === null) ? null : day, // null or number
date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date()
today: (day === now.getDate() && month === now.getMonth() && year === now.getFullYear()) // boolean
day: day === 0 || day === null ? null : day, // null or number
date: day === 0 || day === null ? null : new Date(year, month, day), // null or Date()
today:
day === now.getDate() &&
month === now.getMonth() &&
year === now.getFullYear(), // boolean
});
}
@ -138,51 +128,48 @@ export class DatePicker extends Component<Props, State> {
}
/**
* Display previous month by updating state
*/
* Display previous month by updating state
*/
displayPrevMonth() {
if (this.state.displayedMonth <= 0) {
this.setState({
displayedMonth: 11,
displayedYear: this.state.displayedYear - 1
displayedYear: this.state.displayedYear - 1,
});
}
else {
} else {
this.setState({
displayedMonth: this.state.displayedMonth - 1
displayedMonth: this.state.displayedMonth - 1,
});
}
}
/**
* Display next month by updating state
*/
* Display next month by updating state
*/
displayNextMonth() {
if (this.state.displayedMonth >= 11) {
this.setState({
displayedMonth: 0,
displayedYear: this.state.displayedYear + 1
displayedYear: this.state.displayedYear + 1,
});
}
else {
} else {
this.setState({
displayedMonth: this.state.displayedMonth + 1
displayedMonth: this.state.displayedMonth + 1,
});
}
}
/**
* Display the selected month (gets fired when clicking on the date string)
*/
* Display the selected month (gets fired when clicking on the date string)
*/
displaySelectedMonth() {
if (this.state.selectYearMode) {
this.toggleYearSelector();
}
else {
} else {
if (!this.state.currentDate) return false;
this.setState({
displayedMonth: this.state.currentDate.getMonth(),
displayedYear: this.state.currentDate.getFullYear()
displayedYear: this.state.currentDate.getFullYear(),
});
}
}
@ -194,17 +181,21 @@ export class DatePicker extends Component<Props, State> {
changeDisplayedYear(e: any) {
const element = e.target;
this.toggleYearSelector();
this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 });
this.setState({
displayedYear: parseInt(element.innerHTML, 10),
displayedMonth: 0,
});
}
/**
* Pass the selected date to parent when 'OK' is clicked
*/
* Pass the selected date to parent when 'OK' is clicked
*/
passSavedDateDateToParent() {
this.passDateToParent(this.state.currentDate)
this.passDateToParent(this.state.currentDate);
}
passDateToParent(date: Date) {
if (typeof this.props.dateReceiver === 'function') this.props.dateReceiver(date);
if (typeof this.props.dateReceiver === "function")
this.props.dateReceiver(date);
this.closeDatePicker();
}
@ -233,94 +224,133 @@ export class DatePicker extends Component<Props, State> {
currentDate: initial,
displayedMonth: initial.getMonth(),
displayedYear: initial.getFullYear(),
selectYearMode: false
}
selectYearMode: false,
};
}
render() {
const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state;
const {
currentDate,
displayedMonth,
displayedYear,
selectYearMode,
} = this.state;
return (
<div>
<div class={`datePicker ${ this.props.opened && "datePicker--opened"}`}>
<div class={`datePicker ${this.props.opened && "datePicker--opened"}`}>
<div class="datePicker--titles">
<h3 style={{
color: selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
}} onClick={this.toggleYearSelector}>{currentDate.getFullYear()}</h3>
<h2 style={{
color: !selectYearMode ? 'rgba(255,255,255,.87)' : 'rgba(255,255,255,.57)'
}} onClick={this.displaySelectedMonth}>
{dayArr[currentDate.getDay()]}, {monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
<h3
style={{
color: selectYearMode
? "rgba(255,255,255,.87)"
: "rgba(255,255,255,.57)",
}}
onClick={this.toggleYearSelector}
>
{currentDate.getFullYear()}
</h3>
<h2
style={{
color: !selectYearMode
? "rgba(255,255,255,.87)"
: "rgba(255,255,255,.57)",
}}
onClick={this.displaySelectedMonth}
>
{dayArr[currentDate.getDay()]},{" "}
{monthArrShort[currentDate.getMonth()]} {currentDate.getDate()}
</h2>
</div>
{!selectYearMode && <nav>
<span onClick={this.displayPrevMonth} class="icon"><i style={{ transform: 'rotate(180deg)' }} class="mdi mdi-forward" /></span>
<h4>{monthArrShortFull[displayedMonth]} {displayedYear}</h4>
<span onClick={this.displayNextMonth} class="icon"><i class="mdi mdi-forward" /></span>
</nav>}
{!selectYearMode && (
<nav>
<span onClick={this.displayPrevMonth} class="icon">
<i
style={{ transform: "rotate(180deg)" }}
class="mdi mdi-forward"
/>
</span>
<h4>
{monthArrShortFull[displayedMonth]} {displayedYear}
</h4>
<span onClick={this.displayNextMonth} class="icon">
<i class="mdi mdi-forward" />
</span>
</nav>
)}
<div class="datePicker--scroll">
{!selectYearMode && (
<div class="datePicker--calendar">
<div class="datePicker--dayNames">
{["S", "M", "T", "W", "T", "F", "S"].map((day, i) => (
<span key={i}>{day}</span>
))}
</div>
{!selectYearMode && <div class="datePicker--calendar" >
<div class="datePicker--dayNames">
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day,i) => <span key={i}>{day}</span>)}
</div>
<div onClick={this.dayClicked} class="datePicker--days">
{/*
<div onClick={this.dayClicked} class="datePicker--days">
{/*
Loop through the calendar object returned by getDaysByMonth().
*/}
{this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear)
.map(
day => {
let selected = false;
{this.getDaysByMonth(
this.state.displayedMonth,
this.state.displayedYear,
).map((day) => {
let selected = false;
if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString());
if (currentDate && day.date)
selected =
currentDate.toLocaleDateString() ===
day.date.toLocaleDateString();
return (<span key={day.day}
class={(day.today ? 'datePicker--today ' : '') + (selected ? 'datePicker--selected' : '')}
return (
<span
key={day.day}
class={
(day.today ? "datePicker--today " : "") +
(selected ? "datePicker--selected" : "")
}
disabled={!day.date}
data-value={day.date}
>
{day.day}
</span>)
}
)
}
</span>
);
})}
</div>
</div>
)}
</div>}
{selectYearMode && <div class="datePicker--selectYear">
{(this.props.years || yearArr).map(year => (
<span key={year} class={(year === displayedYear) ? 'selected' : ''} onClick={this.changeDisplayedYear}>
{year}
</span>
))}
</div>}
{selectYearMode && (
<div class="datePicker--selectYear">
{(this.props.years || yearArr).map((year) => (
<span
key={year}
class={year === displayedYear ? "selected" : ""}
onClick={this.changeDisplayedYear}
>
{year}
</span>
))}
</div>
)}
</div>
</div>
<div class="datePicker--background" onClick={this.closeDatePicker} style={{
display: this.props.opened ? 'block' : 'none',
}}
<div
class="datePicker--background"
onClick={this.closeDatePicker}
style={{
display: this.props.opened ? "block" : "none",
}}
/>
</div>
)
);
}
}
for (let i = 2010; i <= now.getFullYear() + 10; i++) {
yearArr.push(i);
}

View File

@ -15,36 +15,41 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, FunctionalComponent } from 'preact';
import { useState } from 'preact/hooks';
import { DurationPicker as TestedComponent } from './DurationPicker';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, FunctionalComponent } from "preact";
import { useState } from "preact/hooks";
import { DurationPicker as TestedComponent } from "./DurationPicker";
export default {
title: 'Components/Picker/Duration',
title: "Components/Picker/Duration",
component: TestedComponent,
argTypes: {
onCreate: { action: 'onCreate' },
goBack: { action: 'goBack' },
}
onCreate: { action: "onCreate" },
goBack: { action: "goBack" },
},
};
function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) {
const r = (args: any) => <Component {...args} />
r.args = props
return r
function createExample<Props>(
Component: FunctionalComponent<Props>,
props: Partial<Props>,
) {
const r = (args: any) => <Component {...args} />;
r.args = props;
return r;
}
export const Example = createExample(TestedComponent, {
days: true, minutes: true, hours: true, seconds: true,
value: 10000000
days: true,
minutes: true,
hours: true,
seconds: true,
value: 10000000,
});
export const WithState = () => {
const [v,s] = useState<number>(1000000)
return <TestedComponent value={v} onChange={s} days minutes hours seconds />
}
const [v, s] = useState<number>(1000000);
return <TestedComponent value={v} onChange={s} days minutes hours seconds />;
};

View File

@ -15,9 +15,9 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@ -30,75 +30,123 @@ export interface Props {
seconds?: boolean;
days?: boolean;
onChange: (value: number) => void;
value: number
value: number;
}
// inspiration taken from https://github.com/flurmbo/react-duration-picker
export function DurationPicker({ days, hours, minutes, seconds, onChange, value }: Props): VNode {
const ss = 1000
const ms = ss * 60
const hs = ms * 60
const ds = hs * 24
const i18n = useTranslator()
export function DurationPicker({
days,
hours,
minutes,
seconds,
onChange,
value,
}: Props): VNode {
const ss = 1000;
const ms = ss * 60;
const hs = ms * 60;
const ds = hs * 24;
const i18n = useTranslator();
return <div class="rdp-picker">
{days && <DurationColumn unit={i18n`days`} max={99}
value={Math.floor(value / ds)}
onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
onChange={diff => onChange(value + diff * ds)}
/>}
{hours && <DurationColumn unit={i18n`hours`} max={23} min={1}
value={Math.floor(value / hs) % 24}
onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
onChange={diff => onChange(value + diff * hs)}
/>}
{minutes && <DurationColumn unit={i18n`minutes`} max={59} min={1}
value={Math.floor(value / ms) % 60}
onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
onChange={diff => onChange(value + diff * ms)}
/>}
{seconds && <DurationColumn unit={i18n`seconds`} max={59}
value={Math.floor(value / ss) % 60}
onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
onChange={diff => onChange(value + diff * ss)}
/>}
</div>
return (
<div class="rdp-picker">
{days && (
<DurationColumn
unit={i18n`days`}
max={99}
value={Math.floor(value / ds)}
onDecrease={value >= ds ? () => onChange(value - ds) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ds) : undefined}
onChange={(diff) => onChange(value + diff * ds)}
/>
)}
{hours && (
<DurationColumn
unit={i18n`hours`}
max={23}
min={1}
value={Math.floor(value / hs) % 24}
onDecrease={value >= hs ? () => onChange(value - hs) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + hs) : undefined}
onChange={(diff) => onChange(value + diff * hs)}
/>
)}
{minutes && (
<DurationColumn
unit={i18n`minutes`}
max={59}
min={1}
value={Math.floor(value / ms) % 60}
onDecrease={value >= ms ? () => onChange(value - ms) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ms) : undefined}
onChange={(diff) => onChange(value + diff * ms)}
/>
)}
{seconds && (
<DurationColumn
unit={i18n`seconds`}
max={59}
value={Math.floor(value / ss) % 60}
onDecrease={value >= ss ? () => onChange(value - ss) : undefined}
onIncrease={value < 99 * ds ? () => onChange(value + ss) : undefined}
onChange={(diff) => onChange(value + diff * ss)}
/>
)}
</div>
);
}
interface ColProps {
unit: string,
min?: number,
max: number,
value: number,
unit: string;
min?: number;
max: number;
value: number;
onIncrease?: () => void;
onDecrease?: () => void;
onChange?: (diff: number) => void;
}
function InputNumber({ initial, onChange }: { initial: number, onChange: (n: number) => void }) {
const [value, handler] = useState<{v:string}>({
v: toTwoDigitString(initial)
})
function InputNumber({
initial,
onChange,
}: {
initial: number;
onChange: (n: number) => void;
}) {
const [value, handler] = useState<{ v: string }>({
v: toTwoDigitString(initial),
});
return <input
value={value.v}
onBlur={(e) => onChange(parseInt(value.v, 10))}
onInput={(e) => {
e.preventDefault()
const n = Number.parseInt(e.currentTarget.value, 10);
if (isNaN(n)) return handler({v:toTwoDigitString(initial)})
return handler({v:toTwoDigitString(n)})
}}
style={{ width: 50, border: 'none', fontSize: 'inherit', background: 'inherit' }} />
return (
<input
value={value.v}
onBlur={(e) => onChange(parseInt(value.v, 10))}
onInput={(e) => {
e.preventDefault();
const n = Number.parseInt(e.currentTarget.value, 10);
if (isNaN(n)) return handler({ v: toTwoDigitString(initial) });
return handler({ v: toTwoDigitString(n) });
}}
style={{
width: 50,
border: "none",
fontSize: "inherit",
background: "inherit",
}}
/>
);
}
function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onChange }: ColProps): VNode {
const cellHeight = 35
function DurationColumn({
unit,
min = 0,
max,
value,
onIncrease,
onDecrease,
onChange,
}: ColProps): VNode {
const cellHeight = 35;
return (
<div class="rdp-column-container">
<div class="rdp-masked-div">
@ -106,46 +154,55 @@ function DurationColumn({ unit, min = 0, max, value, onIncrease, onDecrease, onC
<hr class="rdp-reticule" style={{ top: cellHeight * 3 - 1 }} />
<div class="rdp-column" style={{ top: 0 }}>
<div class="rdp-cell" key={value - 2}>
{onDecrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
onClick={onDecrease}>
<span class="icon">
<i class="mdi mdi-chevron-up" />
</span>
</button>}
{onDecrease && (
<button
style={{ width: "100%", textAlign: "center", margin: 5 }}
onClick={onDecrease}
>
<span class="icon">
<i class="mdi mdi-chevron-up" />
</span>
</button>
)}
</div>
<div class="rdp-cell" key={value - 1}>
{value > min ? toTwoDigitString(value - 1) : ''}
{value > min ? toTwoDigitString(value - 1) : ""}
</div>
<div class="rdp-cell rdp-center" key={value}>
{onChange ?
<InputNumber initial={value} onChange={(n) => onChange(n - value)} /> :
{onChange ? (
<InputNumber
initial={value}
onChange={(n) => onChange(n - value)}
/>
) : (
toTwoDigitString(value)
}
)}
<div>{unit}</div>
</div>
<div class="rdp-cell" key={value + 1}>
{value < max ? toTwoDigitString(value + 1) : ''}
{value < max ? toTwoDigitString(value + 1) : ""}
</div>
<div class="rdp-cell" key={value + 2}>
{onIncrease && <button style={{ width: '100%', textAlign: 'center', margin: 5 }}
onClick={onIncrease}>
<span class="icon">
<i class="mdi mdi-chevron-down" />
</span>
</button>}
{onIncrease && (
<button
style={{ width: "100%", textAlign: "center", margin: 5 }}
onClick={onIncrease}
>
<span class="icon">
<i class="mdi mdi-chevron-down" />
</span>
</button>
)}
</div>
</div>
</div>
</div>
);
}
function toTwoDigitString(n: number) {
if (n < 10) {
return `0${n}`;

View File

@ -15,19 +15,19 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createContext, h, VNode } from 'preact';
import { useContext } from 'preact/hooks';
import { AnastasisReducerApi } from '../hooks/use-anastasis-reducer';
import { createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
import { AnastasisReducerApi } from "../hooks/use-anastasis-reducer";
type Type = AnastasisReducerApi | undefined;
const initial = undefined
const initial = undefined;
const Context = createContext<Type>(initial)
const Context = createContext<Type>(initial);
interface Props {
value: AnastasisReducerApi;
@ -36,6 +36,6 @@ interface Props {
export const AnastasisProvider = ({ value, children }: Props): VNode => {
return h(Context.Provider, { value, children });
}
};
export const useAnastasisContext = (): Type => useContext(Context);

View File

@ -15,13 +15,13 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createContext, h, VNode } from 'preact'
import { useContext, useEffect } from 'preact/hooks'
import { useLang } from '../hooks'
import { createContext, h, VNode } from "preact";
import { useContext, useEffect } from "preact/hooks";
import { useLang } from "../hooks";
import * as jedLib from "jed";
import { strings } from "../i18n/strings";
@ -31,13 +31,13 @@ interface Type {
changeLanguage: (l: string) => void;
}
const initial = {
lang: 'en',
lang: "en",
handler: null,
changeLanguage: () => {
// do not change anything
}
}
const Context = createContext<Type>(initial)
},
};
const Context = createContext<Type>(initial);
interface Props {
initial?: string;
@ -45,15 +45,22 @@ interface Props {
forceLang?: string;
}
export const TranslationProvider = ({ initial, children, forceLang }: Props): VNode => {
const [lang, changeLanguage] = useLang(initial)
export const TranslationProvider = ({
initial,
children,
forceLang,
}: Props): VNode => {
const [lang, changeLanguage] = useLang(initial);
useEffect(() => {
if (forceLang) {
changeLanguage(forceLang)
changeLanguage(forceLang);
}
})
const handler = new jedLib.Jed(strings[lang] || strings['en']);
return h(Context.Provider, { value: { lang, handler, changeLanguage }, children });
}
});
const handler = new jedLib.Jed(strings[lang] || strings["en"]);
return h(Context.Provider, {
value: { lang, handler, changeLanguage },
children,
});
};
export const useTranslationContext = (): Type => useContext(Context);

View File

@ -1,20 +1,20 @@
declare module "*.css" {
const mapping: Record<string, string>;
export default mapping;
const mapping: Record<string, string>;
export default mapping;
}
declare module '*.svg' {
const content: any;
export default content;
declare module "*.svg" {
const content: any;
export default content;
}
declare module '*.jpeg' {
const content: any;
export default content;
declare module "*.jpeg" {
const content: any;
export default content;
}
declare module '*.png' {
const content: any;
export default content;
declare module "*.png" {
const content: any;
export default content;
}
declare module 'jed' {
const x: any;
export = x;
declare module "jed" {
const x: any;
export = x;
}

View File

@ -15,9 +15,9 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { useState } from "preact/hooks";
// import { cancelPendingRequest } from "./backend";
@ -34,36 +34,39 @@ export interface AsyncOperationApi<T> {
error: string | undefined;
}
export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }): AsyncOperationApi<T> {
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 [isSlow, setSlow] = useState(false);
const request = async (...args: any) => {
if (!fn) return;
setLoading(true);
const handler = setTimeout(() => {
setSlow(true)
}, tooLong)
setSlow(true);
}, tooLong);
try {
console.log("calling async", args)
console.log("calling async", args);
const result = await fn(...args);
console.log("async back", result)
console.log("async back", result);
setData(result);
} catch (error) {
setError(error);
}
setLoading(false);
setSlow(false)
clearTimeout(handler)
setSlow(false);
clearTimeout(handler);
};
function cancel() {
// cancelPendingRequest()
setLoading(false);
setSlow(false)
setSlow(false);
}
return {
@ -72,6 +75,6 @@ export function useAsync<T>(fn?: (...args: any) => Promise<T>, { slowTolerance:
data,
isSlow,
isLoading,
error
error,
};
}

View File

@ -15,81 +15,110 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { StateUpdater, useState } from "preact/hooks";
export type ValueOrFunction<T> = T | ((p: T) => T)
export type ValueOrFunction<T> = T | ((p: T) => T);
const calculateRootPath = () => {
const rootPath = typeof window !== undefined ? window.location.origin + window.location.pathname : '/'
return rootPath
}
const rootPath =
typeof window !== undefined
? window.location.origin + window.location.pathname
: "/";
return rootPath;
};
export function useBackendURL(url?: string): [string, boolean, StateUpdater<string>, () => void] {
const [value, setter] = useNotNullLocalStorage('backend-url', url || calculateRootPath())
const [triedToLog, setTriedToLog] = useLocalStorage('tried-login')
export function useBackendURL(
url?: string,
): [string, boolean, StateUpdater<string>, () => void] {
const [value, setter] = useNotNullLocalStorage(
"backend-url",
url || calculateRootPath(),
);
const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
const checkedSetter = (v: ValueOrFunction<string>) => {
setTriedToLog('yes')
return setter(p => (v instanceof Function ? v(p) : v).replace(/\/$/, ''))
}
setTriedToLog("yes");
return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
};
const resetBackend = () => {
setTriedToLog(undefined)
}
return [value, !!triedToLog, checkedSetter, resetBackend]
setTriedToLog(undefined);
};
return [value, !!triedToLog, checkedSetter, resetBackend];
}
export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] {
return useLocalStorage('backend-token')
export function useBackendDefaultToken(): [
string | undefined,
StateUpdater<string | undefined>,
] {
return useLocalStorage("backend-token");
}
export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] {
const [token, setToken] = useLocalStorage(`backend-token-${id}`)
const [defaultToken, defaultSetToken] = useBackendDefaultToken()
export function useBackendInstanceToken(
id: string,
): [string | undefined, StateUpdater<string | undefined>] {
const [token, setToken] = useLocalStorage(`backend-token-${id}`);
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token
if (id === 'default') {
return [defaultToken, defaultSetToken]
if (id === "default") {
return [defaultToken, defaultSetToken];
}
return [token, setToken]
return [token, setToken];
}
export function useLang(initial?: string): [string, StateUpdater<string>] {
const browserLang = typeof window !== "undefined" ? navigator.language || (navigator as any).userLanguage : undefined;
const defaultLang = (browserLang || initial || 'en').substring(0, 2)
return useNotNullLocalStorage('lang-preference', defaultLang)
const browserLang =
typeof window !== "undefined"
? navigator.language || (navigator as any).userLanguage
: undefined;
const defaultLang = (browserLang || initial || "en").substring(0, 2);
return useNotNullLocalStorage("lang-preference", defaultLang);
}
export function useLocalStorage(key: string, initialValue?: string): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>((): string | undefined => {
return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
export function useLocalStorage(
key: string,
initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
const [storedValue, setStoredValue] = useState<string | undefined>(():
| string
| undefined => {
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
});
const setValue = (value?: string | ((val?: string) => string | undefined)) => {
setStoredValue(p => {
const toStore = value instanceof Function ? value(p) : value
const setValue = (
value?: string | ((val?: string) => string | undefined),
) => {
setStoredValue((p) => {
const toStore = value instanceof Function ? value(p) : value;
if (typeof window !== "undefined") {
if (!toStore) {
window.localStorage.removeItem(key)
window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, toStore);
}
}
return toStore
})
return toStore;
});
};
return [storedValue, setValue];
}
export function useNotNullLocalStorage(key: string, initialValue: string): [string, StateUpdater<string>] {
export function useNotNullLocalStorage(
key: string,
initialValue: string,
): [string, StateUpdater<string>] {
const [storedValue, setStoredValue] = useState<string>((): string => {
return typeof window !== "undefined" ? window.localStorage.getItem(key) || initialValue : initialValue;
return typeof window !== "undefined"
? window.localStorage.getItem(key) || initialValue
: initialValue;
});
const setValue = (value: string | ((val: string) => string)) => {
@ -97,7 +126,7 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
if (!valueToStore) {
window.localStorage.removeItem(key)
window.localStorage.removeItem(key);
} else {
window.localStorage.setItem(key, valueToStore);
}
@ -106,5 +135,3 @@ export function useNotNullLocalStorage(key: string, initialValue: string): [stri
return [storedValue, setValue];
}

View File

@ -27,23 +27,25 @@ import { useTranslationContext } from "../context/translation";
export function useTranslator() {
const ctx = useTranslationContext();
const jed = ctx.handler
return function str(stringSeq: TemplateStringsArray, ...values: any[]): string {
const jed = ctx.handler;
return function str(
stringSeq: TemplateStringsArray,
...values: any[]
): string {
const s = toI18nString(stringSeq);
if (!s) return s
if (!s) return s;
const tr = jed
.translate(s)
.ifPlural(1, s)
.fetch(...values);
return tr;
}
};
}
/**
* Convert template strings to a msgid
*/
function toI18nString(stringSeq: ReadonlyArray<string>): string {
function toI18nString(stringSeq: ReadonlyArray<string>): string {
let s = "";
for (let i = 0; i < stringSeq.length; i++) {
s += stringSeq[i];
@ -54,7 +56,6 @@ export function useTranslator() {
return s;
}
interface TranslateSwitchProps {
target: number;
children: ComponentChildren;
@ -110,7 +111,7 @@ function getTranslatedChildren(
// Text
result.push(tr[i]);
} else {
const childIdx = Number.parseInt(tr[i],10) - 1;
const childIdx = Number.parseInt(tr[i], 10) - 1;
result.push(placeholderChildren[childIdx]);
}
}
@ -131,9 +132,9 @@ function getTranslatedChildren(
*/
export function Translate({ children }: TranslateProps): VNode {
const s = stringifyChildren(children);
const ctx = useTranslationContext()
const ctx = useTranslationContext();
const translation: string = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children)
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
}
@ -154,14 +155,16 @@ export function TranslateSwitch({ children, target }: TranslateSwitchProps) {
let plural: VNode<TranslationPluralProps> | undefined;
// const children = this.props.children;
if (children) {
(children instanceof Array ? children : [children]).forEach((child: any) => {
if (child.type === TranslatePlural) {
plural = child;
}
if (child.type === TranslateSingular) {
singular = child;
}
});
(children instanceof Array ? children : [children]).forEach(
(child: any) => {
if (child.type === TranslatePlural) {
plural = child;
}
if (child.type === TranslateSingular) {
singular = child;
}
},
);
}
if (!singular || !plural) {
console.error("translation not found");
@ -182,9 +185,12 @@ interface TranslationPluralProps {
/**
* See [[TranslateSwitch]].
*/
export function TranslatePlural({ children, target }: TranslationPluralProps): VNode {
export function TranslatePlural({
children,
target,
}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
const ctx = useTranslationContext()
const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, 1);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
@ -193,11 +199,13 @@ export function TranslatePlural({ children, target }: TranslationPluralProps): V
/**
* See [[TranslateSwitch]].
*/
export function TranslateSingular({ children, target }: TranslationPluralProps): VNode {
export function TranslateSingular({
children,
target,
}: TranslationPluralProps): VNode {
const s = stringifyChildren(children);
const ctx = useTranslationContext()
const ctx = useTranslationContext();
const translation = ctx.handler.ngettext(s, s, target);
const result = getTranslatedChildren(translation, children);
return <Fragment>{result}</Fragment>;
}

View File

@ -15,30 +15,30 @@
*/
/*eslint quote-props: ["error", "consistent"]*/
export const strings: {[s: string]: any} = {};
export const strings: { [s: string]: any } = {};
strings['de'] = {
"domain": "messages",
"locale_data": {
"messages": {
strings["de"] = {
domain: "messages",
locale_data: {
messages: {
"": {
"domain": "messages",
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
domain: "messages",
plural_forms: "nplurals=2; plural=(n != 1);",
lang: "",
},
}
}
},
},
};
strings['en'] = {
"domain": "messages",
"locale_data": {
"messages": {
strings["en"] = {
domain: "messages",
locale_data: {
messages: {
"": {
"domain": "messages",
"plural_forms": "nplurals=2; plural=(n != 1);",
"lang": ""
domain: "messages",
plural_forms: "nplurals=2; plural=(n != 1);",
lang: "",
},
}
}
},
},
};

View File

@ -1,4 +1,4 @@
import App from './components/app';
import './scss/main.scss';
import App from "./components/app";
import "./scss/main.scss";
export default App;

View File

@ -15,24 +15,23 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { AddingProviderScreen as TestedComponent } from './AddingProviderScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { AddingProviderScreen as TestedComponent } from "./AddingProviderScreen";
export default {
title: 'Pages/ManageProvider',
title: "Pages/ManageProvider",
component: TestedComponent,
args: {
order: 1,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
@ -40,20 +39,31 @@ export const NewProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
} as ReducerState);
export const NewProviderWithoutProviderList = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
authentication_providers: {}
authentication_providers: {},
} as ReducerState);
export const NewVideoProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
} as ReducerState, { providerType: 'video'});
export const NewVideoProvider = createExample(
TestedComponent,
{
...reducerStatesExample.authEditing,
} as ReducerState,
{ providerType: "video" },
);
export const NewSmsProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
} as ReducerState, { providerType: 'sms'});
export const NewSmsProvider = createExample(
TestedComponent,
{
...reducerStatesExample.authEditing,
} as ReducerState,
{ providerType: "sms" },
);
export const NewIBANProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
} as ReducerState, { providerType: 'iban' });
export const NewIBANProvider = createExample(
TestedComponent,
{
...reducerStatesExample.authEditing,
} as ReducerState,
{ providerType: "iban" },
);

View File

@ -11,185 +11,250 @@ interface Props {
onCancel: () => void;
}
async function testProvider(url: string, expectedMethodType?: string): Promise<void> {
async function testProvider(
url: string,
expectedMethodType?: string,
): Promise<void> {
try {
const response = await fetch(new URL("config", url).href)
const json = await (response.json().catch(d => ({})))
const response = await fetch(new URL("config", url).href);
const json = await response.json().catch((d) => ({}));
if (!("methods" in json) || !Array.isArray(json.methods)) {
throw Error("This provider doesn't have authentication method. Check the provider URL")
throw Error(
"This provider doesn't have authentication method. Check the provider URL",
);
}
console.log("expected", expectedMethodType)
console.log("expected", expectedMethodType);
if (!expectedMethodType) {
return
return;
}
let found = false
let found = false;
for (let i = 0; i < json.methods.length && !found; i++) {
found = json.methods[i].type === expectedMethodType
found = json.methods[i].type === expectedMethodType;
}
if (!found) {
throw Error(`This provider does not support authentication method ${expectedMethodType}`)
throw Error(
`This provider does not support authentication method ${expectedMethodType}`,
);
}
return
return;
} catch (e) {
console.log("error", e)
const error = e instanceof Error ?
Error(`There was an error testing this provider, try another one. ${e.message}`) :
Error(`There was an error testing this provider, try another one.`)
throw error
console.log("error", e);
const error =
e instanceof Error
? Error(
`There was an error testing this provider, try another one. ${e.message}`,
)
: Error(`There was an error testing this provider, try another one.`);
throw error;
}
}
export function AddingProviderScreen({ providerType, onCancel }: Props): VNode {
const reducer = useAnastasisContext();
const [providerURL, setProviderURL] = useState("");
const [error, setError] = useState<string | undefined>()
const [testing, setTesting] = useState(false)
const providerLabel = providerType ? authMethods[providerType].label : undefined
const [error, setError] = useState<string | undefined>();
const [testing, setTesting] = useState(false);
const providerLabel = providerType
? authMethods[providerType].label
: undefined;
//FIXME: move this timeout logic into a hook
const timeout = useRef<number | undefined>(undefined);
useEffect(() => {
if (timeout) window.clearTimeout(timeout.current)
if (timeout) window.clearTimeout(timeout.current);
timeout.current = window.setTimeout(async () => {
const url = providerURL.endsWith('/') ? providerURL : (providerURL + '/')
const url = providerURL.endsWith("/") ? providerURL : providerURL + "/";
if (!providerURL || authProviders.includes(url)) return;
try {
setTesting(true)
await testProvider(url, providerType)
setTesting(true);
await testProvider(url, providerType);
// this is use as tested but everything when ok
// undefined will mean that the field is not dirty
setError("")
setError("");
} catch (e) {
console.log("tuvieja", e)
if (e instanceof Error) setError(e.message)
console.log("tuvieja", e);
if (e instanceof Error) setError(e.message);
}
setTesting(false)
setTesting(false);
}, 200);
}, [providerURL, reducer])
}, [providerURL, reducer]);
if (!reducer) {
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || !("authentication_providers" in reducer.currentReducerState)) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
!("authentication_providers" in reducer.currentReducerState)
) {
return <div>invalid state</div>;
}
async function addProvider(provider_url: string): Promise<void> {
await reducer?.transition("add_provider", { provider_url })
onCancel()
await reducer?.transition("add_provider", { provider_url });
onCancel();
}
function deleteProvider(provider_url: string): void {
reducer?.transition("delete_provider", { provider_url })
reducer?.transition("delete_provider", { provider_url });
}
const allAuthProviders = reducer.currentReducerState.authentication_providers || {}
const authProviders = Object.keys(allAuthProviders).filter(provUrl => {
const allAuthProviders =
reducer.currentReducerState.authentication_providers || {};
const authProviders = Object.keys(allAuthProviders).filter((provUrl) => {
const p = allAuthProviders[provUrl];
if (!providerLabel) {
return p && ("currency" in p)
return p && "currency" in p;
} else {
return p && ("currency" in p) && p.methods.findIndex(m => m.type === providerType) !== -1
return (
p &&
"currency" in p &&
p.methods.findIndex((m) => m.type === providerType) !== -1
);
}
})
});
let errors = !providerURL ? 'Add provider URL' : undefined
let errors = !providerURL ? "Add provider URL" : undefined;
let url: string | undefined;
try {
url = new URL("",providerURL).href
url = new URL("", providerURL).href;
} catch {
errors = 'Check the URL'
errors = "Check the URL";
}
if (!!error && !errors) {
errors = error
errors = error;
}
if (!errors && authProviders.includes(url!)) {
errors = 'That provider is already known'
errors = "That provider is already known";
}
return (
<AnastasisClientFrame hideNav
<AnastasisClientFrame
hideNav
title="Backup: Manage providers"
hideNext={errors}>
hideNext={errors}
>
<div>
{!providerLabel ?
<p>
Add a provider url
</p> :
<p>
Add a provider url for a {providerLabel} service
</p>
}
{!providerLabel ? (
<p>Add a provider url</p>
) : (
<p>Add a provider url for a {providerLabel} service</p>
)}
<div class="container">
<TextInput
label="Provider URL"
placeholder="https://provider.com"
grabFocus
error={errors}
bind={[providerURL, setProviderURL]} />
bind={[providerURL, setProviderURL]}
/>
</div>
<p class="block">
Example: https://kudos.demo.anastasis.lu
</p>
<p class="block">Example: https://kudos.demo.anastasis.lu</p>
{testing && <p class="has-text-info">Testing</p>}
<div class="block" style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={onCancel}>Cancel</button>
<div
class="block"
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={onCancel}>
Cancel
</button>
<span data-tooltip={errors}>
<button class="button is-info" disabled={error !== "" || testing} onClick={() => addProvider(url!)}>Add</button>
<button
class="button is-info"
disabled={error !== "" || testing}
onClick={() => addProvider(url!)}
>
Add
</button>
</span>
</div>
{authProviders.length > 0 ? (
!providerLabel ?
!providerLabel ? (
<p class="subtitle">Current providers</p>
) : (
<p class="subtitle">
Current providers
</p> : <p class="subtitle">
Current providers for {providerLabel} service
</p>
)
) : !providerLabel ? (
<p class="subtitle">No known providers, add one.</p>
) : (
!providerLabel ? <p class="subtitle">
No known providers, add one.
</p> : <p class="subtitle">
No known providers for {providerLabel} service
</p>
<p class="subtitle">No known providers for {providerLabel} service</p>
)}
{authProviders.map(k => {
const p = allAuthProviders[k] as AuthenticationProviderStatusOk
return <TableRow url={k} info={p} onDelete={deleteProvider} />
{authProviders.map((k) => {
const p = allAuthProviders[k] as AuthenticationProviderStatusOk;
return <TableRow url={k} info={p} onDelete={deleteProvider} />;
})}
</div>
</AnastasisClientFrame>
);
}
function TableRow({ url, info, onDelete }: { onDelete: (s: string) => void, url: string, info: AuthenticationProviderStatusOk }) {
const [status, setStatus] = useState("checking")
function TableRow({
url,
info,
onDelete,
}: {
onDelete: (s: string) => void;
url: string;
info: AuthenticationProviderStatusOk;
}) {
const [status, setStatus] = useState("checking");
useEffect(function () {
testProvider(url.endsWith('/') ? url.substring(0, url.length - 1) : url)
.then(function () { setStatus('responding') })
.catch(function () { setStatus('failed to contact') })
})
return <div class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
<div class="subtitle">{url}</div>
<dl>
<dt><b>Business Name</b></dt>
<dd>{info.business_name}</dd>
<dt><b>Supported methods</b></dt>
<dd>{info.methods.map(m => m.type).join(',')}</dd>
<dt><b>Maximum storage</b></dt>
<dd>{info.storage_limit_in_megabytes} Mb</dd>
<dt><b>Status</b></dt>
<dd>{status}</dd>
</dl>
testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url)
.then(function () {
setStatus("responding");
})
.catch(function () {
setStatus("failed to contact");
});
});
return (
<div
class="box"
style={{ display: "flex", justifyContent: "space-between" }}
>
<div>
<div class="subtitle">{url}</div>
<dl>
<dt>
<b>Business Name</b>
</dt>
<dd>{info.business_name}</dd>
<dt>
<b>Supported methods</b>
</dt>
<dd>{info.methods.map((m) => m.type).join(",")}</dd>
<dt>
<b>Maximum storage</b>
</dt>
<dd>{info.storage_limit_in_megabytes} Mb</dd>
<dt>
<b>Status</b>
</dt>
<dd>{status}</dd>
</dl>
</div>
<div
class="block"
style={{
marginTop: "auto",
marginBottom: "auto",
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
}}
>
<button class="button is-danger" onClick={() => onDelete(url)}>
Remove
</button>
</div>
</div>
<div class="block" style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}>
<button class="button is-danger" onClick={() => onDelete(url)}>Remove</button>
</div>
</div>
);
}

View File

@ -15,76 +15,83 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { AttributeEntryScreen as TestedComponent } from './AttributeEntryScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { AttributeEntryScreen as TestedComponent } from "./AttributeEntryScreen";
export default {
title: 'Pages/PersonalInformation',
title: "Pages/PersonalInformation",
component: TestedComponent,
args: {
order: 3,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const Backup = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
required_attributes: [{
name: 'first name',
label: 'first',
type: 'string',
uuid: 'asdasdsa1',
widget: 'wid',
}, {
name: 'last name',
label: 'second',
type: 'string',
uuid: 'asdasdsa2',
widget: 'wid',
}, {
name: 'birthdate',
label: 'birthdate',
type: 'date',
uuid: 'asdasdsa3',
widget: 'calendar',
}]
required_attributes: [
{
name: "first name",
label: "first",
type: "string",
uuid: "asdasdsa1",
widget: "wid",
},
{
name: "last name",
label: "second",
type: "string",
uuid: "asdasdsa2",
widget: "wid",
},
{
name: "birthdate",
label: "birthdate",
type: "date",
uuid: "asdasdsa3",
widget: "calendar",
},
],
} as ReducerState);
export const Recovery = createExample(TestedComponent, {
...reducerStatesExample.recoveryAttributeEditing,
required_attributes: [{
name: 'first',
label: 'first',
type: 'string',
uuid: 'asdasdsa1',
widget: 'wid',
}, {
name: 'pepe',
label: 'second',
type: 'string',
uuid: 'asdasdsa2',
widget: 'wid',
}, {
name: 'pepe2',
label: 'third',
type: 'date',
uuid: 'asdasdsa3',
widget: 'calendar',
}]
required_attributes: [
{
name: "first",
label: "first",
type: "string",
uuid: "asdasdsa1",
widget: "wid",
},
{
name: "pepe",
label: "second",
type: "string",
uuid: "asdasdsa2",
widget: "wid",
},
{
name: "pepe2",
label: "third",
type: "date",
uuid: "asdasdsa3",
widget: "calendar",
},
],
} as ReducerState);
export const WithNoRequiredAttribute = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
required_attributes: undefined
required_attributes: undefined,
} as ReducerState);
const allWidgets = [
@ -107,23 +114,22 @@ const allWidgets = [
"anastasis_gtk_ia_tax_de",
"anastasis_gtk_xx_prime",
"anastasis_gtk_xx_square",
]
];
function typeForWidget(name: string): string {
if (["anastasis_gtk_xx_prime",
"anastasis_gtk_xx_square",
].includes(name)) return "number";
if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date"
if (["anastasis_gtk_xx_prime", "anastasis_gtk_xx_square"].includes(name))
return "number";
if (["anastasis_gtk_ia_birthdate"].includes(name)) return "date";
return "string";
}
export const WithAllPosibleWidget = createExample(TestedComponent, {
...reducerStatesExample.backupAttributeEditing,
required_attributes: allWidgets.map(w => ({
required_attributes: allWidgets.map((w) => ({
name: w,
label: `widget: ${w}`,
type: typeForWidget(w),
uuid: `uuid-${w}`,
widget: w
}))
widget: w,
})),
} as ReducerState);

View File

@ -9,24 +9,32 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame, withProcessLabel } from "./index";
export function AttributeEntryScreen(): VNode {
const reducer = useAnastasisContext()
const state = reducer?.currentReducerState
const currentIdentityAttributes = state && "identity_attributes" in state ? (state.identity_attributes || {}) : {}
const [attrs, setAttrs] = useState<Record<string, string>>(currentIdentityAttributes);
const reducer = useAnastasisContext();
const state = reducer?.currentReducerState;
const currentIdentityAttributes =
state && "identity_attributes" in state
? state.identity_attributes || {}
: {};
const [attrs, setAttrs] = useState<Record<string, string>>(
currentIdentityAttributes,
);
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || !("required_attributes" in reducer.currentReducerState)) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
!("required_attributes" in reducer.currentReducerState)
) {
return <div>invalid state</div>;
}
const reqAttr = reducer.currentReducerState.required_attributes || []
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
const value = attrs[spec.name];
const error = checkIfValid(value, spec);
hasErrors = hasErrors || error !== undefined;
return (
<AttributeEntryField
key={i}
@ -34,23 +42,24 @@ export function AttributeEntryScreen(): VNode {
setValue={(v: string) => setAttrs({ ...attrs, [spec.name]: v })}
spec={spec}
errorMessage={error}
value={value} />
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,
})}
onNext={() =>
reducer.transition("enter_user_attributes", {
identity_attributes: attrs,
})
}
>
<div class="columns" style={{ maxWidth: 'unset' }}>
<div class="columns" style={{ maxWidth: "unset" }}>
<div class="column">{fieldList}</div>
<div class="column">
{fieldList}
</div>
<div class="column" >
<p>This personal information will help to locate your secret.</p>
<h1 class="title">This stays private</h1>
<p>The information you have entered here:</p>
@ -61,9 +70,12 @@ export function AttributeEntryScreen(): VNode {
</span>
Will be hashed, and therefore unreadable
</li>
<li><span class="icon is-right">
<i class="mdi mdi-circle-small" />
</span>The non-hashed version is not shared</li>
<li>
<span class="icon is-right">
<i class="mdi mdi-circle-small" />
</span>
The non-hashed version is not shared
</li>
</ul>
</div>
</div>
@ -78,22 +90,22 @@ interface AttributeEntryFieldProps {
spec: UserAttributeSpec;
errorMessage: string | undefined;
}
const possibleBirthdayYear: Array<number> = []
const possibleBirthdayYear: Array<number> = [];
for (let i = 0; i < 100; i++) {
possibleBirthdayYear.push(2020 - i)
possibleBirthdayYear.push(2020 - i);
}
function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
return (
<div>
{props.spec.type === 'date' &&
{props.spec.type === "date" &&
<DateInput
grabFocus={props.isFirst}
label={props.spec.label}
years={possibleBirthdayYear}
error={props.errorMessage}
bind={[props.value, props.setValue]}
/>}
/>
}
{props.spec.type === 'number' &&
<PhoneNumberInput
grabFocus={props.isFirst}
@ -102,14 +114,14 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
bind={[props.value, props.setValue]}
/>
}
{props.spec.type === 'string' &&
{props.spec.type === "string" && (
<TextInput
grabFocus={props.isFirst}
label={props.spec.label}
error={props.errorMessage}
bind={[props.value, props.setValue]}
/>
}
)}
<div class="block">
This stays private
<span class="icon is-right">
@ -119,40 +131,43 @@ function AttributeEntryField(props: AttributeEntryFieldProps): VNode {
</div>
);
}
const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/
const YEAR_REGEX = /^[0-9]+-[0-9]+-[0-9]+$/;
function checkIfValid(value: string, spec: UserAttributeSpec): string | undefined {
const pattern = spec['validation-regex']
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 re = new RegExp(pattern);
if (!re.test(value)) return "The value is invalid";
}
const logic = spec['validation-logic']
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'
if (func && typeof func === "function" && !func(value))
return "Please check the value";
}
const optional = spec.optional
const optional = spec.optional;
if (!optional && !value) {
return 'This value is required'
return "This value is required";
}
if ("date" === spec.type) {
if (!YEAR_REGEX.test(value)) {
return "The date doesn't follow the format"
return "The date doesn't follow the format";
}
try {
const v = parse(value, 'yyyy-MM-dd', new Date());
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"
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"
return "A birthdate cannot be in the future";
}
} catch (e) {
return "Could not parse the date"
return "Could not parse the date";
}
}
return undefined
return undefined;
}

View File

@ -15,73 +15,84 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { AuthenticationEditorScreen as TestedComponent } from './AuthenticationEditorScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { AuthenticationEditorScreen as TestedComponent } from "./AuthenticationEditorScreen";
export default {
title: 'Pages/backup/AuthorizationMethod',
title: "Pages/backup/AuthorizationMethod",
component: TestedComponent,
args: {
order: 4,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const InitialState = createExample(TestedComponent, reducerStatesExample.authEditing);
export const InitialState = createExample(
TestedComponent,
reducerStatesExample.authEditing,
);
export const OneAuthMethodConfigured = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
authentication_methods: [{
type: 'question',
instructions: 'what time is it?',
challenge: 'asd',
}]
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',
}]
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: []
authentication_methods: [],
} as ReducerState);

View File

@ -20,7 +20,9 @@ export function AuthenticationEditorScreen(): VNode {
KnownAuthMethods | undefined
>(undefined);
const [tooFewAuths, setTooFewAuths] = useState(false);
const [manageProvider, setManageProvider] = useState<string | undefined>(undefined)
const [manageProvider, setManageProvider] = useState<string | undefined>(
undefined,
);
// const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
const reducer = useAnastasisContext();
@ -68,11 +70,14 @@ export function AuthenticationEditorScreen(): VNode {
}
if (manageProvider !== undefined) {
return <AddingProviderScreen
onCancel={() => setManageProvider(undefined)}
providerType={isKnownAuthMethods(manageProvider) ? manageProvider : undefined}
/>
return (
<AddingProviderScreen
onCancel={() => setManageProvider(undefined)}
providerType={
isKnownAuthMethods(manageProvider) ? manageProvider : undefined
}
/>
);
}
if (selectedMethod) {
@ -100,7 +105,7 @@ export function AuthenticationEditorScreen(): VNode {
description="No providers founds"
label="Add a provider manually"
onConfirm={() => {
setManageProvider(selectedMethod)
setManageProvider(selectedMethod);
}}
>
<p>
@ -193,7 +198,7 @@ export function AuthenticationEditorScreen(): VNode {
description="No providers founds"
label="Add a provider manually"
onConfirm={() => {
setManageProvider("")
setManageProvider("");
}}
>
<p>
@ -214,7 +219,10 @@ export function AuthenticationEditorScreen(): VNode {
authentication method is defined by the backup provider list.
</p>
<p class="block">
<button class="button is-info" onClick={() => setManageProvider("")}>
<button
class="button is-info"
onClick={() => setManageProvider("")}
>
Manage backup providers
</button>
</p>

View File

@ -15,48 +15,51 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { BackupFinishedScreen as TestedComponent } from './BackupFinishedScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { BackupFinishedScreen as TestedComponent } from "./BackupFinishedScreen";
export default {
title: 'Pages/backup/Finished',
title: "Pages/backup/Finished",
component: TestedComponent,
args: {
order: 8,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const WithoutName = createExample(TestedComponent, reducerStatesExample.backupFinished);
export const WithoutName = createExample(
TestedComponent,
reducerStatesExample.backupFinished,
);
export const WithName = createExample(TestedComponent, {...reducerStatesExample.backupFinished,
secret_name: 'super_secret',
export const WithName = createExample(TestedComponent, {
...reducerStatesExample.backupFinished,
secret_name: "super_secret",
} as ReducerState);
export const WithDetails = createExample(TestedComponent, {
...reducerStatesExample.backupFinished,
secret_name: 'super_secret',
secret_name: "super_secret",
success_details: {
'http://anastasis.net': {
"http://anastasis.net": {
policy_expiration: {
t_ms: 'never'
t_ms: "never",
},
policy_version: 0
policy_version: 0,
},
'http://taler.net': {
"http://taler.net": {
policy_expiration: {
t_ms: new Date().getTime() + 60*60*24*1000
t_ms: new Date().getTime() + 60 * 60 * 24 * 1000,
},
policy_version: 1
policy_version: 1,
},
}
},
} as ReducerState);

View File

@ -4,41 +4,62 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function BackupFinishedScreen(): VNode {
const reducer = useAnastasisContext()
const reducer = useAnastasisContext();
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
}
const details = reducer.currentReducerState.success_details
const details = reducer.currentReducerState.success_details;
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>
Your secret was successfully backed up.
</p>}
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>Your secret was successfully backed up.</p>
)}
{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 (
<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>
);
})}
</div>}
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
</div>
</AnastasisClientFrame>);
{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 (
<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>
);
})}
</div>
)}
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
}

View File

@ -19,7 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, RecoveryStates, ReducerState } from "anastasis-core";
import {
ChallengeFeedbackStatus,
RecoveryStates,
ReducerState,
} from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { ChallengeOverviewScreen as TestedComponent } from "./ChallengeOverviewScreen";
@ -247,20 +251,20 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
"uuid-1": { state: ChallengeFeedbackStatus.Solved.toString() },
"uuid-2": {
state: ChallengeFeedbackStatus.Message.toString(),
message: 'Challenge should be solved'
message: "Challenge should be solved",
},
"uuid-3": {
state: ChallengeFeedbackStatus.AuthIban.toString(),
challenge_amount: "EUR:1",
credit_iban: "DE12345789000",
business_name: "Data Loss Incorporated",
wire_transfer_subject: "Anastasis 987654321"
wire_transfer_subject: "Anastasis 987654321",
},
"uuid-4": {
state: ChallengeFeedbackStatus.Payment.toString(),
taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG"
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
},
"uuid-5": {
state: ChallengeFeedbackStatus.RateLimitExceeded.toString(),
@ -269,7 +273,7 @@ export const OnePolicyWithAllTheChallengesInDifferentState = createExample(
"uuid-6": {
state: ChallengeFeedbackStatus.Redirect.toString(),
redirect_url: "https://videoconf.example.com/",
http_status: 303
http_status: 303,
},
"uuid-7": {
state: ChallengeFeedbackStatus.ServerFailure.toString(),

View File

@ -11,23 +11,34 @@ function OverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }) {
}
switch (feedback.state) {
case ChallengeFeedbackStatus.Message:
return (
<div class="block has-text-danger">{feedback.message}</div>
);
return <div class="block has-text-danger">{feedback.message}</div>;
case ChallengeFeedbackStatus.Solved:
return <div />
return <div />;
case ChallengeFeedbackStatus.Pending:
case ChallengeFeedbackStatus.Solved:
case ChallengeFeedbackStatus.AuthIban:
return null;
case ChallengeFeedbackStatus.ServerFailure:
return <div class="block has-text-danger">Server error.</div>;
case ChallengeFeedbackStatus.RateLimitExceeded:
return <div class="block has-text-danger">There were to many failed attempts.</div>;
return (
<div class="block has-text-danger">
There were to many failed attempts.
</div>
);
case ChallengeFeedbackStatus.Unsupported:
return <div class="block has-text-danger">This client doesn't support solving this type of challenge. Use another version or contact the provider.</div>;
return (
<div class="block has-text-danger">
This client doesn't support solving this type of challenge. Use
another version or contact the provider.
</div>
);
case ChallengeFeedbackStatus.TruthUnknown:
return <div class="block has-text-danger">Provider doesn't recognize the challenge of the policy. Contact the provider for further information.</div>;
return (
<div class="block has-text-danger">
Provider doesn't recognize the challenge of the policy. Contact the
provider for further information.
</div>
);
case ChallengeFeedbackStatus.Redirect:
default:
return <div />;
@ -70,19 +81,25 @@ export function ChallengeOverviewScreen(): VNode {
feedback: challengeFeedback[ch.uuid],
};
}
const policiesWithInfo = policies.map((row) => {
let isPolicySolved = true;
const challenges = row
.map(({ uuid }) => {
const info = knownChallengesMap[uuid];
const isChallengeSolved = info?.feedback?.state === "solved";
isPolicySolved = isPolicySolved && isChallengeSolved;
return { info, uuid, isChallengeSolved };
})
.filter((ch) => ch.info !== undefined);
const policiesWithInfo = policies
.map((row) => {
let isPolicySolved = true;
const challenges = row
.map(({ uuid }) => {
const info = knownChallengesMap[uuid];
const isChallengeSolved = info?.feedback?.state === "solved";
isPolicySolved = isPolicySolved && isChallengeSolved;
return { info, uuid, isChallengeSolved };
})
.filter((ch) => ch.info !== undefined);
return { isPolicySolved, challenges };
});
return {
isPolicySolved,
challenges,
corrupted: row.length > challenges.length,
};
})
.filter((p) => !p.corrupted);
const atLeastThereIsOnePolicySolved =
policiesWithInfo.find((p) => p.isPolicySolved) !== undefined;
@ -92,19 +109,19 @@ export function ChallengeOverviewScreen(): VNode {
: undefined;
return (
<AnastasisClientFrame hideNext={errors} title="Recovery: Solve challenges">
{!policies.length ? (
{!policiesWithInfo.length ? (
<p class="block">
No policies found, try with another version of the secret
</p>
) : policies.length === 1 ? (
) : policiesWithInfo.length === 1 ? (
<p class="block">
One policy found for this secret. You need to solve all the challenges
in order to recover your secret.
</p>
) : (
<p class="block">
We have found {policies.length} polices. You need to solve all the
challenges from one policy in order to recover your secret.
We have found {policiesWithInfo.length} polices. You need to solve all
the challenges from one policy in order to recover your secret.
</p>
)}
{policiesWithInfo.map((policy, policy_index) => {
@ -113,74 +130,100 @@ export function ChallengeOverviewScreen(): VNode {
const method = authMethods[info.type as KnownAuthMethods];
if (!method) {
return <div
key={uuid}
class="block"
style={{ display: "flex", justifyContent: "space-between" }}
>
<div style={{ display: "flex", alignItems: "center" }}>
<span>unknown challenge</span>
return (
<div
key={uuid}
class="block"
style={{ display: "flex", justifyContent: "space-between" }}
>
<div style={{ display: "flex", alignItems: "center" }}>
<span>unknown challenge</span>
</div>
</div>
</div>
);
}
function ChallengeButton({ id, feedback }: { id: string; feedback?: ChallengeFeedback }): VNode {
function ChallengeButton({
id,
feedback,
}: {
id: string;
feedback?: ChallengeFeedback;
}): VNode {
function selectChallenge(): void {
if (reducer) reducer.transition("select_challenge", { uuid: id })
if (reducer) reducer.transition("select_challenge", { uuid: id });
}
if (!feedback) {
return <div>
<button class="button" onClick={selectChallenge}>
Solve
</button>
</div>
return (
<div>
<button
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Solve
</button>
</div>
);
}
switch (feedback.state) {
case ChallengeFeedbackStatus.ServerFailure:
case ChallengeFeedbackStatus.Unsupported:
case ChallengeFeedbackStatus.TruthUnknown:
case ChallengeFeedbackStatus.RateLimitExceeded: return <div />
case ChallengeFeedbackStatus.RateLimitExceeded:
return <div />;
case ChallengeFeedbackStatus.AuthIban:
case ChallengeFeedbackStatus.Payment: return <div>
<button class="button" onClick={selectChallenge}>
Pay
</button>
</div>
case ChallengeFeedbackStatus.Redirect: return <div>
<button class="button" onClick={selectChallenge}>
Go to {feedback.redirect_url}
</button>
</div>
case ChallengeFeedbackStatus.Solved: return <div>
<div class="tag is-success is-large">
Solved
</div>
</div>
default: return <div>
<button class="button" onClick={selectChallenge}>
Solve
</button>
</div>
case ChallengeFeedbackStatus.Payment:
return (
<div>
<button
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Pay
</button>
</div>
);
case ChallengeFeedbackStatus.Redirect:
return (
<div>
<button
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Go to {feedback.redirect_url}
</button>
</div>
);
case ChallengeFeedbackStatus.Solved:
return (
<div>
<div class="tag is-success is-large">Solved</div>
</div>
);
default:
return (
<div>
<button
class="button"
disabled={
atLeastThereIsOnePolicySolved && !policy.isPolicySolved
}
onClick={selectChallenge}
>
Solve
</button>
</div>
);
}
// return <div>
// {feedback.state !== "solved" ? (
// <a
// class="button"
// onClick={() =>
// }
// >
// {isFree ? "Solve" : `Pay and Solve`}
// </a>
// ) : null}
// {feedback.state === "solved" ? (
// // <div class="block is-success" > Solved </div>
// <div class="tag is-success is-large">Solved</div>
// ) : null}
// </div>
}
return (
<div
@ -202,7 +245,6 @@ export function ChallengeOverviewScreen(): VNode {
</div>
<ChallengeButton id={uuid} feedback={info.feedback} />
</div>
);
});
@ -210,11 +252,13 @@ export function ChallengeOverviewScreen(): VNode {
const policyName = policy.challenges
.map((x) => x.info.type)
.join(" + ");
const opa = !atLeastThereIsOnePolicySolved
? undefined
: policy.isPolicySolved
? undefined
: "0.6";
? undefined
: "0.6";
return (
<div
key={policy_index}

View File

@ -15,24 +15,26 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../utils';
import { ChallengePayingScreen as TestedComponent } from './ChallengePayingScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../utils";
import { ChallengePayingScreen as TestedComponent } from "./ChallengePayingScreen";
export default {
title: 'Pages/recovery/__ChallengePaying',
title: "Pages/recovery/__ChallengePaying",
component: TestedComponent,
args: {
order: 10,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const Example = createExample(TestedComponent, reducerStatesExample.challengePaying);
export const Example = createExample(
TestedComponent,
reducerStatesExample.challengePaying,
);

View File

@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function ChallengePayingScreen(): VNode {
const reducer = useAnastasisContext()
const reducer = useAnastasisContext();
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>;
}
const payments = ['']; //reducer.currentReducerState.payments ??
const payments = [""]; //reducer.currentReducerState.payments ??
return (
<AnastasisClientFrame
hideNav
title="Recovery: Challenge Paying"
>
<AnastasisClientFrame hideNav title="Recovery: Challenge Paying">
<p>
Some of the providers require a payment to store the encrypted
authentication information.

View File

@ -16,37 +16,42 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { ContinentSelectionScreen as TestedComponent } from './ContinentSelectionScreen';
*
* @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/Location',
title: "Pages/Location",
component: TestedComponent,
args: {
order: 2,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const BackupSelectContinent = createExample(TestedComponent, reducerStatesExample.backupSelectContinent);
export const BackupSelectContinent = createExample(
TestedComponent,
reducerStatesExample.backupSelectContinent,
);
export const BackupSelectCountry = createExample(TestedComponent, {
...reducerStatesExample.backupSelectContinent,
selected_continent: 'Testcontinent',
selected_continent: "Testcontinent",
} as ReducerState);
export const RecoverySelectContinent = createExample(TestedComponent, reducerStatesExample.recoverySelectContinent);
export const RecoverySelectContinent = createExample(
TestedComponent,
reducerStatesExample.recoverySelectContinent,
);
export const RecoverySelectCountry = createExample(TestedComponent, {
...reducerStatesExample.recoverySelectContinent,
selected_continent: 'Testcontinent',
selected_continent: "Testcontinent",
} as ReducerState);

View File

@ -16,94 +16,126 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { EditPoliciesScreen as TestedComponent } from './EditPoliciesScreen';
*
* @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/ReviewPolicies/EditPolicies',
title: "Pages/backup/ReviewPolicies/EditPolicies",
args: {
order: 6,
},
component: TestedComponent,
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
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});
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

@ -20,7 +20,6 @@ interface Props {
index: number;
cancel: () => void;
confirm: (changes: MethodProvider[]) => void;
}
export interface MethodProvider {
@ -28,106 +27,151 @@ export interface MethodProvider {
provider: string;
}
export function EditPoliciesScreen({ index: policy_index, cancel, confirm }: Props): VNode {
const [changedProvider, setChangedProvider] = useState<Array<string>>([])
export function EditPoliciesScreen({
index: policy_index,
cancel,
confirm,
}: Props): VNode {
const [changedProvider, setChangedProvider] = useState<Array<string>>([]);
const reducer = useAnastasisContext()
const reducer = useAnastasisContext();
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</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 || {})
const selectableProviders: ProviderInfoByType = {};
const allProviders = Object.entries(
reducer.currentReducerState.authentication_providers || {},
);
for (let index = 0; index < allProviders.length; index++) {
const [url, status] = allProviders[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
})
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 allAuthMethods =
reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
const policy = policies[policy_index]
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
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[] = []
const newMethods: MethodProvider[] = [];
allAuthMethods.forEach((method, index) => {
const oldValue = policy?.methods.find(m => m.authentication_method === index)
const oldValue = policy?.methods.find(
(m) => m.authentication_method === index,
);
if (changedProvider[index] === undefined && oldValue !== undefined) {
newMethods.push(oldValue)
newMethods.push(oldValue);
}
if (changedProvider[index] !== undefined && changedProvider[index] !== "") {
if (
changedProvider[index] !== undefined &&
changedProvider[index] !== ""
) {
newMethods.push({
authentication_method: index,
provider: changedProvider[index]
})
provider: changedProvider[index],
});
}
})
confirm(newMethods)
});
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];
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}
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>
))}
</select>
{selectableProviders[type]?.map((prov) => (
<option key={prov.url} value={prov.url}>
{prov.url}
</option>
))}
</select>
</span>
</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>
</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

@ -15,35 +15,40 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { PoliciesPayingScreen as TestedComponent } from './PoliciesPayingScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { PoliciesPayingScreen as TestedComponent } from "./PoliciesPayingScreen";
export default {
title: 'Pages/backup/__PoliciesPaying',
title: "Pages/backup/__PoliciesPaying",
component: TestedComponent,
args: {
order: 9,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const Example = createExample(TestedComponent, reducerStatesExample.policyPay);
export const Example = createExample(
TestedComponent,
reducerStatesExample.policyPay,
);
export const WithSomePaymentRequest = createExample(TestedComponent, {
...reducerStatesExample.policyPay,
policy_payment_requests: [{
payto: 'payto://x-taler-bank/bank.taler/account-a',
provider: 'provider1'
}, {
payto: 'payto://x-taler-bank/bank.taler/account-b',
provider: 'provider2'
}]
policy_payment_requests: [
{
payto: "payto://x-taler-bank/bank.taler/account-a",
provider: "provider1",
},
{
payto: "payto://x-taler-bank/bank.taler/account-b",
provider: "provider2",
},
],
} as ReducerState);

View File

@ -3,20 +3,23 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function PoliciesPayingScreen(): VNode {
const reducer = useAnastasisContext()
const reducer = useAnastasisContext();
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.policy_payment_requests ?? [];
return (
<AnastasisClientFrame hideNav title="Backup: Recovery Document Payments">
<p>
Some of the providers require a payment to store the encrypted
recovery document.
Some of the providers require a payment to store the encrypted recovery
document.
</p>
<ul>
{payments.map((x, i) => {

View File

@ -16,30 +16,32 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { RecoveryFinishedScreen as TestedComponent } from './RecoveryFinishedScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { RecoveryFinishedScreen as TestedComponent } from "./RecoveryFinishedScreen";
export default {
title: 'Pages/recovery/Finished',
title: "Pages/recovery/Finished",
args: {
order: 7,
},
component: TestedComponent,
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const GoodEnding = createExample(TestedComponent, {
...reducerStatesExample.recoveryFinished,
core_secret: { mime: 'text/plain', value: 'hello' }
core_secret: { mime: "text/plain", value: "hello" },
} as ReducerState);
export const BadEnding = createExample(TestedComponent, reducerStatesExample.recoveryFinished);
export const BadEnding = createExample(
TestedComponent,
reducerStatesExample.recoveryFinished,
);

View File

@ -1,39 +1,53 @@
import {
bytesToString,
decodeCrock
} from "@gnu-taler/taler-util";
import { bytesToString, decodeCrock } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function RecoveryFinishedScreen(): VNode {
const reducer = useAnastasisContext()
const reducer = useAnastasisContext();
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>;
}
const encodedSecret = reducer.currentReducerState.core_secret
const encodedSecret = reducer.currentReducerState.core_secret;
if (!encodedSecret) {
return <AnastasisClientFrame title="Recovery Problem" hideNav>
<p>
Secret not found
</p>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
</div>
</AnastasisClientFrame>
return (
<AnastasisClientFrame title="Recovery Problem" hideNav>
<p>Secret not found</p>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
}
const secret = bytesToString(decodeCrock(encodedSecret.value))
const secret = bytesToString(decodeCrock(encodedSecret.value));
return (
<AnastasisClientFrame title="Recovery Finished" hideNav>
<p>
Secret: {secret}
</p>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<p>Your secret: {secret}</p>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);

View File

@ -15,44 +15,51 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { ReviewPoliciesScreen as TestedComponent } from './ReviewPoliciesScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { ReviewPoliciesScreen as TestedComponent } from "./ReviewPoliciesScreen";
export default {
title: 'Pages/backup/ReviewPolicies',
title: "Pages/backup/ReviewPolicies",
args: {
order: 6,
},
component: TestedComponent,
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const HasPoliciesButMethodListIsEmpty = createExample(TestedComponent, {
...reducerStatesExample.policyReview,
policies: [{
methods: [{
authentication_method: 0,
provider: 'asd'
}, {
authentication_method: 1,
provider: 'asd'
}]
}, {
methods: [{
authentication_method: 1,
provider: 'asd'
}]
}],
authentication_methods: []
policies: [
{
methods: [
{
authentication_method: 0,
provider: "asd",
},
{
authentication_method: 1,
provider: "asd",
},
],
},
{
methods: [
{
authentication_method: 1,
provider: "asd",
},
],
},
],
authentication_methods: [],
} as ReducerState);
export const SomePoliciesWithMethods = createExample(TestedComponent, {
@ -62,186 +69,193 @@ export const SomePoliciesWithMethods = createExample(TestedComponent, {
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
}
]
provider: "https://kudos.demo.anastasis.lu/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
}
]
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
}
]
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 0,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
}
]
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 1,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
provider: "https://anastasis.demo.taler.net/",
},
],
},
{
methods: [
{
authentication_method: 2,
provider: "https://kudos.demo.anastasis.lu/"
provider: "https://kudos.demo.anastasis.lu/",
},
{
authentication_method: 3,
provider: "https://anastasis.demo.taler.net/"
provider: "https://anastasis.demo.taler.net/",
},
{
authentication_method: 4,
provider: "https://anastasis.demo.taler.net/"
}
]
}
provider: "https://anastasis.demo.taler.net/",
},
],
},
],
authentication_methods: [
{
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",
},
{
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",
},
],
authentication_methods: [{
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"
},{
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);

View File

@ -6,16 +6,20 @@ import { EditPoliciesScreen } from "./EditPoliciesScreen";
import { AnastasisClientFrame } from "./index";
export function ReviewPoliciesScreen(): VNode {
const [editingPolicy, setEditingPolicy] = useState<number | undefined>()
const reducer = useAnastasisContext()
const [editingPolicy, setEditingPolicy] = useState<number | undefined>();
const reducer = useAnastasisContext();
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
}
const configuredAuthMethods = reducer.currentReducerState.authentication_methods ?? [];
const configuredAuthMethods =
reducer.currentReducerState.authentication_methods ?? [];
const policies = reducer.currentReducerState.policies ?? [];
if (editingPolicy !== undefined) {
@ -28,58 +32,109 @@ export function ReviewPoliciesScreen(): VNode {
policy_index: editingPolicy,
policy: newMethods,
});
setEditingPolicy(undefined)
setEditingPolicy(undefined);
}}
/>
)
);
}
const errors = policies.length < 1 ? 'Need more policies' : 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
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>}
<div class="block" style={{ justifyContent: 'flex-end' }} >
<button class="button is-success" onClick={() => setEditingPolicy(policies.length + 1)}>Add new policy</button>
<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>
)}
<div class="block" style={{ justifyContent: "flex-end" }}>
<button
class="button is-success"
onClick={() => setEditingPolicy(policies.length + 1)}
>
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 }))
.filter(x => !!x)
.map(
(x) =>
configuredAuthMethods[x.authentication_method] && {
...configuredAuthMethods[x.authentication_method],
provider: x.provider,
},
)
.filter((x) => !!x);
const policyName = methods.map(x => x.type).join(" + ");
const policyName = methods.map((x) => x.type).join(" + ");
if (p.methods.length > methods.length) {
//there is at least one authentication method that is corrupted
return null;
}
return (
<div key={policy_index} class="box" style={{ display: 'flex', justifyContent: 'space-between' }}>
<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.length && <p>No auth method found</p>}
{methods.map((m, i) => {
return (
<p key={i} class="block" style={{ display: 'flex', alignItems: 'center' }}>
<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>
{m.instructions} recovery provided by{" "}
<a href={m.provider}>{m.provider}</a>
</span>
</p>
);
})}
</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
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

@ -15,30 +15,29 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { SecretEditorScreen as TestedComponent } from './SecretEditorScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { SecretEditorScreen as TestedComponent } from "./SecretEditorScreen";
export default {
title: 'Pages/backup/SecretInput',
title: "Pages/backup/SecretInput",
component: TestedComponent,
args: {
order: 7,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const WithSecretNamePreselected = createExample(TestedComponent, {
...reducerStatesExample.secretEdition,
secret_name: 'someSecretName',
secret_name: "someSecretName",
} as ReducerState);
export const WithoutName = createExample(TestedComponent, {

View File

@ -15,37 +15,35 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { SecretSelectionScreen as TestedComponent } from './SecretSelectionScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { SecretSelectionScreen as TestedComponent } from "./SecretSelectionScreen";
export default {
title: 'Pages/recovery/SecretSelection',
title: "Pages/recovery/SecretSelection",
component: TestedComponent,
args: {
order: 4,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const Example = createExample(TestedComponent, {
...reducerStatesExample.secretSelection,
recovery_document: {
provider_url: 'https://kudos.demo.anastasis.lu/',
secret_name: 'secretName',
provider_url: "https://kudos.demo.anastasis.lu/",
secret_name: "secretName",
version: 1,
},
} as ReducerState);
export const NoRecoveryDocumentFound = createExample(TestedComponent, {
...reducerStatesExample.secretSelection,
recovery_document: undefined,

View File

@ -8,18 +8,23 @@ import { AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(): VNode {
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
const reducer = useAnastasisContext()
const reducer = useAnastasisContext();
const [manageProvider, setManageProvider] = useState(false)
const currentVersion = (reducer?.currentReducerState
&& ("recovery_document" in reducer.currentReducerState)
&& reducer.currentReducerState.recovery_document?.version) || 0;
const [manageProvider, setManageProvider] = useState(false);
const currentVersion =
(reducer?.currentReducerState &&
"recovery_document" in reducer.currentReducerState &&
reducer.currentReducerState.recovery_document?.version) ||
0;
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || reducer.currentReducerState.recovery_state === undefined) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
reducer.currentReducerState.recovery_state === undefined
) {
return <div>invalid state</div>;
}
async function doSelectVersion(p: string, n: number): Promise<void> {
@ -33,72 +38,101 @@ export function SecretSelectionScreen(): VNode {
});
}
const providerList = Object.keys(reducer.currentReducerState.authentication_providers ?? {})
const recoveryDocument = reducer.currentReducerState.recovery_document
const providerList = Object.keys(
reducer.currentReducerState.authentication_providers ?? {},
);
const recoveryDocument = reducer.currentReducerState.recovery_document;
if (!recoveryDocument) {
return <ChooseAnotherProviderScreen
providers={providerList} selected=""
onChange={(newProv) => doSelectVersion(newProv, 0)}
/>
return (
<ChooseAnotherProviderScreen
providers={providerList}
selected=""
onChange={(newProv) => doSelectVersion(newProv, 0)}
/>
);
}
if (selectingVersion) {
return <SelectOtherVersionProviderScreen providers={providerList}
provider={recoveryDocument.provider_url} version={recoveryDocument.version}
onCancel={() => setSelectingVersion(false)}
onConfirm={doSelectVersion}
/>
return (
<SelectOtherVersionProviderScreen
providers={providerList}
provider={recoveryDocument.provider_url}
version={recoveryDocument.version}
onCancel={() => setSelectingVersion(false)}
onConfirm={doSelectVersion}
/>
);
}
if (manageProvider) {
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />
return <AddingProviderScreen onCancel={() => setManageProvider(false)} />;
}
return (
<AnastasisClientFrame title="Recovery: Select secret">
<div class="columns">
<div class="column">
<div class="box" style={{ border: '2px solid green' }}>
<div class="box" style={{ border: "2px solid green" }}>
<h1 class="subtitle">{recoveryDocument.provider_url}</h1>
<div class="block">
{currentVersion === 0 ? <p>
Set to recover the latest version
</p> : <p>
Set to recover the version number {currentVersion}
</p>}
{currentVersion === 0 ? (
<p>Set to recover the latest version</p>
) : (
<p>Set to recover the version number {currentVersion}</p>
)}
</div>
<div class="buttons is-right">
<button class="button" onClick={(e) => setSelectingVersion(true)}>Change secret's version</button>
<button class="button" onClick={(e) => setSelectingVersion(true)}>
Change secret's version
</button>
</div>
</div>
</div>
<div class="column">
<p>Secret found, you can select another version or continue to the challenges solving</p>
<p class="block">
<button class="button is-info" onClick={() => setManageProvider(true)}>
Manage recovery providers
</button>
<p>
Secret found, you can select another version or continue to the
challenges solving
</p>
<p class="block">
<a onClick={() => setManageProvider(true)}>
Manage recovery providers
</a>
</p>
</div>
</div>
</AnastasisClientFrame>
);
}
function ChooseAnotherProviderScreen({ providers, selected, onChange }: { selected: string; providers: string[]; onChange: (prov: string) => void }): VNode {
function ChooseAnotherProviderScreen({
providers,
selected,
onChange,
}: {
selected: string;
providers: string[];
onChange: (prov: string) => void;
}): VNode {
return (
<AnastasisClientFrame hideNext="Recovery document not found" title="Recovery: Problem">
<AnastasisClientFrame
hideNext="Recovery document not found"
title="Recovery: Problem"
>
<p>No recovery document found, try with another provider</p>
<div class="field">
<label class="label">Provider</label>
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth">
<select onChange={(e) => onChange(e.currentTarget.value)} value={selected}>
<option key="none" disabled selected value=""> Choose a provider </option>
{providers.map(prov => (
<select
onChange={(e) => onChange(e.currentTarget.value)}
value={selected}
>
<option key="none" disabled selected value="">
{" "}
Choose a provider{" "}
</option>
{providers.map((prov) => (
<option key={prov} value={prov}>
{prov}
</option>
@ -114,9 +148,23 @@ function ChooseAnotherProviderScreen({ providers, selected, onChange }: { select
);
}
function SelectOtherVersionProviderScreen({ providers, provider, version, onConfirm, onCancel }: { onCancel: () => void; provider: string; version: number; providers: string[]; onConfirm: (prov: string, v: number) => Promise<void>; }): VNode {
function SelectOtherVersionProviderScreen({
providers,
provider,
version,
onConfirm,
onCancel,
}: {
onCancel: () => void;
provider: string;
version: number;
providers: string[];
onConfirm: (prov: string, v: number) => Promise<void>;
}): VNode {
const [otherProvider, setOtherProvider] = useState<string>(provider);
const [otherVersion, setOtherVersion] = useState(version > 0 ? String(version) : "");
const [otherVersion, setOtherVersion] = useState(
version > 0 ? String(version) : "",
);
return (
<AnastasisClientFrame hideNav title="Recovery: Select secret">
@ -125,11 +173,11 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
<div class="box">
<h1 class="subtitle">Provider {otherProvider}</h1>
<div class="block">
{version === 0 ? <p>
Set to recover the latest version
</p> : <p>
Set to recover the version number {version}
</p>}
{version === 0 ? (
<p>Set to recover the latest version</p>
) : (
<p>Set to recover the version number {version}</p>
)}
<p>Specify other version below or use the latest</p>
</div>
@ -137,9 +185,15 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
<label class="label">Provider</label>
<div class="control is-expanded has-icons-left">
<div class="select is-fullwidth">
<select onChange={(e) => setOtherProvider(e.currentTarget.value)} value={otherProvider}>
<option key="none" disabled selected value=""> Choose a provider </option>
{providers.map(prov => (
<select
onChange={(e) => setOtherProvider(e.currentTarget.value)}
value={otherProvider}
>
<option key="none" disabled selected value="">
{" "}
Choose a provider{" "}
</option>
{providers.map((prov) => (
<option key={prov} value={prov}>
{prov}
</option>
@ -156,23 +210,40 @@ function SelectOtherVersionProviderScreen({ providers, provider, version, onConf
label="Version"
placeholder="version number to recover"
grabFocus
bind={[otherVersion, setOtherVersion]} />
bind={[otherVersion, setOtherVersion]}
/>
</div>
</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={onCancel}>Cancel</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={onCancel}>
Cancel
</button>
<div class="buttons">
<AsyncButton class="button" onClick={() => onConfirm(otherProvider, 0)}>Use latest</AsyncButton>
<AsyncButton class="button is-info" onClick={() => onConfirm(otherProvider, parseInt(otherVersion, 10))}>Confirm</AsyncButton>
<AsyncButton
class="button"
onClick={() => onConfirm(otherProvider, 0)}
>
Use latest
</AsyncButton>
<AsyncButton
class="button is-info"
onClick={() =>
onConfirm(otherProvider, parseInt(otherVersion, 10))
}
>
Confirm
</AsyncButton>
</div>
</div>
</div>
<div class="column">
.
</div>
<div class="column">.</div>
</div>
</AnastasisClientFrame>
);
}

View File

@ -15,55 +15,63 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, RecoveryStates, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { SolveScreen as TestedComponent } from './SolveScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import {
ChallengeFeedbackStatus,
RecoveryStates,
ReducerState,
} from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { SolveScreen as TestedComponent } from "./SolveScreen";
export default {
title: 'Pages/recovery/SolveChallenge/Solve',
title: "Pages/recovery/SolveChallenge/Solve",
component: TestedComponent,
args: {
order: 6,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const NoInformation = createExample(TestedComponent, reducerStatesExample.challengeSolving);
export const NoInformation = createExample(
TestedComponent,
reducerStatesExample.challengeSolving,
);
export const NotSupportedChallenge = createExample(TestedComponent, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'chall-type',
uuid: 'ASDASDSAD!1'
}],
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "chall-type",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: 'ASDASDSAD!1',
selected_challenge_uuid: "ASDASDSAD!1",
} as ReducerState);
export const MismatchedChallengeId = createExample(TestedComponent, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'chall-type',
uuid: 'ASDASDSAD!1'
}],
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "chall-type",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: 'no-no-no'
selected_challenge_uuid: "no-no-no",
} as ReducerState);

View File

@ -2,76 +2,126 @@ import { h, VNode } from "preact";
import { AnastasisClientFrame } from ".";
import {
ChallengeFeedback,
ChallengeFeedbackStatus
ChallengeFeedbackStatus,
} from "../../../../anastasis-core/lib";
import { Notifications } from "../../components/Notifications";
import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod";
export function SolveOverviewFeedbackDisplay(props: { feedback?: ChallengeFeedback }): VNode {
export function SolveOverviewFeedbackDisplay(props: {
feedback?: ChallengeFeedback;
}): VNode {
const { feedback } = props;
if (!feedback) {
return <div />;
}
switch (feedback.state) {
case ChallengeFeedbackStatus.Message:
return (<Notifications notifications={[{
type: "INFO",
message: `Message from provider`,
description: feedback.message
}]} />);
case ChallengeFeedbackStatus.Payment:
return <Notifications notifications={[{
type: "INFO",
message: `Message from provider`,
description: <span>
To pay you can <a href={feedback.taler_pay_uri}>click here</a>
</span>
}]} />
case ChallengeFeedbackStatus.AuthIban:
return <Notifications notifications={[{
type: "INFO",
message: `Message from provider`,
description: `Need to send a wire transfer to "${feedback.business_name}"`
}]} />;
case ChallengeFeedbackStatus.ServerFailure:
return (<Notifications notifications={[{
type: "ERROR",
message: `Server error: Code ${feedback.http_status}`,
description: feedback.error_response
}]} />);
case ChallengeFeedbackStatus.RateLimitExceeded:
return (<Notifications notifications={[{
type: "ERROR",
message: `Message from provider`,
description: "There were to many failed attempts."
}]} />);
case ChallengeFeedbackStatus.Redirect:
return (<Notifications notifications={[{
type: "INFO",
message: `Message from provider`,
description: <span>
Please visit this link: <a>{feedback.redirect_url}</a>
</span>
}]} />);
case ChallengeFeedbackStatus.Unsupported:
return (<Notifications notifications={[{
type: "ERROR",
message: `This client doesn't support solving this type of challenge`,
description: `Use another version or contact the provider. Type of challenge "${feedback.unsupported_method}"`
}]} />);
case ChallengeFeedbackStatus.TruthUnknown:
return (<Notifications notifications={[{
type: "ERROR",
message: `Provider doesn't recognize the type of challenge`,
description: "Contact the provider for further information"
}]} />);
default:
return (
<div>
<pre>{JSON.stringify(feedback)}</pre>
</div>
<Notifications
notifications={[
{
type: "INFO",
message: `Message from provider`,
description: feedback.message,
},
]}
/>
);
case ChallengeFeedbackStatus.Payment:
return (
<Notifications
notifications={[
{
type: "INFO",
message: `Message from provider`,
description: (
<span>
To pay you can <a href={feedback.taler_pay_uri}>click here</a>
</span>
),
},
]}
/>
);
case ChallengeFeedbackStatus.AuthIban:
return (
<Notifications
notifications={[
{
type: "INFO",
message: `Message from provider`,
description: `Need to send a wire transfer to "${feedback.business_name}"`,
},
]}
/>
);
case ChallengeFeedbackStatus.ServerFailure:
return (
<Notifications
notifications={[
{
type: "ERROR",
message: `Server error: Code ${feedback.http_status}`,
description: feedback.error_response,
},
]}
/>
);
case ChallengeFeedbackStatus.RateLimitExceeded:
return (
<Notifications
notifications={[
{
type: "ERROR",
message: `Message from provider`,
description: "There were to many failed attempts.",
},
]}
/>
);
case ChallengeFeedbackStatus.Redirect:
return (
<Notifications
notifications={[
{
type: "INFO",
message: `Message from provider`,
description: (
<span>
Please visit this link: <a>{feedback.redirect_url}</a>
</span>
),
},
]}
/>
);
case ChallengeFeedbackStatus.Unsupported:
return (
<Notifications
notifications={[
{
type: "ERROR",
message: `This client doesn't support solving this type of challenge`,
description: `Use another version or contact the provider. Type of challenge "${feedback.unsupported_method}"`,
},
]}
/>
);
case ChallengeFeedbackStatus.TruthUnknown:
return (
<Notifications
notifications={[
{
type: "ERROR",
message: `Provider doesn't recognize the type of challenge`,
description: "Contact the provider for further information",
},
]}
/>
);
default:
return <div />;
}
}
@ -110,8 +160,16 @@ export function SolveScreen(): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
@ -120,26 +178,36 @@ export function SolveScreen(): VNode {
return (
<AnastasisClientFrame hideNav title="Not implemented">
<p>
The challenge selected is not supported for this UI. Please update this
version or try using another policy.
The challenge selected is not supported for this UI. Please update
this version or try using another policy.
</p>
{reducer &&
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
{reducer && (
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
}
)}
</AnastasisClientFrame>
);
}
const chArr = reducer.currentReducerState.recovery_information.challenges;
const selectedUuid = reducer.currentReducerState.selected_challenge_uuid;
const selectedChallenge = chArr.find(ch => ch.uuid === selectedUuid)
const selectedChallenge = chArr.find((ch) => ch.uuid === selectedUuid);
const SolveDialog = !selectedChallenge || !authMethods[selectedChallenge.type as KnownAuthMethods] ?
SolveNotImplemented :
authMethods[selectedChallenge.type as KnownAuthMethods].solve ?? SolveNotImplemented
const SolveDialog =
!selectedChallenge ||
!authMethods[selectedChallenge.type as KnownAuthMethods]
? SolveNotImplemented
: authMethods[selectedChallenge.type as KnownAuthMethods].solve ??
SolveNotImplemented;
return <SolveDialog id={selectedUuid} />
return <SolveDialog id={selectedUuid} />;
}

View File

@ -15,24 +15,26 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../utils';
import { StartScreen as TestedComponent } from './StartScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../utils";
import { StartScreen as TestedComponent } from "./StartScreen";
export default {
title: 'Pages/Start',
title: "Pages/Start",
component: TestedComponent,
args: {
order: 1,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const InitialState = createExample(TestedComponent, reducerStatesExample.initial);
export const InitialState = createExample(
TestedComponent,
reducerStatesExample.initial,
);

View File

@ -1,27 +1,36 @@
import { h, VNode } from "preact";
import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function StartScreen(): VNode {
const reducer = useAnastasisContext()
const reducer = useAnastasisContext();
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
return (
<AnastasisClientFrame hideNav title="Home">
<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>
<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>
<button
class="button is-info"
onClick={() => reducer.startRecover()}
>
<div class="icon">
<i class="mdi mdi-arrow-down" />
</div>
<span>Recover a secret</span>
</button>
@ -30,7 +39,6 @@ export function StartScreen(): VNode {
<span>Restore a session</span>
</button> */}
</div>
</div>
<div class="column" />
</div>

View File

@ -15,29 +15,31 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../utils';
import { TruthsPayingScreen as TestedComponent } from './TruthsPayingScreen';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../utils";
import { TruthsPayingScreen as TestedComponent } from "./TruthsPayingScreen";
export default {
title: 'Pages/backup/__TruthsPaying',
title: "Pages/backup/__TruthsPaying",
component: TestedComponent,
args: {
order: 10,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
export const Example = createExample(TestedComponent, reducerStatesExample.truthsPaying);
export const Example = createExample(
TestedComponent,
reducerStatesExample.truthsPaying,
);
export const WithPaytoList = createExample(TestedComponent, {
...reducerStatesExample.truthsPaying,
payments: ['payto://x-taler-bank/bank/account']
payments: ["payto://x-taler-bank/bank/account"],
} as ReducerState);

View File

@ -3,19 +3,19 @@ import { useAnastasisContext } from "../../context/anastasis";
import { AnastasisClientFrame } from "./index";
export function TruthsPayingScreen(): VNode {
const reducer = useAnastasisContext()
const reducer = useAnastasisContext();
if (!reducer) {
return <div>no reducer in context</div>
return <div>no reducer in context</div>;
}
if (!reducer.currentReducerState || reducer.currentReducerState.backup_state === undefined) {
return <div>invalid state</div>
if (
!reducer.currentReducerState ||
reducer.currentReducerState.backup_state === undefined
) {
return <div>invalid state</div>;
}
const payments = reducer.currentReducerState.payments ?? [];
return (
<AnastasisClientFrame
hideNext={"FIXME"}
title="Backup: Truths Paying"
>
<AnastasisClientFrame hideNext={"FIXME"} title="Backup: Truths Paying">
<p>
Some of the providers require a payment to store the encrypted
authentication information.

View File

@ -15,51 +15,67 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/email',
title: "Pages/backup/AuthorizationMethod/AuthMethods/email",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'email'
const type: KnownAuthMethods = "email";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: []
});
export const Empty = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: 'Email to sebasjm@email.com ',
remove: () => null
}]
});
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: "Email to sebasjm@email.com ",
remove: () => null,
},
],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, 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
}]
});
export const WithMoreExamples = createExample(
TestedComponent[type].setup,
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,
},
],
},
);

View File

@ -1,57 +1,90 @@
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { EmailInput } from "../../../components/fields/EmailInput";
import { AnastasisClientFrame } from "../index";
import { AuthMethodSetupProps } from "./index";
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,}))$/
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 {
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
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.
email. Add the uuid from the challenge
</p>
<div>
<EmailInput
label="Email address"
error={emailError}
placeholder="email@domain.com"
bind={[email, setEmail]} />
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>}
{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}>Cancel</button>
<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={addEmailAuth}>Add</button>
<button
class="button is-info"
disabled={errors !== undefined}
onClick={addEmailAuth}
>
Add
</button>
</span>
</div>
</div>

View File

@ -15,66 +15,76 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/email',
title: "Pages/recovery/SolveChallenge/AuthMethods/email",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'email'
const type: KnownAuthMethods = "email";
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'uuid-1'
}],
policies: [],
export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
{
id: "uuid-1",
},
selected_challenge_uuid: 'uuid-1',
} as ReducerState, {
id: 'uuid-1',
});
);
export const PaymentFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'uuid-1'
}],
policies: [],
export const PaymentFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
challenge_feedback: {
"uuid-1": {
state: ChallengeFeedbackStatus.Payment,
taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
},
},
} as ReducerState,
{
id: "uuid-1",
},
selected_challenge_uuid: 'uuid-1',
challenge_feedback: {
'uuid-1': {
state: ChallengeFeedbackStatus.Payment,
taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG"
}
}
} as ReducerState, {
id: 'uuid-1',
});
);

View File

@ -44,8 +44,16 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
@ -62,8 +70,7 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid]
const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,19 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back();
}
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} />
<p>
An email has been sent to "<b>{selectedChallenge.instructions}</b>". Type the
code below
An email has been sent to "<b>{selectedChallenge.instructions}</b>".
Type the code below.
<b>Here we need to add the code "{selectedUuid}"</b>
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
@ -97,9 +105,11 @@ export function AuthMethodEmailSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}>
Cancel
</button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>}
{!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>
)}
</div>
</AnastasisClientFrame>
);

View File

@ -15,50 +15,66 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/IBAN',
title: "Pages/backup/AuthorizationMethod/AuthMethods/IBAN",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'iban'
const type: KnownAuthMethods = "iban";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: []
});
export const Empty = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: 'Wire transfer from QWEASD123123 with holder Sebastian',
remove: () => null
}]
});
export const WithMoreExamples = createExample(TestedComponent[type].setup, 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
}]
},);
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: "Wire transfer from QWEASD123123 with holder Sebastian",
remove: () => null,
},
],
},
);
export const WithMoreExamples = createExample(
TestedComponent[type].setup,
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,
},
],
},
);

View File

@ -1,7 +1,7 @@
import {
canonicalJson,
encodeCrock,
stringToBytes
stringToBytes,
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@ -9,56 +9,98 @@ import { AuthMethodSetupProps } from ".";
import { TextInput } from "../../../components/fields/TextInput";
import { AnastasisClientFrame } from "../index";
export function AuthMethodIbanSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
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
)
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.
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]} />
bind={[name, setName]}
/>
<TextInput
label="IBAN"
placeholder="DE91100000000123456789"
bind={[account, setAccount]} />
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>}
{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>
<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>
<button
class="button is-info"
disabled={errors !== undefined}
onClick={addIbanAuth}
>
Add
</button>
</span>
</div>
</div>

View File

@ -15,42 +15,46 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/Iban',
title: "Pages/recovery/SolveChallenge/AuthMethods/Iban",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'iban'
const type: KnownAuthMethods = "iban";
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'uuid-1'
}],
policies: [],
export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
{
id: "uuid-1",
},
selected_challenge_uuid: 'uuid-1',
} as ReducerState, {
id: 'uuid-1',
});
);

View File

@ -44,8 +44,16 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
@ -62,8 +70,7 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid]
const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer });
@ -72,19 +79,17 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back();
}
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} />
<p>
Send a wire transfer to the address
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<p>Send a wire transfer to the address,</p>
<button class="button">Check</button>
<div
style={{
@ -96,9 +101,11 @@ export function AuthMethodIbanSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}>
Cancel
</button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>}
{!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>
)}
</div>
</AnastasisClientFrame>
);

View File

@ -16,51 +16,67 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Post',
title: "Pages/backup/AuthorizationMethod/AuthMethods/Post",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'post'
const type: KnownAuthMethods = "post";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: []
});
export const Empty = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: 'Letter to address in postal code QWE456',
remove: () => null
}]
});
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: "Letter to address in postal code QWE456",
remove: () => null,
},
],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, 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
}]
});
export const WithMoreExamples = createExample(
TestedComponent[type].setup,
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,
},
],
},
);

View File

@ -1,6 +1,7 @@
import {
canonicalJson, encodeCrock,
stringToBytes
canonicalJson,
encodeCrock,
stringToBytes,
} from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@ -8,7 +9,11 @@ import { AnastasisClientFrame } from "..";
import { TextInput } from "../../../components/fields/TextInput";
import { AuthMethodSetupProps } from "./index";
export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
export function AuthMethodPostSetup({
addAuthMethod,
cancel,
configured,
}: AuthMethodSetupProps): VNode {
const [fullName, setFullName] = useState("");
const [street, setStreet] = useState("");
const [city, setCity] = useState("");
@ -32,68 +37,83 @@ export function AuthMethodPostSetup({ addAuthMethod, cancel, configured }: AuthM
});
};
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
)
)
)
)
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.
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]}
/>
<TextInput grabFocus label="Full Name" bind={[fullName, setFullName]} />
</div>
<div>
<TextInput
label="Street"
bind={[street, setStreet]}
/>
<TextInput label="Street" bind={[street, setStreet]} />
</div>
<div>
<TextInput
label="City" bind={[city, setCity]}
/>
<TextInput label="City" bind={[city, setCity]} />
</div>
<div>
<TextInput
label="Postal Code" bind={[postcode, setPostcode]}
/>
<TextInput label="Postal Code" bind={[postcode, setPostcode]} />
</div>
<div>
<TextInput
label="Country"
bind={[country, setCountry]}
/>
<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>
{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>
<button
class="button is-info"
disabled={errors !== undefined}
onClick={addPostAuth}
>
Add
</button>
</span>
</div>
</AnastasisClientFrame>

View File

@ -15,42 +15,46 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/post',
title: "Pages/recovery/SolveChallenge/AuthMethods/post",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'post'
const type: KnownAuthMethods = "post";
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'uuid-1'
}],
policies: [],
export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
{
id: "uuid-1",
},
selected_challenge_uuid: 'uuid-1',
} as ReducerState, {
id: 'uuid-1',
});
);

View File

@ -44,8 +44,16 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
@ -62,8 +70,7 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid]
const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,16 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back();
}
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} />
<p>
Wait for the answer
</p>
<p>Wait for the answer</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div
@ -96,9 +101,11 @@ export function AuthMethodPostSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}>
Cancel
</button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>}
{!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>
)}
</div>
</AnastasisClientFrame>
);

View File

@ -16,51 +16,69 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Question',
title: "Pages/backup/AuthorizationMethod/AuthMethods/Question",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'question'
const type: KnownAuthMethods = "question";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: []
});
export const Empty = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: 'Is integer factorization polynomial? (non-quantum computer)',
remove: () => null
}]
});
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions:
"Is integer factorization polynomial? (non-quantum computer)",
remove: () => null,
},
],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, 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
}]
});
export const WithMoreExamples = createExample(
TestedComponent[type].setup,
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,
},
],
},
);

View File

@ -1,27 +1,31 @@
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
import { TextInput } from "../../../components/fields/TextInput";
export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: AuthMethodSetupProps): VNode {
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 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
)
const errors = !questionText
? "Add your security question"
: !answerText
? "Add the answer to your question"
: undefined;
return (
<AnastasisClientFrame hideNav title="Add Security Question">
<div>
@ -36,7 +40,8 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
label="Security question"
grabFocus
placeholder="Your question"
bind={[questionText, setQuestionText]} />
bind={[questionText, setQuestionText]}
/>
</div>
<div>
<TextInput
@ -46,25 +51,53 @@ export function AuthMethodQuestionSetup({ cancel, addAuthMethod, configured }: A
/>
</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={cancel}>Cancel</button>
<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>
<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:
</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>}
{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>
</AnastasisClientFrame >
</AnastasisClientFrame>
);
}

View File

@ -15,186 +15,205 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/question',
title: "Pages/recovery/SolveChallenge/AuthMethods/question",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'question'
const type: KnownAuthMethods = "question";
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'uuid-1'
}],
policies: [],
export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
{
id: "uuid-1",
},
selected_challenge_uuid: 'uuid-1',
} as ReducerState, {
id: 'uuid-1',
});
);
export const MessageFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: 'ASDASDSAD!1',
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
'ASDASDSAD!1': {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Message,
message: 'Challenge should be solved'
}
}
} as ReducerState);
export const ServerFailureFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
policies: [],
message: "Challenge should be solved",
},
},
selected_challenge_uuid: 'ASDASDSAD!1',
challenge_feedback: {
'ASDASDSAD!1': {
state: ChallengeFeedbackStatus.ServerFailure,
http_status: 500,
error_response: "Couldn't connect to mysql"
}
}
} as ReducerState);
export const ServerFailureFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.ServerFailure,
http_status: 500,
error_response: "Couldn't connect to mysql",
},
},
} as ReducerState,
);
export const RedirectFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: 'ASDASDSAD!1',
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
'ASDASDSAD!1': {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Redirect,
http_status: 302,
redirect_url: 'http://video.taler.net'
}
}
} as ReducerState);
export const MessageRateLimitExceededFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
policies: [],
redirect_url: "http://video.taler.net",
},
},
selected_challenge_uuid: 'ASDASDSAD!1',
challenge_feedback: {
'ASDASDSAD!1': {
state: ChallengeFeedbackStatus.RateLimitExceeded,
}
}
} as ReducerState);
export const MessageRateLimitExceededFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.RateLimitExceeded,
},
},
} as ReducerState,
);
export const UnsupportedFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: 'ASDASDSAD!1',
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
'ASDASDSAD!1': {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Unsupported,
http_status: 500,
unsupported_method: 'Question'
}
}
unsupported_method: "Question",
},
},
} as ReducerState);
export const TruthUnknownFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: 'ASDASDSAD!1',
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
'ASDASDSAD!1': {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.TruthUnknown,
}
}
},
},
} as ReducerState);
export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: 'ASDASDSAD!1',
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
'ASDASDSAD!1': {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.AuthIban,
challenge_amount: "EUR:1",
credit_iban: "DE12345789000",
@ -210,30 +229,30 @@ export const AuthIbanFeedback = createExample(TestedComponent[type].solve, {
wire_transfer_subject: "foo",
},
method: "iban",
}
}
},
},
} as ReducerState);
export const PaymentFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'ASDASDSAD!1'
}],
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "ASDASDSAD!1",
},
],
policies: [],
},
selected_challenge_uuid: 'ASDASDSAD!1',
selected_challenge_uuid: "ASDASDSAD!1",
challenge_feedback: {
'ASDASDSAD!1': {
"ASDASDSAD!1": {
state: ChallengeFeedbackStatus.Payment,
taler_pay_uri : "taler://pay/...",
provider : "https://localhost:8080/",
payment_secret : "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG"
}
}
taler_pay_uri: "taler://pay/...",
provider: "https://localhost:8080/",
payment_secret: "3P4561HAMHRRYEYD6CM6J7TS5VTD5SR2K2EXJDZEFSX92XKHR4KG",
},
},
} as ReducerState);

View File

@ -44,8 +44,16 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
@ -62,8 +70,7 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid]
const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,16 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back();
}
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} />
<p>
Answer the question please
</p>
<p>Answer the question please</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div
@ -96,9 +101,11 @@ export function AuthMethodQuestionSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}>
Cancel
</button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>}
{!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>
)}
</div>
</AnastasisClientFrame>
);

View File

@ -16,51 +16,67 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Sms',
title: "Pages/backup/AuthorizationMethod/AuthMethods/Sms",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'sms'
const type: KnownAuthMethods = "sms";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: []
});
export const Empty = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: 'SMS to +11-1234-2345',
remove: () => null
}]
});
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: "SMS to +11-1234-2345",
remove: () => null,
},
],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, 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
}]
});
export const WithMoreExamples = createExample(
TestedComponent[type].setup,
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,
},
],
},
);

View File

@ -1,14 +1,15 @@
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import { AuthMethodSetupProps } from ".";
import { PhoneNumberInput } from "../../../components/fields/NumberInput";
import { AnastasisClientFrame } from "../index";
export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
export function AuthMethodSmsSetup({
addAuthMethod,
cancel,
configured,
}: AuthMethodSetupProps): VNode {
const [mobileNumber, setMobileNumber] = useState("");
const addSmsAuth = (): void => {
addAuthMethod({
@ -23,7 +24,7 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
useLayoutEffect(() => {
inputRef.current?.focus();
}, []);
const errors = !mobileNumber ? 'Add a mobile number' : undefined
const errors = !mobileNumber ? "Add a mobile number" : undefined;
return (
<AnastasisClientFrame hideNav title="Add SMS authentication">
<div>
@ -37,23 +38,52 @@ export function AuthMethodSmsSetup({ addAuthMethod, cancel, configured }: AuthMe
label="Mobile number"
placeholder="Your mobile number"
grabFocus
bind={[mobileNumber, setMobileNumber]} />
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>
{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>
<button
class="button is-info"
disabled={errors !== undefined}
onClick={addSmsAuth}
>
Add
</button>
</span>
</div>
</div>

View File

@ -15,42 +15,46 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/sms',
title: "Pages/recovery/SolveChallenge/AuthMethods/sms",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'sms'
const type: KnownAuthMethods = "sms";
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'uuid-1'
}],
policies: [],
export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
{
id: "uuid-1",
},
selected_challenge_uuid: 'uuid-1',
} as ReducerState, {
id: 'uuid-1',
});
);

View File

@ -44,8 +44,16 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
@ -62,8 +70,7 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid]
const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,18 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back();
}
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} />
<p>
An sms has been sent to "<b>{selectedChallenge.instructions}</b>". Type the code
below
An sms has been sent to "<b>{selectedChallenge.instructions}</b>". Type
the code below
</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
@ -97,9 +104,11 @@ export function AuthMethodSmsSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}>
Cancel
</button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>}
{!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>
)}
</div>
</AnastasisClientFrame>
);

View File

@ -16,49 +16,65 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/TOTP',
title: "Pages/backup/AuthorizationMethod/AuthMethods/TOTP",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'totp'
const type: KnownAuthMethods = "totp";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: []
});
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: 'Enter 8 digits code for "Anastasis"',
remove: () => null
}]
});
export const WithMoreExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: 'Enter 8 digits code for "Anastasis1"',
remove: () => null
},{
challenge: 'qwe',
type,
instructions: 'Enter 8 digits code for "Anastasis2"',
remove: () => null
}]
});
export const Empty = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: 'Enter 8 digits code for "Anastasis"',
remove: () => null,
},
],
},
);
export const WithMoreExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: 'Enter 8 digits code for "Anastasis1"',
remove: () => null,
},
{
challenge: "qwe",
type,
instructions: 'Enter 8 digits code for "Anastasis2"',
remove: () => null,
},
],
},
);

View File

@ -1,7 +1,4 @@
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useMemo, useState } from "preact/hooks";
import { AuthMethodSetupProps } from "./index";
@ -10,30 +7,37 @@ import { TextInput } from "../../../components/fields/TextInput";
import { QR } from "../../../components/QR";
import { base32enc, computeTOTPandCheck } from "./totp";
export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthMethodSetupProps): VNode {
export function AuthMethodTotpSetup({
addAuthMethod,
cancel,
configured,
}: AuthMethodSetupProps): VNode {
const [name, setName] = useState("anastasis");
const [test, setTest] = useState("");
const digits = 8
const digits = 8;
const secretKey = useMemo(() => {
const array = new Uint8Array(32)
return window.crypto.getRandomValues(array)
}, [])
const array = new Uint8Array(32);
return window.crypto.getRandomValues(array);
}, []);
const secret32 = base32enc(secretKey);
const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`
const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`;
const addTotpAuth = (): void => addAuthMethod({
authentication_method: {
type: "totp",
instructions: `Enter ${digits} digits code for "${name}"`,
challenge: encodeCrock(stringToBytes(totpURL)),
},
});
const addTotpAuth = (): void =>
addAuthMethod({
authentication_method: {
type: "totp",
instructions: `Enter ${digits} digits code for "${name}"`,
challenge: encodeCrock(stringToBytes(totpURL)),
},
});
const testCodeMatches = computeTOTPandCheck(secretKey, 8, parseInt(test, 10));
const errors = !name ? 'The TOTP name is missing' : (
!testCodeMatches ? 'The test code doesnt match' : undefined
);
const errors = !name
? "The TOTP name is missing"
: !testCodeMatches
? "The test code doesnt match"
: undefined;
return (
<AnastasisClientFrame hideNav title="Add TOTP authentication">
<p>
@ -42,10 +46,7 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
with your TOTP App to import the TOTP secret into your TOTP App.
</p>
<div class="block">
<TextInput
label="TOTP Name"
grabFocus
bind={[name, setName]} />
<TextInput label="TOTP Name" grabFocus bind={[name, setName]} />
</div>
<div style={{ height: 300 }}>
<QR text={totpURL} />
@ -53,25 +54,51 @@ export function AuthMethodTotpSetup({ addAuthMethod, cancel, configured }: AuthM
<p>
After scanning the code with your TOTP App, test it in the input below.
</p>
<TextInput
label="Test code"
bind={[test, setTest]} />
{configured.length > 0 && <section class="section">
<div class="block">
Your TOTP 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>}
<TextInput label="Test code" bind={[test, setTest]} />
{configured.length > 0 && (
<section class="section">
<div class="block">Your TOTP 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>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={cancel}>Cancel</button>
<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>
<button
class="button is-info"
disabled={errors !== undefined}
onClick={addTotpAuth}
>
Add
</button>
</span>
</div>
</div>

View File

@ -15,42 +15,46 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/totp',
title: "Pages/recovery/SolveChallenge/AuthMethods/totp",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'totp'
const type: KnownAuthMethods = "totp";
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'uuid-1'
}],
policies: [],
export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
{
id: "uuid-1",
},
selected_challenge_uuid: 'uuid-1',
} as ReducerState, {
id: 'uuid-1',
});
);

View File

@ -44,8 +44,16 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
@ -62,8 +70,7 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid]
const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,16 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back();
}
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} />
<p>
enter the totp solution
</p>
<p>enter the totp solution</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div
@ -96,9 +101,11 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}>
Cancel
</button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>}
{!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>
)}
</div>
</AnastasisClientFrame>
);

View File

@ -16,51 +16,68 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
import logoImage from '../../../assets/logo.jpeg'
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
import logoImage from "../../../assets/logo.jpeg";
export default {
title: 'Pages/backup/AuthorizationMethod/AuthMethods/Video',
title: "Pages/backup/AuthorizationMethod/AuthMethods/Video",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'video'
const type: KnownAuthMethods = "video";
export const Empty = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: []
});
export const Empty = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [],
},
);
export const WithOneExample = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: logoImage,
remove: () => null
}]
});
export const WithOneExample = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: logoImage,
remove: () => null,
},
],
},
);
export const WithMoreExamples = createExample(TestedComponent[type].setup, reducerStatesExample.authEditing, {
configured: [{
challenge: 'qwe',
type,
instructions: logoImage,
remove: () => null
},{
challenge: 'qwe',
type,
instructions: logoImage,
remove: () => null
}]
});
export const WithMoreExamples = createExample(
TestedComponent[type].setup,
reducerStatesExample.authEditing,
{
configured: [
{
challenge: "qwe",
type,
instructions: logoImage,
remove: () => null,
},
{
challenge: "qwe",
type,
instructions: logoImage,
remove: () => null,
},
],
},
);

View File

@ -1,53 +1,86 @@
import {
encodeCrock,
stringToBytes
} from "@gnu-taler/taler-util";
import { encodeCrock, stringToBytes } from "@gnu-taler/taler-util";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { ImageInput } from "../../../components/fields/ImageInput";
import { AuthMethodSetupProps } from "./index";
import { AnastasisClientFrame } from "../index";
export function AuthMethodVideoSetup({cancel, addAuthMethod, configured}: AuthMethodSetupProps): VNode {
export function AuthMethodVideoSetup({
cancel,
addAuthMethod,
configured,
}: AuthMethodSetupProps): VNode {
const [image, setImage] = useState("");
const addVideoAuth = (): void => {
addAuthMethod({
authentication_method: {
type: "video",
instructions: 'Join a video call',
instructions: "Join a video call",
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.
video call. During that call, a human will use the photograph to verify
your identity.
</p>
<div style={{textAlign:'center'}}>
<div style={{ textAlign: "center" }}>
<ImageInput
label="Choose photograph"
grabFocus
bind={[image, setImage]} />
bind={[image, setImage]}
/>
</div>
{configured.length > 0 && <section class="section">
{configured.length > 0 && (
<section class="section">
<div class="block">Your photographs:</div>
<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>
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>
</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
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>

View File

@ -15,42 +15,46 @@
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from 'anastasis-core';
import { createExample, reducerStatesExample } from '../../../utils';
import { authMethods as TestedComponent, KnownAuthMethods } from './index';
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ChallengeFeedbackStatus, ReducerState } from "anastasis-core";
import { createExample, reducerStatesExample } from "../../../utils";
import { authMethods as TestedComponent, KnownAuthMethods } from "./index";
export default {
title: 'Pages/recovery/SolveChallenge/AuthMethods/video',
title: "Pages/recovery/SolveChallenge/AuthMethods/video",
component: TestedComponent,
args: {
order: 5,
},
argTypes: {
onUpdate: { action: 'onUpdate' },
onBack: { action: 'onBack' },
onUpdate: { action: "onUpdate" },
onBack: { action: "onBack" },
},
};
const type: KnownAuthMethods = 'video'
const type: KnownAuthMethods = "video";
export const WithoutFeedback = createExample(TestedComponent[type].solve, {
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [{
cost: 'USD:1',
instructions: 'does P equals NP?',
type: 'question',
uuid: 'uuid-1'
}],
policies: [],
export const WithoutFeedback = createExample(
TestedComponent[type].solve,
{
...reducerStatesExample.challengeSolving,
recovery_information: {
challenges: [
{
cost: "USD:1",
instructions: "does P equals NP?",
type: "question",
uuid: "uuid-1",
},
],
policies: [],
},
selected_challenge_uuid: "uuid-1",
} as ReducerState,
{
id: "uuid-1",
},
selected_challenge_uuid: 'uuid-1',
} as ReducerState, {
id: 'uuid-1',
});
);

View File

@ -44,8 +44,16 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
return (
<AnastasisClientFrame hideNav title="Recovery problem">
<div>invalid state</div>
<div style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
<button class="button" onClick={() => reducer.back()}>Back</button>
<div
style={{
marginTop: "2em",
display: "flex",
justifyContent: "space-between",
}}
>
<button class="button" onClick={() => reducer.back()}>
Back
</button>
</div>
</AnastasisClientFrame>
);
@ -62,8 +70,7 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
challenges[ch.uuid] = ch;
}
const selectedChallenge = challenges[selectedUuid];
const feedback = challengeFeedback[selectedUuid]
const feedback = challengeFeedback[selectedUuid];
async function onNext(): Promise<void> {
return reducer?.transition("solve_challenge", { answer });
@ -72,18 +79,16 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
reducer?.back();
}
const shouldHideConfirm = feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded
|| feedback?.state === ChallengeFeedbackStatus.Redirect
|| feedback?.state === ChallengeFeedbackStatus.Unsupported
|| feedback?.state === ChallengeFeedbackStatus.TruthUnknown
const shouldHideConfirm =
feedback?.state === ChallengeFeedbackStatus.RateLimitExceeded ||
feedback?.state === ChallengeFeedbackStatus.Redirect ||
feedback?.state === ChallengeFeedbackStatus.Unsupported ||
feedback?.state === ChallengeFeedbackStatus.TruthUnknown;
return (
<AnastasisClientFrame hideNav title="Add email authentication">
<SolveOverviewFeedbackDisplay feedback={feedback} />
<p>
You are gonna be called to check your identity
</p>
<p>You are gonna be called to check your identity</p>
<TextInput label="Answer" grabFocus bind={[answer, setAnswer]} />
<div
@ -96,9 +101,11 @@ export function AuthMethodVideoSolve({ id }: AuthMethodSolveProps): VNode {
<button class="button" onClick={onCancel}>
Cancel
</button>
{!shouldHideConfirm && <AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>}
{!shouldHideConfirm && (
<AsyncButton class="button is-info" onClick={onNext}>
Confirm
</AsyncButton>
)}
</div>
</AnastasisClientFrame>
);

View File

@ -1,9 +1,9 @@
import { AuthMethod } from "anastasis-core";
import { h, VNode } from "preact";
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';
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";
import { AuthMethodEmailSetup as EmailSetup } from "./AuthMethodEmailSetup";
import { AuthMethodEmailSolve as EmailSolve } from "./AuthMethodEmailSolve";
import { AuthMethodIbanSetup as IbanSetup } from "./AuthMethodIbanSetup";
@ -20,8 +20,7 @@ import { AuthMethodSmsSolve as SmsSolve } from "./AuthMethodSmsSolve";
import { AuthMethodTotpSolve as TotpSolve } from "./AuthMethodTotpSolve";
import { AuthMethodVideoSolve as VideoSolve } from "./AuthMethodVideoSolve";
export type AuthMethodWithRemove = AuthMethod & { remove: () => void }
export type AuthMethodWithRemove = AuthMethod & { remove: () => void };
export interface AuthMethodSetupProps {
method: string;
@ -43,10 +42,18 @@ interface AuthMethodConfiguration {
}
// export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
const ALL_METHODS = ['sms', 'email', 'post', 'question', 'video' , 'totp', 'iban'] as const;
export type KnownAuthMethods = (typeof ALL_METHODS)[number];
const ALL_METHODS = [
"sms",
"email",
"post",
"question",
"video",
"totp",
"iban",
] as const;
export type KnownAuthMethods = typeof ALL_METHODS[number];
export function isKnownAuthMethods(value: string): value is KnownAuthMethods {
return ALL_METHODS.includes(value as KnownAuthMethods)
return ALL_METHODS.includes(value as KnownAuthMethods);
}
type KnowMethodConfig = {
@ -96,5 +103,5 @@ export const authMethods: KnowMethodConfig = {
setup: VideoSetup,
solve: VideoSolve,
skip: true,
}
}
},
};

View File

@ -1,54 +1,61 @@
/* eslint-disable @typescript-eslint/camelcase */
import jssha from 'jssha'
import jssha from "jssha";
const SEARCH_RANGE = 16
const timeStep = 30
const SEARCH_RANGE = 16;
const timeStep = 30;
export function computeTOTPandCheck(secretKey: Uint8Array, digits: number, code: number): boolean {
const now = new Date().getTime()
export function computeTOTPandCheck(
secretKey: Uint8Array,
digits: number,
code: number,
): boolean {
const now = new Date().getTime();
const epoch = Math.floor(Math.round(now / 1000.0) / timeStep);
for (let ms = -SEARCH_RANGE; ms < SEARCH_RANGE; ms++) {
const movingFactor = (epoch + ms).toString(16).padStart(16, "0");
const hmacSha = new jssha('SHA-1', 'HEX', { hmacKey: { value: secretKey, format: 'UINT8ARRAY' } });
const hmacSha = new jssha("SHA-1", "HEX", {
hmacKey: { value: secretKey, format: "UINT8ARRAY" },
});
hmacSha.update(movingFactor);
const hmac_text = hmacSha.getHMAC('UINT8ARRAY');
const hmac_text = hmacSha.getHMAC("UINT8ARRAY");
const offset = (hmac_text[hmac_text.length - 1] & 0xf)
const offset = hmac_text[hmac_text.length - 1] & 0xf;
const otp = ((
(hmac_text[offset + 0] << 24) +
(hmac_text[offset + 1] << 16) +
(hmac_text[offset + 2] << 8) +
(hmac_text[offset + 3])
) & 0x7fffffff) % Math.pow(10, digits)
const otp =
(((hmac_text[offset + 0] << 24) +
(hmac_text[offset + 1] << 16) +
(hmac_text[offset + 2] << 8) +
hmac_text[offset + 3]) &
0x7fffffff) %
Math.pow(10, digits);
if (otp == code) return true
if (otp == code) return true;
}
return false
return false;
}
const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split('')
const encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".split("");
export function base32enc(buffer: Uint8Array): string {
let rpos = 0
let bits = 0
let vbit = 0
let rpos = 0;
let bits = 0;
let vbit = 0;
let result = ""
while ((rpos < buffer.length) || (vbit > 0)) {
if ((rpos < buffer.length) && (vbit < 5)) {
let result = "";
while (rpos < buffer.length || vbit > 0) {
if (rpos < buffer.length && vbit < 5) {
bits = (bits << 8) | buffer[rpos++];
vbit += 8;
}
if (vbit < 5) {
bits <<= (5 - vbit);
bits <<= 5 - vbit;
vbit = 5;
}
result += encTable__[(bits >> (vbit - 5)) & 31];
vbit -= 5;
}
return result
return result;
}
// const array = new Uint8Array(256)

View File

@ -1,16 +1,16 @@
import { FunctionalComponent, h } from 'preact';
import { Link } from 'preact-router/match';
import { FunctionalComponent, h } from "preact";
import { Link } from "preact-router/match";
const Notfound: FunctionalComponent = () => {
return (
<div>
<h1>Error 404</h1>
<p>That page doesn&apos;t exist.</p>
<Link href="/">
<h4>Back to Home</h4>
</Link>
</div>
);
return (
<div>
<h1>Error 404</h1>
<p>That page doesn&apos;t exist.</p>
<Link href="/">
<h4>Back to Home</h4>
</Link>
</div>
);
};
export default Notfound;

View File

@ -1,43 +1,42 @@
import { FunctionalComponent, h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { FunctionalComponent, h } from "preact";
import { useEffect, useState } from "preact/hooks";
interface Props {
user: string;
user: string;
}
const Profile: FunctionalComponent<Props> = (props: Props) => {
const { user } = props;
const [time, setTime] = useState<number>(Date.now());
const [count, setCount] = useState<number>(0);
const { user } = props;
const [time, setTime] = useState<number>(Date.now());
const [count, setCount] = useState<number>(0);
// gets called when this route is navigated to
useEffect(() => {
const timer = window.setInterval(() => setTime(Date.now()), 1000);
// gets called when this route is navigated to
useEffect(() => {
const timer = window.setInterval(() => setTime(Date.now()), 1000);
// gets called just before navigating away from the route
return (): void => {
clearInterval(timer);
};
}, []);
// update the current time
const increment = (): void => {
setCount(count + 1);
// gets called just before navigating away from the route
return (): void => {
clearInterval(timer);
};
}, []);
return (
<div>
<h1>Profile: {user}</h1>
<p>This is the user profile for a user named {user}.</p>
// update the current time
const increment = (): void => {
setCount(count + 1);
};
<div>Current time: {new Date(time).toLocaleString()}</div>
return (
<div>
<h1>Profile: {user}</h1>
<p>This is the user profile for a user named {user}.</p>
<p>
<button onClick={increment}>Click Me</button> Clicked {count}{' '}
times.
</p>
</div>
);
<div>Current time: {new Date(time).toLocaleString()}</div>
<p>
<button onClick={increment}>Click Me</button> Clicked {count} times.
</p>
</div>
);
};
export default Profile;

View File

@ -1,15 +1,56 @@
<!--
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
-->
<!DOCTYPE html>
<html lang="en" class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded">
<head>
<meta charset="utf-8">
<title><% preact.title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png">
<% preact.headEnd %>
</head>
<body>
<% preact.bodyEnd %>
</body>
<html
lang="en"
class="has-aside-left has-aside-mobile-transition has-navbar-fixed-top has-aside-expanded"
>
<head>
<meta charset="utf-8" />
<title><%= htmlWebpackPlugin.options.title %></title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link
rel="icon"
href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
/>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
<meta
name="theme-color"
content="<%= htmlWebpackPlugin.options.manifest.theme_color %>"
/>
<% } %> <% for (const index in htmlWebpackPlugin.files.css) { %> <% const
file = htmlWebpackPlugin.files.css[index] %>
<style data-href="<%= file %>">
<%= compilation.assets[file.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</style>
<% } %>
</head>
<body>
<script>
<%= compilation.assets[htmlWebpackPlugin.files.chunks["polyfills"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</script>
<script>
<%= compilation.assets[htmlWebpackPlugin.files.chunks["bundle"].entry.substr(htmlWebpackPlugin.files.publicPath.length)].source() %>
</script>
</body>
</html>

View File

@ -1,45 +1,67 @@
/* eslint-disable @typescript-eslint/camelcase */
import { BackupStates, RecoveryStates, ReducerState } from 'anastasis-core';
import { FunctionalComponent, h, VNode } from 'preact';
import { AnastasisProvider } from '../context/anastasis';
import { BackupStates, RecoveryStates, ReducerState } from "anastasis-core";
import { FunctionalComponent, h, VNode } from "preact";
import { AnastasisProvider } from "../context/anastasis";
export function createExample<Props>(Component: FunctionalComponent<Props>, currentReducerState?: ReducerState, props?: Partial<Props>): { (args: Props): VNode } {
export function createExample<Props>(
Component: FunctionalComponent<Props>,
currentReducerState?: ReducerState,
props?: Partial<Props>,
): { (args: Props): VNode } {
const r = (args: Props): VNode => {
return <AnastasisProvider value={{
currentReducerState,
currentError: undefined,
back: async () => { null },
dismissError: async () => { null },
reset: () => { null },
runTransaction: async () => { null },
startBackup: () => { null },
startRecover: () => { null },
transition: async () => { null },
}}>
<Component {...args} />
</AnastasisProvider>
}
r.args = props
return r
return (
<AnastasisProvider
value={{
currentReducerState,
currentError: undefined,
back: async () => {
null;
},
dismissError: async () => {
null;
},
reset: () => {
null;
},
runTransaction: async () => {
null;
},
startBackup: () => {
null;
},
startRecover: () => {
null;
},
transition: async () => {
null;
},
}}
>
<Component {...args} />
</AnastasisProvider>
);
};
r.args = props;
return r;
}
const base = {
continents: [
{
name: "Europe"
name: "Europe",
},
{
name: "India"
name: "India",
},
{
name: "Asia"
name: "Asia",
},
{
name: "North America"
name: "North America",
},
{
name: "Testcontinent"
}
name: "Testcontinent",
},
],
countries: [
{
@ -47,33 +69,33 @@ const base = {
name: "Testland",
continent: "Testcontinent",
continent_i18n: {
de_DE: "Testkontinent"
de_DE: "Testkontinent",
},
name_i18n: {
de_DE: "Testlandt",
de_CH: "Testlandi",
fr_FR: "Testpais",
en_UK: "Testland"
en_UK: "Testland",
},
currency: "TESTKUDOS",
call_code: "+00"
call_code: "+00",
},
{
code: "xy",
name: "Demoland",
continent: "Testcontinent",
continent_i18n: {
de_DE: "Testkontinent"
de_DE: "Testkontinent",
},
name_i18n: {
de_DE: "Demolandt",
de_CH: "Demolandi",
fr_FR: "Demopais",
en_UK: "Demoland"
en_UK: "Demoland",
},
currency: "KUDOS",
call_code: "+01"
}
call_code: "+01",
},
],
authentication_providers: {
"http://localhost:8086/": {
@ -85,18 +107,20 @@ const base = {
methods: [
{
type: "question",
usage_fee: "COL:0"
}, {
usage_fee: "COL:0",
},
{
type: "sms",
usage_fee: "COL:0"
}, {
usage_fee: "COL:0",
},
{
type: "email",
usage_fee: "COL:0"
usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0"
truth_upload_fee: "COL:0",
},
"https://kudos.demo.anastasis.lu/": {
http_status: 200,
@ -107,15 +131,16 @@ const base = {
methods: [
{
type: "question",
usage_fee: "COL:0"
}, {
usage_fee: "COL:0",
},
{
type: "email",
usage_fee: "COL:0"
usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0"
truth_upload_fee: "COL:0",
},
"https://anastasis.demo.taler.net/": {
http_status: 200,
@ -126,43 +151,45 @@ const base = {
methods: [
{
type: "question",
usage_fee: "COL:0"
}, {
usage_fee: "COL:0",
},
{
type: "sms",
usage_fee: "COL:0"
}, {
usage_fee: "COL:0",
},
{
type: "totp",
usage_fee: "COL:0"
usage_fee: "COL:0",
},
],
salt: "WBMDD76BR1E90YQ5AHBMKPH7GW",
storage_limit_in_megabytes: 16,
truth_upload_fee: "COL:0"
truth_upload_fee: "COL:0",
},
"http://localhost:8087/": {
code: 8414,
hint: "request to provider failed"
hint: "request to provider failed",
},
"http://localhost:8088/": {
code: 8414,
hint: "request to provider failed"
hint: "request to provider failed",
},
"http://localhost:8089/": {
code: 8414,
hint: "request to provider failed"
}
hint: "request to provider failed",
},
},
// expiration: {
// d_ms: 1792525051855 // check t_ms
// },
} as Partial<ReducerState>
} as Partial<ReducerState>;
export const reducerStatesExample = {
initial: undefined,
recoverySelectCountry: {
...base,
recovery_state: RecoveryStates.CountrySelecting
recovery_state: RecoveryStates.CountrySelecting,
} as ReducerState,
recoverySelectContinent: {
...base,
@ -190,11 +217,11 @@ export const reducerStatesExample = {
} as ReducerState,
recoveryAttributeEditing: {
...base,
recovery_state: RecoveryStates.UserAttributesCollecting
recovery_state: RecoveryStates.UserAttributesCollecting,
} as ReducerState,
backupSelectCountry: {
...base,
backup_state: BackupStates.CountrySelecting
backup_state: BackupStates.CountrySelecting,
} as ReducerState,
backupSelectContinent: {
...base,
@ -218,15 +245,14 @@ export const reducerStatesExample = {
} as ReducerState,
authEditing: {
...base,
backup_state: BackupStates.AuthenticationsEditing
backup_state: BackupStates.AuthenticationsEditing,
} as ReducerState,
backupAttributeEditing: {
...base,
backup_state: BackupStates.UserAttributesCollecting
backup_state: BackupStates.UserAttributesCollecting,
} as ReducerState,
truthsPaying: {
...base,
backup_state: BackupStates.TruthsPaying
backup_state: BackupStates.TruthsPaying,
} as ReducerState,
}
};

View File

@ -7,10 +7,12 @@ importers:
'@linaria/esbuild': ^3.0.0-beta.13
'@linaria/shaker': ^3.0.0-beta.13
esbuild: ^0.12.29
prettier: ^2.2.1
devDependencies:
'@linaria/esbuild': 3.0.0-beta.13
'@linaria/shaker': 3.0.0-beta.13
esbuild: 0.12.29
prettier: 2.2.1
packages/anastasis-core:
specifiers:
@ -62,6 +64,7 @@ importers:
'@typescript-eslint/eslint-plugin': ^5.3.0
'@typescript-eslint/parser': ^5.3.0
anastasis-core: workspace:^0.0.1
base64-inline-loader: 1.1.1
bulma: ^0.9.3
bulma-checkbox: ^1.1.1
bulma-radio: ^1.1.1
@ -86,6 +89,7 @@ importers:
dependencies:
'@gnu-taler/taler-util': link:../taler-util
anastasis-core: link:../anastasis-core
base64-inline-loader: 1.1.1
date-fns: 2.25.0
jed: 1.1.1
preact: 10.5.15
@ -10800,7 +10804,6 @@ packages:
ajv: ^6.9.1
dependencies:
ajv: 6.12.6
dev: true
/ajv/6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@ -10809,7 +10812,6 @@ packages:
fast-json-stable-stringify: 2.1.0
json-schema-traverse: 0.4.1
uri-js: 4.4.1
dev: true
/ajv/7.0.3:
resolution: {integrity: sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==}
@ -11871,6 +11873,17 @@ packages:
pascalcase: 0.1.1
dev: true
/base64-inline-loader/1.1.1:
resolution: {integrity: sha512-v/bHvXQ8sW28t9ZwBsFGgyqZw2bpT49/dtPTtlmixoSM/s9wnOngOKFVQLRH8BfMTy6jTl5m5CdvqpZt8y5d6g==}
engines: {node: '>=6.2', npm: '>=3.8'}
peerDependencies:
webpack: ^4.x
dependencies:
file-loader: 1.1.11
loader-utils: 1.4.0
mime-types: 2.1.33
dev: false
/base64-js/1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
@ -11907,7 +11920,6 @@ packages:
/big.js/5.2.2:
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
dev: true
/binary-extensions/1.13.1:
resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==}
@ -13794,7 +13806,7 @@ packages:
dependencies:
globby: 11.0.4
graceful-fs: 4.2.8
is-glob: 4.0.1
is-glob: 4.0.3
is-path-cwd: 2.2.0
is-path-inside: 3.0.3
p-map: 4.0.0
@ -14177,7 +14189,6 @@ packages:
/emojis-list/3.0.0:
resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==}
engines: {node: '>= 4'}
dev: true
/emotion-theming/10.0.27_5f216699bc8c1f24088b3bf77b7cbbdf:
resolution: {integrity: sha512-MlF1yu/gYh8u+sLUqA0YuA9JX0P4Hb69WlKc/9OLo+WCXuX6sy/KoIa+qJimgmr2dWqnypYKYPX37esjDBbhdw==}
@ -15174,7 +15185,6 @@ packages:
/fast-deep-equal/3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
dev: true
/fast-diff/1.2.0:
resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==}
@ -15217,7 +15227,6 @@ packages:
/fast-json-stable-stringify/2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
dev: true
/fast-levenshtein/2.0.6:
resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=}
@ -15296,6 +15305,16 @@ packages:
flat-cache: 3.0.4
dev: true
/file-loader/1.1.11:
resolution: {integrity: sha512-TGR4HU7HUsGg6GCOPJnFk06RhWgEWFLAGWiT6rcD+GRC2keU3s9RGJ+b3Z6/U73jwwNb2gKLJ7YCrp+jvU4ALg==}
engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'}
peerDependencies:
webpack: ^2.0.0 || ^3.0.0 || ^4.0.0
dependencies:
loader-utils: 1.4.0
schema-utils: 0.4.7
dev: false
/file-loader/6.2.0_webpack@4.46.0:
resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==}
engines: {node: '>= 10.13.0'}
@ -18694,7 +18713,6 @@ packages:
/json-schema-traverse/0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
dev: true
/json-schema-traverse/1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
@ -18730,7 +18748,6 @@ packages:
hasBin: true
dependencies:
minimist: 1.2.5
dev: true
/json5/2.1.3:
resolution: {integrity: sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==}
@ -18970,7 +18987,6 @@ packages:
big.js: 5.2.2
emojis-list: 3.0.0
json5: 1.0.1
dev: true
/loader-utils/2.0.0:
resolution: {integrity: sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==}
@ -19420,14 +19436,12 @@ packages:
/mime-db/1.50.0:
resolution: {integrity: sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==}
engines: {node: '>= 0.6'}
dev: true
/mime-types/2.1.33:
resolution: {integrity: sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.50.0
dev: true
/mime/1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
@ -19501,7 +19515,6 @@ packages:
/minimist/1.2.5:
resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==}
dev: true
/minipass-collect/1.0.2:
resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==}
@ -22013,7 +22026,6 @@ packages:
/punycode/2.1.1:
resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==}
engines: {node: '>=6'}
dev: true
/pupa/2.1.1:
resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==}
@ -23312,6 +23324,14 @@ packages:
object-assign: 4.1.1
dev: true
/schema-utils/0.4.7:
resolution: {integrity: sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==}
engines: {node: '>= 4'}
dependencies:
ajv: 6.12.6
ajv-keywords: 3.5.2_ajv@6.12.6
dev: false
/schema-utils/1.0.0:
resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==}
engines: {node: '>= 4'}
@ -25212,7 +25232,6 @@ packages:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:
punycode: 2.1.1
dev: true
/urix/0.1.0:
resolution: {integrity: sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=}