From 5153d057b0e474ab2d534141e04c51c92d0c171f Mon Sep 17 00:00:00 2001
From: Philippe Loctaux
Date: Sun, 16 Apr 2023 22:00:29 +0200
Subject: [PATCH] admin: added list of users
---
Cargo.lock | 1 +
crates/database/queries/users/get_all.sql | 15 ++
crates/database/sqlx-data.json | 84 ++++++++++
crates/database/src/tables/users.rs | 7 +
crates/ezidam/src/icons.rs | 4 +-
crates/ezidam/src/menu/items/admin.rs | 9 ++
crates/ezidam/src/page.rs | 5 +
crates/ezidam/src/routes/admin.rs | 12 ++
crates/ezidam/src/routes/admin/users.rs | 21 +++
.../pages/admin/users/list.html.tera | 144 ++++++++++++++++++
crates/users/Cargo.toml | 3 +-
crates/users/src/database.rs | 8 +
crates/users/src/lib.rs | 3 +-
13 files changed, 313 insertions(+), 3 deletions(-)
create mode 100644 crates/database/queries/users/get_all.sql
create mode 100644 crates/ezidam/src/routes/admin/users.rs
create mode 100644 crates/ezidam/templates/pages/admin/users/list.html.tera
diff --git a/Cargo.lock b/Cargo.lock
index 9b0142a..374923d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3281,6 +3281,7 @@ dependencies = [
"email_address",
"hash",
"id",
+ "serde",
"thiserror",
]
diff --git a/crates/database/queries/users/get_all.sql b/crates/database/queries/users/get_all.sql
new file mode 100644
index 0000000..f2479ff
--- /dev/null
+++ b/crates/database/queries/users/get_all.sql
@@ -0,0 +1,15 @@
+select id,
+ created_at as "created_at: DateTime",
+ updated_at as "updated_at: DateTime",
+ is_admin as "is_admin: bool",
+ username,
+ name,
+ email,
+ password,
+ password_recover,
+ paper_key,
+ is_archived as "is_archived: bool",
+ timezone
+from users
+
+order by created_at desc
diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json
index 400fdce..4c076ba 100644
--- a/crates/database/sqlx-data.json
+++ b/crates/database/sqlx-data.json
@@ -446,6 +446,90 @@
},
"query": "update users\n\nset paper_key = ?\n\nwhere id is ?"
},
+ "6a4a17c69175a677961779db048bff43d01e8773e7c4a6b25b9ee9b3c6fbacd5": {
+ "describe": {
+ "columns": [
+ {
+ "name": "id",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "created_at: DateTime",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "updated_at: DateTime",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "is_admin: bool",
+ "ordinal": 3,
+ "type_info": "Int64"
+ },
+ {
+ "name": "username",
+ "ordinal": 4,
+ "type_info": "Text"
+ },
+ {
+ "name": "name",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "email",
+ "ordinal": 6,
+ "type_info": "Text"
+ },
+ {
+ "name": "password",
+ "ordinal": 7,
+ "type_info": "Text"
+ },
+ {
+ "name": "password_recover",
+ "ordinal": 8,
+ "type_info": "Text"
+ },
+ {
+ "name": "paper_key",
+ "ordinal": 9,
+ "type_info": "Text"
+ },
+ {
+ "name": "is_archived: bool",
+ "ordinal": 10,
+ "type_info": "Int64"
+ },
+ {
+ "name": "timezone",
+ "ordinal": 11,
+ "type_info": "Text"
+ }
+ ],
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true,
+ true,
+ true,
+ false,
+ false
+ ],
+ "parameters": {
+ "Right": 0
+ }
+ },
+ "query": "select id,\n created_at as \"created_at: DateTime\",\n updated_at as \"updated_at: DateTime\",\n is_admin as \"is_admin: bool\",\n username,\n name,\n email,\n password,\n password_recover,\n paper_key,\n is_archived as \"is_archived: bool\",\n timezone\nfrom users\n\norder by created_at desc\n"
+ },
"73f0d480c8dbef497a458070a32e65f0140f9a6e098ea082870a9c904629a97b": {
"describe": {
"columns": [
diff --git a/crates/database/src/tables/users.rs b/crates/database/src/tables/users.rs
index 19a5421..0173cb3 100644
--- a/crates/database/src/tables/users.rs
+++ b/crates/database/src/tables/users.rs
@@ -97,6 +97,13 @@ impl Users {
.map_err(handle_error)
}
+ pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result, Error> {
+ sqlx::query_file_as!(Self, "queries/users/get_all.sql")
+ .fetch_all(conn)
+ .await
+ .map_err(handle_error)
+ }
+
pub async fn set_username(
conn: impl SqliteExecutor<'_>,
id: &str,
diff --git a/crates/ezidam/src/icons.rs b/crates/ezidam/src/icons.rs
index 7803e7f..e663003 100644
--- a/crates/ezidam/src/icons.rs
+++ b/crates/ezidam/src/icons.rs
@@ -40,7 +40,8 @@ impl Icon {
"id-badge-2", IdBadge2, r#""#,
"user", User, r#""#,
"at", At, r#""#,
- "paperclip-large", PaperclipLarge, r#""#
+ "paperclip-large", PaperclipLarge, r#""#,
+ "users", Users, r#""#
}
}
@@ -62,6 +63,7 @@ pub fn icons_to_templates(tera: &mut Tera) {
Icon::User,
Icon::At,
Icon::PaperclipLarge,
+ Icon::Users,
];
// For each icon, it will output: ("icons/name", "")
diff --git a/crates/ezidam/src/menu/items/admin.rs b/crates/ezidam/src/menu/items/admin.rs
index a04cba8..d22c074 100644
--- a/crates/ezidam/src/menu/items/admin.rs
+++ b/crates/ezidam/src/menu/items/admin.rs
@@ -7,6 +7,7 @@ pub enum AdminMenu {
Exit,
Dashboard,
Apps,
+ Users,
Settings,
}
@@ -16,6 +17,7 @@ impl AdminMenu {
AdminMenu::Exit => "exit",
AdminMenu::Dashboard => "dashboard",
AdminMenu::Apps => "apps",
+ AdminMenu::Users => "users",
AdminMenu::Settings => "settings",
}
}
@@ -42,6 +44,13 @@ impl AdminMenu {
icon: Icon::Apps.svg,
sub: None,
},
+ MainItem {
+ id: AdminMenu::Users.id(),
+ label: "Users",
+ link: uri!(routes::admin::users::admin_users_list).to_string(),
+ icon: Icon::Users.svg,
+ sub: None,
+ },
MainItem {
id: AdminMenu::Settings.id(),
label: "Server settings",
diff --git a/crates/ezidam/src/page.rs b/crates/ezidam/src/page.rs
index f1d67ab..47f25dc 100644
--- a/crates/ezidam/src/page.rs
+++ b/crates/ezidam/src/page.rs
@@ -24,6 +24,7 @@ pub enum Page {
UserPersonalSettings(UserPersonalSettings),
UserSecuritySettings(UserSecuritySettings),
UserVisualSettings(UserVisualSettings),
+ AdminUsersList(AdminUsersList),
}
impl Page {
@@ -44,6 +45,7 @@ impl Page {
Page::UserPersonalSettings(_) => "pages/settings/personal",
Page::UserSecuritySettings(_) => "pages/settings/security",
Page::UserVisualSettings(_) => "pages/settings/visual",
+ Page::AdminUsersList(_) => "pages/admin/users/list",
}
}
@@ -64,6 +66,7 @@ impl Page {
Page::UserPersonalSettings(_) => "Personal settings",
Page::UserSecuritySettings(_) => "Security settings",
Page::UserVisualSettings(_) => "Visual settings",
+ Page::AdminUsersList(_) => "Users",
}
}
@@ -86,6 +89,7 @@ impl Page {
Page::UserPersonalSettings(_) => Some(UserMenu::Settings.into()),
Page::UserSecuritySettings(_) => Some(UserMenu::Settings.into()),
Page::UserVisualSettings(_) => Some(UserMenu::Settings.into()),
+ Page::AdminUsersList(_) => Some(AdminMenu::Users.into()),
}
}
@@ -106,6 +110,7 @@ impl Page {
Page::UserPersonalSettings(personal) => Box::new(personal),
Page::UserSecuritySettings(security) => Box::new(security),
Page::UserVisualSettings(visual) => Box::new(visual),
+ Page::AdminUsersList(list) => Box::new(list),
}
}
}
diff --git a/crates/ezidam/src/routes/admin.rs b/crates/ezidam/src/routes/admin.rs
index d98eb56..ffadb91 100644
--- a/crates/ezidam/src/routes/admin.rs
+++ b/crates/ezidam/src/routes/admin.rs
@@ -1,11 +1,13 @@
use self::apps::*;
use self::settings::*;
+use self::users::*;
use dashboard::*;
use rocket::{routes, Route};
pub mod apps;
pub mod dashboard;
pub mod settings;
+pub mod users;
pub fn routes() -> Vec {
routes![
@@ -21,6 +23,7 @@ pub fn routes() -> Vec {
admin_apps_view_form,
admin_apps_new_secret,
admin_apps_archive,
+ admin_users_list,
]
}
@@ -28,6 +31,7 @@ pub mod content {
use apps::App;
use jwt::JwtClaims;
use rocket::serde::Serialize;
+ use users::User;
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
@@ -74,4 +78,12 @@ pub mod content {
pub user: JwtClaims,
pub app: App,
}
+
+ #[derive(Serialize)]
+ #[serde(crate = "rocket::serde")]
+ #[derive(Clone)]
+ pub struct AdminUsersList {
+ pub user: JwtClaims,
+ pub users: Vec,
+ }
}
diff --git a/crates/ezidam/src/routes/admin/users.rs b/crates/ezidam/src/routes/admin/users.rs
new file mode 100644
index 0000000..ee33b55
--- /dev/null
+++ b/crates/ezidam/src/routes/admin/users.rs
@@ -0,0 +1,21 @@
+use crate::routes::prelude::*;
+use rocket::get;
+use users::User;
+
+#[get("/admin/users")]
+pub async fn admin_users_list(
+ mut db: Connection,
+ admin: JwtAdmin,
+ flash: Option>,
+) -> Result {
+ let users = User::get_all(&mut *db).await?;
+
+ let page = Page::AdminUsersList(super::content::AdminUsersList {
+ user: admin.0,
+ users,
+ });
+
+ Ok(flash
+ .map(|flash| Page::with_flash(page.clone(), flash))
+ .unwrap_or_else(|| page.into()))
+}
diff --git a/crates/ezidam/templates/pages/admin/users/list.html.tera b/crates/ezidam/templates/pages/admin/users/list.html.tera
new file mode 100644
index 0000000..65a19cb
--- /dev/null
+++ b/crates/ezidam/templates/pages/admin/users/list.html.tera
@@ -0,0 +1,144 @@
+{% extends "shell" %}
+
+{% block content %}
+{% set new_user_label = "New user" %}
+{% set new_user_link = "users/new" %}
+
+
+
+
+
+ {% if flash %}
+
+
{{ flash.1 }}
+
+ {% endif %}
+
+
+
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ Security |
+ Actions |
+
+
+
+
+
+ {% for user_loop in users %}
+
+ |
+ {{ user::avatar(username=user_loop.username, name=user_loop.name, size="xs", css="me-1") }}
+
+
+ {% if user_loop.name %}
+ {{ user_loop.name }}
+ {% else %}
+ {{ user_loop.username }}
+ {% endif %}
+
+
+ |
+
+ {% if user_loop.is_archived == true %}
+ Archived
+ {% else %}
+ Active
+ {% endif %}
+ |
+
+ {% if user_loop.email %}
+ {{ user_loop.email }}
+ {% else %}
+ N/A
+ {% endif %}
+ |
+
+ {{ user_loop.created_at | date(format="%F %T", timezone=user.zoneinfo | default(value="UTC")) }}
+ |
+
+ {% if user_loop.password %}
+
+ {% if user_loop.paper_key %}
+ Good
+ {% else %}
+ No paper key
+ {% endif %}
+
+ {% else %}
+ No password
+
+ {% endif %}
+ |
+
+ {% if user_loop.id != user.sub %}
+ Details
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+
+
+
+
+{% endblock content %}
+
+{% block libs_js %}
+
+{% endblock lib_js %}
+
+{% block additional_js %}
+
+{% endblock additional_js %}
diff --git a/crates/users/Cargo.toml b/crates/users/Cargo.toml
index 461d414..a2518c3 100644
--- a/crates/users/Cargo.toml
+++ b/crates/users/Cargo.toml
@@ -8,5 +8,6 @@ database = { path = "../database" }
hash = { path = "../hash" }
id = { path = "../id" }
thiserror = { workspace = true }
-chrono = { workspace = true }
+chrono = { workspace = true, features = ["serde"] }
email_address = { workspace = true }
+serde = { workspace = true }
diff --git a/crates/users/src/database.rs b/crates/users/src/database.rs
index c8e6a5d..e2e5dff 100644
--- a/crates/users/src/database.rs
+++ b/crates/users/src/database.rs
@@ -108,6 +108,14 @@ impl User {
.map(Self::from))
}
+ pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result, Error> {
+ Ok(DatabaseUsers::get_all(conn)
+ .await?
+ .into_iter()
+ .map(Self::from)
+ .collect::>())
+ }
+
pub async fn set_username(
&self,
conn: impl SqliteExecutor<'_>,
diff --git a/crates/users/src/lib.rs b/crates/users/src/lib.rs
index 1b26629..aa230ee 100644
--- a/crates/users/src/lib.rs
+++ b/crates/users/src/lib.rs
@@ -3,10 +3,11 @@ mod error;
use chrono::{DateTime, Utc};
use id::UserID;
+use serde::Serialize;
pub use crate::error::Error;
-#[derive(Debug)]
+#[derive(Serialize, Debug, Clone)]
pub struct User {
id: UserID,
created_at: DateTime,