roles: sql, crate, struct, id, rocket id, list page

This commit is contained in:
Philippe Loctaux 2023-05-07 12:26:29 +02:00
parent 2e055e2037
commit efaf2bda5a
23 changed files with 462 additions and 2 deletions

View file

@ -31,3 +31,4 @@ authorization_codes = { path = "../authorization_codes" }
refresh_tokens = { path = "../refresh_tokens" }
email = { path = "../email" }
totp = { path = "../totp" }
roles = { path = "../roles" }

View file

@ -93,6 +93,12 @@ impl From<totp::Error> for Error {
}
}
impl From<roles::Error> for Error {
fn from(e: roles::Error) -> Self {
Error::internal_server_error(e)
}
}
// std Types
impl From<String> for Error {

View file

@ -48,7 +48,8 @@ impl Icon {
"2fa-large", TwoFaLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-2fa" 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 16h-4l3.47 -4.66a2 2 0 1 0 -3.47 -1.54"></path><path d="M10 16v-8h4"></path><path d="M10 12l3 0"></path><path d="M17 16v-6a2 2 0 0 1 4 0v6"></path><path d="M17 13l4 0"></path></svg>"#,
"check", Check, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" 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="M5 12l5 5l10 -10"></path></svg>"#,
"x", X, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" 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="M18 6l-12 12"></path><path d="M6 6l12 12"></path></svg>"#,
"progress", Progress, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-progress" 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 20.777a8.942 8.942 0 0 1 -2.48 -.969"></path><path d="M14 3.223a9.003 9.003 0 0 1 0 17.554"></path><path d="M4.579 17.093a8.961 8.961 0 0 1 -1.227 -2.592"></path><path d="M3.124 10.5c.16 -.95 .468 -1.85 .9 -2.675l.169 -.305"></path><path d="M6.907 4.579a8.954 8.954 0 0 1 3.093 -1.356"></path></svg>"#
"progress", Progress, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-progress" 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 20.777a8.942 8.942 0 0 1 -2.48 -.969"></path><path d="M14 3.223a9.003 9.003 0 0 1 0 17.554"></path><path d="M4.579 17.093a8.961 8.961 0 0 1 -1.227 -2.592"></path><path d="M3.124 10.5c.16 -.95 .468 -1.85 .9 -2.675l.169 -.305"></path><path d="M6.907 4.579a8.954 8.954 0 0 1 3.093 -1.356"></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>"#
}
}
@ -78,6 +79,7 @@ pub fn icons_to_templates(tera: &mut Tera) {
Icon::Check,
Icon::X,
Icon::Progress,
Icon::UsersGroup,
];
// For each icon, it will output: ("icons/name", "<svg>...</svg>")

View file

@ -1,4 +1,4 @@
use id::{AppID, Error, UserID};
use id::{AppID, Error, RoleID, UserID};
use rocket::http::impl_from_uri_param_identity;
use rocket::http::uri::fmt::{Formatter, Path, UriDisplay};
use rocket::request::FromParam;
@ -48,3 +48,23 @@ impl UriDisplay<Path> for RocketAppID {
}
impl_from_uri_param_identity!([Path] RocketAppID);
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct RocketRoleID(pub RoleID);
impl<'r> FromParam<'r> for RocketRoleID {
type Error = Error;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
RoleID::from_str(param).map(Self)
}
}
impl UriDisplay<Path> for RocketRoleID {
fn fmt(&self, f: &mut Formatter<Path>) -> fmt::Result {
UriDisplay::fmt(&self.0 .0, f)
}
}
impl_from_uri_param_identity!([Path] RocketRoleID);

View file

@ -8,6 +8,7 @@ pub enum AdminMenu {
Dashboard,
Apps,
Users,
Roles,
Settings,
}
@ -18,6 +19,7 @@ impl AdminMenu {
AdminMenu::Dashboard => "dashboard",
AdminMenu::Apps => "apps",
AdminMenu::Users => "users",
AdminMenu::Roles => "roles",
AdminMenu::Settings => "settings",
}
}
@ -51,6 +53,13 @@ impl AdminMenu {
icon: Icon::Users.svg,
sub: None,
},
MainItem {
id: AdminMenu::Roles.id(),
label: "Roles",
link: uri!(routes::admin::roles::admin_roles_list).to_string(),
icon: Icon::UsersGroup.svg,
sub: None,
},
MainItem {
id: AdminMenu::Settings.id(),
label: "Server settings",

View file

@ -32,6 +32,7 @@ pub enum Page {
AuthorizeTotp(AuthorizeTotp),
AdminUsersView(AdminUsersView),
AdminUsersNew(AdminUsersNew),
AdminRolesList(AdminRolesList),
}
impl Page {
@ -60,6 +61,7 @@ impl Page {
Page::AuthorizeTotp(_) => "pages/oauth/totp",
Page::AdminUsersView(_) => "pages/admin/users/view",
Page::AdminUsersNew(_) => "pages/admin/users/new",
Page::AdminRolesList(_) => "pages/admin/roles/list",
}
}
@ -88,6 +90,7 @@ impl Page {
Page::AuthorizeTotp(_) => "Verifying your account",
Page::AdminUsersView(_) => "User info",
Page::AdminUsersNew(_) => "New user",
Page::AdminRolesList(_) => "Roles",
}
}
@ -118,6 +121,7 @@ impl Page {
Page::AuthorizeTotp(_) => None,
Page::AdminUsersView(_) => Some(AdminMenu::Users.into()),
Page::AdminUsersNew(_) => Some(AdminMenu::Users.into()),
Page::AdminRolesList(_) => Some(AdminMenu::Roles.into()),
}
}
@ -146,6 +150,7 @@ impl Page {
Page::AuthorizeTotp(totp) => Box::new(totp),
Page::AdminUsersView(view) => Box::new(view),
Page::AdminUsersNew(new) => Box::new(new),
Page::AdminRolesList(list) => Box::new(list),
}
}
}

View file

@ -1,4 +1,5 @@
use self::apps::*;
use self::roles::*;
use self::settings::*;
use self::users::*;
use dashboard::*;
@ -6,6 +7,7 @@ use rocket::{routes, Route};
pub mod apps;
pub mod dashboard;
pub mod roles;
pub mod settings;
pub mod users;
@ -35,6 +37,7 @@ pub fn routes() -> Vec<Route> {
admin_users_totp_secret_disable,
admin_users_totp_backup_delete,
admin_users_info_update,
admin_roles_list,
]
}
@ -42,6 +45,7 @@ pub mod content {
use apps::App;
use jwt::JwtClaims;
use rocket::serde::Serialize;
use roles::Role;
use users::User;
#[derive(Serialize)]
@ -122,4 +126,12 @@ pub mod content {
pub struct AdminUsersNew {
pub user: JwtClaims,
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone)]
pub struct AdminRolesList {
pub user: JwtClaims,
pub roles: Vec<Role>,
}
}

View file

@ -0,0 +1,25 @@
use crate::routes::prelude::*;
use rocket::{get, post};
use roles::Role;
#[get("/admin/roles")]
pub async fn admin_roles_list(
mut db: Connection<Database>,
admin: JwtAdmin,
flash: Option<FlashMessage<'_>>,
) -> Result<Template> {
let mut transaction = db.begin().await?;
let roles = Role::get_all(&mut transaction).await?;
transaction.commit().await?;
let page = Page::AdminRolesList(super::content::AdminRolesList {
user: admin.0,
roles,
});
Ok(flash
.map(|flash| Page::with_flash(page.clone(), flash))
.unwrap_or_else(|| page.into()))
}

View file

@ -0,0 +1,127 @@
{% extends "shell" %}
{% block content %}
{% set new_role_label = "New role" %}
{% set new_role_link = "roles/new" %}
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row align-items-center">
{% if roles | length != 0 %}
<div class="col">
<div class="page-pretitle">
Admin dashboard
</div>
<h2 class="page-title">
Roles
</h2>
</div>
<div class="col-auto ms-auto">
<div class="btn-list">
<a href="{{ new_role_link }}" class="btn btn-primary d-none d-sm-inline-block">
{% include "icons/plus" %}
{{ new_role_label }}
</a>
<a href="{{ new_role_link }}" class="btn btn-primary d-sm-none btn-icon"
aria-label="{{ new_role_label }}">
{% include "icons/plus" %}
</a>
</div>
</div>
{% endif %}
</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 %}
{% if roles | length != 0 %}
<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-creation-date">Creation Date</button>
</th>
<th class="w-1">
Actions
</th>
</tr>
</thead>
<!-- Table content -->
<tbody class="table-tbody">
{% for role in roles %}
<tr>
<td class="sort-name">{{ role.label }}</td>
<td class="sort-status text-nowrap">
{% if role.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-creation-date" data-date='{{ role.created_at | date(format="%s") }}'>
{{ role.created_at | date(format="%F %T", timezone=user.zoneinfo | default(value="UTC")) }}
</td>
<td>
<a href="roles/{{ role.name }}">Details</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="d-flex flex-column justify-content-center">
<div class="empty">
<p class="empty-title">No roles</p>
<p class="empty-subtitle text-muted">
Create a role to limit users in applications outside ezidam.
</p>
<div class="empty-action">
<a href="{{ new_role_link }}" class="btn btn-primary">
{% include "icons/plus" %}
{{ new_role_label }}
</a>
</div>
</div>
</div>
{% endif %}
</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',
{attr: 'data-date', name: 'sort-creation-date'},
]
});
})
</script>
{% endblock additional_js %}