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 ?"
|
||||
},
|
||||
"2ee77458d93ae79d957e03e6ca50c1bf690f40b6cc6ec5f008eb0e928f376659": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
}
|
||||
},
|
||||
"query": "update roles\n\nset is_archived = ?\n\nwhere name is ?"
|
||||
},
|
||||
"32d35bdd1f4cf64ce0ff7beb7a11591e0f35eab7211692bcde8230c68e4cedf3": {
|
||||
"describe": {
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
|
|
|
|||
|
|
@ -31,4 +31,28 @@ impl Roles {
|
|||
.await
|
||||
.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),
|
||||
AdminRolesList(AdminRolesList),
|
||||
AdminRolesNew(AdminRolesNew),
|
||||
AdminRolesView(AdminRolesView),
|
||||
}
|
||||
|
||||
impl Page {
|
||||
|
|
@ -64,6 +65,7 @@ impl Page {
|
|||
Page::AdminUsersNew(_) => "pages/admin/users/new",
|
||||
Page::AdminRolesList(_) => "pages/admin/roles/list",
|
||||
Page::AdminRolesNew(_) => "pages/admin/roles/new",
|
||||
Page::AdminRolesView(_) => "pages/admin/roles/view",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +96,7 @@ impl Page {
|
|||
Page::AdminUsersNew(_) => "New user",
|
||||
Page::AdminRolesList(_) => "Roles",
|
||||
Page::AdminRolesNew(_) => "New role",
|
||||
Page::AdminRolesView(_) => "Role info",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +129,7 @@ impl Page {
|
|||
Page::AdminUsersNew(_) => Some(AdminMenu::Users.into()),
|
||||
Page::AdminRolesList(_) => 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::AdminRolesList(list) => Box::new(list),
|
||||
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_new,
|
||||
admin_roles_new_form,
|
||||
admin_roles_view,
|
||||
admin_roles_archive,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -143,4 +145,13 @@ pub mod content {
|
|||
pub struct AdminRolesNew {
|
||||
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::tokens::JWT_DURATION_MINUTES;
|
||||
use rocket::{get, post};
|
||||
use roles::Role;
|
||||
use std::str::FromStr;
|
||||
|
|
@ -71,9 +72,107 @@ pub async fn admin_roles_new_form(
|
|||
|
||||
transaction.commit().await?;
|
||||
|
||||
let id = RocketRoleID(name);
|
||||
Ok(Flash::new(
|
||||
Redirect::to(uri!(admin_roles_list)),
|
||||
Redirect::to(uri!(admin_roles_view(id))),
|
||||
FlashKind::Success,
|
||||
"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"
|
||||
required>
|
||||
<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>
|
||||
</small>
|
||||
</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(())
|
||||
}
|
||||
|
||||
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