feat(config + admin panel): handle features activated

- review admin dashboard to reflect features enabled/disabled
- hide mount option if feature is disabled
- remove QUOTA option as it is not wired
This commit is contained in:
Edouard Vanbelle
2026-09-11 13:02:33 +02:00
parent 5083eaeaba
commit 8d1fde2747
16 changed files with 303 additions and 134 deletions
+8 -2
View File
@@ -161,9 +161,15 @@ pub struct DriveKindUsageDto {
pub struct DashboardStatsDto {
// System info
pub server_version: String,
pub auth_enabled: bool,
pub oidc_configured: bool,
pub quotas_enabled: bool,
/// Currently-connected message-bus WebSocket sessions. One per
/// browser tab that reached a folder view and hasn't closed the
/// tab yet. Zero when `OXICLOUD_MESSAGEBUS_ENABLE=false`.
/// Snapshot value — a subsequent request can see a different
/// number if a connection opened/closed in between. Renders on
/// the admin dashboard's "Live activity" section next to
/// `online_sessions` (HTTP-driven distinct-user count).
pub active_ws_sessions: u64,
// ── User accounts (static breakdown of auth.users) ──
// All four are counts of the SAME table under different
// predicates. `active`, `admin`, `external` are all subsets of
+1 -10
View File
@@ -2243,7 +2243,6 @@ impl MagicLinkConfig {
#[derive(Debug, Clone)]
pub struct FeaturesConfig {
pub enable_auth: bool,
pub enable_user_storage_quotas: bool,
pub enable_file_sharing: bool,
pub enable_trash: bool,
pub enable_search: bool,
@@ -2483,8 +2482,7 @@ impl Default for GrantCleanupConfig {
impl Default for FeaturesConfig {
fn default() -> Self {
Self {
enable_auth: true, // Enable authentication by default
enable_user_storage_quotas: false,
enable_auth: true, // Enable authentication by default
enable_file_sharing: true, // Enable file sharing by default
enable_trash: true, // Enable trash feature
enable_search: true, // Enable search feature
@@ -3353,13 +3351,6 @@ impl AppConfig {
config.features.enable_auth = val;
}
if let Ok(enable_user_storage_quotas) =
env::var("OXICLOUD_ENABLE_USER_STORAGE_QUOTAS").map(|v| v.parse::<bool>())
&& let Ok(val) = enable_user_storage_quotas
{
config.features.enable_user_storage_quotas = val;
}
if let Ok(enable_file_sharing) =
env::var("OXICLOUD_ENABLE_FILE_SHARING").map(|v| v.parse::<bool>())
&& let Ok(val) = enable_file_sharing
+10
View File
@@ -2333,6 +2333,7 @@ impl AppServiceFactory {
mount_router,
bus,
rt_ticket_store,
active_ws_sessions: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
auth_service: auth_services,
opaque_service,
opaque_repo,
@@ -3250,6 +3251,15 @@ pub struct AppState {
pub rt_ticket_store: Arc<
crate::infrastructure::services::rt_ticket_store::RtTicketStore,
>,
/// Live count of currently-connected message-bus WS sessions.
/// Incremented on entry to `rt_ws::handle_session`, decremented
/// via a `Drop` guard on ANY exit (normal close, error, panic
/// unwind). Surfaced on the admin dashboard's "Live activity"
/// section so operators can gauge WS pressure at a glance — one
/// connection per open browser tab that reaches a folder view.
/// Zero-cost when idle: `Relaxed` atomic load/store on the fd
/// path, no allocation.
pub active_ws_sessions: Arc<std::sync::atomic::AtomicUsize>,
pub auth_service: Option<AuthServices>,
/// OPAQUE aPAKE substrate (RFC 9807). Populated only when
/// [`OpaqueConfig::effective_mode`] is not `Off` — that method
+40 -14
View File
@@ -53,18 +53,38 @@ struct AdminUsersPageResponse {
}
/// Admin API routes — all require admin role.
pub fn admin_routes() -> Router<Arc<AppState>> {
///
/// Takes an `AppState` reference so feature-flag gating at route-
/// registration time is possible (external-mounts admin surface
/// mirrors the `OXICLOUD_ENABLE_EXTERNAL_MOUNTS` flag; when the flag
/// is off the runtime `MountRegistry` isn't loaded, so exposing the
/// CRUD would let admins configure mounts that silently don't work).
pub fn admin_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
use super::admin_external_mounts as ext_mounts;
Router::new()
// External file mounts
.route(
"/external-mounts",
get(ext_mounts::list_external_mounts).post(ext_mounts::create_external_mount),
)
.route(
"/external-mounts/{id}",
delete(ext_mounts::delete_external_mount),
)
let mut router = Router::new();
// External file mounts — CRUD registered only when the feature
// is enabled server-side. Matches the pattern used for the
// message bus (`/api/rt/ws` unmounted when
// `OXICLOUD_MESSAGEBUS_ENABLE=false`): a disabled feature stays
// fully hidden from the admin panel too. Without this guard the
// admin panel would load, editor would save DB rows, but the
// runtime `MountRegistry` (gated by the same flag in
// `common/di.rs`) wouldn't load them — a silently-broken UX.
// FE mirrors via `serverConfig.features.external_mounts`.
if app_state.core.config.features.enable_external_mounts {
router = router
.route(
"/external-mounts",
get(ext_mounts::list_external_mounts).post(ext_mounts::create_external_mount),
)
.route(
"/external-mounts/{id}",
delete(ext_mounts::delete_external_mount),
);
}
router = router
// OIDC settings
.route("/settings/oidc", get(get_oidc_settings))
.route("/settings/oidc", put(save_oidc_settings))
@@ -207,7 +227,9 @@ pub fn admin_routes() -> Router<Arc<AppState>> {
.route(
"/drives/{id}/members/{kind}/{sid}",
axum::routing::patch(update_drive_member_admin).delete(remove_drive_member_admin),
)
);
router
}
// Every route under `/api/admin/*` is gated by the
@@ -1054,9 +1076,13 @@ pub async fn get_dashboard_stats(
let stats = DashboardStatsDto {
server_version: env!("CARGO_PKG_VERSION").to_string(),
auth_enabled: true,
oidc_configured: auth_app.oidc_enabled(),
quotas_enabled: true, // Feature flag could be checked here
// Snapshot the current live-WS-session count. `Relaxed` because
// the counter itself uses `Relaxed`; slight staleness on the
// dashboard is fine — it's a UI gauge, not a control input.
active_ws_sessions: state
.active_ws_sessions
.load(std::sync::atomic::Ordering::Relaxed) as u64,
total_users: stats_row.get("total_users"),
active_users: stats_row.get("active_users"),
admin_users: stats_row.get("admin_users"),
@@ -82,9 +82,10 @@ pub struct FeaturesDto {
/// 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,
// NOTE: no `quotas` field. The former `enable_user_storage_quotas`
// flag was removed (dead config with zero consumers). Actual
// per-user quotas are set via the admin panel and resolved by
// `StorageUsageService` unconditionally.
/// Music player + playlists. See `FeaturesConfig::enable_music`.
pub music: bool,
/// Photo-map ("Places") tab. See `FeaturesConfig::enable_places`.
@@ -122,7 +123,6 @@ pub async fn get_config(State(state): State<Arc<AppState>>) -> Json<ServerConfig
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,
+27
View File
@@ -310,7 +310,34 @@ enum SessionOut {
EvictFolders(Vec<Uuid>),
}
/// RAII guard that decrements the live-session counter on ANY exit
/// path from `handle_session` — clean close, protocol error, panic
/// unwind, tokio task cancellation. Keeping the decrement in `Drop`
/// (not scattered inline before every `break;` / `return;`) means we
/// physically cannot leak a live count when a new exit branch is
/// added. `Arc` so it stays valid even if the task is aborted from
/// outside.
struct SessionCountGuard(Arc<std::sync::atomic::AtomicUsize>);
impl Drop for SessionCountGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
}
}
async fn handle_session(mut socket: WebSocket, caller_id: Uuid, state: Arc<AppState>) {
// Live-session counter — incremented here, decremented on ANY
// exit path via the `Drop` guard below (clean close, error,
// panic unwind, task abort). Feeds the admin dashboard's
// "Live activity" section. `Relaxed` because the counter is
// approximate-by-design — a slightly stale read on the
// dashboard is fine, and the atomic hop stays sub-nanosecond
// on the hot path (session open / close).
state
.active_ws_sessions
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let _session_count_guard = SessionCountGuard(Arc::clone(&state.active_ws_sessions));
// Outbound queue — every path that produces a client-bound frame
// enqueues here; the writer half of the select drains. Also
// carries internal `EvictFolders` control signals from the
+1 -1
View File
@@ -662,7 +662,7 @@ pub fn create_api_routes(app_state: &Arc<AppState>) -> Router<Arc<AppState>> {
// gate automatically — implementors no longer have to remember
// to call `require_admin(&state, &headers).await?` inline, and a
// forgotten call can't silently expose a non-admin surface.
let admin_router = admin_handler::admin_routes()
let admin_router = admin_handler::admin_routes(app_state)
.layer(axum::middleware::from_fn(
crate::interfaces::middleware::auth::require_admin,
))