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
+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,
))