admin/users: view user, archive + restore
This commit is contained in:
parent
4a63bfa9a9
commit
e600405f22
6 changed files with 492 additions and 4 deletions
|
|
@ -45,7 +45,10 @@ impl Icon {
|
||||||
"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>"#,
|
"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>"#,
|
||||||
"mail", Mail, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" 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="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path><path d="M3 7l9 6l9 -6"></path></svg>"#,
|
"mail", Mail, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-mail" 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="M3 7a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-10z"></path><path d="M3 7l9 6l9 -6"></path></svg>"#,
|
||||||
"password", Password, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-password" 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 10v4"></path><path d="M10 13l4 -2"></path><path d="M10 11l4 2"></path><path d="M5 10v4"></path><path d="M3 13l4 -2"></path><path d="M3 11l4 2"></path><path d="M19 10v4"></path><path d="M17 13l4 -2"></path><path d="M17 11l4 2"></path></svg>"#,
|
"password", Password, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-password" 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 10v4"></path><path d="M10 13l4 -2"></path><path d="M10 11l4 2"></path><path d="M5 10v4"></path><path d="M3 13l4 -2"></path><path d="M3 11l4 2"></path><path d="M19 10v4"></path><path d="M17 13l4 -2"></path><path d="M17 11l4 2"></path></svg>"#,
|
||||||
"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>"#
|
"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>"#
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,6 +75,9 @@ pub fn icons_to_templates(tera: &mut Tera) {
|
||||||
Icon::Mail,
|
Icon::Mail,
|
||||||
Icon::Password,
|
Icon::Password,
|
||||||
Icon::TwoFaLarge,
|
Icon::TwoFaLarge,
|
||||||
|
Icon::Check,
|
||||||
|
Icon::X,
|
||||||
|
Icon::Progress,
|
||||||
];
|
];
|
||||||
|
|
||||||
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use rocket::serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct RocketUserID(pub UserID);
|
pub struct RocketUserID(pub UserID);
|
||||||
|
|
||||||
|
|
@ -18,7 +18,15 @@ impl<'r> FromParam<'r> for RocketUserID {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
impl UriDisplay<Path> for RocketUserID {
|
||||||
|
fn fmt(&self, f: &mut Formatter<Path>) -> fmt::Result {
|
||||||
|
UriDisplay::fmt(&self.0 .0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_from_uri_param_identity!([Path] RocketUserID);
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct RocketAppID(pub AppID);
|
pub struct RocketAppID(pub AppID);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ pub enum Page {
|
||||||
ResetPassword(ResetPassword),
|
ResetPassword(ResetPassword),
|
||||||
UserSecurityTotp(UserSecurityTotp),
|
UserSecurityTotp(UserSecurityTotp),
|
||||||
AuthorizeTotp(AuthorizeTotp),
|
AuthorizeTotp(AuthorizeTotp),
|
||||||
|
AdminUsersView(AdminUsersView),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Page {
|
impl Page {
|
||||||
|
|
@ -54,6 +55,7 @@ impl Page {
|
||||||
Page::ResetPassword(_) => "pages/reset-password",
|
Page::ResetPassword(_) => "pages/reset-password",
|
||||||
Page::UserSecurityTotp(_) => "pages/settings/totp",
|
Page::UserSecurityTotp(_) => "pages/settings/totp",
|
||||||
Page::AuthorizeTotp(_) => "pages/oauth/totp",
|
Page::AuthorizeTotp(_) => "pages/oauth/totp",
|
||||||
|
Page::AdminUsersView(_) => "pages/admin/users/view",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,6 +81,7 @@ impl Page {
|
||||||
Page::ResetPassword(_) => "Reset password",
|
Page::ResetPassword(_) => "Reset password",
|
||||||
Page::UserSecurityTotp(_) => "Enable One-time password",
|
Page::UserSecurityTotp(_) => "Enable One-time password",
|
||||||
Page::AuthorizeTotp(_) => "Verifying your account",
|
Page::AuthorizeTotp(_) => "Verifying your account",
|
||||||
|
Page::AdminUsersView(_) => "User info",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,6 +109,7 @@ impl Page {
|
||||||
Page::ResetPassword(_) => None,
|
Page::ResetPassword(_) => None,
|
||||||
Page::UserSecurityTotp(_) => Some(UserMenu::Settings.into()),
|
Page::UserSecurityTotp(_) => Some(UserMenu::Settings.into()),
|
||||||
Page::AuthorizeTotp(_) => None,
|
Page::AuthorizeTotp(_) => None,
|
||||||
|
Page::AdminUsersView(_) => Some(AdminMenu::Users.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,6 +135,7 @@ impl Page {
|
||||||
Page::ResetPassword(reset) => Box::new(reset),
|
Page::ResetPassword(reset) => Box::new(reset),
|
||||||
Page::UserSecurityTotp(totp) => Box::new(totp),
|
Page::UserSecurityTotp(totp) => Box::new(totp),
|
||||||
Page::AuthorizeTotp(totp) => Box::new(totp),
|
Page::AuthorizeTotp(totp) => Box::new(totp),
|
||||||
|
Page::AdminUsersView(view) => Box::new(view),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ pub fn routes() -> Vec<Route> {
|
||||||
admin_apps_new_secret,
|
admin_apps_new_secret,
|
||||||
admin_apps_archive,
|
admin_apps_archive,
|
||||||
admin_users_list,
|
admin_users_list,
|
||||||
|
admin_users_view,
|
||||||
|
admin_users_archive,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,4 +88,13 @@ pub mod content {
|
||||||
pub user: JwtClaims,
|
pub user: JwtClaims,
|
||||||
pub users: Vec<User>,
|
pub users: Vec<User>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AdminUsersView {
|
||||||
|
pub user: JwtClaims,
|
||||||
|
pub jwt_duration: i64,
|
||||||
|
pub local: User,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
use crate::routes::prelude::*;
|
use crate::routes::prelude::*;
|
||||||
use rocket::get;
|
use crate::tokens::JWT_DURATION_MINUTES;
|
||||||
|
use authorization_codes::AuthorizationCode;
|
||||||
|
use rocket::{get, post};
|
||||||
|
use settings::Settings;
|
||||||
|
use users::totp_login_request::TotpLoginRequest;
|
||||||
use users::User;
|
use users::User;
|
||||||
|
|
||||||
#[get("/admin/users")]
|
#[get("/admin/users")]
|
||||||
|
|
@ -19,3 +23,116 @@ pub async fn admin_users_list(
|
||||||
.map(|flash| Page::with_flash(page.clone(), flash))
|
.map(|flash| Page::with_flash(page.clone(), flash))
|
||||||
.unwrap_or_else(|| page.into()))
|
.unwrap_or_else(|| page.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/admin/users/<id>")]
|
||||||
|
pub async fn admin_users_view(
|
||||||
|
admin_not_current: JwtAdminNotCurrent,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
id: RocketUserID,
|
||||||
|
flash: Option<FlashMessage<'_>>,
|
||||||
|
) -> Result<Template> {
|
||||||
|
let user_id = id.0;
|
||||||
|
let user = User::get_by_id(&mut *db, &user_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found(user_id.to_string()))?;
|
||||||
|
|
||||||
|
let page = Page::AdminUsersView(super::content::AdminUsersView {
|
||||||
|
user: admin_not_current.0,
|
||||||
|
jwt_duration: JWT_DURATION_MINUTES,
|
||||||
|
local: user,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(flash
|
||||||
|
.map(|flash| Page::with_flash(page.clone(), flash))
|
||||||
|
.unwrap_or_else(|| page.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct ArchiveUserForm {
|
||||||
|
pub archive: Option<bool>,
|
||||||
|
pub restore: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/admin/users/<id>/archive", data = "<form>")]
|
||||||
|
pub async fn admin_users_archive(
|
||||||
|
_admin_not_current: JwtAdminNotCurrent,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
id: RocketUserID,
|
||||||
|
form: Form<ArchiveUserForm>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
let (redirect, flash_kind, flash_message) = match (form.archive, form.restore) {
|
||||||
|
(Some(true), _) => {
|
||||||
|
// Archive user
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get ID of first admin
|
||||||
|
let settings = Settings::get(&mut transaction).await?;
|
||||||
|
let first_admin = settings
|
||||||
|
.first_admin()
|
||||||
|
.ok_or_else(|| Error::bad_request("First user is not set"))?;
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
let user = User::get_by_id(&mut transaction, &id.0)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Could not find user"))?;
|
||||||
|
|
||||||
|
// If attempting to archive first admin user
|
||||||
|
if user.id().as_ref() == first_admin {
|
||||||
|
return Err(Error::bad_request("Can't archive first admin user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new status
|
||||||
|
user.set_archive_status(&mut transaction, true).await?;
|
||||||
|
|
||||||
|
// Revoke refresh tokens
|
||||||
|
refresh_tokens::RefreshToken::revoke_all_for_user(&mut transaction, user.id()).await?;
|
||||||
|
|
||||||
|
// Use all authorization codes
|
||||||
|
AuthorizationCode::use_all_for_user(&mut transaction, user.id()).await?;
|
||||||
|
|
||||||
|
// Use all totp login requests
|
||||||
|
if user.totp_secret().is_some() {
|
||||||
|
TotpLoginRequest::use_all_for_user(&mut transaction, user.id()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
(
|
||||||
|
Redirect::to(uri!(admin_users_list)),
|
||||||
|
FlashKind::Success,
|
||||||
|
"User has been archived.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
(_, Some(true)) => {
|
||||||
|
// Restore user
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
let user = User::get_by_id(&mut transaction, &id.0)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found("Could not find user"))?;
|
||||||
|
|
||||||
|
// Set new status
|
||||||
|
user.set_archive_status(&mut transaction, false).await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
(
|
||||||
|
Redirect::to(uri!(admin_users_view(id))),
|
||||||
|
FlashKind::Success,
|
||||||
|
"User has been restored.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Nothing to do
|
||||||
|
(
|
||||||
|
Redirect::to(uri!(admin_users_view(id))),
|
||||||
|
FlashKind::Warning,
|
||||||
|
"Nothing to do.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Flash::new(redirect, flash_kind, flash_message))
|
||||||
|
}
|
||||||
|
|
|
||||||
341
crates/ezidam/templates/pages/admin/users/view.html.tera
Normal file
341
crates/ezidam/templates/pages/admin/users/view.html.tera
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
{% 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">
|
||||||
|
Users
|
||||||
|
</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 local.is_archived %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Archived user</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>This user is archived.</p>
|
||||||
|
|
||||||
|
<form action="{{ local.id }}/archive" method="post">
|
||||||
|
<button type="submit" name="restore" value="true"
|
||||||
|
class="btn btn-primary">
|
||||||
|
Restore user
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">User information</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="datagrid">
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">
|
||||||
|
<label class="required" for="username">Username</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-content">
|
||||||
|
<div class="input-icon">
|
||||||
|
<span class="input-icon-addon">
|
||||||
|
{% include "icons/id-badge-2" %}
|
||||||
|
</span>
|
||||||
|
<input name="username" id="username" value="{{ local.username }}" type="text"
|
||||||
|
placeholder="Enter a username"
|
||||||
|
class="form-control"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">
|
||||||
|
<label for="name">Full Name</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-content">
|
||||||
|
<div class="input-icon">
|
||||||
|
<span class="input-icon-addon">
|
||||||
|
{% include "icons/user" %}
|
||||||
|
</span>
|
||||||
|
<input name="name" id="name" value="{{ local.name }}" type="text"
|
||||||
|
placeholder="Napoleon Bonaparte"
|
||||||
|
class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">
|
||||||
|
<label for="email">Email address</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-content">
|
||||||
|
<div class="input-icon">
|
||||||
|
<span class="input-icon-addon">
|
||||||
|
{% include "icons/at" %}
|
||||||
|
</span>
|
||||||
|
<input name="email" id="email" value="{{ local.email }}" type="email"
|
||||||
|
placeholder="napoleon@bonaparte.fr"
|
||||||
|
class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">Admin status</div>
|
||||||
|
<div class="datagrid-content">
|
||||||
|
<div class="mt-2">
|
||||||
|
<label class="form-check">
|
||||||
|
{% if local.is_admin %}
|
||||||
|
<input class="form-check-input" type="checkbox" checked>
|
||||||
|
{% else %}
|
||||||
|
<input class="form-check-input" type="checkbox">
|
||||||
|
{% endif %}
|
||||||
|
<span class="form-check-label">Administrator</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer text-end">
|
||||||
|
<div class="d-flex">
|
||||||
|
<button type="submit" class="btn btn-primary ms-auto">Save</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Security</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="datagrid">
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">Password</div>
|
||||||
|
<div class="datagrid-content">
|
||||||
|
<div>
|
||||||
|
{% if local.password %}
|
||||||
|
<span class="text-green">{% include "icons/check" %}</span>
|
||||||
|
Password is set
|
||||||
|
{% else %}
|
||||||
|
<span class="text-red">{% include "icons/x" %}</span>
|
||||||
|
Password is not set
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if local.password %}
|
||||||
|
<div class="mt-1">
|
||||||
|
{% if local.paper_key %}
|
||||||
|
<span class="text-green">{% include "icons/check" %}</span>
|
||||||
|
Paper key is set
|
||||||
|
{% else %}
|
||||||
|
<span class="text-red">{% include "icons/x" %}</span>
|
||||||
|
Paper key is not set
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if local.password_recover %}
|
||||||
|
<div class="mt-1">
|
||||||
|
<span class="text-yellow">{% include "icons/progress" %}</span>
|
||||||
|
Password reset has been requested
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">2FA (TOTP)</div>
|
||||||
|
<div class="datagrid-content">
|
||||||
|
<div>
|
||||||
|
{% if local.totp_secret %}
|
||||||
|
<span class="text-green">{% include "icons/check" %}</span>
|
||||||
|
TOTP is enabled
|
||||||
|
{% else %}
|
||||||
|
<span class="text-red">{% include "icons/x" %}</span>
|
||||||
|
TOTP is not enabled
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if local.totp_secret %}
|
||||||
|
<div class="mt-1">
|
||||||
|
{% if local.totp_backup %}
|
||||||
|
<span class="text-green">{% include "icons/check" %}</span>
|
||||||
|
TOTP backup is enabled
|
||||||
|
{% else %}
|
||||||
|
<span class="text-red">{% include "icons/x" %}</span>
|
||||||
|
TOTP backup is not enabled
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Timings</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="datagrid">
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">Account Creation</div>
|
||||||
|
<div class="datagrid-content">
|
||||||
|
{{ local.created_at | date(format="%F %T", timezone=user.zoneinfo | default(value="UTC")) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">Account Modification</div>
|
||||||
|
<div class="datagrid-content">
|
||||||
|
{{ local.updated_at | date(format="%F %T", timezone=user.zoneinfo | default(value="UTC")) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="datagrid-item">
|
||||||
|
<div class="datagrid-title">Account Timezone</div>
|
||||||
|
<div class="datagrid-content">{{ local.timezone }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 card">
|
||||||
|
<div class="card-header bg-danger-lt">
|
||||||
|
<h3 class="card-title">Danger zone</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Archive user -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modal-archive">
|
||||||
|
Archive user
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if local.password %}
|
||||||
|
<h2 class="mb-4">Password</h2>
|
||||||
|
|
||||||
|
<!-- Reset password -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modal-password-reset">
|
||||||
|
Force password reset
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reset paper key -->
|
||||||
|
{% if local.paper_key %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modal-paper-key">
|
||||||
|
Reset paper key
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if local.totp_secret %}
|
||||||
|
<h2 class="mb-4">TOTP</h2>
|
||||||
|
|
||||||
|
<!-- Disable TOTP -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modal-totp-secret">
|
||||||
|
Disable TOTP
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if local.totp_backup %}
|
||||||
|
<!-- Delete TOTP backup -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modal-totp-backup">
|
||||||
|
Delete TOTP backup
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</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 user?</h3>
|
||||||
|
<div class="mt-2">This user will not be able to log in.</div>
|
||||||
|
<div class="mt-2">This action can take up to {{ jwt_duration }} minutes to be effective.</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="{{ local.id }}/archive" method="post">
|
||||||
|
<button type="submit" name="archive" value="true"
|
||||||
|
class="btn btn-danger w-100">
|
||||||
|
Archive user
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block libs_js %}
|
||||||
|
{% endblock lib_js %}
|
||||||
|
|
||||||
|
{% block additional_js %}
|
||||||
|
{% endblock additional_js %}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue