admin/users: update username/name/email/admin status

This commit is contained in:
Philippe Loctaux 2023-05-04 23:53:32 +02:00
parent bdd5eca9f1
commit 05373a2800
7 changed files with 202 additions and 50 deletions

View file

@ -0,0 +1,5 @@
update users
set is_admin = ?
where id is ?

View file

@ -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": [

View file

@ -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(()))
}
} }

View file

@ -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,
] ]
} }

View file

@ -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."
),
))
}

View file

@ -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">

View file

@ -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(())
}
} }