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
41 changes: 34 additions & 7 deletions src-tauri/src/bin/defguard-client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use defguard_client::{
DB_POOL,
},
enterprise::provisioning::handle_client_initialization,
events::handle_deep_link,
periodic::run_periodic_tasks,
service,
tray::{configure_tray_icon, setup_tray, show_main_window},
Expand All @@ -34,6 +35,7 @@ use defguard_client::{
};
use log::{Level, LevelFilter};
use tauri::{AppHandle, Builder, Manager, RunEvent, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt;
use tauri_plugin_log::{Target, TargetKind};

#[macro_use]
Expand Down Expand Up @@ -272,6 +274,14 @@ fn main() {

let app_handle = app.app_handle();

// Single Rust-side entry point for all deep link events (runtime).
{
let handle = app_handle.clone();
app.deep_link().on_open_url(move |event| {
handle_deep_link(&handle, &event.urls());
});
}

// Prepare `AppConfig`.
let config = AppConfig::new(app_handle);

Expand Down Expand Up @@ -367,14 +377,26 @@ fn main() {
}
#[cfg(not(target_os = "linux"))]
{
let has_locations = tauri::async_runtime::block_on(
defguard_client::window_manager::has_non_service_locations()
);
if has_locations {
WindowManager::open_tray(app_handle)?;
} else {
info!("No locations found, showing full view on startup.");
// If the app was cold-launched by a deep link the full view must open, not the tray.
let launched_by_deep_link = app_handle
.deep_link()
.get_current()
.ok()
.flatten()
.is_some();
if launched_by_deep_link {
info!("App launched via deep link, opening full view directly.");
let _ = WindowManager::open_full_view(app_handle);
} else {
let has_locations = tauri::async_runtime::block_on(
defguard_client::window_manager::has_non_service_locations()
);
if has_locations {
WindowManager::open_tray(app_handle)?;
} else {
info!("No locations found, showing full view on startup.");
let _ = WindowManager::open_full_view(app_handle);
}
}
}

Expand Down Expand Up @@ -417,6 +439,11 @@ fn main() {
);
tauri::async_runtime::block_on(startup(app_handle));

// Handle a deep link that launched the app (startup case).
if let Ok(Some(urls)) = app_handle.deep_link().get_current() {
handle_deep_link(app_handle, &urls);
}

// Handle Ctrl-C.
debug!("Setting up Ctrl-C handler.");
let app_handle_clone = app_handle.clone();
Expand Down
18 changes: 15 additions & 3 deletions src-tauri/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use serde::Serialize;
use tauri::{AppHandle, Emitter, Url};
use tauri::{AppHandle, Emitter, Manager, Url};
use tauri_plugin_notification::NotificationExt;

use crate::{tray::show_main_window, ConnectionType};
use crate::{
window_manager::{WindowManager, NEW_UI_WINDOW_ID},
ConnectionType,
};

// Match src/pages/client/types.ts.
#[non_exhaustive]
Expand Down Expand Up @@ -115,7 +118,16 @@ pub fn handle_deep_link(app_handle: &AppHandle, urls: &[Url]) {
}
}
if let (Some(token), Some(url)) = (token, url) {
show_main_window(app_handle);
info!("Deep link received: token={token}, url={url}");
// If the compact tray window is visible, hide it before opening main view.
if let Some(tray_win) = app_handle.get_webview_window(NEW_UI_WINDOW_ID) {
if tray_win.is_visible().unwrap_or(false) {
let _ = tray_win.hide();
}
}
if let Err(e) = WindowManager::open_full_view(app_handle) {
warn!("Deep link: failed to open main window: {e}");
}
let _ = app_handle.emit(
EventKey::AddInstance.into(),
AddInstancePayload {
Expand Down
1 change: 1 addition & 0 deletions src/pages/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export enum TauriEventKey {
DEAD_CONNECTION_DROPPED = 'dead-connection-dropped',
DEAD_CONNECTION_RECONNECTED = 'dead-connection-reconnected',
APPLICATION_CONFIG_CHANGED = 'application-config-changed',
ADD_INSTANCE = 'add-instance',
MFA_TRIGGER = 'mfa-trigger',
VERSION_MISMATCH = 'version-mismatch',
UUID_MISMATCH = 'uuid-mismatch',
Expand Down
121 changes: 13 additions & 108 deletions src/shared/components/providers/DeepLinkProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,125 +1,30 @@
import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { listen } from '@tauri-apps/api/event';
import { error } from '@tauri-apps/plugin-log';
import { type PropsWithChildren, useCallback, useEffect, useRef } from 'react';
import z, { string } from 'zod';
import { type PropsWithChildren, useEffect } from 'react';
import { type AddInstancePayload, TauriEventKey } from '../../../pages/client/types';
import useAddInstance from '../../hooks/useAddInstance';
import { errorDetail } from '../../utils/errorDetail';

enum DeepLink {
AddInstance = 'addinstance',
}

export const linkStorageKey = 'lastSuccessfullyHandledDeepLink';

export const storeLink = (value: string) => {
sessionStorage.setItem(linkStorageKey, value);
};

const readStoreLink = (): string | null => {
return sessionStorage.getItem(linkStorageKey);
};

const addInstanceLinkSchema = z.object({
token: string().trim().min(1),
url: string().trim().min(1).url(),
});

const AddInstanceLink = z.object({
link: z.literal(DeepLink.AddInstance),
data: addInstanceLinkSchema,
});

const validLinkPayload = z.discriminatedUnion('link', [AddInstanceLink]);

type LinkPayload = z.infer<typeof validLinkPayload>;

const linkIntoPayload = (link: URL | null): LinkPayload | null => {
if (link == null) return null;

const searchData = Object.fromEntries(new URLSearchParams(link.search));
const linkKey = [link.hostname, link.pathname]
.map((l) => l.trim().replaceAll('/', ''))
.filter((l) => l !== '')[0] as string;
const payload = {
link: linkKey,
data: searchData,
};
const result = validLinkPayload.safeParse(payload);
if (result.success) {
return result.data;
} else {
error(`Link ${link} was rejected due to schema validation.`);
}
return null;
};

export const DeepLinkProvider = ({ children }: PropsWithChildren) => {
const mounted = useRef(false);

const { handleAddInstance } = useAddInstance();

const handleValidLink = useCallback(
async (payload: LinkPayload, rawLink?: string) => {
const { data, link } = payload;
switch (link) {
case DeepLink.AddInstance:
await handleAddInstance(data, rawLink);
break;
}
if (rawLink) {
storeLink(rawLink);
}
},
[handleAddInstance],
);

// biome-ignore lint/correctness/useExhaustiveDependencies: only on mount
useEffect(() => {
if (!mounted.current) {
mounted.current = true;

let unlisten: (() => void) | undefined;
(async () => {
const start = await getCurrent();
if (start != null) {
const lastLink = readStoreLink();
// if the link is exact as last successfully executed link
// this is only necessary bcs in dev mode window is hot reloaded causing the startup link to be handled multiple times over.
if (lastLink != null && lastLink === start[0]) {
return;
}
const payload = linkIntoPayload(new URL(start[0]));
if (payload != null) {
try {
handleValidLink(payload, start[0]);
} catch (e) {
const detail = errorDetail(e);
error(`Failed to handle startup deep link "${payload.link}": ${detail}`);
}
}
}
unlisten = await onOpenUrl((urls) => {
if (urls?.length) {
const link = urls[0];
const payload = linkIntoPayload(new URL(link));
if (payload != null) {
try {
handleValidLink(payload);
} catch (e) {
const detail = errorDetail(e);
error(
`Failed to handle valid deep link "${payload?.link}" action: ${detail}`,
);
}
}
}
});
})();
return () => {
unlisten?.();
};
}
}, []);
const unlisten = listen<AddInstancePayload>(TauriEventKey.ADD_INSTANCE, (event) => {
handleAddInstance(event.payload).catch((e) => {
error(`Failed to handle add-instance event: ${errorDetail(e)}`);
});
});

return () => {
unlisten.then((fn) => fn());
};
}, [handleAddInstance]);

return <>{children}</>;
};
Loading