mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-15 23:32:46 +00:00
feat(server): make web ui optional
Add a server-side web.enabled toggle and CLI overrides so Fabro can run with API and health only while disabling the embedded SPA, browser auth routes, and web-only helper endpoints.
This commit is contained in:
parent
eb4b71cafa
commit
b101bfd5d5
12 changed files with 296 additions and 23 deletions
|
|
@ -44,6 +44,7 @@ ca = "/etc/fabro/tls/ca.pem"
|
|||
type = "Jwt"
|
||||
|
||||
[web]
|
||||
enabled = true
|
||||
url = "https://fabro-web.example.com"
|
||||
|
||||
[web.auth]
|
||||
|
|
@ -103,6 +104,8 @@ Several `settings.toml` settings can be overridden via `fabro server start` flag
|
|||
| Flag | Default | Description |
|
||||
|---|---|---|
|
||||
| `--bind` | `~/.fabro/fabro.sock` | Address to bind: `IP` or `IP:port` for TCP, or a path for Unix socket |
|
||||
| `--web` | enabled | Enable the embedded web UI, browser auth routes, and web-only helper endpoints |
|
||||
| `--no-web` | disabled | Disable the embedded web UI, browser auth routes, and web-only helper endpoints |
|
||||
| `--foreground` | — | Run in the foreground instead of daemonizing |
|
||||
| `--model` | — | Override default LLM model |
|
||||
| `--provider` | — | Override default LLM provider |
|
||||
|
|
@ -113,6 +116,17 @@ Several `settings.toml` settings can be overridden via `fabro server start` flag
|
|||
|
||||
CLI flags take precedence over `settings.toml` values. See [Run Configuration — Precedence](/execution/run-configuration#precedence) for the full resolution order.
|
||||
|
||||
### `[web]` section
|
||||
|
||||
Control the embedded SPA and browser-oriented routes.
|
||||
|
||||
| Key | Description | Default |
|
||||
|---|---|---|
|
||||
| `enabled` | Serve the embedded SPA, `/auth/*`, and the web-only helper endpoints under `/api/v1` | `true` |
|
||||
| `url` | External web UI URL used for OAuth redirects | `http://localhost:3000` |
|
||||
|
||||
When `enabled = false`, the server still exposes the machine API and `/health`, but `/`, `/auth/*`, SPA client routes, `/api/v1/auth/me`, `/api/v1/setup/*`, and `/api/v1/demo/toggle` all return `404`.
|
||||
|
||||
### Run defaults
|
||||
|
||||
The `[llm]`, `[setup]`, `[sandbox]`, `[checkpoint]`, and `[vars]` sections in `settings.toml` act as defaults for every run.
|
||||
|
|
|
|||
|
|
@ -4653,6 +4653,9 @@ components:
|
|||
description: Web UI configuration.
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether the embedded web UI and browser-oriented routes are enabled.
|
||||
url:
|
||||
type: string
|
||||
description: Web UI URL.
|
||||
|
|
|
|||
|
|
@ -328,6 +328,7 @@ Start the Fabro server daemon. By default, the server launches as a background p
|
|||
fabro server start # background daemon on Unix socket
|
||||
fabro server start --bind 127.0.0.1 # TCP on 32276, or random port if 32276 is busy
|
||||
fabro server start --bind 127.0.0.1:8080 # TCP on a specific port
|
||||
fabro server start --no-web # API and /health only
|
||||
fabro server start --foreground # blocking foreground mode
|
||||
fabro server start --sandbox daytona --max-concurrent-runs 4
|
||||
```
|
||||
|
|
@ -335,6 +336,8 @@ fabro server start --sandbox daytona --max-concurrent-runs 4
|
|||
| Flag | Description | Default |
|
||||
|---|---|---|
|
||||
| `--bind <ADDR>` | Address to bind: `IP` or `IP:port` for TCP, or a path for Unix socket | `~/.fabro/fabro.sock` |
|
||||
| `--web` | Enable the embedded web UI, browser auth routes, and web-only helper endpoints | Enabled |
|
||||
| `--no-web` | Disable the embedded web UI, browser auth routes, and web-only helper endpoints | Disabled |
|
||||
| `--foreground` | Run in the foreground instead of daemonizing | — |
|
||||
| `--model <MODEL>` | Override default LLM model | — |
|
||||
| `--provider <PROVIDER>` | Override default LLM provider | — |
|
||||
|
|
@ -345,6 +348,8 @@ fabro server start --sandbox daytona --max-concurrent-runs 4
|
|||
|
||||
Demo mode is per-request: send the `X-Fabro-Demo: 1` header to get static demo data with auth disabled.
|
||||
|
||||
When `--no-web` is set, the server still exposes the machine API under `/api/v1` and `/health`, but it returns `404` for `/`, `/auth/*`, SPA client routes, and the web-only helper endpoints under `/api/v1`.
|
||||
|
||||
## `fabro server stop`
|
||||
|
||||
Stop the running server daemon. Sends SIGTERM and waits for graceful shutdown, escalating to SIGKILL after the timeout.
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ fn ensure_server_running_with_bind(
|
|||
|
||||
let serve_args = ServeArgs {
|
||||
bind: None,
|
||||
web: false,
|
||||
no_web: false,
|
||||
model: None,
|
||||
provider: None,
|
||||
dry_run: false,
|
||||
|
|
@ -207,6 +209,12 @@ fn execute_daemon(
|
|||
if let Some(ref provider) = serve_args.provider {
|
||||
cmd.args(["--provider", provider]);
|
||||
}
|
||||
if serve_args.web {
|
||||
cmd.arg("--web");
|
||||
}
|
||||
if serve_args.no_web {
|
||||
cmd.arg("--no-web");
|
||||
}
|
||||
if serve_args.dry_run {
|
||||
cmd.arg("--dry-run");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,14 +35,18 @@ fn help() {
|
|||
Address to bind to (IP or IP:port for TCP, or path containing / for Unix socket)
|
||||
--no-upgrade-check
|
||||
Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
|
||||
--model <MODEL>
|
||||
Override default LLM model
|
||||
--quiet
|
||||
Suppress non-essential output [env: FABRO_QUIET=]
|
||||
--provider <PROVIDER>
|
||||
Override default LLM provider
|
||||
--web
|
||||
Enable the embedded web UI and browser auth routes
|
||||
--no-web
|
||||
Disable the embedded web UI, browser auth routes, and web-only helper endpoints
|
||||
--verbose
|
||||
Enable verbose output [env: FABRO_VERBOSE=]
|
||||
--model <MODEL>
|
||||
Override default LLM model
|
||||
--provider <PROVIDER>
|
||||
Override default LLM provider
|
||||
--dry-run
|
||||
Execute with simulated LLM backend
|
||||
--sandbox <SANDBOX>
|
||||
|
|
@ -188,8 +192,11 @@ fn start_with_tcp_host_only_bind_warns_and_falls_back_when_default_port_is_unava
|
|||
let context = test_context!();
|
||||
let storage_root = isolated_storage_dir();
|
||||
let storage_dir = storage_root.path().join("storage");
|
||||
let occupied = std::net::TcpListener::bind(("127.0.0.1", 32276))
|
||||
.expect("test requires default TCP port 32276 to be free before occupying it");
|
||||
let occupied = match std::net::TcpListener::bind(("127.0.0.1", 32276)) {
|
||||
Ok(listener) => Some(listener),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => None,
|
||||
Err(error) => panic!("failed to bind default TCP port 32276: {error}"),
|
||||
};
|
||||
|
||||
let mut filters = context.filters();
|
||||
filters.push((r"pid \d+".to_string(), "pid [PID]".to_string()));
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ impl TryFrom<GitConfig> for GitSettings {
|
|||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, crate::Combine)]
|
||||
pub struct WebConfig {
|
||||
pub enabled: Option<bool>,
|
||||
pub url: Option<String>,
|
||||
pub auth: Option<AuthConfig>,
|
||||
}
|
||||
|
|
@ -145,6 +146,7 @@ fn default_web_url() -> String {
|
|||
impl From<WebConfig> for WebSettings {
|
||||
fn from(value: WebConfig) -> Self {
|
||||
Self {
|
||||
enabled: value.enabled.unwrap_or(true),
|
||||
url: value.url.unwrap_or_else(default_web_url),
|
||||
auth: value.auth.map(Into::into).unwrap_or_default(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1495,6 +1495,7 @@ mod settings {
|
|||
storage_dir: Some("/home/fabro/.fabro".into()),
|
||||
max_concurrent_runs: Some(10),
|
||||
web: Some(WebSettings {
|
||||
enabled: true,
|
||||
url: "https://fabro.example.com".into(),
|
||||
auth: AuthSettings {
|
||||
provider: AuthProvider::Github,
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ use crate::github_webhooks::WebhookManager;
|
|||
use crate::jwt_auth::{AuthMode, AuthStrategy, resolve_auth_mode_with_lookup};
|
||||
use crate::secret_store::SecretStore;
|
||||
use crate::server::{
|
||||
build_app_state_with_path, build_router, reconcile_incomplete_runs_on_startup,
|
||||
shutdown_active_workers, spawn_scheduler,
|
||||
build_app_state_with_path, reconcile_incomplete_runs_on_startup, shutdown_active_workers,
|
||||
spawn_scheduler,
|
||||
};
|
||||
use crate::tls::{ClientAuth, build_rustls_config, serve_tls_with_shutdown};
|
||||
use fabro_llm::client::Client as LlmClient;
|
||||
|
|
@ -47,6 +47,14 @@ pub struct ServeArgs {
|
|||
#[arg(long)]
|
||||
pub bind: Option<String>,
|
||||
|
||||
/// Enable the embedded web UI and browser auth routes
|
||||
#[arg(long, conflicts_with = "no_web")]
|
||||
pub web: bool,
|
||||
|
||||
/// Disable the embedded web UI, browser auth routes, and web-only helper endpoints
|
||||
#[arg(long, conflicts_with = "web")]
|
||||
pub no_web: bool,
|
||||
|
||||
/// Override default LLM model
|
||||
#[arg(long)]
|
||||
pub model: Option<String>,
|
||||
|
|
@ -85,6 +93,9 @@ fn apply_serve_overrides(base: &Settings, args: &ServeArgs, dry_run_mode: bool)
|
|||
if dry_run_mode {
|
||||
settings.dry_run = Some(true);
|
||||
}
|
||||
if args.web || args.no_web {
|
||||
settings.web.get_or_insert_default().enabled = args.web;
|
||||
}
|
||||
if let Some(ref model) = args.model {
|
||||
settings.llm.get_or_insert_default().model = Some(model.clone());
|
||||
}
|
||||
|
|
@ -250,6 +261,13 @@ where
|
|||
.unwrap_or(5);
|
||||
(auth_mode, client_auth, max_concurrent_runs)
|
||||
};
|
||||
let web_enabled = shared_settings
|
||||
.read()
|
||||
.expect("config lock poisoned")
|
||||
.web
|
||||
.as_ref()
|
||||
.map(|web| web.enabled)
|
||||
.unwrap_or(true);
|
||||
|
||||
let store_path = storage.store_dir();
|
||||
let object_store = build_object_store(&store_path)?;
|
||||
|
|
@ -281,7 +299,11 @@ where
|
|||
);
|
||||
}
|
||||
spawn_scheduler(Arc::clone(&state));
|
||||
let router = build_router(Arc::clone(&state), auth_mode);
|
||||
let router = crate::server::build_router_with_options(
|
||||
Arc::clone(&state),
|
||||
auth_mode,
|
||||
crate::server::RouterOptions { web_enabled },
|
||||
);
|
||||
|
||||
let bind_request = match args.bind {
|
||||
Some(ref s) => bind::parse_bind(s)?,
|
||||
|
|
@ -631,6 +653,8 @@ mod tests {
|
|||
provider: None,
|
||||
dry_run: false,
|
||||
sandbox: None,
|
||||
web: false,
|
||||
no_web: false,
|
||||
max_concurrent_runs: None,
|
||||
config: None,
|
||||
};
|
||||
|
|
@ -644,6 +668,52 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_runtime_settings_enables_web_from_cli_flag() {
|
||||
let base: Settings = toml::from_str(
|
||||
r#"
|
||||
[web]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let args = ServeArgs {
|
||||
bind: None,
|
||||
model: None,
|
||||
provider: None,
|
||||
dry_run: false,
|
||||
sandbox: None,
|
||||
web: true,
|
||||
no_web: false,
|
||||
max_concurrent_runs: None,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let resolved = apply_runtime_settings(&base, &args, false, &PathBuf::from("/srv/fabro"));
|
||||
|
||||
assert!(resolved.web.expect("web settings should exist").enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_runtime_settings_disables_web_from_cli_flag() {
|
||||
let base = Settings::default();
|
||||
let args = ServeArgs {
|
||||
bind: None,
|
||||
model: None,
|
||||
provider: None,
|
||||
dry_run: false,
|
||||
sandbox: None,
|
||||
web: false,
|
||||
no_web: true,
|
||||
max_concurrent_runs: None,
|
||||
config: None,
|
||||
};
|
||||
|
||||
let resolved = apply_runtime_settings(&base, &args, false, &PathBuf::from("/srv/fabro"));
|
||||
|
||||
assert!(!resolved.web.expect("web settings should exist").enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_title_formats_boot_listening_and_stopping() {
|
||||
let bind = Bind::Tcp("127.0.0.1:3000".parse().unwrap());
|
||||
|
|
|
|||
|
|
@ -785,20 +785,46 @@ fn start_optional_slack_service(state: &Arc<AppState>) {
|
|||
|
||||
/// Build the axum Router with all run endpoints and embedded static assets.
|
||||
pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {
|
||||
build_router_with_options(state, auth_mode, RouterOptions::default())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RouterOptions {
|
||||
pub web_enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for RouterOptions {
|
||||
fn default() -> Self {
|
||||
Self { web_enabled: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the axum Router with configurable web surface routing.
|
||||
pub fn build_router_with_options(
|
||||
state: Arc<AppState>,
|
||||
auth_mode: AuthMode,
|
||||
options: RouterOptions,
|
||||
) -> Router {
|
||||
start_optional_slack_service(&state);
|
||||
let middleware_state = Arc::clone(&state);
|
||||
let api_common = Router::new()
|
||||
.route("/openapi.json", get(openapi_spec))
|
||||
.merge(web_auth::api_routes());
|
||||
let api_common = if options.web_enabled {
|
||||
Router::new()
|
||||
.route("/openapi.json", get(openapi_spec))
|
||||
.merge(web_auth::api_routes())
|
||||
} else {
|
||||
Router::new().route("/openapi.json", get(openapi_spec))
|
||||
};
|
||||
|
||||
let demo_router = Router::new()
|
||||
.nest("/api/v1", api_common.clone().merge(demo_routes()))
|
||||
.layer(axum::Extension(AuthMode::Disabled))
|
||||
.with_state(state.clone());
|
||||
|
||||
let real_router = Router::new()
|
||||
.nest("/api/v1", api_common.merge(real_routes()))
|
||||
.nest("/auth", web_auth::routes())
|
||||
let mut real_router = Router::new().nest("/api/v1", api_common.merge(real_routes()));
|
||||
if options.web_enabled {
|
||||
real_router = real_router.nest("/auth", web_auth::routes());
|
||||
}
|
||||
let real_router = real_router
|
||||
.layer(axum::Extension(auth_mode))
|
||||
.with_state(state);
|
||||
|
||||
|
|
@ -806,7 +832,7 @@ pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {
|
|||
let demo = demo_router.clone();
|
||||
let real = real_router.clone();
|
||||
async move {
|
||||
if req.headers().get("x-fabro-demo").is_some_and(|v| v == "1") {
|
||||
if options.web_enabled && req.headers().get("x-fabro-demo").is_some_and(|v| v == "1") {
|
||||
demo.oneshot(req).await
|
||||
} else {
|
||||
real.oneshot(req).await
|
||||
|
|
@ -837,19 +863,27 @@ pub fn build_router(state: Arc<AppState>, auth_mode: AuthMode) -> Router {
|
|||
},
|
||||
);
|
||||
|
||||
Router::new()
|
||||
.route("/health", get(health))
|
||||
.layer(middleware::from_fn_with_state(
|
||||
let mut router = Router::new().route("/health", get(health));
|
||||
if options.web_enabled {
|
||||
router = router.layer(middleware::from_fn_with_state(
|
||||
middleware_state,
|
||||
cookie_and_demo_middleware,
|
||||
))
|
||||
));
|
||||
}
|
||||
|
||||
router
|
||||
.fallback_service(service_fn(move |req: axum_extract::Request| {
|
||||
let dispatch = dispatch.clone();
|
||||
async move {
|
||||
let path = req.uri().path().to_string();
|
||||
if path.starts_with("/api/v1/") || path.starts_with("/auth/") || path == "/health" {
|
||||
let dispatch_path = path.starts_with("/api/v1/")
|
||||
|| path == "/health"
|
||||
|| (options.web_enabled && path.starts_with("/auth/"));
|
||||
if dispatch_path {
|
||||
dispatch.oneshot(req).await
|
||||
} else if matches!(req.method(), &Method::GET | &Method::HEAD) {
|
||||
} else if options.web_enabled
|
||||
&& matches!(req.method(), &Method::GET | &Method::HEAD)
|
||||
{
|
||||
Ok::<_, std::convert::Infallible>(static_files::serve(&path))
|
||||
} else {
|
||||
Ok::<_, std::convert::Infallible>(StatusCode::NOT_FOUND.into_response())
|
||||
|
|
@ -6156,6 +6190,7 @@ mod tests {
|
|||
async fn auth_login_github_redirects_to_github() {
|
||||
let mut settings = Settings::default();
|
||||
settings.web = Some(WebSettings {
|
||||
enabled: true,
|
||||
url: "http://localhost:3000".to_string(),
|
||||
auth: AuthSettings {
|
||||
provider: AuthProvider::Github,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Method, Request, StatusCode};
|
||||
use fabro_server::jwt_auth::AuthMode;
|
||||
use fabro_server::server::{build_router, create_app_state};
|
||||
use fabro_server::server::{
|
||||
RouterOptions, build_router, build_router_with_options, create_app_state,
|
||||
create_app_state_with_options,
|
||||
};
|
||||
use fabro_types::Settings;
|
||||
use std::path::PathBuf;
|
||||
use tower::ServiceExt;
|
||||
|
||||
|
|
@ -82,6 +86,122 @@ async fn source_maps_are_not_served() {
|
|||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_enabled_serves_web_only_routes() {
|
||||
let app = build_router(create_app_state(), AuthMode::Disabled);
|
||||
|
||||
let auth_me_request = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/auth/me")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let auth_me_response = app.clone().oneshot(auth_me_request).await.unwrap();
|
||||
assert_eq!(auth_me_response.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
let setup_status_request = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/setup/status")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let setup_status_response = app.clone().oneshot(setup_status_request).await.unwrap();
|
||||
assert_eq!(setup_status_response.status(), StatusCode::OK);
|
||||
|
||||
let demo_toggle_request = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/v1/demo/toggle")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(r#"{"enabled":true}"#))
|
||||
.unwrap();
|
||||
let demo_toggle_response = app.oneshot(demo_toggle_request).await.unwrap();
|
||||
assert_eq!(demo_toggle_response.status(), StatusCode::OK);
|
||||
assert!(
|
||||
demo_toggle_response.headers().contains_key("set-cookie"),
|
||||
"demo toggle should set a cookie"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_disabled_returns_404_for_web_routes_and_keeps_machine_api() {
|
||||
let settings: Settings = toml::from_str(
|
||||
r#"
|
||||
[web]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.expect("settings fixture should parse");
|
||||
let app = build_router_with_options(
|
||||
create_app_state_with_options(settings, 5),
|
||||
AuthMode::Disabled,
|
||||
RouterOptions { web_enabled: false },
|
||||
);
|
||||
|
||||
for (method, path, body) in [
|
||||
("GET", "/", Body::empty()),
|
||||
("GET", "/runs/abc", Body::empty()),
|
||||
("GET", "/auth/login/github", Body::empty()),
|
||||
("GET", "/api/v1/auth/me", Body::empty()),
|
||||
("GET", "/api/v1/setup/status", Body::empty()),
|
||||
(
|
||||
"POST",
|
||||
"/api/v1/demo/toggle",
|
||||
Body::from(r#"{"enabled":true}"#),
|
||||
),
|
||||
] {
|
||||
let request = Request::builder()
|
||||
.method(method)
|
||||
.uri(path)
|
||||
.header("content-type", "application/json")
|
||||
.body(body)
|
||||
.unwrap();
|
||||
|
||||
let response = app.clone().oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{method} {path}");
|
||||
}
|
||||
|
||||
let settings_request = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/api/v1/settings")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let settings_response = app.clone().oneshot(settings_request).await.unwrap();
|
||||
assert_eq!(settings_response.status(), StatusCode::OK);
|
||||
|
||||
let health_request = Request::builder()
|
||||
.method("GET")
|
||||
.uri("/health")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let health_response = app.oneshot(health_request).await.unwrap();
|
||||
assert_eq!(health_response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_disabled_ignores_demo_header_dispatch() {
|
||||
let settings: Settings = toml::from_str(
|
||||
r#"
|
||||
[web]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.expect("settings fixture should parse");
|
||||
let app = build_router_with_options(
|
||||
create_app_state_with_options(settings, 5),
|
||||
AuthMode::Disabled,
|
||||
RouterOptions { web_enabled: false },
|
||||
);
|
||||
let run_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
|
||||
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("/api/v1/runs/{run_id}"))
|
||||
.header("X-Fabro-Demo", "1")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
fn find_dist_source_map() -> String {
|
||||
let dist_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../apps/fabro-web/dist");
|
||||
let mut entries = std::fs::read_dir(&dist_dir)
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ fn fully_populated_server_config() -> Settings {
|
|||
storage_dir: Some("/data".into()),
|
||||
max_concurrent_runs: Some(10),
|
||||
web: Some(WebSettings {
|
||||
enabled: true,
|
||||
url: "https://example.com".into(),
|
||||
auth: AuthSettings {
|
||||
provider: AuthProvider::Github,
|
||||
|
|
|
|||
|
|
@ -97,12 +97,18 @@ pub struct GitSettings {
|
|||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
|
||||
pub struct WebSettings {
|
||||
#[serde(default = "default_web_enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_web_url")]
|
||||
pub url: String,
|
||||
#[serde(default)]
|
||||
pub auth: AuthSettings,
|
||||
}
|
||||
|
||||
fn default_web_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_web_url() -> String {
|
||||
"http://localhost:3000".to_string()
|
||||
}
|
||||
|
|
@ -110,6 +116,7 @@ fn default_web_url() -> String {
|
|||
impl Default for WebSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: default_web_enabled(),
|
||||
url: default_web_url(),
|
||||
auth: AuthSettings::default(),
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue