admin dashboard: apps, users, roles, logins in the last 24 hours

This commit is contained in:
Philippe Loctaux 2023-05-08 19:43:44 +02:00
parent 264eee9044
commit a05646a19b
8 changed files with 217 additions and 1 deletions

View file

@ -22,6 +22,26 @@ impl From<DatabaseAuthorizationCodes> for AuthorizationCode {
} }
impl AuthorizationCode { impl AuthorizationCode {
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, Error> {
Ok(DatabaseAuthorizationCodes::get_all(conn)
.await?
.into_iter()
.map(Self::from)
.collect::<Vec<_>>())
}
pub async fn used_in_last_24_hours(conn: impl SqliteExecutor<'_>) -> Result<usize, Error> {
let all = Self::get_all(conn).await?;
let last_24_hours = Utc::now() - Duration::hours(24);
Ok(all
.into_iter()
.filter_map(|code| code.used_at)
.filter(|&date| date >= last_24_hours)
.count())
}
pub async fn insert( pub async fn insert(
conn: impl SqliteExecutor<'_>, conn: impl SqliteExecutor<'_>,
code: &str, code: &str,

View file

@ -0,0 +1,7 @@
select code,
app,
user,
created_at as "created_at: DateTime<Utc>",
expires_at as "expires_at: DateTime<Utc>",
used_at as "used_at: DateTime<Utc>"
from authorization_codes

View file

@ -1332,6 +1332,54 @@
}, },
"query": "update users\n\nset email = ?\n\nwhere id is ?" "query": "update users\n\nset email = ?\n\nwhere id is ?"
}, },
"c3dcd38a2d4ff391aed4a2ac3f393646319950334494ecb5fa7effe9806d07ab": {
"describe": {
"columns": [
{
"name": "code",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "app",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "user",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "created_at: DateTime<Utc>",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "expires_at: DateTime<Utc>",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "used_at: DateTime<Utc>",
"ordinal": 5,
"type_info": "Text"
}
],
"nullable": [
false,
false,
false,
false,
false,
true
],
"parameters": {
"Right": 0
}
},
"query": "select code,\n app,\n user,\n created_at as \"created_at: DateTime<Utc>\",\n expires_at as \"expires_at: DateTime<Utc>\",\n used_at as \"used_at: DateTime<Utc>\"\nfrom authorization_codes\n"
},
"c6157ec3928527ec0ac5f493a5a91faff7e3668204a179e827a87d6279a02c40": { "c6157ec3928527ec0ac5f493a5a91faff7e3668204a179e827a87d6279a02c40": {
"describe": { "describe": {
"columns": [], "columns": [],

View file

@ -17,6 +17,13 @@ pub struct AuthorizationCodes {
} }
impl AuthorizationCodes { impl AuthorizationCodes {
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, Error> {
sqlx::query_file_as!(Self, "queries/authorization_codes/get_all.sql")
.fetch_all(conn)
.await
.map_err(handle_error)
}
pub async fn insert( pub async fn insert(
conn: impl SqliteExecutor<'_>, conn: impl SqliteExecutor<'_>,
code: &str, code: &str,

View file

@ -53,7 +53,8 @@ impl Icon {
"users-group", UsersGroup, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users-group" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1"></path><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M17 10h2a2 2 0 0 1 2 2v1"></path><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M3 13v-1a2 2 0 0 1 2 -2h2"></path></svg>"#, "users-group", UsersGroup, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users-group" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1"></path><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M17 10h2a2 2 0 0 1 2 2v1"></path><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M3 13v-1a2 2 0 0 1 2 -2h2"></path></svg>"#,
"users-group-large", UsersGroupLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-users-group" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1"></path><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M17 10h2a2 2 0 0 1 2 2v1"></path><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M3 13v-1a2 2 0 0 1 2 -2h2"></path></svg>"#, "users-group-large", UsersGroupLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-users-group" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10 13a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M8 21v-1a2 2 0 0 1 2 -2h4a2 2 0 0 1 2 2v1"></path><path d="M15 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M17 10h2a2 2 0 0 1 2 2v1"></path><path d="M5 5a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M3 13v-1a2 2 0 0 1 2 -2h2"></path></svg>"#,
"adjustments", Adjustments, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-adjustments" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M6 4v4"></path><path d="M6 12v8"></path><path d="M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M12 4v10"></path><path d="M12 18v2"></path><path d="M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M18 4v1"></path><path d="M18 9v11"></path></svg>"#, "adjustments", Adjustments, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-adjustments" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M4 10a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M6 4v4"></path><path d="M6 12v8"></path><path d="M10 16a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M12 4v10"></path><path d="M12 18v2"></path><path d="M16 7a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path><path d="M18 4v1"></path><path d="M18 9v11"></path></svg>"#,
"device-floppy", DeviceFloppy, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-device-floppy" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"></path><path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path><path d="M14 4l0 4l-6 0l0 -4"></path></svg>"# "device-floppy", DeviceFloppy, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-device-floppy" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2"></path><path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path><path d="M14 4l0 4l-6 0l0 -4"></path></svg>"#,
"login", Login, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-login" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"></path><path d="M20 12h-13l3 -3m0 6l-3 -3"></path></svg>"#
} }
} }
@ -88,6 +89,7 @@ pub fn icons_to_templates(tera: &mut Tera) {
Icon::UsersGroupLarge, Icon::UsersGroupLarge,
Icon::Adjustments, Icon::Adjustments,
Icon::DeviceFloppy, Icon::DeviceFloppy,
Icon::Login,
]; ];
// For each icon, it will output: ("icons/name", "<svg>...</svg>") // For each icon, it will output: ("icons/name", "<svg>...</svg>")

View file

@ -69,6 +69,10 @@ pub mod content {
#[derive(Clone)] #[derive(Clone)]
pub struct AdminDashboard { pub struct AdminDashboard {
pub user: JwtClaims, pub user: JwtClaims,
pub users: Vec<User>,
pub roles: Vec<Role>,
pub apps: Vec<App>,
pub number_logins_last_24_hours: usize,
} }
#[derive(Serialize)] #[derive(Serialize)]

View file

@ -1,9 +1,34 @@
use crate::routes::prelude::*; use crate::routes::prelude::*;
use apps::App;
use authorization_codes::AuthorizationCode;
use rocket::get; use rocket::get;
use roles::Role;
use users::User;
#[get("/admin")] #[get("/admin")]
pub async fn admin_dashboard(mut db: Connection<Database>, admin: JwtAdmin) -> Result<Page> { pub async fn admin_dashboard(mut db: Connection<Database>, admin: JwtAdmin) -> Result<Page> {
let mut transaction = db.begin().await?;
// Get users
let users = User::get_all(&mut transaction).await?;
// Get roles
let roles = Role::get_all(&mut transaction).await?;
// Get apps
let apps = App::get_all(&mut transaction, None).await?;
// Get number of logins in the last 24 hours
let number_logins_last_24_hours =
AuthorizationCode::used_in_last_24_hours(&mut transaction).await?;
transaction.commit().await?;
Ok(Page::AdminDashboard(super::content::AdminDashboard { Ok(Page::AdminDashboard(super::content::AdminDashboard {
user: admin.0, user: admin.0,
users,
roles,
apps,
number_logins_last_24_hours,
})) }))
} }

View file

@ -1,4 +1,107 @@
{% extends "shell" %} {% extends "shell" %}
{% block content %} {% block content %}
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row align-items-center">
<div class="col">
<div class="page-pretitle">
Admin panel
</div>
<h2 class="page-title">
Dashboard
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<div class="row row-cards">
<!-- Apps -->
<div class="col-md-6 col-xl-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-primary text-white avatar">
{% include "icons/apps" %}
</span>
</div>
<div class="col">
<div class="font-weight-medium">
{{ apps | length }} Applications
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Users -->
<div class="col-md-6 col-xl-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
{% include "icons/user" %}
</span>
</div>
<div class="col">
<div class="font-weight-medium">
{{ users | length }} Users
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Roles -->
<div class="col-md-6 col-xl-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-red text-white avatar">
{% include "icons/users-group" %}
</span>
</div>
<div class="col">
<div class="font-weight-medium">
{{ roles | length }} Roles
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Number of logins today -->
<div class="col-md-6 col-xl-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-yellow text-white avatar">
{% include "icons/login" %}
</span>
</div>
<div class="col">
<div class="font-weight-medium">
{{ number_logins_last_24_hours }} logins in the last 24 hours
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock content %} {% endblock content %}