Skip to content

Commit

Permalink
add frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
t-aleksander committed Nov 14, 2024
1 parent 81ab97d commit e129737
Show file tree
Hide file tree
Showing 19 changed files with 872 additions and 70 deletions.
2 changes: 1 addition & 1 deletion proto
7 changes: 7 additions & 0 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use axum::{extract::FromRequestParts, http::request::Parts};
use axum_client_ip::{InsecureClientIp, LeftmostXForwardedFor};
use axum_extra::{headers::UserAgent, TypedHeader};
use tokio::{sync::oneshot::Receiver, time::timeout};
use tonic::Code;

use super::proto::DeviceInfo;
use crate::{error::ApiError, proto::core_response::Payload};
Expand Down Expand Up @@ -53,6 +54,12 @@ async fn get_core_response(rx: Receiver<Payload>) -> Result<Payload, ApiError> {
if let Ok(core_response) = timeout(CORE_RESPONSE_TIMEOUT, rx).await {
debug!("Got gRPC response from Defguard core: {core_response:?}");
if let Ok(Payload::CoreError(core_error)) = core_response {
if core_error.status_code == Code::FailedPrecondition as i32
&& core_error.message == "no valid license"
{
debug!("Tried to get core response related to an enterprise feature but the enterprise is not enabled, ignoring it...");
return Err(ApiError::EnterpriseNotEnabled);
}
error!(
"Received an error response from the core service. | status code: {} message: {}",
core_error.status_code, core_error.message
Expand Down
34 changes: 23 additions & 11 deletions src/handlers/openid_login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ use crate::{
error::ApiError,
handlers::get_core_response,
http::AppState,
proto::{core_request, core_response, AuthCallbackRequest, AuthInfoRequest},
proto::{
core_request, core_response, AuthCallbackRequest, AuthCallbackResponse, AuthInfoRequest,
},
};

const COOKIE_MAX_AGE: Duration = Duration::days(1);
Expand All @@ -30,12 +32,16 @@ pub(crate) fn router() -> Router<AppState> {
#[derive(Serialize)]
struct AuthInfo {
url: String,
button_display_name: Option<String>,
}

impl AuthInfo {
#[must_use]
fn new(url: String) -> Self {
Self { url }
fn new(url: String, button_display_name: Option<String>) -> Self {
Self {
url,
button_display_name,
}
}
}

Expand Down Expand Up @@ -76,7 +82,7 @@ async fn auth_info(
.build();
let private_cookies = private_cookies.add(nonce_cookie).add(csrf_cookie);

let auth_info = AuthInfo::new(response.url);
let auth_info = AuthInfo::new(response.url, response.button_display_name);
Ok((private_cookies, Json(auth_info)))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
Expand All @@ -86,16 +92,22 @@ async fn auth_info(

#[derive(Debug, Deserialize)]
pub struct AuthenticationResponse {
id_token: String,
code: String,
state: String,
}

#[derive(Serialize)]
struct CallbackResponseData {
url: String,
token: String,
}

#[instrument(level = "debug", skip(state))]
async fn auth_callback(
State(state): State<AppState>,
mut private_cookies: PrivateCookieJar,
Json(payload): Json<AuthenticationResponse>,
) -> Result<PrivateCookieJar, ApiError> {
) -> Result<(PrivateCookieJar, Json<CallbackResponseData>), ApiError> {
let nonce = private_cookies
.get(NONCE_COOKIE_NAME)
.ok_or(ApiError::Unauthorized("Nonce cookie not found".into()))?
Expand All @@ -116,7 +128,7 @@ async fn auth_callback(
.remove(Cookie::from(CSRF_COOKIE_NAME));

let request = AuthCallbackRequest {
id_token: payload.id_token,
code: payload.code,
nonce,
callback_url: state.callback_url().to_string(),
};
Expand All @@ -125,11 +137,11 @@ async fn auth_callback(
.grpc_server
.send(Some(core_request::Payload::AuthCallback(request)), None)?;
let payload = get_core_response(rx).await?;
if let core_response::Payload::Empty(()) = payload {
debug!("Received auth callback response (empty message)");
Ok(private_cookies)
if let core_response::Payload::AuthCallback(AuthCallbackResponse { url, token }) = payload {
debug!("Received auth callback response {url:?} {token:?}");
Ok((private_cookies, Json(CallbackResponseData { url, token })))
} else {
error!("Received invalid gRPC response type: {payload:#?}");
error!("Received invalid gRPC response type during handling the OpenID authentication callback: {payload:#?}");
Err(ApiError::InvalidResponseType)
}
}
4 changes: 2 additions & 2 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ impl AppState {
#[must_use]
pub(crate) fn callback_url(&self) -> Url {
let mut url = self.url.clone();
// Append "/api/v1/openid/callback" to the URL.
// Append "/openid/callback" to the URL.
if let Ok(mut path_segments) = url.path_segments_mut() {
path_segments.extend(&["api", "v1", "openid", "callback"]);
path_segments.extend(&["openid", "callback"]);
}
url
}
Expand Down
5 changes: 5 additions & 0 deletions web/src/components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { PasswordResetPage } from '../../pages/passwordReset/PasswordResetPage';
import { SessionTimeoutPage } from '../../pages/sessionTimeout/SessionTimeoutPage';
import { TokenPage } from '../../pages/token/TokenPage';
import { routes } from '../../shared/routes';
import { OpenIDCallbackPage } from '../../pages/openidCallback/OpenIDCallback';

dayjs.extend(duration);
dayjs.extend(utc);
Expand Down Expand Up @@ -52,6 +53,10 @@ const router = createBrowserRouter([
path: routes.passwordReset,
element: <PasswordResetPage />,
},
{
path: routes.openidCallback,
element: <OpenIDCallbackPage />,
},
{
path: '/*',
element: <Navigate to="/" replace />,
Expand Down
15 changes: 15 additions & 0 deletions web/src/pages/openidCallback/OpenIDCallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import './style.scss';

import { LogoContainer } from '../../components/LogoContainer/LogoContainer';
import { PageContainer } from '../../shared/components/layout/PageContainer/PageContainer';
import { OpenIDCallbackCard } from './components/OpenIDCallbackCard';
// import { TokenCard } from './components/TokenCard';

export const OpenIDCallbackPage = () => {
return (
<PageContainer id="openidcallback-page">
<LogoContainer />
<OpenIDCallbackCard />
</PageContainer>
);
};
176 changes: 176 additions & 0 deletions web/src/pages/openidCallback/components/OpenIDCallbackCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import './style.scss';

Check failure on line 1 in web/src/pages/openidCallback/components/OpenIDCallbackCard.tsx

View workflow job for this annotation

GitHub Actions / lint-web

Run autofix to sort these imports!

import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';

Check failure on line 4 in web/src/pages/openidCallback/components/OpenIDCallbackCard.tsx

View workflow job for this annotation

GitHub Actions / lint-web

'useEffect' is defined but never used
import { useBreakpoint } from 'use-breakpoint';

import { AxiosError } from 'axios';
import { useI18nContext } from '../../../i18n/i18n-react';
import { ActionButton } from '../../../shared/components/layout/ActionButton/ActionButton';
import { ActionButtonVariant } from '../../../shared/components/layout/ActionButton/types';
import { BigInfoBox } from '../../../shared/components/layout/BigInfoBox/BigInfoBox';
import { Button } from '../../../shared/components/layout/Button/Button';
import { ButtonStyleVariant } from '../../../shared/components/layout/Button/types';
import { Card } from '../../../shared/components/layout/Card/Card';
import { Input } from '../../../shared/components/layout/Input/Input';
import { LoaderSpinner } from '../../../shared/components/layout/LoaderSpinner/LoaderSpinner';
import { MessageBox } from '../../../shared/components/layout/MessageBox/MessageBox';
import { MessageBoxType } from '../../../shared/components/layout/MessageBox/types';
import SvgIconDownload from '../../../shared/components/svg/IconDownload';
import { deviceBreakpoints } from '../../../shared/constants';
import { useApi } from '../../../shared/hooks/api/useApi';

type ErrorResponse = {
error: string;
};

export const OpenIDCallbackCard = () => {
const { openIDCallback } = useApi();
const { breakpoint } = useBreakpoint(deviceBreakpoints);
const { LL } = useI18nContext();

Check failure on line 30 in web/src/pages/openidCallback/components/OpenIDCallbackCard.tsx

View workflow job for this annotation

GitHub Actions / lint-web

'LL' is assigned a value but never used
const [error, setError] = useState<string | null>(null);

const { isLoading, data } = useQuery(
[],
() => {
const hashFragment = window.location.hash.substring(1);
const params = new URLSearchParams(hashFragment);
const error = params.get('error');
if (error) {
setError(error);
return;
}
const id_token = params.get('id_token');
const state = params.get('state');
if (!id_token || !state) {
setError(
"Missing id_token or state in the callback's URL. The provider might not be configured correctly.",

Check failure on line 47 in web/src/pages/openidCallback/components/OpenIDCallbackCard.tsx

View workflow job for this annotation

GitHub Actions / lint-web

This line has a length of 109. Maximum allowed is 90
);
return;
}
if (id_token && state) {
return openIDCallback({
id_token,
state,
});
}
},
{
retry: false,
refetchOnWindowFocus: false,
refetchOnMount: false,
onError: (error: AxiosError) => {
console.error(error);
const errorResponse = error.response?.data as ErrorResponse;
console.log('errorResponse', errorResponse);
if (errorResponse.error) {
setError(errorResponse.error);
} else {
setError(String(error));
}
},
onSuccess(data) {
if (!data?.token || !data?.url) {
setError("The server's response is missing the token or url.");
}
},
},
);

if (isLoading) {
return (
<div className="loader-container">
<LoaderSpinner size={100} />
</div>
);
}

if (error) {
return (
<Card shaded={breakpoint !== 'mobile'} className="openidcallback-card">
<MessageBox
type={MessageBoxType.ERROR}
message={`There was an error while validating your account: ${error}`}
/>
<Button
text="Go back"
styleVariant={ButtonStyleVariant.PRIMARY}
onClick={() => {
window.location.href = '/';
}}
/>
</Card>
);
}

return (
<>
<Card shaded={breakpoint !== 'mobile'} className="openidcallback-card">
<h2>Start your enrollment process</h2>
<BigInfoBox
message={
'Thank you for validating your account, please follow instruction below for configuring your VPN connection.'

Check failure on line 112 in web/src/pages/openidCallback/components/OpenIDCallbackCard.tsx

View workflow job for this annotation

GitHub Actions / lint-web

This line has a length of 121. Maximum allowed is 90
}
/>
<div className="steps">
<p>1. Please download and install defguard VPN Desktop Client</p>
<div className="download-link">
<Button
text="Download Desktop Client"
styleVariant={ButtonStyleVariant.PRIMARY}
icon={<SvgIconDownload color="#fff" />}
onClick={() => {
window.open('https://defguard.net/download/', '_blank');
}}
/>
</div>
<p>
2. Open the client and <i>Add Instance</i>. Copy the data provided below into
the corresponding fields. You can also learn more about the process in our{' '}

Check failure on line 129 in web/src/pages/openidCallback/components/OpenIDCallbackCard.tsx

View workflow job for this annotation

GitHub Actions / lint-web

This line has a length of 91. Maximum allowed is 90
<a

Check failure on line 130 in web/src/pages/openidCallback/components/OpenIDCallbackCard.tsx

View workflow job for this annotation

GitHub Actions / lint-web

Using target="_blank" without rel="noreferrer" (which implies rel="noopener") is a security risk in older browsers: see https://mathiasbynens.github.io/rel-noopener/#recommendations
href="https://docs.defguard.net/help/configuring-vpn/add-new-instance"
target="_blank"
>
documentation
</a>
.
</p>
<Card className="token-input shaded">
<h2>Please provide instance URL and token</h2>
<div className="labelled-input">
<label>Instance URL:</label>
<div className="input-copy">
<Input value={data?.url} readOnly />
<ActionButton
variant={ActionButtonVariant.COPY}
onClick={() => {
navigator.clipboard.writeText(data?.url);
}}
/>
</div>
</div>

<div className="labelled-input">
<label>Token:</label>
<div className="input-copy">
<Input value={data?.token} readOnly />
<ActionButton
variant={ActionButtonVariant.COPY}
onClick={() => {
navigator.clipboard.writeText(data?.token);
}}
/>
</div>
</div>

<Button
text="Add instance"
styleVariant={ButtonStyleVariant.PRIMARY}
disabled={true}
/>
</Card>
</div>
</Card>
</>
);
};
Loading

0 comments on commit e129737

Please sign in to comment.