Managed to load server configuration from WebUI
This commit is contained in:
parent
955067ad9c
commit
46648db093
16
remote_backend/Cargo.lock
generated
16
remote_backend/Cargo.lock
generated
@ -19,6 +19,21 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-cors"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9e772b3bcafe335042b5db010ab7c09013dad6eac4915c91d8d50902769f331"
|
||||||
|
dependencies = [
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web",
|
||||||
|
"derive_more",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.6.0"
|
version = "3.6.0"
|
||||||
@ -1691,6 +1706,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
|||||||
name = "remote_backend"
|
name = "remote_backend"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"actix-cors",
|
||||||
"actix-identity",
|
"actix-identity",
|
||||||
"actix-remote-ip",
|
"actix-remote-ip",
|
||||||
"actix-session",
|
"actix-session",
|
||||||
|
@ -16,6 +16,7 @@ actix-web = "4.5.1"
|
|||||||
actix-remote-ip = "0.1.0"
|
actix-remote-ip = "0.1.0"
|
||||||
actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
actix-session = { version = "0.9.0", features = ["cookie-session"] }
|
||||||
actix-identity = "0.7.1"
|
actix-identity = "0.7.1"
|
||||||
|
actix-cors = "0.7.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
reqwest = { version = "0.12.4", features = ["json"] }
|
reqwest = { version = "0.12.4", features = ["json"] }
|
||||||
|
@ -10,7 +10,7 @@ pub struct AppConfig {
|
|||||||
pub listen_address: String,
|
pub listen_address: String,
|
||||||
|
|
||||||
/// Website main origin
|
/// Website main origin
|
||||||
#[clap(short, long, env, default_value = "http://localhost:3000")]
|
#[clap(short, long, env, default_value = "http://localhost:5173")]
|
||||||
pub website_origin: String,
|
pub website_origin: String,
|
||||||
|
|
||||||
/// Proxy IP, might end with a star "*"
|
/// Proxy IP, might end with a star "*"
|
||||||
|
@ -6,6 +6,7 @@ use std::fmt::{Display, Formatter};
|
|||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
pub mod auth_controller;
|
pub mod auth_controller;
|
||||||
|
pub mod server_controller;
|
||||||
|
|
||||||
/// Custom error to ease controller writing
|
/// Custom error to ease controller writing
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
17
remote_backend/src/controllers/server_controller.rs
Normal file
17
remote_backend/src/controllers/server_controller.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use crate::app_config::AppConfig;
|
||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::extractors::auth_extractor::AuthExtractor;
|
||||||
|
use actix_web::HttpResponse;
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct ServerConfig {
|
||||||
|
authenticated: bool,
|
||||||
|
disable_auth: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn config(auth: AuthExtractor) -> HttpResult {
|
||||||
|
Ok(HttpResponse::Ok().json(ServerConfig {
|
||||||
|
authenticated: auth.is_authenticated(),
|
||||||
|
disable_auth: AppConfig::get().unsecure_disable_login,
|
||||||
|
}))
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
use actix_cors::Cors;
|
||||||
use actix_identity::config::LogoutBehaviour;
|
use actix_identity::config::LogoutBehaviour;
|
||||||
use actix_identity::IdentityMiddleware;
|
use actix_identity::IdentityMiddleware;
|
||||||
use actix_remote_ip::RemoteIPConfig;
|
use actix_remote_ip::RemoteIPConfig;
|
||||||
@ -9,9 +10,9 @@ use actix_web::web::Data;
|
|||||||
use actix_web::{web, App, HttpServer};
|
use actix_web::{web, App, HttpServer};
|
||||||
use light_openid::basic_state_manager::BasicStateManager;
|
use light_openid::basic_state_manager::BasicStateManager;
|
||||||
use remote_backend::app_config::AppConfig;
|
use remote_backend::app_config::AppConfig;
|
||||||
use remote_backend::controllers::auth_controller;
|
use remote_backend::constants;
|
||||||
|
use remote_backend::controllers::{auth_controller, server_controller};
|
||||||
use remote_backend::middlewares::auth_middleware::AuthChecker;
|
use remote_backend::middlewares::auth_middleware::AuthChecker;
|
||||||
use remote_backend::{constants, virtweb_client};
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
@ -40,15 +41,28 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.login_deadline(Some(Duration::from_secs(constants::MAX_SESSION_DURATION)))
|
.login_deadline(Some(Duration::from_secs(constants::MAX_SESSION_DURATION)))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
let cors = Cors::default()
|
||||||
|
.allowed_origin(&AppConfig::get().website_origin)
|
||||||
|
.allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||||
|
.allowed_header("X-Auth-Token")
|
||||||
|
.allow_any_header()
|
||||||
|
.supports_credentials()
|
||||||
|
.max_age(3600);
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.wrap(Logger::default())
|
.wrap(Logger::default())
|
||||||
.wrap(AuthChecker)
|
.wrap(AuthChecker)
|
||||||
.wrap(identity_middleware)
|
.wrap(identity_middleware)
|
||||||
.wrap(session_mw)
|
.wrap(session_mw)
|
||||||
|
.wrap(cors)
|
||||||
.app_data(state_manager.clone())
|
.app_data(state_manager.clone())
|
||||||
.app_data(Data::new(RemoteIPConfig {
|
.app_data(Data::new(RemoteIPConfig {
|
||||||
proxy: AppConfig::get().proxy_ip.clone(),
|
proxy: AppConfig::get().proxy_ip.clone(),
|
||||||
}))
|
}))
|
||||||
|
.route(
|
||||||
|
"/api/server/config",
|
||||||
|
web::get().to(server_controller::config),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/auth/start_oidc",
|
"/api/auth/start_oidc",
|
||||||
web::get().to(auth_controller::start_oidc),
|
web::get().to(auth_controller::start_oidc),
|
||||||
|
1
remote_frontend/.env
Normal file
1
remote_frontend/.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_APP_BACKEND=http://localhost:8002/api
|
1797
remote_frontend/package-lock.json
generated
1797
remote_frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fluentui/react-components": "^9.49.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
|
@ -1 +1,14 @@
|
|||||||
export function App(): React.ReactElement {}
|
import { ServerApi } from "./api/ServerApi";
|
||||||
|
import { AsyncWidget } from "./widgets/AsyncWidget";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={1}
|
||||||
|
errMsg="Failed to load server configuration!"
|
||||||
|
load={ServerApi.LoadConfig}
|
||||||
|
loadingMessage="Loading server configuration..."
|
||||||
|
build={() => <>todo</>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
174
remote_frontend/src/api/ApiClient.ts
Normal file
174
remote_frontend/src/api/ApiClient.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
interface RequestParams {
|
||||||
|
uri: string;
|
||||||
|
method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
|
||||||
|
allowFail?: boolean;
|
||||||
|
jsonData?: any;
|
||||||
|
formData?: FormData;
|
||||||
|
upProgress?: (progress: number) => void;
|
||||||
|
downProgress?: (e: { progress: number; total: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface APIResponse {
|
||||||
|
data: any;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(message: string, public code: number, public data: any) {
|
||||||
|
super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIClient {
|
||||||
|
/**
|
||||||
|
* Get backend URL
|
||||||
|
*/
|
||||||
|
static backendURL(): string {
|
||||||
|
const URL = import.meta.env.VITE_APP_BACKEND ?? "";
|
||||||
|
if (URL.length === 0) throw new Error("Backend URL undefined!");
|
||||||
|
return URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check out whether the backend is accessed through
|
||||||
|
* HTTPS or not
|
||||||
|
*/
|
||||||
|
static IsBackendSecure(): boolean {
|
||||||
|
return this.backendURL().startsWith("https");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a request on the backend
|
||||||
|
*/
|
||||||
|
static async exec(args: RequestParams): Promise<APIResponse> {
|
||||||
|
let body: string | undefined | FormData = undefined;
|
||||||
|
let headers: any = {};
|
||||||
|
|
||||||
|
// JSON request
|
||||||
|
if (args.jsonData) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
body = JSON.stringify(args.jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form data request
|
||||||
|
else if (args.formData) {
|
||||||
|
body = args.formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = this.backendURL() + args.uri;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
let status: number;
|
||||||
|
|
||||||
|
// Make the request with XMLHttpRequest
|
||||||
|
if (args.upProgress) {
|
||||||
|
const res: XMLHttpRequest = await new Promise((resolve, reject) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.upload.addEventListener("progress", (e) =>
|
||||||
|
args.upProgress!(e.loaded / e.total)
|
||||||
|
);
|
||||||
|
xhr.addEventListener("load", () => resolve(xhr));
|
||||||
|
xhr.addEventListener("error", () =>
|
||||||
|
reject(new Error("File upload failed"))
|
||||||
|
);
|
||||||
|
xhr.addEventListener("abort", () =>
|
||||||
|
reject(new Error("File upload aborted"))
|
||||||
|
);
|
||||||
|
xhr.addEventListener("timeout", () =>
|
||||||
|
reject(new Error("File upload timeout"))
|
||||||
|
);
|
||||||
|
xhr.open(args.method, url, true);
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
for (const key in headers) {
|
||||||
|
if (headers.hasOwnProperty(key))
|
||||||
|
xhr.setRequestHeader(key, headers[key]);
|
||||||
|
}
|
||||||
|
xhr.send(body);
|
||||||
|
});
|
||||||
|
|
||||||
|
status = res.status;
|
||||||
|
if (res.responseType === "json") data = JSON.parse(res.responseText);
|
||||||
|
else data = res.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the request with fetch
|
||||||
|
else {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: args.method,
|
||||||
|
body: body,
|
||||||
|
headers: headers,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process response
|
||||||
|
// JSON response
|
||||||
|
if (res.headers.get("content-type") === "application/json")
|
||||||
|
data = await res.json();
|
||||||
|
// Text / XML response
|
||||||
|
else if (
|
||||||
|
["application/xml", "text/plain"].includes(
|
||||||
|
res.headers.get("content-type") ?? ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data = await res.text();
|
||||||
|
// Binary file, tracking download progress
|
||||||
|
else if (res.body !== null && args.downProgress) {
|
||||||
|
// Track download progress
|
||||||
|
const contentEncoding = res.headers.get("content-encoding");
|
||||||
|
const contentLength = contentEncoding
|
||||||
|
? null
|
||||||
|
: res.headers.get("content-length");
|
||||||
|
|
||||||
|
const total = parseInt(contentLength ?? "0", 10);
|
||||||
|
let loaded = 0;
|
||||||
|
|
||||||
|
const resInt = new Response(
|
||||||
|
new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
const reader = res.body!.getReader();
|
||||||
|
|
||||||
|
const read = async () => {
|
||||||
|
try {
|
||||||
|
const ret = await reader.read();
|
||||||
|
if (ret.done) {
|
||||||
|
controller.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loaded += ret.value.byteLength;
|
||||||
|
args.downProgress!({ progress: loaded, total });
|
||||||
|
controller.enqueue(ret.value);
|
||||||
|
read();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
controller.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
read();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
data = await resInt.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not track progress (binary file)
|
||||||
|
else data = await res.blob();
|
||||||
|
|
||||||
|
status = res.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle expired tokens
|
||||||
|
if (status === 412) {
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.allowFail && (status < 200 || status > 299))
|
||||||
|
throw new ApiError("Request failed!", status, data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
status: status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
30
remote_frontend/src/api/ServerApi.ts
Normal file
30
remote_frontend/src/api/ServerApi.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
export interface ServerConfig {
|
||||||
|
authenticated: boolean;
|
||||||
|
disable_auth: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: ServerConfig | null = null;
|
||||||
|
|
||||||
|
export class ServerApi {
|
||||||
|
/**
|
||||||
|
* Get server configuration
|
||||||
|
*/
|
||||||
|
static async LoadConfig(): Promise<void> {
|
||||||
|
config = (
|
||||||
|
await APIClient.exec({
|
||||||
|
uri: "/server/config",
|
||||||
|
method: "GET",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached configuration
|
||||||
|
*/
|
||||||
|
static get Config(): ServerConfig {
|
||||||
|
if (config === null) throw new Error("Missing configuration!");
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,14 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,18 @@ import React from "react";
|
|||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { App } from "./App";
|
import { App } from "./App";
|
||||||
|
import {
|
||||||
|
FluentProvider,
|
||||||
|
teamsHighContrastTheme,
|
||||||
|
} from "@fluentui/react-components";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<FluentProvider
|
||||||
|
theme={teamsHighContrastTheme}
|
||||||
|
style={{ display: "flex", flex: 1 }}
|
||||||
|
>
|
||||||
<App />
|
<App />
|
||||||
|
</FluentProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
77
remote_frontend/src/widgets/AsyncWidget.tsx
Normal file
77
remote_frontend/src/widgets/AsyncWidget.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { Button, Spinner } from "@fluentui/react-components";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Loading,
|
||||||
|
Ready,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsyncWidget(p: {
|
||||||
|
loadKey: any;
|
||||||
|
load: () => Promise<void>;
|
||||||
|
errMsg: string;
|
||||||
|
build: () => React.ReactElement;
|
||||||
|
ready?: boolean;
|
||||||
|
buildLoading?: () => React.ReactElement;
|
||||||
|
buildError?: (e: string) => React.ReactElement;
|
||||||
|
errAdditionalElement?: () => React.ReactElement;
|
||||||
|
loadingMessage?: string;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const [state, setState] = useState(State.Loading);
|
||||||
|
|
||||||
|
const counter = useRef<any | null>(null);
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
setState(State.Loading);
|
||||||
|
await p.load();
|
||||||
|
setState(State.Ready);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setState(State.Error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (counter.current === p.loadKey) return;
|
||||||
|
counter.current = p.loadKey;
|
||||||
|
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state === State.Error)
|
||||||
|
return (
|
||||||
|
p.buildError?.(p.errMsg) ?? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
flex: "1",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ margin: "50px" }}>{p.errMsg}</div>
|
||||||
|
|
||||||
|
<Button onClick={load}>Try again</Button>
|
||||||
|
|
||||||
|
{p.errAdditionalElement && p.errAdditionalElement()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state === State.Loading || p.ready === false)
|
||||||
|
return (
|
||||||
|
p.buildLoading?.() ?? (
|
||||||
|
<Spinner
|
||||||
|
labelPosition="below"
|
||||||
|
label={p.loadingMessage}
|
||||||
|
style={{ margin: "auto" }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return p.build();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user