Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions new-ui/src/pages/full/AddPage/AddPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
import { ThemeSpacing } from '../../../shared/types';
import { mfaConfigurableInstances } from '../../../shared/utils/mfa';
import { AddCard } from './components/AddCard/AddCard';
import { useStartMfaConfiguration } from './hooks/useStartMfaConfiguration';

export const AddPage = () => {
const navigate = useNavigate();
Expand All @@ -22,9 +21,6 @@ export const AddPage = () => {
[instances],
);

const { mutate: startConfigureMfa, isPending: configureMfaPending } =
useStartMfaConfiguration();

return (
<FullPage id="add-page-view" hideScrollContainer>
<FullPageTitle title="Add Defguard items" spacing={ThemeSpacing.Xl} />
Expand Down Expand Up @@ -60,16 +56,10 @@ export const AddPage = () => {
description={`Add new MFA methods to securely authenticate when connecting to a location, with flexible options to match your security requirements.`}
actionText="Add MFA"
onClick={() => {
// Below two instances there is nothing to pick, so the flow starts here.
if (mfaInstances.length > 1) {
navigate({
to: '/full/add/mfa',
});
return;
}
startConfigureMfa(mfaInstances[0]);
navigate({
to: '/full/add/mfa',
});
}}
loading={configureMfaPending}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
import clsx from 'clsx';
import './style.scss';
import { Icon, IconKind } from '../../../../../shared/components/Icon';
import { RadioIndicator } from '../../../../../shared/components/RadioIndicator/RadioIndicator';
import { ThemeVariable } from '../../../../../shared/types';
import { IconKind } from '../../../../../shared/components/Icon';
import { SelectionItem } from '../../../../../shared/components/SelectionItem/SelectionItem';

interface Props {
instanceId: number;
instanceName: string;
selected: boolean;
onClick: () => void;
disabled?: boolean;
onClick?: () => void;
}

export const InstanceSelector = ({
instanceId,
instanceName,
selected,
onClick,
disabled = false,
}: Props) => {
return (
<div
className={clsx('instance-selector', { selected })}
<SelectionItem
className="instance-selector"
data-instance-id={instanceId}
text={instanceName}
icon={IconKind.Globe}
selected={selected}
disabled={disabled}
onClick={onClick}
>
<div className="track">
<div className="icon-col side-col">
<Icon icon={IconKind.Globe} size={20} staticColor={ThemeVariable.FgWhite70} />
</div>
<div className="content-col">
<p className="name">{instanceName}</p>
</div>
<div className="selection side-col">
<RadioIndicator active={selected} />
</div>
</div>
</div>
/>
);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Badge } from '../../../../../../shared/components/Badge/Badge';
import { Checkbox } from '../../../../../../shared/components/Checkbox/Checkbox';
import { Icon } from '../../../../../../shared/components/Icon';
import { TooltipContent } from '../../../../../../shared/providers/tooltip/TooltipContent';
Expand Down Expand Up @@ -44,11 +45,7 @@ export const MethodRow = ({
/>
{(configured || isPresent(hint)) && (
<div className="right">
{configured && (
<div className="configured-badge">
<p>{`Configured`}</p>
</div>
)}
{configured && <Badge text="Configured" disabled={disabled} />}
{isPresent(hint) && (
<TooltipProvider>
<TooltipTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,5 @@
align-items: center;
justify-content: flex-end;
user-select: none;

.configured-badge {
user-select: none;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 0 var(--spacing-sm);
flex-grow: 0;
flex-shrink: 1;
border-radius: 8px;
background-color: var(--bg-white-5);
min-height: 24px;

p,
span {
font: var(--t-body-xs-400);
color: var(--fg-white-50);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Button } from '../../../../../shared/components/Button/Button';
import { ButtonVariant } from '../../../../../shared/components/Button/types';
import { Controls } from '../../../../../shared/components/Controls/Controls';
import { ExternalProviderButton } from '../../../../../shared/components/ExternalProviderButton/ExternalProviderButton';
import { FullPageTitle } from '../../../../../shared/components/FullPageTitle/FullPageTitle';
import { FullPage } from '../../../../../shared/layouts/FullPage/FullPage';
import { isPresent } from '../../../../../shared/utils/isPresent';
import { findOpenIdProvider, openIdProviderName } from '../../../../../shared/utils/mfa';
import { useConfigureMfaStore } from '../../hooks/useConfigureMfaStore';
import { useConfigureVerifyOidc } from './useConfigureVerifyOidc';
import '../style.scss';
Expand All @@ -13,8 +15,11 @@ interface Props {
}

export const ConfigureVerifyOidcStep = ({ onSessionExpired }: Props) => {
const instance = useConfigureMfaStore((s) => s.instance) ?? undefined;
const providerName = openIdProviderName(instance);
const { start, abort, isOpening, isPolling, error } = useConfigureVerifyOidc({
onSessionExpired,
autoStart: true,
});

const handleBack = async () => {
Expand All @@ -35,14 +40,14 @@ export const ConfigureVerifyOidcStep = ({ onSessionExpired }: Props) => {
<span>
{isPolling
? 'Complete the sign-in in your browser. This page will update automatically.'
: 'Authenticate via your OpenID provider. A browser window will open for you to sign in.'}
: `A browser window will open for you to sign in with ${providerName}.`}
</span>
</p>
{isPresent(error) && <p className="error">{error}</p>}
<div className="oidc-action">
<Button
text={isPolling ? 'Open again' : 'Auth with OpenID'}
variant={ButtonVariant.Primary}
<ExternalProviderButton
text={`Authenticate with ${providerName}`}
provider={findOpenIdProvider(instance) ?? 'custom'}
loading={isOpening}
onClick={() => {
void start();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { act, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useConfigureMfaStore } from '../../hooks/useConfigureMfaStore';
import { useConfigureVerifyOidc } from './useConfigureVerifyOidc';
Expand Down Expand Up @@ -33,8 +33,8 @@ const deferred = <T>() => {
return { promise, resolve };
};

const renderOidc = () =>
renderHook(() => useConfigureVerifyOidc({ onSessionExpired: vi.fn() }));
const renderOidc = (autoStart = false) =>
renderHook(() => useConfigureVerifyOidc({ onSessionExpired: vi.fn(), autoStart }));

describe('useConfigureVerifyOidc', () => {
beforeEach(() => {
Expand All @@ -58,6 +58,44 @@ describe('useConfigureVerifyOidc', () => {
expect(mocks.mfaConfigAuthorizeOidc).toHaveBeenCalledWith(sessionId);
});

it('reopens the browser while polling without starting a second poll', async () => {
const polling = deferred<typeof authorizeResult>();
mocks.mfaConfigAuthorizeOidc.mockReturnValue(polling.promise);
const { result } = renderOidc();

await act(async () => {
await result.current.start();
});
await act(async () => {
await result.current.start();
});

expect(mocks.openLink).toHaveBeenCalledTimes(2);
expect(mocks.mfaConfigAuthorizeOidc).toHaveBeenCalledTimes(1);

await act(async () => {
polling.resolve(authorizeResult);
});
});

it('opens the browser on mount when auto-starting', async () => {
renderOidc(true);

await waitFor(() => {
expect(mocks.openLink).toHaveBeenCalledWith(oidcUrl);
});
expect(mocks.mfaConfigOidcUrl).toHaveBeenCalledTimes(1);
expect(mocks.mfaConfigAuthorizeOidc).toHaveBeenCalledWith(sessionId);
});

it('does not auto-start when unmounted before the deferred start', async () => {
const { unmount } = renderOidc(true);
unmount();
await new Promise((resolve) => setTimeout(resolve, 0));

expect(mocks.mfaConfigOidcUrl).not.toHaveBeenCalled();
});

it('does not open the browser when unmounted while the URL is pending', async () => {
const fetching = deferred<string>();
mocks.mfaConfigOidcUrl.mockReturnValue(fetching.promise);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { useMfaConfigErrorHandler } from '../../hooks/useMfaConfigErrorHandler';

type Options = {
onSessionExpired: () => void;
autoStart: boolean;
};

/** reopening the page starts a new attempt on Core, and the running poll picks it up */
export const useConfigureVerifyOidc = ({ onSessionExpired }: Options) => {
export const useConfigureVerifyOidc = ({ onSessionExpired, autoStart }: Options) => {
const [isOpening, setIsOpening] = useState(false);
const [isPolling, setIsPolling] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -82,5 +83,17 @@ export const useConfigureVerifyOidc = ({ onSessionExpired }: Options) => {
};
}, []);

// deferred a tick so a StrictMode replay or an instant unmount clears it before it runs
// biome-ignore lint/correctness/useExhaustiveDependencies: auto-start only on mount
useEffect(() => {
if (!autoStart) return;
const timer = window.setTimeout(() => {
void start();
}, 0);
return () => {
window.clearTimeout(timer);
};
}, [autoStart]);

return { start, abort, isOpening, isPolling, error };
};
Loading
Loading