Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/ruscker-admin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ hyper-rustls = "0.27"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
# Cron parsing for scheduled jobs (#986) — verified on crates.io (3.0.1).
croner = "3"
# Percent-encode filter values onto pager query strings (#1021). Already
# in the tree transitively; named directly so the usage is declared.
urlencoding = "2"
futures-util = { workspace = true }
async-stream = { workspace = true }
lol_html = { workspace = true }
Expand Down
30 changes: 30 additions & 0 deletions crates/ruscker-admin/assets/i18n/en/landing.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1042,3 +1042,33 @@ admin-users-mfa-reset-confirm = Reset this user's 2FA? The key and ALL recovery
admin-users-mfa-reset = Reset 2FA
admin-users-mfa-not-configured = 2FA not configured
admin-users-mfa-reset-ok = 2FA and its recovery codes were reset.

# User activity (#1021)
admin-activity-title = User Activity
admin-activity-subtitle = Logins and app accesses, newest first.
admin-activity-tab-users = User activity
admin-activity-tab-admin = Administrative activity
admin-activity-event = Event
admin-activity-event-all = All events
admin-activity-event-login = Login
admin-activity-event-access = App access
admin-activity-user = User
admin-activity-user-all = All users
admin-activity-app = App
admin-activity-app-all = All apps
admin-activity-period = Period
admin-activity-period-all = Any time
admin-activity-period-24h = Last 24 hours
admin-activity-period-7d = Last 7 days
admin-activity-period-30d = Last 30 days
admin-activity-col-when = When
admin-activity-col-user = User
admin-activity-col-event = Event
admin-activity-col-app = App
admin-activity-col-ip = IP
admin-activity-empty = No activity yet — or the filter matches nothing.
admin-activity-anonymous = Anonymous
admin-activity-pager-status = Page { $page } of { $pages } · { $total } { $total ->
[one] record
*[other] records
}
30 changes: 30 additions & 0 deletions crates/ruscker-admin/assets/i18n/es/landing.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1042,3 +1042,33 @@ admin-users-mfa-reset-confirm = ¿Restablecer el 2FA de este usuario? La clave y
admin-users-mfa-reset = Restablecer 2FA
admin-users-mfa-not-configured = 2FA no configurado
admin-users-mfa-reset-ok = Se restablecieron el 2FA y sus códigos de recuperación.

# User activity (#1021)
admin-activity-title = Actividad de Usuarios
admin-activity-subtitle = Inicios de sesión y accesos a aplicaciones, del más reciente al más antiguo.
admin-activity-tab-users = Actividad de usuarios
admin-activity-tab-admin = Actividad administrativa
admin-activity-event = Evento
admin-activity-event-all = Todos los eventos
admin-activity-event-login = Inicio de sesión
admin-activity-event-access = Acceso a app
admin-activity-user = Usuario
admin-activity-user-all = Todos los usuarios
admin-activity-app = Aplicación
admin-activity-app-all = Todas las aplicaciones
admin-activity-period = Período
admin-activity-period-all = Cualquier período
admin-activity-period-24h = Últimas 24 horas
admin-activity-period-7d = Últimos 7 días
admin-activity-period-30d = Últimos 30 días
admin-activity-col-when = Cuándo
admin-activity-col-user = Usuario
admin-activity-col-event = Evento
admin-activity-col-app = Aplicación
admin-activity-col-ip = IP
admin-activity-empty = Sin actividad todavía — o el filtro no coincide con nada.
admin-activity-anonymous = Anónimo
admin-activity-pager-status = Página { $page } de { $pages } · { $total } { $total ->
[one] registro
*[other] registros
}
30 changes: 30 additions & 0 deletions crates/ruscker-admin/assets/i18n/fr/landing.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1042,3 +1042,33 @@ admin-users-mfa-reset-confirm = Réinitialiser la 2FA de cet utilisateur ? La cl
admin-users-mfa-reset = Réinitialiser la 2FA
admin-users-mfa-not-configured = 2FA non configurée
admin-users-mfa-reset-ok = La 2FA et ses codes de récupération ont été réinitialisés.

# User activity (#1021)
admin-activity-title = Activité des Utilisateurs
admin-activity-subtitle = Connexions et accès aux applications, du plus récent au plus ancien.
admin-activity-tab-users = Activité des utilisateurs
admin-activity-tab-admin = Activité administrative
admin-activity-event = Événement
admin-activity-event-all = Tous les événements
admin-activity-event-login = Connexion
admin-activity-event-access = Accès à une app
admin-activity-user = Utilisateur
admin-activity-user-all = Tous les utilisateurs
admin-activity-app = Application
admin-activity-app-all = Toutes les applications
admin-activity-period = Période
admin-activity-period-all = Toute période
admin-activity-period-24h = Dernières 24 heures
admin-activity-period-7d = 7 derniers jours
admin-activity-period-30d = 30 derniers jours
admin-activity-col-when = Quand
admin-activity-col-user = Utilisateur
admin-activity-col-event = Événement
admin-activity-col-app = Application
admin-activity-col-ip = IP
admin-activity-empty = Aucune activité pour l'instant — ou le filtre ne correspond à rien.
admin-activity-anonymous = Anonyme
admin-activity-pager-status = Page { $page } sur { $pages } · { $total } { $total ->
[one] enregistrement
*[other] enregistrements
}
30 changes: 30 additions & 0 deletions crates/ruscker-admin/assets/i18n/pt/landing.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1046,3 +1046,33 @@ admin-users-mfa-reset-confirm = Redefinir o 2FA deste usuário? A chave e TODOS
admin-users-mfa-reset = Redefinir 2FA
admin-users-mfa-not-configured = 2FA não configurado
admin-users-mfa-reset-ok = O 2FA e os códigos de recuperação foram redefinidos.

# User activity (#1021)
admin-activity-title = Atividades dos Usuários
admin-activity-subtitle = Logins e acessos a aplicações, do mais recente para o mais antigo.
admin-activity-tab-users = Atividades dos usuários
admin-activity-tab-admin = Atividades administrativas
admin-activity-event = Evento
admin-activity-event-all = Todos os eventos
admin-activity-event-login = Login
admin-activity-event-access = Acesso a app
admin-activity-user = Usuário
admin-activity-user-all = Todos os usuários
admin-activity-app = Aplicação
admin-activity-app-all = Todas as aplicações
admin-activity-period = Período
admin-activity-period-all = Qualquer período
admin-activity-period-24h = Últimas 24 horas
admin-activity-period-7d = Últimos 7 dias
admin-activity-period-30d = Últimos 30 dias
admin-activity-col-when = Quando
admin-activity-col-user = Usuário
admin-activity-col-event = Evento
admin-activity-col-app = Aplicação
admin-activity-col-ip = IP
admin-activity-empty = Nenhuma atividade ainda — ou o filtro não bate em nada.
admin-activity-anonymous = Anônimo
admin-activity-pager-status = Página { $page } de { $pages } · { $total } { $total ->
[one] registro
*[other] registros
}
57 changes: 57 additions & 0 deletions crates/ruscker-admin/src/db/user_activity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,32 @@ pub async fn count_filtered(db: &ConfigDb, filter: &ActivityFilter) -> Result<i6
Ok(count)
}

/// Distinct non-null usernames ever recorded — populates the user filter
/// dropdown. Ordered so the select is stable.
pub async fn distinct_usernames(db: &ConfigDb) -> Result<Vec<String>> {
let sql =
"SELECT DISTINCT username FROM user_activity WHERE username IS NOT NULL ORDER BY username ASC";
let rows: Vec<(String,)> = match db {
ConfigDb::Sqlite(pool) => sqlx::query_as(sql).fetch_all(pool).await,
ConfigDb::Postgres(pool) => sqlx::query_as(sql).fetch_all(pool).await,
}
.context("distinct activity usernames")?;
Ok(rows.into_iter().map(|(u,)| u).collect())
}

/// Distinct non-null spec ids ever recorded — populates the app filter
/// dropdown.
pub async fn distinct_specs(db: &ConfigDb) -> Result<Vec<String>> {
let sql =
"SELECT DISTINCT spec_id FROM user_activity WHERE spec_id IS NOT NULL ORDER BY spec_id ASC";
let rows: Vec<(String,)> = match db {
ConfigDb::Sqlite(pool) => sqlx::query_as(sql).fetch_all(pool).await,
ConfigDb::Postgres(pool) => sqlx::query_as(sql).fetch_all(pool).await,
}
.context("distinct activity specs")?;
Ok(rows.into_iter().map(|(s,)| s).collect())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -340,6 +366,30 @@ mod tests {
assert!(page1.last().unwrap().id > page3.first().unwrap().id);
}

#[tokio::test]
async fn distinct_dropdowns_exclude_nulls_and_sort() {
let db = ConfigDb::Sqlite(open_memory().await.unwrap());
insert_batch(
&db,
&[
access(Some("bob"), "ops", "a1"),
access(Some("alice"), "sales", "a2"),
access(None, "public", "a3"), // anonymous → no username
login("alice", "l1"), // login → no spec
],
)
.await
.unwrap();
assert_eq!(
distinct_usernames(&db).await.unwrap(),
vec!["alice".to_string(), "bob".to_string()]
);
assert_eq!(
distinct_specs(&db).await.unwrap(),
vec!["ops".to_string(), "public".to_string(), "sales".to_string()]
);
}

#[tokio::test]
async fn since_until_filter_bounds_the_time_window() {
let db = ConfigDb::Sqlite(open_memory().await.unwrap());
Expand Down Expand Up @@ -437,5 +487,12 @@ mod tests {
};
assert_eq!(count_filtered(&db, &by_user).await.unwrap(), 2);
assert_eq!(all.iter().filter(|r| r.username.is_none()).count(), 1);

// Distinct dropdowns exclude NULLs and are ordered.
assert_eq!(distinct_usernames(&db).await.unwrap(), vec!["alice".to_string()]);
assert_eq!(
distinct_specs(&db).await.unwrap(),
vec!["public".to_string(), "sales".to_string()]
);
}
}
2 changes: 2 additions & 0 deletions crates/ruscker-admin/src/routes/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::AppState;

pub mod audit;
pub mod blocks;
pub mod user_activity;
pub mod credentials;
pub mod dashboard;
pub mod disk;
Expand Down Expand Up @@ -64,6 +65,7 @@ pub fn routes() -> Router<AppState> {
.merge(landing::routes())
.merge(blocks::routes())
.merge(audit::routes())
.merge(user_activity::routes())
.merge(groups::routes())
.merge(logs::routes())
.merge(schedules_ui::routes())
Expand Down
Loading