Skip to content

Commit

Permalink
feat: mgmt ui works remotely
Browse files Browse the repository at this point in the history
- mgmt ui now requires an api secret
- requets from ui to api now include api secret
- expand cors permissions of mgmt api
- static assets are embedded into ui
  • Loading branch information
hjr3 committed Feb 12, 2024
1 parent 415ae56 commit 6349f08
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 29 deletions.
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions crates/proxy/src/mgmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::result::Result as StdResult;

use anyhow::{Context, Result};
use axum::extract::{Extension, Json, Path, Query, Request, State};
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response};
use axum::{
Expand Down Expand Up @@ -165,11 +165,11 @@ pub fn router(pool: SqlitePool, origin_cache: OriginCache, config: &Config) -> R
.route("/attempts", get(list_attempts))
.route("/attempts/:id", get(get_attempt))
.route("/queue", post(add_request_to_queue))
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http())
.layer(Extension(pool))
.layer(Extension(origin_cache))
.route_layer(middleware::from_fn_with_state(state, auth))
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::very_permissive().expose_headers([header::CONTENT_RANGE]))
}

#[derive(Debug, Deserialize)]
Expand Down
2 changes: 2 additions & 0 deletions crates/ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ tower-http = { version = "0.4.4", features = ["fs", "trace"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = { version = "2.4.1", features = ["serde"] }
rust-embed = { version = "8.2.0", features = ["axum-ex"] }
mime_guess = "2.0.4"
81 changes: 63 additions & 18 deletions crates/ui/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
use std::net::SocketAddr;

use axum::{extract::State, response::Html, routing::get, Router};
use bpaf::{construct, long, Parser};
use tower_http::{
services::{ServeDir, ServeFile},
trace::TraceLayer,
use axum::{
extract::State,
http::{header, StatusCode, Uri},
response::{Html, IntoResponse, Response},
routing::get,
Router,
};
use bpaf::{construct, long, Parser};
use rust_embed::RustEmbed;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

mod error;

#[derive(RustEmbed)]
#[folder = "static/"]
struct Assets;

#[derive(Clone, Debug)]
struct Config {
api_url: String,
api_secret: String,
}

#[tokio::main]
Expand All @@ -29,16 +38,20 @@ async fn main() {
.help("URL of the Soldr Management API")
.argument::<String>("API_URL");

let parser = construct!(Config { api_url });
let api_secret = long("api-secret")
.help("Soldr Management API secret key")
.argument::<String>("API_SECRET");

let parser = construct!(Config {
api_url,
api_secret
});
let ui_parser = parser.to_options().descr("Soldr UI");
let config: Config = ui_parser.run();

let app = Router::new()
.route("/hello", get(|| async { "Hello, World!" }))
.nest_service("/assets", ServeDir::new("static/assets"))
.nest_service("/vite.svg", ServeFile::new("static/vite.svg"))
.route("/", get(serve_html))
.route("/*path", get(serve_html))
.fallback(static_handler)
.layer(TraceLayer::new_for_http())
.with_state(config);

Expand All @@ -50,14 +63,46 @@ async fn main() {
.unwrap();
}

async fn serve_html(State(config): State<Config>) -> Html<String> {
let html = include_str!("../static/index.html");
async fn static_handler(State(config): State<Config>, uri: Uri) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/');

if path.is_empty() || path == "index.html" {
return index_html(config).await;
}

match Assets::get(path) {
Some(content) => {
let mime = mime_guess::from_path(path).first_or_octet_stream();

([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response()
}
None => {
if path.contains('.') {
return not_found().await;
}

index_html(config).await
}
}
}

async fn index_html(config: Config) -> Response {
match Assets::get("index.html") {
Some(content) => {
let script_tag = format!(
r#"<script type="module">window.config = {{ apiUrl: "{}", apiSecret: "{}" }};</script>"#,
config.api_url, config.api_secret
);

let html = String::from_utf8(content.data.into_owned()).unwrap();

let script_tag = format!(
r#"<script type="module">window.apiUrl = "{}";</script>"#,
config.api_url
);
let html = html.replace("<!-- __SOLDR_UI_CONFIG__ -->", script_tag.as_str());
let html = html.replace("<!-- __SOLDR_UI_CONFIG__ -->", script_tag.as_str());
Html(html).into_response()
}
None => not_found().await,
}
}

Html(html)
async fn not_found() -> Response {
(StatusCode::NOT_FOUND, "404").into_response()
}
Empty file removed crates/ui/static/.gitkeep
Empty file.
2 changes: 2 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ build: build-ui build-core

build-ui:
cd packages/ui && npm run build
mkdir -p crates/ui/static
rm -fr crates/ui/static/*
cp -r packages/ui/dist/* crates/ui/static/

build-core:
Expand Down
1 change: 1 addition & 0 deletions packages/ui/.env.development
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
VITE_MGMT_API_URL=http://localhost:3443
VITE_MGMT_API_SECRET="a secret with minimum length of 32 characters"
2 changes: 1 addition & 1 deletion packages/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/soldering-iron.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Soldr - Management UI</title>
<link
Expand Down
28 changes: 28 additions & 0 deletions packages/ui/public/soldering-iron.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion packages/ui/public/vite.svg

This file was deleted.

23 changes: 17 additions & 6 deletions packages/ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Admin, Resource } from 'react-admin';
import { Admin, Resource, fetchUtils } from 'react-admin';
import simpleRestProvider from 'ra-data-simple-rest';

import Layout from './Layout';
Expand All @@ -9,19 +9,30 @@ import Attempts from './Attempts';

declare global {
interface Window {
apiUrl?: string;
config: {
apiUrl?: string;
apiSecret?: string;
};
}
}

const config = {
apiUrl: import.meta.env.PROD ? window.apiUrl : import.meta.env.VITE_MGMT_API_URL,
};
const config = import.meta.env.PROD
? window.config
: {
apiUrl: import.meta.env.VITE_MGMT_API_URL,
apiSecret: import.meta.env.VITE_MGMT_API_SECRET,
};

if (!config.apiUrl) {
throw new Error('API URL is required');
}

const dataProvider = simpleRestProvider(config.apiUrl);
const httpClient = (url: string, options: fetchUtils.Options = {}) => {
const user = { token: `Basic ${btoa(config.apiSecret)}`, authenticated: true };
return fetchUtils.fetchJson(url, { ...options, user });
};

const dataProvider = simpleRestProvider(config.apiUrl, httpClient);

const App = () => (
<Admin dataProvider={dataProvider} layout={Layout} dashboard={Dashboard}>
Expand Down

0 comments on commit 6349f08

Please sign in to comment.