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
1 change: 1 addition & 0 deletions new-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"@biomejs/biome": "2.5.14",
"@floating-ui/react": "^0.27.20",
"@fluentui/keyboard-keys": "^9.0.9",
"@stablelib/base64": "^2.0.1",
"@stablelib/x25519": "^2.0.1",
"@tanstack/react-form": "^1.33.5",
Expand Down
17 changes: 17 additions & 0 deletions new-ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion new-ui/src/pages/full/AddInstancePage/AddInstancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const AddInstancePage = () => {
});

return (
<FullPage id="add-instance-view">
<FullPage id="add-instance-view" withControls>
<FullPageTitle title="Add instance" />
<p className="page-description">{`To add an instance, provide the instance URL along with a valid provisioning token. These credentials are issued by your administrator and are required to initiate the setup.`}</p>
<form
Expand Down
9 changes: 0 additions & 9 deletions new-ui/src/pages/full/AddInstancePage/style.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
#add-instance-view {
> .scroll-container {
display: flex;
flex-flow: column;
}

.page-description {
font: var(--t-body-sm-400);
color: var(--fg-white-70);
Expand All @@ -13,8 +8,4 @@
form {
display: contents;
}

.controls {
margin-top: auto;
}
}
35 changes: 33 additions & 2 deletions new-ui/src/pages/full/AddPage/AddPage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import './style.scss';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { useMemo } from 'react';
import { FullPageTitle } from '../../../shared/components/FullPageTitle/FullPageTitle';
import { FullPage } from '../../../shared/layouts/FullPage/FullPage';
import {
getInstancesQueryOptions,
mfaConfigurableInstances,
tunnelsDisabled,
} from '../../../shared/rust-api/query';
import { ThemeSpacing } from '../../../shared/types';
import { AddCard } from './components/AddCard/AddCard';
import { useStartMfaConfiguration } from './hooks/useStartMfaConfiguration';

export const AddPage = () => {
const navigate = useNavigate();
const { data: instances } = useQuery(getInstancesQueryOptions);
const { data: instances, isPending } = useQuery(getInstancesQueryOptions);

const mfaInstances = useMemo(
() => mfaConfigurableInstances(instances ?? []),
[instances],
);

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

return (
<FullPage id="add-page-view">
<FullPageTitle title="Add Defguard items" spacing={ThemeSpacing.Xl} />
Expand All @@ -28,7 +40,7 @@ export const AddPage = () => {
actionText="Add instance"
description={`Establish a secure connection to your Defguard instance effortlessly by configuring it with a single token—no manual setup.`}
/>
{!tunnelsDisabled(instances ?? []) && (
{!isPending && !tunnelsDisabled(instances ?? []) && (
<AddCard
image="wireguard"
onClick={() => {
Expand All @@ -41,6 +53,25 @@ export const AddPage = () => {
description={`Add and configure a WireGuard tunnel to securely route traffic through an encrypted connection using predefined configuration.`}
/>
)}
{!isPending && mfaInstances.length > 0 && (
<AddCard
image="lock"
title="Add new MFA method"
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]);
}}
loading={configureMfaPending}
/>
)}
</div>
</FullPage>
);
Expand Down
32 changes: 29 additions & 3 deletions new-ui/src/pages/full/AddPage/components/AddCard/AddCard.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import './style.scss';
import clsx from 'clsx';
import { Button } from '../../../../../shared/components/Button/Button';
import { ButtonSize, ButtonVariant } from '../../../../../shared/components/Button/types';
import defaultImage from './assets/image_1.png';
import yubiImage from './assets/image_2.png';
import wireguardImage from './assets/image_3.png';
import lockImage from './assets/image_4.png';

interface Props {
actionText: string;
title: string;
description: string;
image: 'default' | 'yubi' | 'wireguard';
image: 'default' | 'yubi' | 'wireguard' | 'lock';
loading?: boolean;
onClick?: () => void;
}

export const AddCard = ({ actionText, description, title, image, onClick }: Props) => {
export const AddCard = ({
actionText,
description,
title,
image,
loading = false,
onClick,
}: Props) => {
const renderImage = () => {
switch (image) {
case 'default':
Expand Down Expand Up @@ -46,6 +56,16 @@ export const AddCard = ({ actionText, description, title, image, onClick }: Prop
loading="eager"
/>
);
case 'lock':
return (
<img
src={lockImage}
width={165.57}
height={219.5}
decoding="async"
loading="eager"
/>
);
}
};

Expand All @@ -56,7 +76,13 @@ export const AddCard = ({ actionText, description, title, image, onClick }: Prop
<div className="contents">
<p className="title">{title}</p>
<p className="description">{description}</p>
<Button size="primary" variant="outlined" text={actionText} onClick={onClick} />
<Button
loading={loading}
size={ButtonSize.Primary}
variant={ButtonVariant.Secondary}
text={actionText}
onClick={onClick}
/>
</div>
</div>
</div>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 11 additions & 6 deletions new-ui/src/pages/full/AddPage/components/AddCard/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
}
}

&.image-lock {
.images {
img {
position: absolute;
top: -10px;
left: -10px;
}
}
}

.title {
font: var(--t-body-sm-500);
color: var(--fg-white-100);
Expand All @@ -49,7 +59,7 @@
.description {
color: var(--color);
font: var(--t-body-xs-400);
padding-bottom: var(--spacing-xl);
padding-bottom: var(--spacing-2xl);
transition-duration: 160ms;
transition-timing-function: ease-out;
transition-property: color;
Expand All @@ -72,11 +82,6 @@
background: var(--bg-dark-blue-20);
box-sizing: border-box;
padding: var(--spacing-md) var(--spacing-md) var(--spacing-md) var(--spacing-xl);

.btn-wrap,
.btn {
width: 100%;
}
}
}
}
41 changes: 41 additions & 0 deletions new-ui/src/pages/full/AddPage/hooks/useStartMfaConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { error as logError } from '@tauri-apps/plugin-log';
import { Snackbar } from '../../../../shared/providers/snackbar/snackbar';
import {
isMfaConfigMissingToken,
isMfaConfigUnsupported,
} from '../../../../shared/rust-api/mfaError';
import type { InstanceInfo } from '../../../../shared/rust-api/types';
import { startMfaConfiguration } from '../../ConfigureMfaPage/hooks/useConfigureMfaStore';

/** Opens a session and enters the wizard. On failure it stays put, so another instance
* can be tried. */
export const useStartMfaConfiguration = () => {
const navigate = useNavigate();

return useMutation({
mutationFn: (instance: InstanceInfo) => startMfaConfiguration(instance),
onSuccess: () => {
navigate({
to: '/full/configure-mfa',
});
},
onError: (err) => {
void logError(`MFA configuration start failed: ${err}`);
if (isMfaConfigUnsupported(err)) {
Snackbar.error(
'This Defguard instance does not support configuring MFA from the client.',
);
return;
}
if (isMfaConfigMissingToken(err)) {
Snackbar.error(
'This device has no polling token; update the instance and try again.',
);
return;
}
Snackbar.error('Could not start MFA configuration.');
},
});
};
2 changes: 1 addition & 1 deletion new-ui/src/pages/full/AddTunnelPage/AddTunnelPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useTunnelWizardStore } from '../TunnelWizardPage/hooks/useTunnelWizardS
export const AddTunnelPage = () => {
const navigate = useNavigate();
return (
<FullPage id="add-tunnel-page">
<FullPage id="add-tunnel-page" withControls>
<FullPageTitle title="Add WireGuard Tunnel" />
<div className="contents">
<p className="page-description">{`A WireGuard tunnel is a secure, encrypted connection that allows your device or network to communicate safely over the internet.It ensures that your data is protected and transmitted through a private, trusted channel.`}</p>
Expand Down
4 changes: 0 additions & 4 deletions new-ui/src/pages/full/AddTunnelPage/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,4 @@
color: var(--fg-white-70);
}
}

.controls {
margin-top: auto;
}
}
33 changes: 33 additions & 0 deletions new-ui/src/pages/full/ConfigureMfaPage/ConfigureMfaPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useNavigate } from '@tanstack/react-router';
import { useCallback } from 'react';
import { ConfigureMfaTimeoutProvider } from './components/ConfigureMfaTimeoutProvider';
import {
discardMfaConfiguration,
useConfigureMfaStore,
} from './hooks/useConfigureMfaStore';
import { ConfigureMfaVerify } from './verify/ConfigureMfaVerify';
import { ConfigureMfaWizard } from './wizard/ConfigureMfaWizard';

export const ConfigureMfaPage = () => {
const navigate = useNavigate();
const authorized = useConfigureMfaStore((s) => s.authorized);

const leave = useCallback(() => {
navigate({ to: '/full/add' });
}, [navigate]);

const handleSessionExpired = useCallback(() => {
void discardMfaConfiguration();
leave();
}, [leave]);

return (
<ConfigureMfaTimeoutProvider>
{authorized ? (
<ConfigureMfaWizard onCancel={leave} onSessionExpired={handleSessionExpired} />
) : (
<ConfigureMfaVerify onCancel={leave} onSessionExpired={handleSessionExpired} />
)}
</ConfigureMfaTimeoutProvider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useNavigate } from '@tanstack/react-router';
import { type PropsWithChildren, useEffect } from 'react';
import { Snackbar } from '../../../../shared/providers/snackbar/snackbar';
import {
discardMfaConfiguration,
useConfigureMfaStore,
} from '../hooks/useConfigureMfaStore';

/** Recover at the deadline rather than let the user type a code that cannot land. */
export const ConfigureMfaTimeoutProvider = ({ children }: PropsWithChildren) => {
const navigate = useNavigate();
const deadline = useConfigureMfaStore((s) => s.deadline);

useEffect(() => {
if (!deadline) return;

const expire = () => {
void discardMfaConfiguration();
Snackbar.error('MFA configuration session expired, start again.');
void navigate({ to: '/full/add', replace: true });
};

const ms = new Date(deadline).getTime() - Date.now();
if (ms <= 0) {
expire();
return;
}

const timer = setTimeout(expire, ms);
return () => clearTimeout(timer);
}, [deadline, navigate]);

return children;
};
Loading
Loading