admin/users: update username/name/email/admin status
This commit is contained in:
parent
bdd5eca9f1
commit
05373a2800
7 changed files with 202 additions and 50 deletions
5
crates/database/queries/users/set_admin_status.sql
Normal file
5
crates/database/queries/users/set_admin_status.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
update users
|
||||||
|
|
||||||
|
set is_admin = ?
|
||||||
|
|
||||||
|
where id is ?
|
||||||
|
|
@ -546,6 +546,16 @@
|
||||||
},
|
},
|
||||||
"query": "update users\n\nset paper_key = ?\n\nwhere id is ?"
|
"query": "update users\n\nset paper_key = ?\n\nwhere id is ?"
|
||||||
},
|
},
|
||||||
|
"6ff12f357d884a50035d708577a7f3109a07a1ca193cb3082d13687af65c6de0": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "update users\n\nset is_admin = ?\n\nwhere id is ?"
|
||||||
|
},
|
||||||
"71c74369dc5d374d8ec5aa347b5f599728b74e545df3e986e3e7e66882f73ba0": {
|
"71c74369dc5d374d8ec5aa347b5f599728b74e545df3e986e3e7e66882f73ba0": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
|
||||||
|
|
@ -257,4 +257,18 @@ impl Users {
|
||||||
|
|
||||||
Ok((query.rows_affected() == 1).then_some(()))
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_admin_status(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
id: &str,
|
||||||
|
value: bool,
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
let query: SqliteQueryResult =
|
||||||
|
sqlx::query_file!("queries/users/set_admin_status.sql", value, id)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)?;
|
||||||
|
|
||||||
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
admin_users_paper_key_reset,
|
admin_users_paper_key_reset,
|
||||||
admin_users_totp_secret_disable,
|
admin_users_totp_secret_disable,
|
||||||
admin_users_totp_backup_delete,
|
admin_users_totp_backup_delete,
|
||||||
|
admin_users_info_update,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ use crate::routes::root::forgot_password::ResetPasswordEmail;
|
||||||
use crate::tokens::JWT_DURATION_MINUTES;
|
use crate::tokens::JWT_DURATION_MINUTES;
|
||||||
use authorization_codes::AuthorizationCode;
|
use authorization_codes::AuthorizationCode;
|
||||||
use chrono_humanize::Humanize;
|
use chrono_humanize::Humanize;
|
||||||
|
use email_address::EmailAddress;
|
||||||
use rocket::State;
|
use rocket::State;
|
||||||
use rocket::{get, post};
|
use rocket::{get, post};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
use std::str::FromStr;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use users::totp_login_request::TotpLoginRequest;
|
use users::totp_login_request::TotpLoginRequest;
|
||||||
use users::{password_reset::PasswordResetToken, User};
|
use users::{password_reset::PasswordResetToken, User};
|
||||||
|
|
@ -446,3 +448,111 @@ pub async fn admin_users_totp_backup_delete(
|
||||||
flash_message,
|
flash_message,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct UpdateUserForm<'r> {
|
||||||
|
pub username: &'r str,
|
||||||
|
pub name: &'r str,
|
||||||
|
pub email: &'r str,
|
||||||
|
pub is_admin: Option<&'r str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/admin/users/<id>/info", data = "<form>")]
|
||||||
|
pub async fn admin_users_info_update(
|
||||||
|
_admin_not_current: JwtAdminNotCurrent,
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
id: RocketUserID,
|
||||||
|
form: Form<UpdateUserForm<'_>>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
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"))?;
|
||||||
|
|
||||||
|
if user.is_archived() {
|
||||||
|
return Err(Error::forbidden("User is archived"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update username
|
||||||
|
if user.username().0 != form.username {
|
||||||
|
// Parse username
|
||||||
|
let username = match Username::from_str(form.username) {
|
||||||
|
Ok(username) => username,
|
||||||
|
Err(_) => {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(admin_users_view(id))),
|
||||||
|
FlashKind::Danger,
|
||||||
|
INVALID_USERNAME_ERROR,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = user.set_username(&mut transaction, &username).await {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(admin_users_view(id))),
|
||||||
|
FlashKind::Danger,
|
||||||
|
e.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update name
|
||||||
|
if !form.name.is_empty()
|
||||||
|
&& user
|
||||||
|
.name()
|
||||||
|
// If it exists in database, check if provided value is different
|
||||||
|
.map(|current| current != form.name)
|
||||||
|
// If it does not exist, use provided value
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
user.set_name(&mut transaction, form.name).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update email
|
||||||
|
if !form.email.is_empty()
|
||||||
|
&& user
|
||||||
|
.email()
|
||||||
|
// If it exists in database, check if provided value is different
|
||||||
|
.map(|current| current != form.email)
|
||||||
|
// If it does not exist, use provided value
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
// Parse email address
|
||||||
|
let email = match EmailAddress::from_str(form.email) {
|
||||||
|
Ok(email) => email,
|
||||||
|
Err(e) => {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(admin_users_view(id))),
|
||||||
|
FlashKind::Danger,
|
||||||
|
e.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = user.set_email(&mut transaction, email).await {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(admin_users_view(id))),
|
||||||
|
FlashKind::Danger,
|
||||||
|
e.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin status
|
||||||
|
let new_status = matches!(form.is_admin, Some("on"));
|
||||||
|
if user.is_admin() != new_status {
|
||||||
|
user.set_admin_status(&mut transaction, new_status).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(admin_users_view(id))),
|
||||||
|
FlashKind::Success,
|
||||||
|
format!(
|
||||||
|
"User has been updated.\
|
||||||
|
<br>Some changes can take up to {JWT_DURATION_MINUTES} minutes to appear."
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,84 +46,86 @@
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">User information</h3>
|
<h3 class="card-title">User information</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<form action="{{ local.id }}/info" method="post">
|
||||||
<div class="datagrid">
|
<div class="card-body">
|
||||||
<div class="datagrid-item">
|
<div class="datagrid">
|
||||||
<div class="datagrid-title">
|
<div class="datagrid-item">
|
||||||
<label class="required" for="username">Username</label>
|
<div class="datagrid-title">
|
||||||
</div>
|
<label class="required" for="username">Username</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="datagrid-content">
|
<div class="datagrid-content">
|
||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
<span class="input-icon-addon">
|
<span class="input-icon-addon">
|
||||||
{% include "icons/id-badge-2" %}
|
{% include "icons/id-badge-2" %}
|
||||||
</span>
|
</span>
|
||||||
<input name="username" id="username" value="{{ local.username }}" type="text"
|
<input name="username" id="username" value="{{ local.username }}" type="text"
|
||||||
placeholder="Enter a username"
|
placeholder="Enter a username"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
required>
|
required>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="datagrid-item">
|
<div class="datagrid-item">
|
||||||
<div class="datagrid-title">
|
<div class="datagrid-title">
|
||||||
<label for="name">Full Name</label>
|
<label for="name">Full Name</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="datagrid-content">
|
<div class="datagrid-content">
|
||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
<span class="input-icon-addon">
|
<span class="input-icon-addon">
|
||||||
{% include "icons/user" %}
|
{% include "icons/user" %}
|
||||||
</span>
|
</span>
|
||||||
<input name="name" id="name" value="{{ local.name }}" type="text"
|
<input name="name" id="name" value="{{ local.name }}" type="text"
|
||||||
placeholder="Napoleon Bonaparte"
|
placeholder="Napoleon Bonaparte"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="datagrid-item">
|
<div class="datagrid-item">
|
||||||
<div class="datagrid-title">
|
<div class="datagrid-title">
|
||||||
<label for="email">Email address</label>
|
<label for="email">Email address</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="datagrid-content">
|
<div class="datagrid-content">
|
||||||
<div class="input-icon">
|
<div class="input-icon">
|
||||||
<span class="input-icon-addon">
|
<span class="input-icon-addon">
|
||||||
{% include "icons/at" %}
|
{% include "icons/at" %}
|
||||||
</span>
|
</span>
|
||||||
<input name="email" id="email" value="{{ local.email }}" type="email"
|
<input name="email" id="email" value="{{ local.email }}" type="email"
|
||||||
placeholder="napoleon@bonaparte.fr"
|
placeholder="napoleon@bonaparte.fr"
|
||||||
class="form-control">
|
class="form-control">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="datagrid-item">
|
<div class="datagrid-item">
|
||||||
<div class="datagrid-title">Admin status</div>
|
<div class="datagrid-title">Admin status</div>
|
||||||
<div class="datagrid-content">
|
<div class="datagrid-content">
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<label class="form-check">
|
<label class="form-check">
|
||||||
{% if local.is_admin %}
|
{% if local.is_admin %}
|
||||||
<input class="form-check-input" type="checkbox" checked>
|
<input class="form-check-input" type="checkbox" name="is_admin" checked>
|
||||||
{% else %}
|
{% else %}
|
||||||
<input class="form-check-input" type="checkbox">
|
<input class="form-check-input" type="checkbox" name="is_admin">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="form-check-label">Administrator</span>
|
<span class="form-check-label">Administrator</span>
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-footer text-end">
|
<div class="card-footer text-end">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<button type="submit" class="btn btn-primary ms-auto">Save</button>
|
<button type="submit" class="btn btn-primary ms-auto">Save</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 card">
|
<div class="mt-4 card">
|
||||||
|
|
|
||||||
|
|
@ -270,4 +270,14 @@ impl User {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_admin_status(
|
||||||
|
&self,
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
value: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
DatabaseUsers::set_admin_status(conn, self.id.as_ref(), value).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue