diff --git a/Cargo.lock b/Cargo.lock index fbc37a0..033c153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2213,7 +2213,7 @@ dependencies = [ [[package]] name = "textsender_auth" -version = "0.1.19" +version = "0.1.20" dependencies = [ "argon2", "async-std", diff --git a/Cargo.toml b/Cargo.toml index 173aa37..8da8884 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_auth" -version = "0.1.19" +version = "0.1.20" edition = "2024" rust-version = "1.95" diff --git a/src/callers/login.rs b/src/callers/login.rs index 60ad921..3d81475 100644 --- a/src/callers/login.rs +++ b/src/callers/login.rs @@ -59,6 +59,24 @@ pub mod request { } } } + + #[derive(Deserialize, utoipa::ToSchema)] + pub struct UserUpdateNameRequest { + pub user_id: uuid::Uuid, + pub firstname: String, + pub lastname: String, + } + + impl UserUpdateNameRequest { + pub fn is_valid(&self) -> (bool, Option) { + if self.user_id.is_nil() || self.firstname.is_empty() || self.lastname.is_empty() { + let reason = String::from("Missing fields"); + (false, Some(reason)) + } else { + (true, None) + } + } + } } pub mod response { @@ -96,6 +114,12 @@ pub mod response { pub message: String, pub data: Vec, } + + #[derive(Deserialize, Serialize, utoipa::ToSchema)] + pub struct UserUpdateNameResponse { + pub message: String, + pub data: Vec, + } } /// Endpoint for a user login @@ -715,3 +739,92 @@ pub async fn update_password( } } } + +/// Endpoint for a updating password +#[utoipa::path( + patch, + path = super::endpoints::UPDATE_USER_NAME, + request_body( + content = request::UserUpdateNameRequest, + description = "Data required to update name of a user", + content_type = "application/json" + ), + responses( + (status = 200, description = "Names were updated", body = response::UserUpdateNameResponse), + (status = 304, description = "Nothing to change", body = response::UserUpdateNameResponse), + (status = 400, description = "Bad data", body = response::UserUpdateNameResponse), + (status = 500, description = "Something went wrong", body = response::UserUpdateNameResponse) + ) +)] +pub async fn update_name_of_user( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, +) -> ( + axum::http::StatusCode, + axum::Json, +) { + let (valid_request, reason) = payload.is_valid(); + if !valid_request { + ( + axum::http::StatusCode::BAD_REQUEST, + axum::Json(response::UserUpdateNameResponse { + message: reason.unwrap_or_default(), + data: Vec::new(), + }), + ) + } else { + match repo::user::get_with_id(&pool, &payload.user_id).await { + Ok(user) => { + if user.firstname == payload.firstname || user.lastname == payload.lastname { + ( + axum::http::StatusCode::NOT_MODIFIED, + axum::Json(response::UserUpdateNameResponse { + message: String::from("No change"), + data: Vec::new(), + }), + ) + } else { + match repo::user::update_name( + &pool, + &user.id, + &payload.firstname, + &payload.lastname, + ) + .await + { + Ok(_) => ( + axum::http::StatusCode::OK, + axum::Json(response::UserUpdateNameResponse { + message: String::from(super::messages::SUCCESSFUL_MESSAGE), + data: vec![textsender_models::user::User { + id: user.id, + phone_number: user.phone_number, + firstname: payload.firstname, + lastname: payload.lastname, + username: user.username, + created: user.created, + last_login: user.last_login, + ..Default::default() + }], + }), + ), + Err(err) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::UserUpdateNameResponse { + message: err.to_string(), + data: Vec::new(), + }), + ), + } + } + } + Err(err) => ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response::UserUpdateNameResponse { + message: err.to_string(), + data: Vec::new(), + }), + ), + } + } +} diff --git a/src/callers/mod.rs b/src/callers/mod.rs index cbd82aa..0dbf0a8 100644 --- a/src/callers/mod.rs +++ b/src/callers/mod.rs @@ -17,4 +17,6 @@ pub mod endpoints { pub const REFRESH_TOKEN: &str = "/api/v1/token/refresh"; /// Endpoint constant for updating password pub const UPDATE_PASSWORD: &str = "/api/v1/user/password/update"; + /// Endpoint constant for updating user's name + pub const UPDATE_USER_NAME: &str = "/api/v1/user/name/update"; } diff --git a/src/lib.rs b/src/lib.rs index fbfa407..d56cab5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,6 +114,10 @@ pub mod init { callers::endpoints::UPDATE_PASSWORD, patch(callers::login::update_password), ) + .route( + callers::endpoints::UPDATE_USER_NAME, + patch(callers::login::update_name_of_user), + ) .layer(cors::configure_cors().await) } diff --git a/src/repo/mod.rs b/src/repo/mod.rs index d0ff331..81f37a9 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -130,6 +130,34 @@ pub mod user { } } + pub async fn update_name( + pool: &sqlx::PgPool, + id: &uuid::Uuid, + firstname: &str, + lastname: &str, + ) -> Result<(), sqlx::Error> { + match sqlx::query( + r#" + UPDATE "user" SET firstname = $1, lastname = $2 WHERE id = $3 + "#, + ) + .bind(firstname) + .bind(lastname) + .bind(id) + .execute(pool) + .await + { + Ok(row) => { + if row.rows_affected() > 0 { + Ok(()) + } else { + Err(sqlx::Error::RowNotFound) + } + } + Err(err) => Err(err), + } + } + pub async fn exists(pool: &sqlx::PgPool, username: &String) -> Result { let result = sqlx::query( r#" diff --git a/tests/tests.rs b/tests/tests.rs index e6e09b3..d1927a4 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -172,6 +172,7 @@ pub mod requests { app.clone().oneshot(req).await } + /// Function to call update password endpoint pub async fn update_password( app: &axum::Router, user_id: &uuid::Uuid, @@ -198,6 +199,32 @@ pub mod requests { Err(err) => Err(err), } } + + /// Function to call update name of user endpoint + pub async fn update_name_of_user( + app: &axum::Router, + user_id: &uuid::Uuid, + firstname: &str, + lastname: &str, + ) -> Result { + let payload = serde_json::json!({ + "user_id": user_id, + "firstname": firstname, + "lastname": lastname, + }); + match axum::http::Request::builder() + .method(axum::http::Method::PATCH) + .uri(super::callers::endpoints::UPDATE_USER_NAME) + .header(axum::http::header::CONTENT_TYPE, "application/json") + .body(axum::body::Body::from(payload.to_string())) + { + Ok(req) => match app.clone().oneshot(req).await { + Ok(resp) => Ok(resp), + Err(err) => Err(axum::http::Error::from(err)), + }, + Err(err) => Err(err), + } + } } /// Test user firstname @@ -213,6 +240,11 @@ const TEST_PHONE_NUMBER: &str = "+10123456789"; /// Test updated user password const TEST_UPDATED_PASSWORD: &str = "3cnf29ry8q27i3yrc928qi37ryndxc2198q7yd9xzq12837e"; +/// Test updated user firstname +const TEST_UPDATED_FIRSTNAME: &str = "Kuoth"; +/// Test updated user lastname +const TEST_UPDATED_LASTNAME: &str = "Wech"; + /// Test service username const TEST_SERVICE_USERNAME: &str = "swoon"; /// Test service passphrase @@ -453,6 +485,41 @@ mod flow { Err(err) => Err(std::io::Error::other(err.to_string())), } } + + pub async fn update_name_of_user( + app: &axum::Router, + user_id: &uuid::Uuid, + firstname: &str, + lastname: &str, + ) -> Result { + match requests::update_name_of_user(app, user_id, firstname, lastname).await { + Ok(response) => { + if axum::http::StatusCode::OK != response.status() { + Err(std::io::Error::other(format!( + "Status code is off {:?}", + response.status() + ))) + } else { + match util::convert_response::( + response, + ) + .await + { + Ok(response) => { + if response.data.len() > 0 { + let user = response.data[0].clone(); + Ok(user) + } else { + Err(std::io::Error::other("No data returned")) + } + } + Err(err) => Err(err), + } + } + } + Err(err) => Err(std::io::Error::other(err.to_string())), + } + } } #[tokio::test] @@ -853,3 +920,72 @@ async fn test_update_password() { } } } + +#[tokio::test] +async fn test_update_name_of_password() { + let tm_pool = db_mgr::get_pool().await.unwrap(); + let db_name = db_mgr::generate_db_name().await; + + match db_mgr::create_database(&tm_pool, &db_name).await { + Ok(_) => { + println!("Success"); + } + Err(e) => { + assert!(false, "Error: {:?}", e.to_string()); + } + } + + let pool = db_mgr::connect_to_db(&db_name).await.unwrap(); + + db::init::migrations(&pool).await; + + let app = init::routes().await.layer(axum::Extension(pool)); + + match flow::register_user(&app).await { + Ok(user) => match flow::login_user(&app, &user.username, TEST_PASSWORD).await { + Ok(login_result) => { + assert_eq!( + false, + login_result.access_token.is_empty(), + "Access token is empty when it should not be" + ); + + match flow::update_name_of_user( + &app, + &user.id, + TEST_UPDATED_FIRSTNAME, + TEST_UPDATED_LASTNAME, + ) + .await + { + Ok(user) => { + assert_eq!( + TEST_UPDATED_FIRSTNAME, user.firstname, + "Firstname do not match" + ); + assert_eq!( + TEST_UPDATED_LASTNAME, user.lastname, + "Lastname do not match" + ); + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + } + Err(err) => { + assert!(false, "Error: {err:?}"); + } + }, + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } + + match db_mgr::drop_database(&tm_pool, &db_name).await { + Ok(()) => {} + Err(err) => { + assert!(false, "Error: {err:?}"); + } + } +}