admin/roles: view, archive and restore
This commit is contained in:
parent
8fa2fb7ddc
commit
d778380d8b
10 changed files with 363 additions and 2 deletions
7
crates/database/queries/roles/get_one_by_name.sql
Normal file
7
crates/database/queries/roles/get_one_by_name.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
select name,
|
||||||
|
label,
|
||||||
|
created_at as "created_at: DateTime<Utc>",
|
||||||
|
is_archived as "is_archived: bool"
|
||||||
|
from roles
|
||||||
|
|
||||||
|
where name is (?)
|
||||||
5
crates/database/queries/roles/set_archive_status.sql
Normal file
5
crates/database/queries/roles/set_archive_status.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
update roles
|
||||||
|
|
||||||
|
set is_archived = ?
|
||||||
|
|
||||||
|
where name is ?
|
||||||
|
|
@ -110,6 +110,16 @@
|
||||||
},
|
},
|
||||||
"query": "update apps\n\nset is_archived = 1\n\nwhere id is ?"
|
"query": "update apps\n\nset is_archived = 1\n\nwhere id is ?"
|
||||||
},
|
},
|
||||||
|
"2ee77458d93ae79d957e03e6ca50c1bf690f40b6cc6ec5f008eb0e928f376659": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "update roles\n\nset is_archived = ?\n\nwhere name is ?"
|
||||||
|
},
|
||||||
"32d35bdd1f4cf64ce0ff7beb7a11591e0f35eab7211692bcde8230c68e4cedf3": {
|
"32d35bdd1f4cf64ce0ff7beb7a11591e0f35eab7211692bcde8230c68e4cedf3": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
|
@ -864,6 +874,42 @@
|
||||||
},
|
},
|
||||||
"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,\n totp_secret,\n totp_backup\nfrom users\n\norder by created_at desc\n"
|
"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,\n totp_secret,\n totp_backup\nfrom users\n\norder by created_at desc\n"
|
||||||
},
|
},
|
||||||
|
"9a3aef02e55fc436e9c09c1ee6d29477c2746765f6e7d5679058ec6525ee7253": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "label",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at: DateTime<Utc>",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "is_archived: bool",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Int64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "select name,\n label,\n created_at as \"created_at: DateTime<Utc>\",\n is_archived as \"is_archived: bool\"\nfrom roles\n\nwhere name is (?)\n"
|
||||||
|
},
|
||||||
"9dc379b0f3a3c944a33ef01dc40489b29b2a61b9f73f6ca4b5df7e9fb9bccf90": {
|
"9dc379b0f3a3c944a33ef01dc40489b29b2a61b9f73f6ca4b5df7e9fb9bccf90": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,28 @@ impl Roles {
|
||||||
.await
|
.await
|
||||||
.map_err(handle_error)
|
.map_err(handle_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_one_by_name(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<Self>, Error> {
|
||||||
|
sqlx::query_file_as!(Self, "queries/roles/get_one_by_name.sql", name)
|
||||||
|
.fetch_optional(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_archive_status(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
id: &str,
|
||||||
|
value: bool,
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
let query: SqliteQueryResult =
|
||||||
|
sqlx::query_file!("queries/roles/set_archive_status.sql", value, id)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)?;
|
||||||
|
|
||||||
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ pub enum Page {
|
||||||
AdminUsersNew(AdminUsersNew),
|
AdminUsersNew(AdminUsersNew),
|
||||||
AdminRolesList(AdminRolesList),
|
AdminRolesList(AdminRolesList),
|
||||||
AdminRolesNew(AdminRolesNew),
|
AdminRolesNew(AdminRolesNew),
|
||||||
|
AdminRolesView(AdminRolesView),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
|
|
@ -64,6 +65,7 @@ impl Page {
|
||||||
Page::AdminUsersNew(_) => "pages/admin/users/new",
|
Page::AdminUsersNew(_) => "pages/admin/users/new",
|
||||||
Page::AdminRolesList(_) => "pages/admin/roles/list",
|
Page::AdminRolesList(_) => "pages/admin/roles/list",
|
||||||
Page::AdminRolesNew(_) => "pages/admin/roles/new",
|
Page::AdminRolesNew(_) => "pages/admin/roles/new",
|
||||||
|
Page::AdminRolesView(_) => "pages/admin/roles/view",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,6 +96,7 @@ impl Page {
|
||||||
Page::AdminUsersNew(_) => "New user",
|
Page::AdminUsersNew(_) => "New user",
|
||||||
Page::AdminRolesList(_) => "Roles",
|
Page::AdminRolesList(_) => "Roles",
|
||||||
Page::AdminRolesNew(_) => "New role",
|
Page::AdminRolesNew(_) => "New role",
|
||||||
|
Page::AdminRolesView(_) => "Role info",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,6 +129,7 @@ impl Page {
|
||||||
Page::AdminUsersNew(_) => Some(AdminMenu::Users.into()),
|
Page::AdminUsersNew(_) => Some(AdminMenu::Users.into()),
|
||||||
Page::AdminRolesList(_) => Some(AdminMenu::Roles.into()),
|
Page::AdminRolesList(_) => Some(AdminMenu::Roles.into()),
|
||||||
Page::AdminRolesNew(_) => Some(AdminMenu::Roles.into()),
|
Page::AdminRolesNew(_) => Some(AdminMenu::Roles.into()),
|
||||||
|
Page::AdminRolesView(_) => Some(AdminMenu::Roles.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,6 +160,7 @@ impl Page {
|
||||||
Page::AdminUsersNew(new) => Box::new(new),
|
Page::AdminUsersNew(new) => Box::new(new),
|
||||||
Page::AdminRolesList(list) => Box::new(list),
|
Page::AdminRolesList(list) => Box::new(list),
|
||||||
Page::AdminRolesNew(new) => Box::new(new),
|
Page::AdminRolesNew(new) => Box::new(new),
|
||||||
|
Page::AdminRolesView(view) => Box::new(view),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ pub fn routes() -> Vec<Route> {
|
||||||
admin_roles_list,
|
admin_roles_list,
|
||||||
admin_roles_new,
|
admin_roles_new,
|
||||||
admin_roles_new_form,
|
admin_roles_new_form,
|
||||||
|
admin_roles_view,
|
||||||
|
admin_roles_archive,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,4 +145,13 @@ pub mod content {
|
||||||
pub struct AdminRolesNew {
|
pub struct AdminRolesNew {
|
||||||
pub user: JwtClaims,
|
pub user: JwtClaims,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminRolesView {
|
||||||
|
pub user: JwtClaims,
|
||||||
|
pub role: Role,
|
||||||
|
pub jwt_duration: i64,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::routes::prelude::*;
|
use crate::routes::prelude::*;
|
||||||
|
use crate::tokens::JWT_DURATION_MINUTES;
|
||||||
use rocket::{get, post};
|
use rocket::{get, post};
|
||||||
use roles::Role;
|
use roles::Role;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
@ -71,9 +72,107 @@ pub async fn admin_roles_new_form(
|
||||||
|
|
||||||
transaction.commit().await?;
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let id = RocketRoleID(name);
|
||||||
Ok(Flash::new(
|
Ok(Flash::new(
|
||||||
Redirect::to(uri!(admin_roles_list)),
|
Redirect::to(uri!(admin_roles_view(id))),
|
||||||
FlashKind::Success,
|
FlashKind::Success,
|
||||||
"Role has been created.",
|
"Role has been created.",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/admin/roles/<id>")]
|
||||||
|
pub async fn admin_roles_view(
|
||||||
|
admin: JwtAdmin,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
id: RocketRoleID,
|
||||||
|
flash: Option<FlashMessage<'_>>,
|
||||||
|
) -> Result<Template> {
|
||||||
|
let role_id = id.0;
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
let role = Role::get_by_name(&mut transaction, &role_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found(role_id.to_string()))?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
let page = Page::AdminRolesView(super::content::AdminRolesView {
|
||||||
|
user: admin.0,
|
||||||
|
role,
|
||||||
|
jwt_duration: JWT_DURATION_MINUTES,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(flash
|
||||||
|
.map(|flash| Page::with_flash(page.clone(), flash))
|
||||||
|
.unwrap_or_else(|| page.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct ArchiveRoleForm {
|
||||||
|
pub archive: Option<bool>,
|
||||||
|
pub restore: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/admin/roles/<id>/archive", data = "<form>")]
|
||||||
|
pub async fn admin_roles_archive(
|
||||||
|
_admin: JwtAdmin,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
id: RocketRoleID,
|
||||||
|
form: Form<ArchiveRoleForm>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
let (redirect, flash_kind, flash_message) = match (form.archive, form.restore) {
|
||||||
|
(Some(true), _) => {
|
||||||
|
// Archive role
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get role
|
||||||
|
let role = Role::get_by_name(&mut transaction, &id.0)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Could not find role"))?;
|
||||||
|
|
||||||
|
// Set new status
|
||||||
|
role.set_archive_status(&mut transaction, true).await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
(
|
||||||
|
Redirect::to(uri!(admin_roles_list)),
|
||||||
|
FlashKind::Success,
|
||||||
|
"Role has been archived.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(_, Some(true)) => {
|
||||||
|
// Restore role
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get role
|
||||||
|
let role = Role::get_by_name(&mut transaction, &id.0)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Could not find role"))?;
|
||||||
|
|
||||||
|
// Set new status
|
||||||
|
role.set_archive_status(&mut transaction, false).await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
(
|
||||||
|
Redirect::to(uri!(admin_roles_view(id))),
|
||||||
|
FlashKind::Success,
|
||||||
|
"Role has been restored.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Nothing to do
|
||||||
|
(
|
||||||
|
Redirect::to(uri!(admin_roles_view(id))),
|
||||||
|
FlashKind::Warning,
|
||||||
|
"Nothing to do.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Flash::new(redirect, flash_kind, flash_message))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required>
|
required>
|
||||||
<small class="form-hint">
|
<small class="form-hint">
|
||||||
This name identifies the role inside the system. It is not public.<br>
|
This name is for internal usage. It is not public and cannot be changed.<br>
|
||||||
Allowed characters: <code>[a-z][0-9]-</code>
|
Allowed characters: <code>[a-z][0-9]-</code>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
145
crates/ezidam/templates/pages/admin/roles/view.html.tera
Normal file
145
crates/ezidam/templates/pages/admin/roles/view.html.tera
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
{% extends "shell" %}
|
||||||
|
|
||||||
|
{% 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 dashboard
|
||||||
|
</div>
|
||||||
|
<h2 class="page-title">
|
||||||
|
Roles
|
||||||
|
</h2>
|
||||||
|
</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 | safe }}</h4>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if role.is_archived %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Archived role</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>This role is archived.</p>
|
||||||
|
|
||||||
|
<form action="{{ role.name }}/archive" method="post">
|
||||||
|
<button type="submit" name="restore" value="true"
|
||||||
|
class="btn btn-primary">
|
||||||
|
Restore role
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Role information</h3>
|
||||||
|
</div>
|
||||||
|
<form action="{{ role.name }}/info" method="post">
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Name -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="name">Name</label>
|
||||||
|
<div>
|
||||||
|
<input id="name" value="{{ role.name }}" type="text"
|
||||||
|
class="form-control cursor-not-allowed" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Label -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label required" for="label">Label</label>
|
||||||
|
<div>
|
||||||
|
<input name="label" value="{{ role.label }}" id="label" type="text"
|
||||||
|
placeholder="Enter label"
|
||||||
|
class="form-control"
|
||||||
|
required>
|
||||||
|
<small class="form-hint">
|
||||||
|
The label is shown to the users.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer text-end">
|
||||||
|
<div class="d-flex">
|
||||||
|
|
||||||
|
<a class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modal-archive">
|
||||||
|
Archive
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary ms-auto">Save</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Archive modal -->
|
||||||
|
<div class="modal modal-blur" tabindex="-1" id="modal-archive">
|
||||||
|
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<div class="modal-status bg-danger"></div>
|
||||||
|
|
||||||
|
<div class="modal-body text-center py-4">
|
||||||
|
|
||||||
|
<div class="text-danger mb-2">
|
||||||
|
{% include "icons/alert-triangle-large" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Do you want to archive this role?</h3>
|
||||||
|
<div class="mt-2">This role will not be able to be used by users.</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
This action can take up to {{ jwt_duration }} minutes to be effective for all users.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="w-100">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<a href="#" class="btn w-100" data-bs-dismiss="modal">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<form action="{{ role.name }}/archive" method="post">
|
||||||
|
<button type="submit" name="archive" value="true"
|
||||||
|
class="btn btn-danger w-100">
|
||||||
|
Archive role
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block libs_js %}
|
||||||
|
{% endblock lib_js %}
|
||||||
|
|
||||||
|
{% block additional_js %}
|
||||||
|
{% endblock additional_js %}
|
||||||
|
|
@ -41,4 +41,23 @@ impl Role {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_name(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
name: &RoleID,
|
||||||
|
) -> Result<Option<Self>, Error> {
|
||||||
|
Ok(DatabaseRoles::get_one_by_name(conn, name.as_ref())
|
||||||
|
.await?
|
||||||
|
.map(Self::from))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_archive_status(
|
||||||
|
&self,
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
value: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
DatabaseRoles::set_archive_status(conn, self.name.as_ref(), value).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue