personal settings: update username, name, email
This commit is contained in:
parent
1346b57b30
commit
a47e4c204a
14 changed files with 247 additions and 5 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -724,6 +724,7 @@ dependencies = [
|
||||||
"authorization_codes",
|
"authorization_codes",
|
||||||
"base64 0.21.0",
|
"base64 0.21.0",
|
||||||
"database_pool",
|
"database_pool",
|
||||||
|
"email_address",
|
||||||
"erased-serde",
|
"erased-serde",
|
||||||
"futures",
|
"futures",
|
||||||
"hash",
|
"hash",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
nanoid = "0.4"
|
nanoid = "0.4"
|
||||||
nanoid-dictionary = "0.4"
|
nanoid-dictionary = "0.4"
|
||||||
|
email_address = { version = "0.2", default-features = false }
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
||||||
5
crates/database/queries/users/set_email.sql
Normal file
5
crates/database/queries/users/set_email.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
update users
|
||||||
|
|
||||||
|
set email = ?
|
||||||
|
|
||||||
|
where id is ?
|
||||||
5
crates/database/queries/users/set_name.sql
Normal file
5
crates/database/queries/users/set_name.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
update users
|
||||||
|
|
||||||
|
set name = ?
|
||||||
|
|
||||||
|
where id is ?
|
||||||
5
crates/database/queries/users/set_username.sql
Normal file
5
crates/database/queries/users/set_username.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
update users
|
||||||
|
|
||||||
|
set username = ?
|
||||||
|
|
||||||
|
where id is ?
|
||||||
|
|
@ -268,6 +268,16 @@
|
||||||
},
|
},
|
||||||
"query": "insert into users (id, is_admin, username, password)\nvalues (?, ?, ?, ?)\n"
|
"query": "insert into users (id, is_admin, username, password)\nvalues (?, ?, ?, ?)\n"
|
||||||
},
|
},
|
||||||
|
"545f19b0373c7ffe16864eb242c15a0092355e120c5cbe006c877afdfc4a4e8c": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "update users\n\nset username = ?\n\nwhere id is ?"
|
||||||
|
},
|
||||||
"56a9c0dff010858189a95087d014c7d0ce930da5d841b9d788a9c0e84b580bc6": {
|
"56a9c0dff010858189a95087d014c7d0ce930da5d841b9d788a9c0e84b580bc6": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
@ -518,6 +528,16 @@
|
||||||
},
|
},
|
||||||
"query": "update settings\n\nset url = ?\n\nwhere id is 0\n"
|
"query": "update settings\n\nset url = ?\n\nwhere id is 0\n"
|
||||||
},
|
},
|
||||||
|
"8c131e1f73ffa01fc3e5e08071a786b85f23b9638d1c7eaa7b633c052703c911": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "update users\n\nset name = ?\n\nwhere id is ?"
|
||||||
|
},
|
||||||
"93b15a942a6c7db595990f00e14fde26d6d36b8c8de9935179d41f6c7c755978": {
|
"93b15a942a6c7db595990f00e14fde26d6d36b8c8de9935179d41f6c7c755978": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
|
@ -656,6 +676,16 @@
|
||||||
},
|
},
|
||||||
"query": "update refresh_tokens\n\nset revoked_at = CURRENT_TIMESTAMP\n\nwhere user is ?\n and revoked_at is null"
|
"query": "update refresh_tokens\n\nset revoked_at = CURRENT_TIMESTAMP\n\nwhere user is ?\n and revoked_at is null"
|
||||||
},
|
},
|
||||||
|
"c28c88869239edc02c073f461645eca82d816650fabe65464e2059d5908d8a28": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "update users\n\nset email = ?\n\nwhere id is ?"
|
||||||
|
},
|
||||||
"c5a57c971d07532ec0cc897b5ac06e0814e506f9c24647d1eaf44174dc0a5954": {
|
"c5a57c971d07532ec0cc897b5ac06e0814e506f9c24647d1eaf44174dc0a5954": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,44 @@ impl Users {
|
||||||
.await
|
.await
|
||||||
.map_err(handle_error)
|
.map_err(handle_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_username(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
id: &str,
|
||||||
|
username: &str,
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
let query: SqliteQueryResult =
|
||||||
|
sqlx::query_file!("queries/users/set_username.sql", username, id)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)?;
|
||||||
|
|
||||||
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_name(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
id: &str,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
let query: SqliteQueryResult = sqlx::query_file!("queries/users/set_name.sql", name, id)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)?;
|
||||||
|
|
||||||
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_email(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
id: &str,
|
||||||
|
email: &str,
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
let query: SqliteQueryResult = sqlx::query_file!("queries/users/set_email.sql", email, id)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)?;
|
||||||
|
|
||||||
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ identicon-rs = "4.0"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
base64 = "0.21.0"
|
base64 = "0.21.0"
|
||||||
rocket_cors = "0.6.0-alpha2"
|
rocket_cors = "0.6.0-alpha2"
|
||||||
|
email_address = { workspace = true }
|
||||||
|
|
||||||
# local crates
|
# local crates
|
||||||
database_pool = { path = "../database_pool" }
|
database_pool = { path = "../database_pool" }
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ impl UserMenu {
|
||||||
icon: Icon::Settings.svg,
|
icon: Icon::Settings.svg,
|
||||||
sub: Some(vec![SubItem {
|
sub: Some(vec![SubItem {
|
||||||
label: "Personal",
|
label: "Personal",
|
||||||
link: uri!(routes::settings::user_settings_personal).to_string(),
|
link: uri!(routes::settings::personal::user_settings_personal).to_string(),
|
||||||
}]),
|
}]),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
use super::prelude::*;
|
use super::prelude::*;
|
||||||
pub use personal::*;
|
use personal::*;
|
||||||
use rocket::get;
|
use rocket::get;
|
||||||
|
|
||||||
pub mod personal;
|
pub mod personal;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![user_settings, user_settings_personal]
|
routes![
|
||||||
|
user_settings,
|
||||||
|
user_settings_personal,
|
||||||
|
user_settings_personal_form,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/settings")]
|
#[get("/settings")]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
use crate::routes::prelude::*;
|
use crate::routes::prelude::*;
|
||||||
use rocket::get;
|
use crate::tokens::JWT_DURATION_MINUTES;
|
||||||
|
use email_address::EmailAddress;
|
||||||
|
use rocket::{get, post};
|
||||||
|
use std::str::FromStr;
|
||||||
use users::User;
|
use users::User;
|
||||||
|
|
||||||
#[get("/settings/personal")]
|
#[get("/settings/personal")]
|
||||||
|
|
@ -23,3 +26,91 @@ pub async fn user_settings_personal(
|
||||||
.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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct UpdatePersonalSettings<'r> {
|
||||||
|
pub username: &'r str,
|
||||||
|
pub name: &'r str,
|
||||||
|
pub email: &'r str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/settings/personal", data = "<form>")]
|
||||||
|
pub async fn user_settings_personal_form(
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
jwt_user: JwtUser,
|
||||||
|
form: Form<UpdatePersonalSettings<'_>>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
let user = User::get_by_login(&mut transaction, &jwt_user.0.subject)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found(jwt_user.0.subject.to_string()))?;
|
||||||
|
|
||||||
|
if user.is_archived() {
|
||||||
|
return Err(Error::forbidden("User is archived"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update username
|
||||||
|
if user.username() != form.username {
|
||||||
|
if let Err(e) = user.set_username(&mut transaction, form.username).await {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(user_settings_personal)),
|
||||||
|
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!(user_settings_personal)),
|
||||||
|
FlashKind::Danger,
|
||||||
|
e.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = user.set_email(&mut transaction, email).await {
|
||||||
|
return Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(user_settings_personal)),
|
||||||
|
FlashKind::Danger,
|
||||||
|
e.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(user_settings_personal)),
|
||||||
|
FlashKind::Success,
|
||||||
|
format!(
|
||||||
|
"Personal settings have been saved.\
|
||||||
|
<br>Some changes can take up to {JWT_DURATION_MINUTES} minutes to appear."
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,4 @@ hash = { path = "../hash" }
|
||||||
id = { path = "../id" }
|
id = { path = "../id" }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
email_address = { version = "0.2", default-features = false }
|
email_address = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::User;
|
use crate::User;
|
||||||
use database::sqlx::SqliteExecutor;
|
use database::sqlx::SqliteExecutor;
|
||||||
|
use database::Error as DatabaseError;
|
||||||
use database::Users as DatabaseUsers;
|
use database::Users as DatabaseUsers;
|
||||||
use email_address::EmailAddress;
|
use email_address::EmailAddress;
|
||||||
use hash::Password;
|
use hash::Password;
|
||||||
|
|
@ -105,4 +106,53 @@ impl User {
|
||||||
.await?
|
.await?
|
||||||
.map(Self::from))
|
.map(Self::from))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_username(
|
||||||
|
&self,
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
username: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
DatabaseUsers::set_username(conn, self.id.as_ref(), username)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e {
|
||||||
|
DatabaseError::UniqueConstraint(column) => {
|
||||||
|
if &column == "username" {
|
||||||
|
Error::UsernameNotAvailable(username.into())
|
||||||
|
} else {
|
||||||
|
Error::ColumnNotAvailable(column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => e.into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_name(&self, conn: impl SqliteExecutor<'_>, name: &str) -> Result<(), Error> {
|
||||||
|
DatabaseUsers::set_name(conn, self.id.as_ref(), name).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_email(
|
||||||
|
&self,
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
email: EmailAddress,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let email = email.as_str();
|
||||||
|
DatabaseUsers::set_email(conn, self.id.as_ref(), email)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e {
|
||||||
|
DatabaseError::UniqueConstraint(column) => {
|
||||||
|
if &column == "email" {
|
||||||
|
Error::EmailNotAvailable(email.into())
|
||||||
|
} else {
|
||||||
|
Error::ColumnNotAvailable(column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => e.into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,13 @@
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Database: {0}")]
|
#[error("Database: {0}")]
|
||||||
Database(#[from] database::Error),
|
Database(#[from] database::Error),
|
||||||
|
|
||||||
|
#[error("The database column \"{0}\" is not available.")]
|
||||||
|
ColumnNotAvailable(String),
|
||||||
|
|
||||||
|
#[error("The username \"{0}\" is not available.")]
|
||||||
|
UsernameNotAvailable(String),
|
||||||
|
|
||||||
|
#[error("The email \"{0}\" is linked to another user.")]
|
||||||
|
EmailNotAvailable(String),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue