feat(admin): show active session/users on dashboard
This commit is contained in:
@@ -163,10 +163,39 @@ pub struct DashboardStatsDto {
|
||||
pub auth_enabled: bool,
|
||||
pub oidc_configured: bool,
|
||||
pub quotas_enabled: bool,
|
||||
// User stats
|
||||
// ── 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
|
||||
// `total`. `external` is disjoint from `admin` by DB constraint
|
||||
// (`users_external_not_admin`). The dashboard renders these as
|
||||
// one grouped section separate from the live-activity section
|
||||
// below, so admins don't confuse "as-of-now row count" with
|
||||
// "who's here right now".
|
||||
pub total_users: i64,
|
||||
pub active_users: i64,
|
||||
pub admin_users: i64,
|
||||
/// Grant-only accounts (magic-link / OIDC-only / OCM recipients).
|
||||
/// Filtered out of `total_users` / `active_users` since those
|
||||
/// columns count operational seats (see the SELECT comment). Here
|
||||
/// as its own metric because operators of external-heavy
|
||||
/// deployments (public shares, invited-collab shops) need to see
|
||||
/// the invited population at a glance.
|
||||
pub external_users: i64,
|
||||
// ── Live activity (projection over auth.sessions) ──
|
||||
// Both fields change minute-to-minute, unlike the user counts
|
||||
// above which only move on register/deactivate/role-toggle.
|
||||
// Same 5-min window as the Prometheus gauges
|
||||
// (`oxicloud_sessions_online[_users]` in
|
||||
// `session_liveness_gauges.rs`), computed via the shared
|
||||
// `ONLINE_WINDOW` constant so per-user badges + aggregate
|
||||
// counts + this dashboard number stay consistent by construction.
|
||||
/// Distinct users behind non-revoked sessions active in the last
|
||||
/// 5 min. Answers "how many humans are here right now?".
|
||||
pub online_users: i64,
|
||||
/// Non-revoked sessions active in the last 5 min. Answers "how
|
||||
/// many concurrent connections must I serve?". Ratio
|
||||
/// `online_sessions / online_users` is the multi-device factor.
|
||||
pub online_sessions: i64,
|
||||
// ── Per-drive-kind quota accounting ──
|
||||
// One row per drive kind (personal, shared). Pre-dedup, logical
|
||||
// file sizes summed from `drives.used_bytes` (personal rolls up
|
||||
|
||||
@@ -876,7 +876,10 @@ impl AuthApplicationService {
|
||||
is_external = false,
|
||||
"🛂 user registered",
|
||||
);
|
||||
Ok(RegisterResult::Created(Box::new(PublicUserDto::new(created_user, false))))
|
||||
Ok(RegisterResult::Created(Box::new(PublicUserDto::new(
|
||||
created_user,
|
||||
false,
|
||||
))))
|
||||
}
|
||||
|
||||
/// Create the first admin user during initial system setup.
|
||||
@@ -3227,7 +3230,10 @@ impl AuthApplicationService {
|
||||
offset: i64,
|
||||
) -> Result<Vec<PublicUserDto>, DomainError> {
|
||||
let users = self.user_storage.list_users(limit, offset, false).await?;
|
||||
Ok(users.into_iter().map(|u| PublicUserDto::new(u, false)).collect())
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.map(|u| PublicUserDto::new(u, false))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Admin-only: lists users including external (grant-only) recipients.
|
||||
@@ -3241,7 +3247,10 @@ impl AuthApplicationService {
|
||||
) -> Result<Vec<PublicUserDto>, DomainError> {
|
||||
self.require_admin_caller(authorization, caller_id).await?;
|
||||
let users = self.user_storage.list_users(limit, offset, true).await?;
|
||||
Ok(users.into_iter().map(|u| PublicUserDto::new(u, false)).collect())
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.map(|u| PublicUserDto::new(u, false))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Admin-only user listing. Returns `Vec<FullUserDto>` — same
|
||||
@@ -3296,7 +3305,10 @@ impl AuthApplicationService {
|
||||
limit: i64,
|
||||
) -> Result<Vec<PublicUserDto>, DomainError> {
|
||||
let users = self.user_storage.search_users(query, limit, false).await?;
|
||||
Ok(users.into_iter().map(|u| PublicUserDto::new(u, false)).collect())
|
||||
Ok(users
|
||||
.into_iter()
|
||||
.map(|u| PublicUserDto::new(u, false))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Username-only search for the NC sharee autocomplete: identical
|
||||
|
||||
@@ -940,6 +940,51 @@ pub async fn get_dashboard_stats(
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Database query failed: {}", e)))?;
|
||||
|
||||
// External account count — distinct query (not FILTERed into
|
||||
// `stats_row` above) because `stats_row` scopes to
|
||||
// `is_external = false` for the operational-seat counts.
|
||||
// Externals form their own population; the dashboard renders them
|
||||
// as a separate stat card in the "User accounts" section.
|
||||
let external_users: i64 =
|
||||
sqlx::query_scalar(r#"SELECT COUNT(*)::INT8 FROM auth.users WHERE is_external = true"#)
|
||||
.fetch_one(db_pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("External user count failed: {}", e)))?;
|
||||
|
||||
// Live-activity counts — projection over auth.sessions, same
|
||||
// `ONLINE_WINDOW` (5 min) the Prometheus gauges use so the
|
||||
// dashboard number, admin-table green dot, and
|
||||
// `oxicloud_sessions_online` scrape all agree by construction.
|
||||
// Bound as `$1 = window_secs` via `make_interval(secs => $1)`
|
||||
// to keep the single-source-of-truth pattern (no SQL literal
|
||||
// for the window). Both queries hit the partial index
|
||||
// `idx_sessions_last_seen_at WHERE revoked = FALSE` so per-run
|
||||
// cost is ~μs even at tens of thousands of session rows.
|
||||
let online_window_secs: f64 =
|
||||
crate::application::dtos::session_dto::ONLINE_WINDOW.as_secs_f64();
|
||||
let online_sessions: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COUNT(*)::INT8 FROM auth.sessions
|
||||
WHERE revoked = FALSE
|
||||
AND last_seen_at > NOW() - make_interval(secs => $1)
|
||||
"#,
|
||||
)
|
||||
.bind(online_window_secs)
|
||||
.fetch_one(db_pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Online session count failed: {}", e)))?;
|
||||
let online_users: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COUNT(DISTINCT user_id)::INT8 FROM auth.sessions
|
||||
WHERE revoked = FALSE
|
||||
AND last_seen_at > NOW() - make_interval(secs => $1)
|
||||
"#,
|
||||
)
|
||||
.bind(online_window_secs)
|
||||
.fetch_one(db_pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| AppError::internal_error(format!("Online user count failed: {}", e)))?;
|
||||
|
||||
use sqlx::Row;
|
||||
|
||||
// Per-drive-kind quota panel:
|
||||
@@ -1024,6 +1069,9 @@ pub async fn get_dashboard_stats(
|
||||
total_users: stats_row.get("total_users"),
|
||||
active_users: stats_row.get("active_users"),
|
||||
admin_users: stats_row.get("admin_users"),
|
||||
external_users,
|
||||
online_users,
|
||||
online_sessions,
|
||||
drive_usage,
|
||||
users_over_80_percent: stats_row.get("users_over_80"),
|
||||
users_over_quota: stats_row.get("users_over_quota"),
|
||||
|
||||
Reference in New Issue
Block a user