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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user