admin: added list of users
This commit is contained in:
parent
e3bda01eca
commit
5153d057b0
13 changed files with 313 additions and 3 deletions
15
crates/database/queries/users/get_all.sql
Normal file
15
crates/database/queries/users/get_all.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
select id,
|
||||
created_at as "created_at: DateTime<Utc>",
|
||||
updated_at as "updated_at: DateTime<Utc>",
|
||||
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
|
||||
|
|
@ -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<Utc>",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at: DateTime<Utc>",
|
||||
"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<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\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": [
|
||||
|
|
|
|||
|
|
@ -97,6 +97,13 @@ impl Users {
|
|||
.map_err(handle_error)
|
||||
}
|
||||
|
||||
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, 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,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ impl Icon {
|
|||
"id-badge-2", IdBadge2, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-id-badge-2" 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="M7 12h3v4h-3z"></path><path d="M10 6h-6a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h16a1 1 0 0 0 1 -1v-12a1 1 0 0 0 -1 -1h-6"></path><path d="M10 3m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 16h2"></path><path d="M14 12h4"></path></svg>"#,
|
||||
"user", User, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user" 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="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"></path><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path></svg>"#,
|
||||
"at", At, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-at" 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="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M16 12v1.5a2.5 2.5 0 0 0 5 0v-1.5a9 9 0 1 0 -5.5 8.28"></path></svg>"#,
|
||||
"paperclip-large", PaperclipLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-paperclip" 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="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#
|
||||
"paperclip-large", PaperclipLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-paperclip" 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="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#,
|
||||
"users", Users, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-users" 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="M9 7m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path><path d="M21 21v-2a4 4 0 0 0 -3 -3.85"></path></svg>"#
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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", "<svg>...</svg>")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Route> {
|
||||
routes![
|
||||
|
|
@ -21,6 +23,7 @@ pub fn routes() -> Vec<Route> {
|
|||
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<User>,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
crates/ezidam/src/routes/admin/users.rs
Normal file
21
crates/ezidam/src/routes/admin/users.rs
Normal file
|
|
@ -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<Database>,
|
||||
admin: JwtAdmin,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Result<Template> {
|
||||
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()))
|
||||
}
|
||||
144
crates/ezidam/templates/pages/admin/users/list.html.tera
Normal file
144
crates/ezidam/templates/pages/admin/users/list.html.tera
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
{% extends "shell" %}
|
||||
|
||||
{% block content %}
|
||||
{% set new_user_label = "New user" %}
|
||||
{% set new_user_link = "users/new" %}
|
||||
<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 dashboard
|
||||
</div>
|
||||
<h2 class="page-title">
|
||||
Users
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-auto ms-auto">
|
||||
<div class="btn-list">
|
||||
<a href="{{ new_user_link }}" class="btn btn-primary d-none d-sm-inline-block">
|
||||
{% include "icons/plus" %}
|
||||
{{ new_user_label }}
|
||||
</a>
|
||||
<a href="{{ new_user_link }}" class="btn btn-primary d-sm-none btn-icon"
|
||||
aria-label="{{ new_user_label }}">
|
||||
{% include "icons/plus" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
|
||||
{% if flash %}
|
||||
<div class="alert alert-{{flash.0}}" role="alert">
|
||||
<h4 class="alert-title">{{ flash.1 }}</h4>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div id="table-default" class="table-responsive">
|
||||
<table class="table table-hover table-vcenter">
|
||||
<!-- Table header -->
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<button class="table-sort" data-sort="sort-name">Name</button>
|
||||
</th>
|
||||
<th>
|
||||
<button class="table-sort" data-sort="sort-status">Status</button>
|
||||
</th>
|
||||
<th>
|
||||
<button class="table-sort" data-sort="sort-email">Email</button>
|
||||
</th>
|
||||
<th>
|
||||
<button class="table-sort" data-sort="sort-creation-date">Creation Date</button>
|
||||
</th>
|
||||
<th>Security</th>
|
||||
<th class="w-1">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- Table content -->
|
||||
<tbody class="table-tbody">
|
||||
{% for user_loop in users %}
|
||||
<tr>
|
||||
<td class="sort-name text-nowrap">
|
||||
{{ user::avatar(username=user_loop.username, name=user_loop.name, size="xs", css="me-1") }}
|
||||
|
||||
<span>
|
||||
{% if user_loop.name %}
|
||||
{{ user_loop.name }}
|
||||
{% else %}
|
||||
{{ user_loop.username }}
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
</td>
|
||||
<td class="sort-status text-nowrap">
|
||||
{% if user_loop.is_archived == true %}
|
||||
<span class="badge bg-danger me-1"></span> Archived
|
||||
{% else %}
|
||||
<span class="badge bg-success me-1"></span> Active
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="sort-email">
|
||||
{% if user_loop.email %}
|
||||
{{ user_loop.email }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="sort-creation-date" data-date='{{ user_loop.created_at | date(format="%s") }}'>
|
||||
{{ user_loop.created_at | date(format="%F %T", timezone=user.zoneinfo | default(value="UTC")) }}
|
||||
</td>
|
||||
<td class="text-nowrap">
|
||||
{% if user_loop.password %}
|
||||
|
||||
{% if user_loop.paper_key %}
|
||||
<span class="badge bg-success me-1"></span> Good
|
||||
{% else %}
|
||||
<span class="badge bg-warning me-1"></span> No paper key
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<span class="badge bg-danger me-1"></span> No password
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user_loop.id != user.sub %}
|
||||
<a href="users/{{ user_loop.id }}">Details</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block libs_js %}
|
||||
<script src="/libs/list.js/list.min.js" defer></script>
|
||||
{% endblock lib_js %}
|
||||
|
||||
{% block additional_js %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const list = new List('table-default', {
|
||||
sortClass: 'table-sort',
|
||||
listClass: 'table-tbody',
|
||||
valueNames: ['sort-name', 'sort-status', 'sort-email',
|
||||
{attr: 'data-date', name: 'sort-creation-date'},
|
||||
]
|
||||
});
|
||||
})
|
||||
</script>
|
||||
{% endblock additional_js %}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -108,6 +108,14 @@ impl User {
|
|||
.map(Self::from))
|
||||
}
|
||||
|
||||
pub async fn get_all(conn: impl SqliteExecutor<'_>) -> Result<Vec<Self>, Error> {
|
||||
Ok(DatabaseUsers::get_all(conn)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Self::from)
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub async fn set_username(
|
||||
&self,
|
||||
conn: impl SqliteExecutor<'_>,
|
||||
|
|
|
|||
|
|
@ -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<Utc>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue