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
47 changes: 41 additions & 6 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,39 @@ 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<Json<AppInfo<'a>>, ApiError> {
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,
server_state: ServerState,
}

async fn app_info(State(state): State<AppState>) -> Result<Json<AppInfo>, ApiError> {
let version = crate_version!();
Ok(Json(AppInfo { version }))
let server_state = ServerState::from(&state);

Ok(Json(AppInfo {
version,
server_state,
}))
}

async fn healthcheck() -> &'static str {
Expand Down Expand Up @@ -249,9 +275,12 @@ async fn ensure_configured(
request: Request<Body>,
next: Next,
) -> Response<Body> {
// 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;
}

Expand Down Expand Up @@ -282,6 +311,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();

Expand Down
7 changes: 6 additions & 1 deletion web/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 2 additions & 2 deletions web/src/pages/OpenDesktop/OpenDesktopPage.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand All @@ -16,7 +16,7 @@ export const OpenDesktopPage = () => {
}

return (
<PageProcessEnd
<PageInfo
imageSrc={laptopImage}
title={m.open_desktop_title()}
subtitle={m.open_desktop_description()}
Expand Down
26 changes: 26 additions & 0 deletions web/src/pages/ServerWarning/ServerWarningPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PageInfo
icon="warning"
title={title}
subtitle={subtitle}
link="/"
linkText={m.server_warning_retry()}
/>
);
};
4 changes: 2 additions & 2 deletions web/src/pages/SessionEnd/SessionEndPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { m } from '../../paraglide/messages';
import { PageProcessEnd } from '../../shared/components/PageProcessEnd/PageProcessEnd';
import { PageInfo } from '../../shared/components/PageInfo/PageInfo';

export const SessionEndPage = () => {
return (
<PageProcessEnd
<PageInfo
link="/"
icon="disabled"
title={m.session_end_title()}
Expand Down
21 changes: 21 additions & 0 deletions web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -163,6 +172,7 @@ export interface FileRouteTypes {
| '/link-invalid'
| '/open-desktop'
| '/password-reset'
| '/server-warning'
| '/session-end'
| '/openid/callback'
| '/openid/error'
Expand All @@ -180,6 +190,7 @@ export interface FileRouteTypes {
| '/link-invalid'
| '/open-desktop'
| '/password-reset'
| '/server-warning'
| '/session-end'
| '/openid/callback'
| '/openid/error'
Expand All @@ -197,6 +208,7 @@ export interface FileRouteTypes {
| '/link-invalid'
| '/open-desktop'
| '/password-reset'
| '/server-warning'
| '/session-end'
| '/openid/callback'
| '/openid/error'
Expand All @@ -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
Expand All @@ -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'
Expand Down Expand Up @@ -343,6 +363,7 @@ const rootRouteChildren: RootRouteChildren = {
LinkInvalidRoute: LinkInvalidRoute,
OpenDesktopRoute: OpenDesktopRoute,
PasswordResetRoute: PasswordResetRoute,
ServerWarningRoute: ServerWarningRoute,
SessionEndRoute: SessionEndRoute,
OpenidCallbackRoute: OpenidCallbackRoute,
OpenidErrorRoute: OpenidErrorRoute,
Expand Down
16 changes: 15 additions & 1 deletion web/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -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,
});

Expand Down
4 changes: 2 additions & 2 deletions web/src/routes/link-invalid.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createFileRoute } from '@tanstack/react-router';
import { m } from '../paraglide/messages';
import { PageProcessEnd } from '../shared/components/PageProcessEnd/PageProcessEnd';
import { PageInfo } from '../shared/components/PageInfo/PageInfo';

export const Route = createFileRoute('/link-invalid')({
component: RouteComponent,
});

function RouteComponent() {
return (
<PageProcessEnd
<PageInfo
link="/"
icon="disabled"
title={m.link_invalid_title()}
Expand Down
4 changes: 2 additions & 2 deletions web/src/routes/openid/error.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router';
import { m } from '../../paraglide/messages';
import { PageProcessEnd } from '../../shared/components/PageProcessEnd/PageProcessEnd';
import { PageInfo } from '../../shared/components/PageInfo/PageInfo';
import { useOpenidStore } from '../../shared/hooks/useOpenIdStore';

export const Route = createFileRoute('/openid/error')({
Expand All @@ -13,7 +13,7 @@ function RouteComponent() {
);

return (
<PageProcessEnd
<PageInfo
title={m.openid_mfa_redirect_error_title()}
subtitle={openIdError}
icon="disabled"
Expand Down
4 changes: 2 additions & 2 deletions web/src/routes/openid/mfa/callback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router';
import z from 'zod';
import { m } from '../../../paraglide/messages';
import { api } from '../../../shared/api/api';
import { PageProcessEnd } from '../../../shared/components/PageProcessEnd/PageProcessEnd';
import { PageInfo } from '../../../shared/components/PageInfo/PageInfo';
import { useOpenidStore } from '../../../shared/hooks/useOpenIdStore';

const searchSchema = z.object({
Expand Down Expand Up @@ -35,7 +35,7 @@ export const Route = createFileRoute('/openid/mfa/callback')({

function RouteComponent() {
return (
<PageProcessEnd
<PageInfo
title={m.openid_mfa_complete_title()}
subtitle={m.openid_mfa_complete_subtitle()}
icon="check-circle"
Expand Down
4 changes: 2 additions & 2 deletions web/src/routes/password/finish.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createFileRoute } from '@tanstack/react-router';
import { m } from '../../paraglide/messages';
import { PageProcessEnd } from '../../shared/components/PageProcessEnd/PageProcessEnd';
import { PageInfo } from '../../shared/components/PageInfo/PageInfo';

const RouteComponent = () => {
return (
<PageProcessEnd
<PageInfo
title={m.password_end_title()}
subtitle={m.password_end_subtitle()}
link="/"
Expand Down
4 changes: 2 additions & 2 deletions web/src/routes/password/sent.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createFileRoute } from '@tanstack/react-router';
import { m } from '../../paraglide/messages';
import { PageProcessEnd } from '../../shared/components/PageProcessEnd/PageProcessEnd';
import { PageInfo } from '../../shared/components/PageInfo/PageInfo';

export const Route = createFileRoute('/password/sent')({
component: RouteComponent,
});

function RouteComponent() {
return (
<PageProcessEnd
<PageInfo
link="/"
linkText={m.password_sent_link()}
subtitle={m.password_sent_subTitle()}
Expand Down
11 changes: 11 additions & 0 deletions web/src/routes/server-warning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createFileRoute } from '@tanstack/react-router';
import { ServerWarningPage } from '../pages/ServerWarning/ServerWarningPage';
import { api } from '../shared/api/api';

export const Route = createFileRoute('/server-warning')({
loader: async () => {
const response = await api.appInfo.callbackFn({});
return response.data.server_state;
},
component: ServerWarningPage,
});
2 changes: 1 addition & 1 deletion web/src/shared/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
} from './types';

const api = {
appInfo: get<void, AppInfo>('/info'),
appInfo: get<AppInfo>('/info'),
enrollment: {
start: post<TokenRequest, EnrollmentStartResponse>('/enrollment/start'),
},
Expand Down
1 change: 1 addition & 0 deletions web/src/shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type UserInfo = {

export type AppInfo = {
version: string;
server_state: 'setup' | 'disconnected' | 'connected';
};

export type EnrollmentSettings = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Props = {
imageSrc?: string;
};

export const PageProcessEnd = ({
export const PageInfo = ({
link,
linkText,
subtitle,
Expand All @@ -28,7 +28,7 @@ export const PageProcessEnd = ({
imageSrc,
}: Props) => {
return (
<Page className="page-process-end">
<Page className="page-info">
<div className="content">
{imageSrc ? <img src={imageSrc} /> : <Icon icon={icon} size={32} />}
<SizedBox height={ThemeSpacing.Xl} />
Expand Down
Loading