Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update user logic #13

Merged
merged 9 commits into from
Sep 5, 2024
2 changes: 1 addition & 1 deletion macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
macro_rules! ensure {
( $x:expr, $y:expr $(,)? ) => {{
if !$x {
return Err($y)
return Err($y);
}
}};
}
93 changes: 93 additions & 0 deletions services/api/routes/src/update.rs
Original file line number Diff line number Diff line change
@@ -1 +1,94 @@
/*
** Update route should handle updating the information of the existing user.
** A user is allowed to update information such as follows:
** Chosen notifier, set to null to disable notifications
** The user's email address
** User's telegram handle
** User's enabled notifications
**
** Validate user by exists by using only the ID.
** If the ID exists, then it can be validated.
**
** How can we ensure that the owner of the email | tg is making the update?
bolajahmad marked this conversation as resolved.
Show resolved Hide resolved
*/

use crate::{
errors::{custom_error, Error},
update, LOG_TARGET,
};

use common_macros::ensure;
use rocket::{http::Status, put, response::status, serde::json::Json, State};
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use storage::{users::User, DbConn};
use types::{api::ErrorResponse, Notifier};

// If there is data that should not be updated, then pass current value.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(crate = "rocket::serde")]
pub struct UpdateData {
// The ID of the user to update
pub id: u32,
// The email address to update to,
pub email: Option<String>,
#[serde(rename = "tgHandle")]
// The telegram handle to update to
pub tg_handle: Option<String>,
// The desired notifier to use.
// If undefined, notifications will be turned off for user
// Pass current value if not to be updated
pub notifier: Option<Notifier>,
}

#[put("/update_user", data = "<update_data>")]
pub async fn update_user(
conn: &State<DbConn>,
update_data: Json<UpdateData>,
) -> Result<status::Custom<()>, status::Custom<Json<ErrorResponse>>> {
// validate the data passed
// Get data to update and serialize
// Update the data
log::info!(target: LOG_TARGET, "Update user request {:?}", update_data);

// Get connection:
let conn = conn.lock().map_err(|err| {
log::error!(target: LOG_TARGET, "DB connection failed: {:?}", err);
custom_error(Status::InternalServerError, Error::DbConnectionFailed)
})?;

// Ensure user exists
let db_user = verify_existing_id(&conn, update_data.id.clone())?;

let user = User {
email: update_data.email.clone(),
tg_handle: update_data.tg_handle.clone(),
id: update_data.id.clone(),
notifier: if update_data.notifier.clone().is_some() {
update_data.notifier.clone().unwrap()
} else {
db_user.notifier
},
};
let result = User::update(&conn, &user);

match result {
Ok(_) => Ok(status::Custom(Status::Ok, ())),
Err(_) => Err(custom_error(Status::InternalServerError, Error::DbError)),
}
}

fn verify_existing_id(
conn: &Connection,
user_id: u32,
) -> Result<User, status::Custom<Json<ErrorResponse>>> {
let maybe_user = User::query_by_id(&conn, user_id).map_err(|err| {
log::error!(target: LOG_TARGET, "Failed to search user by id: {:?}", err);
custom_error(Status::InternalServerError, Error::DbError)
})?;

match maybe_user {
Some(user) => Ok(user),
None => Err(custom_error(Status::NotFound, Error::UserNotFound)),
}
}
4 changes: 2 additions & 2 deletions services/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use rocket::{Build, Rocket};
use rocket_cors::CorsOptions;
use routes::{query::user, register::register_user};
use routes::{query::user, register::register_user, update::update_user};
use storage_service::init_db;

#[macro_use]
Expand All @@ -20,7 +20,7 @@ pub async fn rocket() -> Rocket<Build> {
rocket::build()
.attach(CorsOptions::default().to_cors().unwrap())
.manage(connection)
.mount("/", routes![register_user, user])
.mount("/", routes![register_user, user, update_user])
}

// There should be three paths: one POST path to set the notification configuration,
Expand Down
37 changes: 30 additions & 7 deletions services/storage/src/users.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rusqlite::{params, Connection, Result};
use rusqlite::{params, Connection, Error, Result};
use serde::{Deserialize, Serialize};
use types::Notifier;

Expand Down Expand Up @@ -108,17 +108,13 @@ impl User {

pub fn create_user(conn: &Connection, user: &User) -> Result<()> {
let User { id, email, tg_handle, .. } = user;
let notifier = match user.notifier {
Notifier::Email => Some("email"),
Notifier::Telegram => Some("telegram"),
_ => None,
};
let notifier = Self::notifier_to_text(&user.notifier);

match notifier {
Some(notifier) => {
conn.execute(
"INSERT INTO users
(id, email, tg_handle, notifier)
(email, tg_handle, notifier)
VALUES (?1, ?2, ?3, ?4)
",
params![id, email, tg_handle, notifier],
Expand All @@ -136,4 +132,31 @@ impl User {
};
Ok(())
}

pub fn update(conn: &Connection, user: &User) -> Result<usize, Error> {
let User { id, email, tg_handle, .. } = user;
let notifier = Self::notifier_to_text(&user.notifier);

let result = if notifier.is_some() {
conn.execute(
"UPDATE users SET email = ?1, tg_handle = ?2, notifier = ?3 WHERE id = ?4",
params![email, tg_handle, notifier, id],
)
} else {
conn.execute(
"UPDATE users SET email = ?1, tg_handle = ?2, notifier = NULL WHERE id = ?3",
params![email, tg_handle, id],
)
};

result
}

fn notifier_to_text(notifier: &Notifier) -> Option<String> {
match notifier {
Notifier::Email => Some(String::from("email")),
Notifier::Telegram => Some("telegram".to_string()),
_ => None,
}
}
}