diff options
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; |