feat(config): add server config + can disable message-bus
- server now provide it's config via /api/config (possibility to feature flag) - client use /api/config to enable / disable some features - capability to disable the message bus, somme OPS may not want this feature and consume persistent connections from server (websocket): OXICLOUD_MESSAGEBUS_ENABLE (true by default)
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
//! `GET /api/config` — public server-configuration discovery.
|
||||
//!
|
||||
//! Advertises the subset of `AppState` a client needs to know at boot:
|
||||
//! feature flags (which optional systems are enabled), server version,
|
||||
//! and the current server-status snapshot (matches whatever the
|
||||
//! `X-Server-Status` header carries live). Everything auth-related
|
||||
//! stays under `GET /api/auth/oidc/providers` — the two endpoints are
|
||||
//! sibling capability advertisements, not one canonical thing.
|
||||
//!
|
||||
//! # Scope
|
||||
//!
|
||||
//! Only fields with **no privacy implications**:
|
||||
//!
|
||||
//! - `features.*` — boolean matrix of enabled subsystems (message bus,
|
||||
//! trash, search, sharing, quotas, plugins, WOPI). Same information
|
||||
//! any logged-in caller could infer from probing endpoints; giving
|
||||
//! it up front is a UX win.
|
||||
//! - `version` — same string the `/api/version` endpoint returns
|
||||
//! (CARGO_PKG_VERSION + git SHA). Public build metadata.
|
||||
//! - `server_status` — a snapshot of the mutable server-status state
|
||||
//! (maintenance mode, degraded mode, etc.). Same shape the
|
||||
//! `X-Server-Status` header stamps on every response; this endpoint
|
||||
//! just lets the FE hydrate the store at boot without waiting for
|
||||
//! the first authenticated response.
|
||||
//!
|
||||
//! Anything requiring auth (per-user preferences, admin-visible
|
||||
//! deployment secrets, session state) does NOT go here — those live
|
||||
//! on `/api/auth/me` or `/api/admin/*`.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{Json, extract::State};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::server_status::{HeaderPayload, build_header_payload};
|
||||
|
||||
/// Server-configuration DTO. Additive over time — clients ignore
|
||||
/// unknown fields, and no field is ever repurposed (same discipline
|
||||
/// as JSON-RPC error codes on the message bus).
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct ServerConfigDto {
|
||||
/// Server version — `CARGO_PKG_VERSION` from `Cargo.toml`. Matches
|
||||
/// what `GET /api/version` returns.
|
||||
pub version: &'static str,
|
||||
|
||||
/// Feature flags — which subsystems the server has enabled.
|
||||
/// Clients gate optional UI on these (e.g. hide the notification
|
||||
/// bell if `features.message_bus` is false, since the bell would
|
||||
/// have no delivery channel).
|
||||
pub features: FeaturesDto,
|
||||
|
||||
/// Live server-status snapshot — exact same shape and field
|
||||
/// names as the `X-Server-Status` response header. Clients use
|
||||
/// this to hydrate their reactive store at boot; subsequent live
|
||||
/// changes propagate through the header on every other request
|
||||
/// (the middleware and this endpoint share `build_header_payload`
|
||||
/// so drift is impossible). Non-optional so the client always
|
||||
/// has a definite value; `readonly: false` with no `migration`
|
||||
/// or `rotation` is the "everything nominal" case.
|
||||
pub server_status: HeaderPayload,
|
||||
}
|
||||
|
||||
/// Feature-flag block within [`ServerConfigDto`]. One boolean per
|
||||
/// optional subsystem. Adding a new feature: append a field with a
|
||||
/// default that matches the server-side default; NEVER remove a field
|
||||
/// (client code may depend on the absence of a `false` value to mean
|
||||
/// "unknown").
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct FeaturesDto {
|
||||
/// Message bus over WebSocket. When `false`, `/api/rt/ws` and
|
||||
/// `/api/rt/ticket` are not registered — clients skip WS setup
|
||||
/// entirely. See `FeaturesConfig::enable_message_bus`.
|
||||
pub message_bus: bool,
|
||||
/// Recycle bin / soft-delete flow. When `false`, deletes are
|
||||
/// permanent — no `/api/trash` endpoint. See
|
||||
/// `FeaturesConfig::enable_trash`.
|
||||
pub trash: bool,
|
||||
/// Full-text and metadata search (`/api/search/*`). See
|
||||
/// `FeaturesConfig::enable_search`.
|
||||
pub search: bool,
|
||||
/// File sharing (public share links + user-to-user grants). See
|
||||
/// `FeaturesConfig::enable_file_sharing`.
|
||||
pub sharing: bool,
|
||||
/// Per-user storage-quota enforcement on the upload path. See
|
||||
/// `FeaturesConfig::enable_user_storage_quotas`.
|
||||
pub quotas: bool,
|
||||
/// Music player + playlists. See `FeaturesConfig::enable_music`.
|
||||
pub music: bool,
|
||||
/// Photo-map ("Places") tab. See `FeaturesConfig::enable_places`.
|
||||
pub places: bool,
|
||||
/// Face detection + identity clustering ("People"). Biometric —
|
||||
/// OFF by default. See `FeaturesConfig::enable_faces`.
|
||||
pub faces: bool,
|
||||
/// Server-side video-thumbnail generation via ffmpeg. See
|
||||
/// `FeaturesConfig::enable_video_thumbnails`.
|
||||
pub video_thumbnails: bool,
|
||||
/// Admin-configured external filesystem mounts. See
|
||||
/// `FeaturesConfig::enable_external_mounts`.
|
||||
pub external_mounts: bool,
|
||||
}
|
||||
|
||||
/// `GET /api/config` — return the public server-configuration
|
||||
/// snapshot. Unauthenticated. No cache header — values change on
|
||||
/// server-restart / feature-toggle / status flip, and the endpoint
|
||||
/// is called at most once per SPA boot per client. Adding a short
|
||||
/// `Cache-Control` TTL later is safe if load ever becomes a concern.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/config",
|
||||
tag = "config",
|
||||
responses(
|
||||
(status = 200, description = "Public server configuration", body = ServerConfigDto),
|
||||
),
|
||||
)]
|
||||
pub async fn get_config(State(state): State<Arc<AppState>>) -> Json<ServerConfigDto> {
|
||||
let f = &state.core.config.features;
|
||||
Json(ServerConfigDto {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
features: FeaturesDto {
|
||||
message_bus: f.enable_message_bus,
|
||||
trash: f.enable_trash,
|
||||
search: f.enable_search,
|
||||
sharing: f.enable_file_sharing,
|
||||
quotas: f.enable_user_storage_quotas,
|
||||
music: f.enable_music,
|
||||
places: f.enable_places,
|
||||
faces: f.enable_faces,
|
||||
video_thumbnails: f.enable_video_thumbnails,
|
||||
external_mounts: f.enable_external_mounts,
|
||||
},
|
||||
server_status: build_header_payload(&state),
|
||||
})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ pub mod caldav_handler;
|
||||
pub mod caller_flags;
|
||||
pub mod carddav_handler;
|
||||
pub mod chunked_upload_handler;
|
||||
pub mod config_handler;
|
||||
pub mod contacts_handler;
|
||||
pub mod dedup_handler;
|
||||
pub mod delta_upload_handler;
|
||||
|
||||
@@ -90,7 +90,7 @@ const OUTBOUND_CHANNEL_CAPACITY: usize = 512;
|
||||
/// default and Cloudflare's 100 s hard limit; behind Traefik we
|
||||
/// document a much longer `idleTimeout` anyway.
|
||||
///
|
||||
/// Overridable at server start via `OXICLOUD_RT_WS_KEEPALIVE_SECONDS`
|
||||
/// Overridable at server start via `OXICLOUD_MESSAGEBUS_KEEPALIVE_SECONDS`
|
||||
/// — test suites drop it to a low value to exercise the keepalive path
|
||||
/// within a bounded wall-clock.
|
||||
const DEFAULT_KEEPALIVE_SECONDS: u64 = 30;
|
||||
@@ -101,7 +101,7 @@ const DEFAULT_KEEPALIVE_SECONDS: u64 = 30;
|
||||
/// useful for smoke tests that toggle the value on the fly.
|
||||
fn keepalive_interval() -> Duration {
|
||||
Duration::from_secs(
|
||||
std::env::var("OXICLOUD_RT_WS_KEEPALIVE_SECONDS")
|
||||
std::env::var("OXICLOUD_MESSAGEBUS_KEEPALIVE_SECONDS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.filter(|&n: &u64| n > 0)
|
||||
|
||||
@@ -55,12 +55,14 @@ use crate::interfaces::api::handlers::auth_handler::SystemStatus;
|
||||
use crate::interfaces::api::handlers::chunked_upload_handler::{
|
||||
CompleteUploadResponse, CreateUploadRequest,
|
||||
};
|
||||
use crate::interfaces::api::handlers::config_handler::{FeaturesDto, ServerConfigDto};
|
||||
use crate::interfaces::api::handlers::contacts_handler::{
|
||||
AddMemberRequest, AddressBookResponse, CreateAddressBookRequest, CreateContactRequest,
|
||||
GroupNameRequest, UpdateAddressBookRequest, UpdateContactRequest,
|
||||
};
|
||||
use crate::interfaces::api::handlers::dedup_handler::{HashCheckResponse, StatsResponse};
|
||||
use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
||||
use crate::interfaces::middleware::server_status::{HeaderPayload, ProgressHeader};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
@@ -332,6 +334,8 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
||||
handlers::subject_group_handler::remove_user_member,
|
||||
handlers::subject_group_handler::remove_group_member,
|
||||
handlers::subject_group_handler::list_effective_members,
|
||||
// Public server-config discovery.
|
||||
handlers::config_handler::get_config,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
@@ -375,6 +379,11 @@ use crate::interfaces::api::handlers::file_handler::MoveFilePayload;
|
||||
ChangePasswordDto,
|
||||
RefreshTokenDto,
|
||||
SystemStatus,
|
||||
// Public server-config discovery — `GET /api/config`.
|
||||
ServerConfigDto,
|
||||
FeaturesDto,
|
||||
HeaderPayload,
|
||||
ProgressHeader,
|
||||
OidcProviderInfoDto,
|
||||
OidcExchangeDto,
|
||||
// Admin sessions panel — wire shape for `/api/admin/sessions`.
|
||||
|
||||
@@ -167,6 +167,16 @@ pub fn create_public_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppStat
|
||||
router = router.route("/version", get(get_version));
|
||||
router = router.route("/openapi.json", get(get_openapi_spec));
|
||||
|
||||
// Server-configuration discovery endpoint — public, unauthenticated.
|
||||
// Returns feature flags, version, and a snapshot of the server-status
|
||||
// header for one-shot boot hydration by the SPA. See
|
||||
// `handlers/config_handler.rs` for the DTO shape and rationale.
|
||||
router = router.route(
|
||||
"/config",
|
||||
get(crate::interfaces::api::handlers::config_handler::get_config)
|
||||
.with_state(app_state.clone()),
|
||||
);
|
||||
|
||||
router
|
||||
}
|
||||
|
||||
@@ -678,11 +688,17 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
// the protected router (auth + DPoP), so the caller proves session
|
||||
// + DPoP-key possession before a ticket is minted. See
|
||||
// `handlers/rt_ticket_handler.rs` and `docs/plan/message-bus.md § F`.
|
||||
router = router.route(
|
||||
"/rt/ticket",
|
||||
post(crate::interfaces::api::handlers::rt_ticket_handler::issue_rt_ticket)
|
||||
.with_state(app_state.clone()),
|
||||
);
|
||||
//
|
||||
// Gated by `enable_message_bus`: when disabled, the route is NOT
|
||||
// registered — Axum returns 404 (no 5xx alerts, no ambiguous 403).
|
||||
// The paired WS route in `main.rs` uses the same guard.
|
||||
if app_state.core.config.features.enable_message_bus {
|
||||
router = router.route(
|
||||
"/rt/ticket",
|
||||
post(crate::interfaces::api::handlers::rt_ticket_handler::issue_rt_ticket)
|
||||
.with_state(app_state.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
// The WS upgrade (`GET /api/rt/ws`) is registered OUTSIDE the
|
||||
// protected-api middleware stack — a browser cannot attach a
|
||||
|
||||
Reference in New Issue
Block a user