aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/anastasis-core/src/index.ts66
-rw-r--r--packages/anastasis-core/src/reducer-types.ts8
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx6
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx99
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx26
-rw-r--r--packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx12
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/index.tsx8
7 files changed, 175 insertions, 50 deletions
diff --git a/packages/anastasis-core/src/index.ts b/packages/anastasis-core/src/index.ts
index f88e6e8bc..15e1e5d97 100644
--- a/packages/anastasis-core/src/index.ts
+++ b/packages/anastasis-core/src/index.ts
@@ -65,6 +65,8 @@ import {
ActionArgsChangeVersion,
TruthMetaData,
ActionArgsUpdatePolicy,
+ ActionArgsAddProvider,
+ ActionArgsDeleteProvider,
} from "./reducer-types.js";
import fetchPonyfill from "fetch-ponyfill";
import {
@@ -1060,9 +1062,15 @@ async function recoveryEnterUserAttributes(
args: ActionArgsEnterUserAttributes,
): Promise<ReducerStateRecovery | ReducerStateError> {
// FIXME: validate attributes
+ const providerUrls = Object.keys(state.authentication_providers ?? {});
+ const newProviders = state.authentication_providers ?? {};
+ for (const url of providerUrls) {
+ newProviders[url] = await getProviderInfo(url);
+ }
const st: ReducerStateRecovery = {
...state,
identity_attributes: args.identity_attributes,
+ authentication_providers: newProviders,
};
return downloadPolicy(st);
}
@@ -1174,6 +1182,60 @@ function transitionRecoveryJump(
};
}
+//FIXME: doest the same that addProviderRecovery, but type are not generic enough
+async function addProviderBackup(
+ state: ReducerStateBackup,
+ args: ActionArgsAddProvider,
+): Promise<ReducerStateBackup> {
+ const info = await getProviderInfo(args.provider_url)
+ return {
+ ...state,
+ authentication_providers: {
+ ...(state.authentication_providers ?? {}),
+ [args.provider_url]: info,
+ },
+ };
+}
+
+//FIXME: doest the same that deleteProviderRecovery, but type are not generic enough
+async function deleteProviderBackup(
+ state: ReducerStateBackup,
+ args: ActionArgsDeleteProvider,
+): Promise<ReducerStateBackup> {
+ const authentication_providers = {... state.authentication_providers ?? {} }
+ delete authentication_providers[args.provider_url]
+ return {
+ ...state,
+ authentication_providers,
+ };
+}
+
+async function addProviderRecovery(
+ state: ReducerStateRecovery,
+ args: ActionArgsAddProvider,
+): Promise<ReducerStateRecovery> {
+ const info = await getProviderInfo(args.provider_url)
+ return {
+ ...state,
+ authentication_providers: {
+ ...(state.authentication_providers ?? {}),
+ [args.provider_url]: info,
+ },
+ };
+}
+
+async function deleteProviderRecovery(
+ state: ReducerStateRecovery,
+ args: ActionArgsDeleteProvider,
+): Promise<ReducerStateRecovery> {
+ const authentication_providers = {... state.authentication_providers ?? {} }
+ delete authentication_providers[args.provider_url]
+ return {
+ ...state,
+ authentication_providers,
+ };
+}
+
async function addAuthentication(
state: ReducerStateBackup,
args: ActionArgsAddAuthentication,
@@ -1408,6 +1470,8 @@ const backupTransitions: Record<
...transitionBackupJump("back", BackupStates.UserAttributesCollecting),
...transition("add_authentication", codecForAny(), addAuthentication),
...transition("delete_authentication", codecForAny(), deleteAuthentication),
+ ...transition("add_provider", codecForAny(), addProviderBackup),
+ ...transition("delete_provider", codecForAny(), deleteProviderBackup),
...transition("next", codecForAny(), nextFromAuthenticationsEditing),
},
[BackupStates.PoliciesReviewing]: {
@@ -1476,6 +1540,8 @@ const recoveryTransitions: Record<
[RecoveryStates.SecretSelecting]: {
...transitionRecoveryJump("back", RecoveryStates.UserAttributesCollecting),
...transitionRecoveryJump("next", RecoveryStates.ChallengeSelecting),
+ ...transition("add_provider", codecForAny(), addProviderRecovery),
+ ...transition("delete_provider", codecForAny(), deleteProviderRecovery),
...transition(
"change_version",
codecForActionArgsChangeVersion(),
diff --git a/packages/anastasis-core/src/reducer-types.ts b/packages/anastasis-core/src/reducer-types.ts
index 51b0045a0..3e6d6c852 100644
--- a/packages/anastasis-core/src/reducer-types.ts
+++ b/packages/anastasis-core/src/reducer-types.ts
@@ -334,6 +334,14 @@ export const codecForActionArgsEnterUserAttributes = () =>
.property("identity_attributes", codecForAny())
.build("ActionArgsEnterUserAttributes");
+export interface ActionArgsAddProvider {
+ provider_url: string;
+}
+
+export interface ActionArgsDeleteProvider {
+ provider_url: string;
+}
+
export interface ActionArgsAddAuthentication {
authentication_method: {
type: string;
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
index a96734caa..08e2b4371 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.stories.tsx
@@ -40,6 +40,12 @@ export const NewProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
} as ReducerState);
+
+export const NewProviderWithoutProviderList = createExample(TestedComponent, {
+ ...reducerStatesExample.authEditing,
+ authentication_providers: {}
+} as ReducerState);
+
export const NewVideoProvider = createExample(TestedComponent, {
...reducerStatesExample.authEditing,
} as ReducerState, { providerType: 'video'});
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
index 5cf6fbb09..7504f4d2b 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
@@ -1,6 +1,6 @@
import { AuthenticationProviderStatusOk } from "anastasis-core";
import { h, VNode } from "preact";
-import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
import { TextInput } from "../../components/fields/TextInput";
import { useAnastasisContext } from "../../context/anastasis";
import { authMethods, KnownAuthMethods } from "./authMethod";
@@ -8,13 +8,13 @@ import { AnastasisClientFrame } from "./index";
interface Props {
providerType?: KnownAuthMethods;
- cancel: () => void;
+ onCancel: () => void;
}
async function testProvider(url: string, expectedMethodType?: string): Promise<void> {
try {
- const response = await fetch(`${url}/config`)
+ 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")
@@ -41,7 +41,7 @@ async function testProvider(url: string, expectedMethodType?: string): Promise<v
}
-export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
+export function AddingProviderScreen({ providerType, onCancel }: Props): VNode {
const reducer = useAnastasisContext();
const [providerURL, setProviderURL] = useState("");
@@ -54,8 +54,8 @@ export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
useEffect(() => {
if (timeout) window.clearTimeout(timeout.current)
timeout.current = window.setTimeout(async () => {
- const url = providerURL.endsWith('/') ? providerURL.substring(0, providerURL.length - 1) : providerURL
- if (!url) return;
+ const url = providerURL.endsWith('/') ? providerURL : (providerURL + '/')
+ if (!providerURL || authProviders.includes(url)) return;
try {
setTesting(true)
await testProvider(url, providerType)
@@ -67,40 +67,50 @@ export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
if (e instanceof Error) setError(e.message)
}
setTesting(false)
- }, 1000);
- }, [providerURL])
+ }, 200);
+ }, [providerURL, reducer])
if (!reducer) {
return <div>no reducer in context</div>;
}
- function addProvider(): void {
- // addAuthMethod({
- // authentication_method: {
- // type: "sms",
- // instructions: `SMS to ${providerURL}`,
- // challenge: encodeCrock(stringToBytes(providerURL)),
- // },
- // });
+ 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()
+ }
+ function deleteProvider(provider_url: string): void {
+ reducer?.transition("delete_provider", { provider_url })
}
+ const allAuthProviders = reducer.currentReducerState.authentication_providers || {}
+ const authProviders = Object.keys(allAuthProviders).filter(provUrl => {
+ const p = allAuthProviders[provUrl];
+ if (!providerLabel) {
+ return p && ("currency" in p)
+ } else {
+ return p && ("currency" in p) && p.methods.findIndex(m => m.type === providerType) !== -1
+ }
+ })
+
let errors = !providerURL ? 'Add provider URL' : undefined
+ let url: string | undefined;
try {
- new URL(providerURL)
+ url = new URL("",providerURL).href
} catch {
errors = 'Check the URL'
}
if (!!error && !errors) {
errors = error
}
-
- if (!reducer.currentReducerState || !("authentication_providers" in reducer.currentReducerState)) {
- return <div>invalid state</div>
+ if (!errors && authProviders.includes(url!)) {
+ errors = 'That provider is already known'
}
- const authProviders = reducer.currentReducerState.authentication_providers || {}
-
return (
<AnastasisClientFrame hideNav
title="Backup: Manage providers"
@@ -119,40 +129,45 @@ export function AddingProviderScreen({ providerType, cancel }: Props): VNode {
label="Provider URL"
placeholder="https://provider.com"
grabFocus
+ error={errors}
bind={[providerURL, setProviderURL]} />
</div>
<p class="block">
Example: https://kudos.demo.anastasis.lu
</p>
-
- {testing && <p class="block has-text-info">Testing</p>}
- {!!error && <p class="block has-text-danger">{error}</p>}
- {error === "" && <p class="block has-text-success">This provider worked!</p>}
-
+ {testing && <p class="has-text-info">Testing</p>}
+
<div class="block" style={{ marginTop: '2em', display: 'flex', justifyContent: 'space-between' }}>
- <button class="button" onClick={cancel}>Cancel</button>
+ <button class="button" onClick={onCancel}>Cancel</button>
<span data-tooltip={errors}>
- <button class="button is-info" disabled={error !== "" || testing} onClick={addProvider}>Add</button>
+ <button class="button is-info" disabled={error !== "" || testing} onClick={() => addProvider(url!)}>Add</button>
</span>
</div>
- <p class="subtitle">
- Current providers
- </p>
- {/* <table class="table"> */}
- {Object.keys(authProviders).map(k => {
- const p = authProviders[k]
- if (("currency" in p)) {
- return <TableRow url={k} info={p} />
- }
- }
+ {authProviders.length > 0 ? (
+ !providerLabel ?
+ <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> : <p class="subtitle">
+ No known providers for {providerLabel} service
+ </p>
)}
- {/* </table> */}
+
+ {authProviders.map(k => {
+ const p = allAuthProviders[k] as AuthenticationProviderStatusOk
+ return <TableRow url={k} info={p} onDelete={deleteProvider} />
+ })}
</div>
</AnastasisClientFrame>
);
}
-function TableRow({ url, info }: { url: string, info: AuthenticationProviderStatusOk }) {
+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)
@@ -174,7 +189,7 @@ function TableRow({ url, info }: { url: string, info: AuthenticationProviderStat
</dl>
</div>
<div class="block" style={{ marginTop: 'auto', marginBottom: 'auto', display: 'flex', justifyContent: 'space-between', flexDirection: 'column' }}>
- <button class="button is-danger" >Remove</button>
+ <button class="button is-danger" onClick={() => onDelete(url)}>Remove</button>
</div>
</div>
} \ No newline at end of file
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
index 0bc735f34..00eb54d4d 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -2,10 +2,12 @@ import { AuthMethod, ReducerStateBackup } from "anastasis-core";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useAnastasisContext } from "../../context/anastasis";
+import { AddingProviderScreen } from "./AddingProviderScreen";
import {
authMethods,
AuthMethodSetupProps,
AuthMethodWithRemove,
+ isKnownAuthMethods,
KnownAuthMethods,
} from "./authMethod";
import { AnastasisClientFrame } from "./index";
@@ -18,6 +20,8 @@ export function AuthenticationEditorScreen(): VNode {
KnownAuthMethods | undefined
>(undefined);
const [tooFewAuths, setTooFewAuths] = useState(false);
+ const [manageProvider, setManageProvider] = useState<string | undefined>(undefined)
+
// const [addingProvider, setAddingProvider] = useState<string | undefined>(undefined)
const reducer = useAnastasisContext();
if (!reducer) {
@@ -63,6 +67,14 @@ export function AuthenticationEditorScreen(): VNode {
}
}
+ if (manageProvider !== undefined) {
+
+ return <AddingProviderScreen
+ onCancel={() => setManageProvider(undefined)}
+ providerType={isKnownAuthMethods(manageProvider) ? manageProvider : undefined}
+ />
+ }
+
if (selectedMethod) {
const cancel = (): void => setSelectedMethod(undefined);
const addMethod = (args: any): void => {
@@ -86,9 +98,9 @@ export function AuthenticationEditorScreen(): VNode {
active
onCancel={cancel}
description="No providers founds"
- label="Add a provider manually (not implemented!)"
+ label="Add a provider manually"
onConfirm={() => {
- null;
+ setManageProvider(selectedMethod)
}}
>
<p>
@@ -179,9 +191,9 @@ export function AuthenticationEditorScreen(): VNode {
active={!noProvidersAck}
onCancel={() => setNoProvidersAck(true)}
description="No providers founds"
- label="Add a provider manually (not implemented!)"
+ label="Add a provider manually"
onConfirm={() => {
- null;
+ setManageProvider("")
}}
>
<p>
@@ -201,11 +213,11 @@ export function AuthenticationEditorScreen(): VNode {
identity via the methods you configure here. The list of
authentication method is defined by the backup provider list.
</p>
- {/* <p class="block">
- <button class="button is-info">
+ <p class="block">
+ <button class="button is-info" onClick={() => setManageProvider("")}>
Manage backup providers
</button>
- </p> */}
+ </p>
{authAvailableSet.size > 0 && (
<p class="block">
We couldn't find provider for some of the authentication methods.
diff --git a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
index cf38d3f18..b1ec2856a 100644
--- a/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/SecretSelectionScreen.tsx
@@ -3,12 +3,14 @@ import { useState } from "preact/hooks";
import { AsyncButton } from "../../components/AsyncButton";
import { NumberInput } from "../../components/fields/NumberInput";
import { useAnastasisContext } from "../../context/anastasis";
+import { AddingProviderScreen } from "./AddingProviderScreen";
import { AnastasisClientFrame } from "./index";
export function SecretSelectionScreen(): VNode {
const [selectingVersion, setSelectingVersion] = useState<boolean>(false);
const reducer = useAnastasisContext()
+ const [manageProvider, setManageProvider] = useState(false)
const currentVersion = (reducer?.currentReducerState
&& ("recovery_document" in reducer.currentReducerState)
&& reducer.currentReducerState.recovery_document?.version) || 0;
@@ -49,6 +51,10 @@ export function SecretSelectionScreen(): VNode {
/>
}
+ if (manageProvider) {
+ return <AddingProviderScreen onCancel={() => setManageProvider(false)} />
+ }
+
return (
<AnastasisClientFrame title="Recovery: Select secret">
<div class="columns">
@@ -69,6 +75,12 @@ export function SecretSelectionScreen(): VNode {
</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>
+
</div>
</div>
</AnastasisClientFrame>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/index.tsx b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
index 07f6ec206..8b0126ce7 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/index.tsx
@@ -41,7 +41,13 @@ interface AuthMethodConfiguration {
solve: (props: AuthMethodSolveProps) => VNode;
skip?: boolean;
}
-export type KnownAuthMethods = "sms" | "email" | "post" | "question" | "video" | "totp" | "iban";
+// 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];
+export function isKnownAuthMethods(value: string): value is KnownAuthMethods {
+ return ALL_METHODS.includes(value as KnownAuthMethods)
+}
type KnowMethodConfig = {
[name in KnownAuthMethods]: AuthMethodConfiguration;