From a47e4c204abe3fb3f3b22a008f240ef220a6f6d3 Mon Sep 17 00:00:00 2001 From: Philippe Loctaux Date: Wed, 5 Apr 2023 23:41:25 +0200 Subject: [PATCH] personal settings: update username, name, email --- Cargo.lock | 1 + Cargo.toml | 1 + crates/database/queries/users/set_email.sql | 5 + crates/database/queries/users/set_name.sql | 5 + .../database/queries/users/set_username.sql | 5 + crates/database/sqlx-data.json | 30 ++++++ crates/database/src/tables/users.rs | 40 ++++++++ crates/ezidam/Cargo.toml | 1 + crates/ezidam/src/menu/items/user.rs | 2 +- crates/ezidam/src/routes/settings.rs | 8 +- crates/ezidam/src/routes/settings/personal.rs | 93 ++++++++++++++++++- crates/users/Cargo.toml | 2 +- crates/users/src/database.rs | 50 ++++++++++ crates/users/src/error.rs | 9 ++ 14 files changed, 247 insertions(+), 5 deletions(-) create mode 100644 crates/database/queries/users/set_email.sql create mode 100644 crates/database/queries/users/set_name.sql create mode 100644 crates/database/queries/users/set_username.sql diff --git a/Cargo.lock b/Cargo.lock index aae1628..83c0a73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,6 +724,7 @@ dependencies = [ "authorization_codes", "base64 0.21.0", "database_pool", + "email_address", "erased-serde", "futures", "hash", diff --git a/Cargo.toml b/Cargo.toml index cf4e409..a38a89b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ serde = "1" serde_json = "1" nanoid = "0.4" nanoid-dictionary = "0.4" +email_address = { version = "0.2", default-features = false } [profile.dev.package.sqlx-macros] opt-level = 3 diff --git a/crates/database/queries/users/set_email.sql b/crates/database/queries/users/set_email.sql new file mode 100644 index 0000000..36550fb --- /dev/null +++ b/crates/database/queries/users/set_email.sql @@ -0,0 +1,5 @@ +update users + +set email = ? + +where id is ? \ No newline at end of file diff --git a/crates/database/queries/users/set_name.sql b/crates/database/queries/users/set_name.sql new file mode 100644 index 0000000..6b34e32 --- /dev/null +++ b/crates/database/queries/users/set_name.sql @@ -0,0 +1,5 @@ +update users + +set name = ? + +where id is ? \ No newline at end of file diff --git a/crates/database/queries/users/set_username.sql b/crates/database/queries/users/set_username.sql new file mode 100644 index 0000000..ecae4c9 --- /dev/null +++ b/crates/database/queries/users/set_username.sql @@ -0,0 +1,5 @@ +update users + +set username = ? + +where id is ? \ No newline at end of file diff --git a/crates/database/sqlx-data.json b/crates/database/sqlx-data.json index fc8eabf..7e61b54 100644 --- a/crates/database/sqlx-data.json +++ b/crates/database/sqlx-data.json @@ -268,6 +268,16 @@ }, "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": { "describe": { "columns": [ @@ -518,6 +528,16 @@ }, "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": { "describe": { "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" }, + "c28c88869239edc02c073f461645eca82d816650fabe65464e2059d5908d8a28": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "update users\n\nset email = ?\n\nwhere id is ?" + }, "c5a57c971d07532ec0cc897b5ac06e0814e506f9c24647d1eaf44174dc0a5954": { "describe": { "columns": [ diff --git a/crates/database/src/tables/users.rs b/crates/database/src/tables/users.rs index 2b004af..8992685 100644 --- a/crates/database/src/tables/users.rs +++ b/crates/database/src/tables/users.rs @@ -95,4 +95,44 @@ impl Users { .await .map_err(handle_error) } + + pub async fn set_username( + conn: impl SqliteExecutor<'_>, + id: &str, + username: &str, + ) -> Result, 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, 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, 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(())) + } } diff --git a/crates/ezidam/Cargo.toml b/crates/ezidam/Cargo.toml index d849858..863fa87 100644 --- a/crates/ezidam/Cargo.toml +++ b/crates/ezidam/Cargo.toml @@ -14,6 +14,7 @@ identicon-rs = "4.0" futures = "0.3" base64 = "0.21.0" rocket_cors = "0.6.0-alpha2" +email_address = { workspace = true } # local crates database_pool = { path = "../database_pool" } diff --git a/crates/ezidam/src/menu/items/user.rs b/crates/ezidam/src/menu/items/user.rs index c722103..a0b67c1 100644 --- a/crates/ezidam/src/menu/items/user.rs +++ b/crates/ezidam/src/menu/items/user.rs @@ -31,7 +31,7 @@ impl UserMenu { icon: Icon::Settings.svg, sub: Some(vec![SubItem { label: "Personal", - link: uri!(routes::settings::user_settings_personal).to_string(), + link: uri!(routes::settings::personal::user_settings_personal).to_string(), }]), }, ] diff --git a/crates/ezidam/src/routes/settings.rs b/crates/ezidam/src/routes/settings.rs index 031c2a8..838454d 100644 --- a/crates/ezidam/src/routes/settings.rs +++ b/crates/ezidam/src/routes/settings.rs @@ -1,11 +1,15 @@ use super::prelude::*; -pub use personal::*; +use personal::*; use rocket::get; pub mod personal; pub fn routes() -> Vec { - routes![user_settings, user_settings_personal] + routes![ + user_settings, + user_settings_personal, + user_settings_personal_form, + ] } #[get("/settings")] diff --git a/crates/ezidam/src/routes/settings/personal.rs b/crates/ezidam/src/routes/settings/personal.rs index 958b881..55066d0 100644 --- a/crates/ezidam/src/routes/settings/personal.rs +++ b/crates/ezidam/src/routes/settings/personal.rs @@ -1,5 +1,8 @@ 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; #[get("/settings/personal")] @@ -23,3 +26,91 @@ pub async fn user_settings_personal( .map(|flash| Page::with_flash(page.clone(), flash)) .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 = "
")] +pub async fn user_settings_personal_form( + mut db: Connection, + jwt_user: JwtUser, + form: Form>, +) -> Result> { + 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.\ +
Some changes can take up to {JWT_DURATION_MINUTES} minutes to appear." + ), + )) +} diff --git a/crates/users/Cargo.toml b/crates/users/Cargo.toml index 13544da..461d414 100644 --- a/crates/users/Cargo.toml +++ b/crates/users/Cargo.toml @@ -9,4 +9,4 @@ hash = { path = "../hash" } id = { path = "../id" } thiserror = { workspace = true } chrono = { workspace = true } -email_address = { version = "0.2", default-features = false } +email_address = { workspace = true } diff --git a/crates/users/src/database.rs b/crates/users/src/database.rs index 1a96d42..3e44d73 100644 --- a/crates/users/src/database.rs +++ b/crates/users/src/database.rs @@ -1,6 +1,7 @@ use crate::error::Error; use crate::User; use database::sqlx::SqliteExecutor; +use database::Error as DatabaseError; use database::Users as DatabaseUsers; use email_address::EmailAddress; use hash::Password; @@ -105,4 +106,53 @@ impl User { .await? .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(()) + } } diff --git a/crates/users/src/error.rs b/crates/users/src/error.rs index 47def78..8fcff0a 100644 --- a/crates/users/src/error.rs +++ b/crates/users/src/error.rs @@ -5,4 +5,13 @@ pub enum Error { #[error("Database: {0}")] 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), }