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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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