Skip to content
Open
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
88 changes: 85 additions & 3 deletions bin/core/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
use axum::{Extension, Router, routing::get};
use axum::{
Extension, Router,
body::{Body, to_bytes},
extract::State,
http::header,
middleware::{self, Next},
response::Response,
routing::get,
};
use komodo_client::entities::user::User;
use mogh_auth_server::middleware::authenticate_request;
use mogh_error::Json;
Expand All @@ -18,14 +26,25 @@ mod openapi;
mod terminal;
mod ws;

const BASE_PATH_PLACEHOLDER: &str = "{{KOMODO_HOST}}";

#[derive(Clone)]
struct UiPathConfig {
base_path: String,
}

#[derive(serde::Deserialize)]
struct Variant {
variant: String,
}

pub fn app() -> Router {
let config = core_config();
Router::new()
let base_url = config.host.clone();
let base_path = normalize_base_path(&base_url);
let ui_path_config = UiPathConfig { base_path };

let app_router = Router::new()
.merge(openapi::serve_docs())
.route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
.nest("/auth", mogh_auth_server::api::router::<KomodoAuthImpl>())
Expand All @@ -42,7 +61,70 @@ pub fn app() -> Router {
&config.ui_path,
config.ui_index_force_no_cache,
))
.layer(cors_layer(config))
.layer(middleware::from_fn_with_state(
ui_path_config.clone(),
replace_base_url_in_html,
));

let mut router = Router::new().merge(app_router.clone());
let nest_base_path = ui_path_config.base_path.trim_end_matches('/');
if !nest_base_path.is_empty() && nest_base_path != "/" {
router = router.nest(nest_base_path, app_router);
}

router.layer(cors_layer(config))
}

async fn replace_base_url_in_html(
State(ui_path_config): State<UiPathConfig>,
request: axum::extract::Request,
next: Next,
) -> Response {
let response = next.run(request).await;

let is_html = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.starts_with("text/html"));

if !is_html {
return response;
}

let (mut parts, body) = response.into_parts();
let Ok(bytes) = to_bytes(body, 4 * 1024 * 1024).await else {
return Response::from_parts(parts, Body::empty());
};

let Ok(contents) = String::from_utf8(bytes.to_vec()) else {
return Response::from_parts(parts, Body::from(bytes));
};

if !contents.contains(BASE_PATH_PLACEHOLDER) {
return Response::from_parts(parts, Body::from(contents));
}

let replaced =
contents.replace(BASE_PATH_PLACEHOLDER, &ui_path_config.base_path);
parts.headers.remove(header::ETAG);
parts.headers.remove(header::CONTENT_LENGTH);

Response::from_parts(parts, Body::from(replaced))
}

fn normalize_base_path(base_url: &str) -> String {
let pathname = url::Url::parse(base_url)
.ok()
.map(|url| url.path().trim().to_string())
.unwrap_or_default();

let trimmed = pathname.trim_matches('/');
if trimmed.is_empty() {
"/".to_string()
} else {
format!("/{trimmed}/")
}
}

fn user_router() -> Router {
Expand Down
9 changes: 5 additions & 4 deletions ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<base href="{{KOMODO_HOST}}" />

<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="shortcut icon" type="image/ico" href="./favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="Komodo" />
<link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="./manifest.json" />

<title>Komodo</title>
</head>
Expand Down
13 changes: 8 additions & 5 deletions ui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@ import "mogh_ui/index.scss";
// Doesn't need to be awaited - applies in background when ready.
import("@/monaco").then(({ default: initMonaco }) => initMonaco());

export const KOMODO_BASE_URL =
import.meta.env.VITE_KOMODO_HOST ?? location.origin;
export const UPDATE_WS_URL =
KOMODO_BASE_URL.replace("http", "ws") + "/ws/update";
const parsedBaseUrl = new URL(document.baseURI);

export const KOMODO_BASE_PATH = parsedBaseUrl.pathname.replace(/\/+$/, "");

export const KOMODO_BASE_URL = `${parsedBaseUrl.origin}${KOMODO_BASE_PATH}`;

export const UPDATE_WS_URL = `${KOMODO_BASE_URL.replace(/^http/, "ws")}/ws/update`;
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});

setAuthUrl(KOMODO_BASE_URL + "/auth");
setAuthUrl(`${KOMODO_BASE_URL}/auth`);

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
Expand Down
3 changes: 2 additions & 1 deletion ui/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { LoadingScreen, useAuthState } from "mogh_ui";
import { useUser } from "@/lib/hooks";
import { MoghAuth } from "komodo_client";
import App from "@/app";
import { KOMODO_BASE_PATH } from "@/main";

const Login = lazy(() => import("@/pages/login"));
const UserDisabled = lazy(() => import("@/pages/user-disabled"));
Expand Down Expand Up @@ -55,7 +56,7 @@ export const Router = () => {
}

return (
<BrowserRouter>
<BrowserRouter basename={KOMODO_BASE_PATH || undefined}>
<Routes>
<Route path="login" element={<Login />} />
<Route element={<RequireAuth />}>
Expand Down
19 changes: 16 additions & 3 deletions ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,21 @@ import react from "@vitejs/plugin-react";
dotenv.config({ path: ".env.development" });

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
export default defineConfig(({ command }) => ({
base: "./",
plugins: [
react(),
{
name: "inject-base-url",
transformIndexHtml: (html) => {
const host = process.env.VITE_KOMODO_HOST;
if (command === "serve") {
return html.replace("{{KOMODO_HOST}}", host ?? "/");
}
return html;
},
},
],
server: {
allowedHosts: process.env.ALLOWED_HOSTS?.split(","),
},
Expand Down Expand Up @@ -55,4 +68,4 @@ export default defineConfig({
},
},
},
});
}));