From c0b687d4c55d93b5af7415133b7c0ad3e0a01721 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Mar 2026 09:39:30 +0100 Subject: [PATCH 1/9] expose server state via /info endpoint --- src/http.rs | 41 +++++++++++++++++++++++++++++++------ web/src/shared/api/types.ts | 1 + 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/http.rs b/src/http.rs index bd3b6fb..f67e1cb 100644 --- a/src/http.rs +++ b/src/http.rs @@ -70,13 +70,33 @@ async fn handle_404() -> (StatusCode, &'static str) { } #[derive(Serialize)] -struct AppInfo<'a> { - version: &'a str, +#[serde(rename_all = "snake_case")] +enum ServerState { + Setup, + Disconnected, + Connected, } -async fn app_info<'a>() -> Result>, ApiError> { +#[derive(Serialize)] +struct AppInfo { + version: &'static str, + server_state: ServerState, +} + +async fn app_info(State(state): State) -> Result, ApiError> { let version = crate_version!(); - Ok(Json(AppInfo { version })) + let server_state = if !state.grpc_server.setup_completed() { + ServerState::Setup + } else if state.grpc_server.connected.load(Ordering::Relaxed) { + ServerState::Connected + } else { + ServerState::Disconnected + }; + + Ok(Json(AppInfo { + version, + server_state, + })) } async fn healthcheck() -> &'static str { @@ -249,9 +269,12 @@ async fn ensure_configured( request: Request, next: Next, ) -> Response { - // Allow healthchecks even before core connects and gives us the cookie key. + // Allow healthchecks and info even before core connects and gives us the cookie key. let path = request.uri().path(); - if matches!(path, "/api/v1/health" | "/api/v1/health-grpc") { + if matches!( + path, + "/api/v1/health" | "/api/v1/health-grpc" | "/api/v1/info" + ) { return next.run(request).await; } @@ -282,6 +305,12 @@ pub async fn run_server( reset_tx, ); + // Preload existing TLS configuration so /api/v1/info can report "disconnected" + // immediately on startup + if let Some(existing_configuration) = config.clone() { + grpc_server.configure(existing_configuration); + } + let server_clone = grpc_server.clone(); let env_config_clone = env_config.clone(); diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index d183fb4..a4e0993 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -30,6 +30,7 @@ export type UserInfo = { export type AppInfo = { version: string; + server_state: 'setup' | 'disconnected' | 'connected'; }; export type EnrollmentSettings = { From 5751fee3c945391faaaf028eb74e68c979f53b6e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Mar 2026 09:57:15 +0100 Subject: [PATCH 2/9] initial frontend warning implementation --- web/src/routeTree.gen.ts | 21 +++++++++++++++++++++ web/src/routes/__root.tsx | 16 +++++++++++++++- web/src/routes/server-warning.tsx | 16 ++++++++++++++++ web/src/shared/api/api.ts | 2 +- 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 web/src/routes/server-warning.tsx diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index d8c7e0f..6f862d8 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -10,6 +10,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as SessionEndRouteImport } from './routes/session-end' +import { Route as ServerWarningRouteImport } from './routes/server-warning' import { Route as PasswordResetRouteImport } from './routes/password-reset' import { Route as OpenDesktopRouteImport } from './routes/open-desktop' import { Route as LinkInvalidRouteImport } from './routes/link-invalid' @@ -30,6 +31,11 @@ const SessionEndRoute = SessionEndRouteImport.update({ path: '/session-end', getParentRoute: () => rootRouteImport, } as any) +const ServerWarningRoute = ServerWarningRouteImport.update({ + id: '/server-warning', + path: '/server-warning', + getParentRoute: () => rootRouteImport, +} as any) const PasswordResetRoute = PasswordResetRouteImport.update({ id: '/password-reset', path: '/password-reset', @@ -109,6 +115,7 @@ export interface FileRoutesByFullPath { '/link-invalid': typeof LinkInvalidRoute '/open-desktop': typeof OpenDesktopRoute '/password-reset': typeof PasswordResetRoute + '/server-warning': typeof ServerWarningRoute '/session-end': typeof SessionEndRoute '/openid/callback': typeof OpenidCallbackRoute '/openid/error': typeof OpenidErrorRoute @@ -126,6 +133,7 @@ export interface FileRoutesByTo { '/link-invalid': typeof LinkInvalidRoute '/open-desktop': typeof OpenDesktopRoute '/password-reset': typeof PasswordResetRoute + '/server-warning': typeof ServerWarningRoute '/session-end': typeof SessionEndRoute '/openid/callback': typeof OpenidCallbackRoute '/openid/error': typeof OpenidErrorRoute @@ -144,6 +152,7 @@ export interface FileRoutesById { '/link-invalid': typeof LinkInvalidRoute '/open-desktop': typeof OpenDesktopRoute '/password-reset': typeof PasswordResetRoute + '/server-warning': typeof ServerWarningRoute '/session-end': typeof SessionEndRoute '/openid/callback': typeof OpenidCallbackRoute '/openid/error': typeof OpenidErrorRoute @@ -163,6 +172,7 @@ export interface FileRouteTypes { | '/link-invalid' | '/open-desktop' | '/password-reset' + | '/server-warning' | '/session-end' | '/openid/callback' | '/openid/error' @@ -180,6 +190,7 @@ export interface FileRouteTypes { | '/link-invalid' | '/open-desktop' | '/password-reset' + | '/server-warning' | '/session-end' | '/openid/callback' | '/openid/error' @@ -197,6 +208,7 @@ export interface FileRouteTypes { | '/link-invalid' | '/open-desktop' | '/password-reset' + | '/server-warning' | '/session-end' | '/openid/callback' | '/openid/error' @@ -215,6 +227,7 @@ export interface RootRouteChildren { LinkInvalidRoute: typeof LinkInvalidRoute OpenDesktopRoute: typeof OpenDesktopRoute PasswordResetRoute: typeof PasswordResetRoute + ServerWarningRoute: typeof ServerWarningRoute SessionEndRoute: typeof SessionEndRoute OpenidCallbackRoute: typeof OpenidCallbackRoute OpenidErrorRoute: typeof OpenidErrorRoute @@ -234,6 +247,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SessionEndRouteImport parentRoute: typeof rootRouteImport } + '/server-warning': { + id: '/server-warning' + path: '/server-warning' + fullPath: '/server-warning' + preLoaderRoute: typeof ServerWarningRouteImport + parentRoute: typeof rootRouteImport + } '/password-reset': { id: '/password-reset' path: '/password-reset' @@ -343,6 +363,7 @@ const rootRouteChildren: RootRouteChildren = { LinkInvalidRoute: LinkInvalidRoute, OpenDesktopRoute: OpenDesktopRoute, PasswordResetRoute: PasswordResetRoute, + ServerWarningRoute: ServerWarningRoute, SessionEndRoute: SessionEndRoute, OpenidCallbackRoute: OpenidCallbackRoute, OpenidErrorRoute: OpenidErrorRoute, diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx index 11a8220..c0640f7 100644 --- a/web/src/routes/__root.tsx +++ b/web/src/routes/__root.tsx @@ -1,7 +1,21 @@ -import { createRootRoute, Outlet } from '@tanstack/react-router'; +import { createRootRoute, Outlet, redirect } from '@tanstack/react-router'; import { SessionGuard } from '../app/SessionGuard'; +import { api } from '../shared/api/api'; export const Route = createRootRoute({ + beforeLoad: async ({ location }) => { + if (location.pathname === '/server-warning') { + return; + } + + const response = await api.appInfo.callbackFn({ params: undefined }); + if (response.data.server_state !== 'connected') { + throw redirect({ + to: '/server-warning', + replace: true, + }); + } + }, component: RootComponent, }); diff --git a/web/src/routes/server-warning.tsx b/web/src/routes/server-warning.tsx new file mode 100644 index 0000000..8594302 --- /dev/null +++ b/web/src/routes/server-warning.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { api } from '../shared/api/api'; + +export const Route = createFileRoute('/server-warning')({ + loader: async () => { + const response = await api.appInfo.callbackFn({ params: undefined }); + return response.data.server_state; + }, + component: ServerWarningPage, +}); + +function ServerWarningPage() { + const serverState = Route.useLoaderData(); + + return
{serverState}
; +} diff --git a/web/src/shared/api/api.ts b/web/src/shared/api/api.ts index 58d3d33..6181cdb 100644 --- a/web/src/shared/api/api.ts +++ b/web/src/shared/api/api.ts @@ -15,7 +15,7 @@ import type { } from './types'; const api = { - appInfo: get('/info'), + appInfo: get('/info'), enrollment: { start: post('/enrollment/start'), }, From 68f1cfd3acc6accb52043070e9206fbfd9504205 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Mar 2026 10:16:29 +0100 Subject: [PATCH 3/9] use PageProcessEnd component to display the warning --- web/src/routes/server-warning.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/web/src/routes/server-warning.tsx b/web/src/routes/server-warning.tsx index 8594302..fcd87c9 100644 --- a/web/src/routes/server-warning.tsx +++ b/web/src/routes/server-warning.tsx @@ -1,5 +1,6 @@ import { createFileRoute } from '@tanstack/react-router'; import { api } from '../shared/api/api'; +import { PageProcessEnd } from '../shared/components/PageProcessEnd/PageProcessEnd'; export const Route = createFileRoute('/server-warning')({ loader: async () => { @@ -12,5 +13,19 @@ export const Route = createFileRoute('/server-warning')({ function ServerWarningPage() { const serverState = Route.useLoaderData(); - return
{serverState}
; + const title = serverState === 'setup' ? 'Server is in setup mode' : 'Core is disconnected'; + const subtitle = + serverState === 'setup' + ? 'Proxy setup is not complete yet. Most actions are unavailable until setup finishes.' + : 'Proxy is configured, but it is not connected to Defguard Core. Try again in a moment.'; + + return ( + + ); } From 1b0d07fd669b4fbddb4e4b6dcc9c3fc97816d679 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Mar 2026 10:26:10 +0100 Subject: [PATCH 4/9] rename PageProcessEnd component to PageInfo --- web/src/pages/OpenDesktop/OpenDesktopPage.tsx | 4 ++-- web/src/pages/SessionEnd/SessionEndPage.tsx | 4 ++-- web/src/routes/link-invalid.tsx | 4 ++-- web/src/routes/openid/error.tsx | 4 ++-- web/src/routes/openid/mfa/callback.tsx | 4 ++-- web/src/routes/password/finish.tsx | 4 ++-- web/src/routes/password/sent.tsx | 4 ++-- web/src/routes/server-warning.tsx | 4 ++-- .../PageProcessEnd.tsx => PageInfo/PageInfo.tsx} | 2 +- .../shared/components/{PageProcessEnd => PageInfo}/style.scss | 0 10 files changed, 17 insertions(+), 17 deletions(-) rename web/src/shared/components/{PageProcessEnd/PageProcessEnd.tsx => PageInfo/PageInfo.tsx} (98%) rename web/src/shared/components/{PageProcessEnd => PageInfo}/style.scss (100%) diff --git a/web/src/pages/OpenDesktop/OpenDesktopPage.tsx b/web/src/pages/OpenDesktop/OpenDesktopPage.tsx index d4e523f..5ecee27 100644 --- a/web/src/pages/OpenDesktop/OpenDesktopPage.tsx +++ b/web/src/pages/OpenDesktop/OpenDesktopPage.tsx @@ -1,7 +1,7 @@ import './style.scss'; import { useSearch } from '@tanstack/react-router'; import { m } from '../../paraglide/messages'; -import { PageProcessEnd } from '../../shared/components/PageProcessEnd/PageProcessEnd'; +import { PageInfo } from '../../shared/components/PageInfo/PageInfo'; import laptopImage from './assets/laptop.png'; export const OpenDesktopPage = () => { @@ -16,7 +16,7 @@ export const OpenDesktopPage = () => { } return ( - { return ( - { return ( - { @@ -20,7 +20,7 @@ function ServerWarningPage() { : 'Proxy is configured, but it is not connected to Defguard Core. Try again in a moment.'; return ( - Date: Mon, 2 Mar 2026 10:36:44 +0100 Subject: [PATCH 5/9] fix linting --- web/src/routes/server-warning.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/routes/server-warning.tsx b/web/src/routes/server-warning.tsx index 037bd60..84139cf 100644 --- a/web/src/routes/server-warning.tsx +++ b/web/src/routes/server-warning.tsx @@ -13,7 +13,8 @@ export const Route = createFileRoute('/server-warning')({ function ServerWarningPage() { const serverState = Route.useLoaderData(); - const title = serverState === 'setup' ? 'Server is in setup mode' : 'Core is disconnected'; + const title = + serverState === 'setup' ? 'Server is in setup mode' : 'Core is disconnected'; const subtitle = serverState === 'setup' ? 'Proxy setup is not complete yet. Most actions are unavailable until setup finishes.' From 5abd8f5952f7883f21fa1f3426e2b44c4115932e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Mar 2026 11:04:19 +0100 Subject: [PATCH 6/9] fix styles --- web/src/shared/components/PageInfo/PageInfo.tsx | 2 +- web/src/shared/components/PageInfo/style.scss | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/web/src/shared/components/PageInfo/PageInfo.tsx b/web/src/shared/components/PageInfo/PageInfo.tsx index 45bd678..01b9596 100644 --- a/web/src/shared/components/PageInfo/PageInfo.tsx +++ b/web/src/shared/components/PageInfo/PageInfo.tsx @@ -28,7 +28,7 @@ export const PageInfo = ({ imageSrc, }: Props) => { return ( - +
{imageSrc ? : } diff --git a/web/src/shared/components/PageInfo/style.scss b/web/src/shared/components/PageInfo/style.scss index b7cb5d0..fbe0d6a 100644 --- a/web/src/shared/components/PageInfo/style.scss +++ b/web/src/shared/components/PageInfo/style.scss @@ -1,4 +1,4 @@ -.page-process-end { +.page-info { .icon[data-kind='check-circle'] { path { fill: var(--fg-success); @@ -11,6 +11,12 @@ } } + .icon[data-kind='warning'] { + path { + fill: var(--fg-attention); + } + } + .page-content { & > svg { padding-bottom: 0 !important; From cf341fd83dbf7fe9e514ef16e81f79be22997c3e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Mar 2026 11:45:03 +0100 Subject: [PATCH 7/9] use translations --- web/messages/en.json | 7 ++++++- web/src/routes/server-warning.tsx | 11 +++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/web/messages/en.json b/web/messages/en.json index e1f8a89..11ca20e 100644 --- a/web/messages/en.json +++ b/web/messages/en.json @@ -108,5 +108,10 @@ "openid_mfa_complete_subtitle": "You have been successfully authenticated. Please close this window and get back to the Defguard VPN Client", "open_desktop_title": "Open the desktop app to continue", "open_desktop_description": "We tried to open the desktop app automatically, but it didn't respond. This can happen if the browser blocks the request or the app didn't start in time.", - "open_desktop_button": "Open desktop app" + "open_desktop_button": "Open desktop app", + "server_warning_setup_title": "Server is in setup mode", + "server_warning_setup_subtitle": "Edge setup is not complete yet. Please contact your administrator.", + "server_warning_disconnected_title": "Core is disconnected", + "server_warning_disconnected_subtitle": "Edge is configured, but it is not connected to Defguard Core. Please contact your administrator.", + "server_warning_retry": "Try again" } diff --git a/web/src/routes/server-warning.tsx b/web/src/routes/server-warning.tsx index 84139cf..b62a8ee 100644 --- a/web/src/routes/server-warning.tsx +++ b/web/src/routes/server-warning.tsx @@ -1,4 +1,5 @@ import { createFileRoute } from '@tanstack/react-router'; +import { m } from '../paraglide/messages'; import { api } from '../shared/api/api'; import { PageInfo } from '../shared/components/PageInfo/PageInfo'; @@ -14,11 +15,13 @@ function ServerWarningPage() { const serverState = Route.useLoaderData(); const title = - serverState === 'setup' ? 'Server is in setup mode' : 'Core is disconnected'; + serverState === 'setup' + ? m.server_warning_setup_title() + : m.server_warning_disconnected_title(); const subtitle = serverState === 'setup' - ? 'Proxy setup is not complete yet. Most actions are unavailable until setup finishes.' - : 'Proxy is configured, but it is not connected to Defguard Core. Try again in a moment.'; + ? m.server_warning_setup_subtitle() + : m.server_warning_disconnected_subtitle(); return ( ); } From 65ffad86da8bcfd1694b22b34e13f45c55032ad4 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Mar 2026 12:27:36 +0100 Subject: [PATCH 8/9] impl From for ServerState --- src/http.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/http.rs b/src/http.rs index f67e1cb..4cbb85e 100644 --- a/src/http.rs +++ b/src/http.rs @@ -77,6 +77,18 @@ enum ServerState { Connected, } +impl From<&AppState> for ServerState { + fn from(state: &AppState) -> Self { + if !state.grpc_server.setup_completed() { + Self::Setup + } else if state.grpc_server.connected.load(Ordering::Relaxed) { + Self::Connected + } else { + Self::Disconnected + } + } +} + #[derive(Serialize)] struct AppInfo { version: &'static str, @@ -85,13 +97,7 @@ struct AppInfo { async fn app_info(State(state): State) -> Result, ApiError> { let version = crate_version!(); - let server_state = if !state.grpc_server.setup_completed() { - ServerState::Setup - } else if state.grpc_server.connected.load(Ordering::Relaxed) { - ServerState::Connected - } else { - ServerState::Disconnected - }; + let server_state = ServerState::from(&state); Ok(Json(AppInfo { version, From 3693e955467af70e2843bb65da921aebed5b652e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 2 Mar 2026 12:53:25 +0100 Subject: [PATCH 9/9] move server warning page to separate component --- .../pages/ServerWarning/ServerWarningPage.tsx | 26 +++++++++++++++++ web/src/routes/server-warning.tsx | 28 ++----------------- 2 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 web/src/pages/ServerWarning/ServerWarningPage.tsx diff --git a/web/src/pages/ServerWarning/ServerWarningPage.tsx b/web/src/pages/ServerWarning/ServerWarningPage.tsx new file mode 100644 index 0000000..4b87ea7 --- /dev/null +++ b/web/src/pages/ServerWarning/ServerWarningPage.tsx @@ -0,0 +1,26 @@ +import { useLoaderData } from '@tanstack/react-router'; +import { m } from '../../paraglide/messages'; +import { PageInfo } from '../../shared/components/PageInfo/PageInfo'; + +export const ServerWarningPage = () => { + const serverState = useLoaderData({ from: '/server-warning' }); + + const title = + serverState === 'setup' + ? m.server_warning_setup_title() + : m.server_warning_disconnected_title(); + const subtitle = + serverState === 'setup' + ? m.server_warning_setup_subtitle() + : m.server_warning_disconnected_subtitle(); + + return ( + + ); +}; diff --git a/web/src/routes/server-warning.tsx b/web/src/routes/server-warning.tsx index b62a8ee..8264c57 100644 --- a/web/src/routes/server-warning.tsx +++ b/web/src/routes/server-warning.tsx @@ -1,35 +1,11 @@ import { createFileRoute } from '@tanstack/react-router'; -import { m } from '../paraglide/messages'; +import { ServerWarningPage } from '../pages/ServerWarning/ServerWarningPage'; import { api } from '../shared/api/api'; -import { PageInfo } from '../shared/components/PageInfo/PageInfo'; export const Route = createFileRoute('/server-warning')({ loader: async () => { - const response = await api.appInfo.callbackFn({ params: undefined }); + const response = await api.appInfo.callbackFn({}); return response.data.server_state; }, component: ServerWarningPage, }); - -function ServerWarningPage() { - const serverState = Route.useLoaderData(); - - const title = - serverState === 'setup' - ? m.server_warning_setup_title() - : m.server_warning_disconnected_title(); - const subtitle = - serverState === 'setup' - ? m.server_warning_setup_subtitle() - : m.server_warning_disconnected_subtitle(); - - return ( - - ); -}