From a1fd63ad0876e7e721166ff3edf97dace3e36508 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Wed, 12 Feb 2025 23:13:48 +0100 Subject: [PATCH] Shopping list items can now be checked/unchecked --- Cargo.lock | 20 +- backend/src/data/db/recipe.rs | 17 +- backend/src/data/db/shopping_list.rs | 12 +- backend/src/main.rs | 88 ++- backend/src/services/ron.rs | 744 ---------------------- backend/src/services/ron/calendar.rs | 94 +++ backend/src/services/ron/mod.rs | 41 ++ backend/src/services/ron/recipe.rs | 417 ++++++++++++ backend/src/services/ron/rights.rs | 158 +++++ backend/src/services/ron/shopping_list.rs | 65 ++ backend/templates/home.html | 3 + common/src/ron_api.rs | 7 + frontend/src/home.rs | 48 +- frontend/src/shopping_list.rs | 16 + 14 files changed, 940 insertions(+), 790 deletions(-) delete mode 100644 backend/src/services/ron.rs create mode 100644 backend/src/services/ron/calendar.rs create mode 100644 backend/src/services/ron/mod.rs create mode 100644 backend/src/services/ron/recipe.rs create mode 100644 backend/src/services/ron/rights.rs create mode 100644 backend/src/services/ron/shopping_list.rs diff --git a/Cargo.lock b/Cargo.lock index c99f661..7096d3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,9 +377,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.28" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" dependencies = [ "clap_builder", "clap_derive", @@ -387,9 +387,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.27" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" dependencies = [ "anstream", "anstyle", @@ -1579,9 +1579,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", ] @@ -2138,9 +2138,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.22" +version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ "log", "once_cell", @@ -2584,9 +2584,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stacker" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" +checksum = "1d08feb8f695b465baed819b03c128dc23f57a694510ab1f06c77f763975685e" dependencies = [ "cc", "cfg-if", diff --git a/backend/src/data/db/recipe.rs b/backend/src/data/db/recipe.rs index ee27553..00b5cec 100644 --- a/backend/src/data/db/recipe.rs +++ b/backend/src/data/db/recipe.rs @@ -200,7 +200,7 @@ WHERE [Step].[id] IN ({}) AND ([user_id] = $1 OR (SELECT [is_admin] FROM [User] ) -> Result { sqlx::query_scalar( r#" -SELECT COUNT(*) +SELECT COUNT(*) = 1 FROM [Recipe] INNER JOIN [User] ON [User].[id] = [Recipe].[user_id] INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id] @@ -244,6 +244,21 @@ WHERE [Ingredient].[id] IN ({}) AND Ok(query.fetch_one(&self.pool).await? == ingredients_ids.len() as u64) } + pub async fn can_edit_shopping_list_entry(&self, user_id: i64, entry_id: i64) -> Result { + sqlx::query_scalar( + r#" +SELECT COUNT(*) = 1 +FROM [ShoppingEntry] +WHERE [id] = $1 AND ([user_id] = $2 OR (SELECT [is_admin] FROM [User] WHERE [id] = $2)) + "#, + ) + .bind(entry_id) + .bind(user_id) + .fetch_one(&self.pool) + .await + .map_err(DBError::from) + } + pub async fn get_recipe(&self, id: i64, complete: bool) -> Result> { match sqlx::query_as::<_, model::Recipe>( r#" diff --git a/backend/src/data/db/shopping_list.rs b/backend/src/data/db/shopping_list.rs index ddb914e..1cd49c3 100644 --- a/backend/src/data/db/shopping_list.rs +++ b/backend/src/data/db/shopping_list.rs @@ -27,7 +27,7 @@ FROM [ShoppingEntry] LEFT JOIN [RecipeScheduled] ON [RecipeScheduled].[id] = [ShoppingEntry].[recipe_scheduled_id] LEFT JOIN [Recipe] ON [Recipe].[id] = [RecipeScheduled].[recipe_id] WHERE [ShoppingEntry].[user_id] = $1 -ORDER BY [is_checked], [recipe_id], [name] +ORDER BY [is_checked], [recipe_id], [name], [ShoppingEntry].[id] "#, ) .bind(user_id) @@ -35,4 +35,14 @@ ORDER BY [is_checked], [recipe_id], [name] .await .map_err(DBError::from) } + + pub async fn set_entry_checked(&self, entry_id: i64, is_checked: bool) -> Result<()> { + sqlx::query("UPDATE [ShoppingEntry] SET [is_checked] = $2 WHERE [id] = $1") + .bind(entry_id) + .bind(is_checked) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(DBError::from) + } } diff --git a/backend/src/main.rs b/backend/src/main.rs index f6c9659..9e1b915 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -107,92 +107,114 @@ async fn main() { // Disabled: update user profile is now made with a post data ('edit_user_post'). // .route("/user/update", put(services::ron::update_user)) .route("/set_lang", put(services::ron::set_lang)) - .route("/recipe/get_titles", get(services::ron::get_titles)) - .route("/recipe/set_title", put(services::ron::set_recipe_title)) + .route("/recipe/get_titles", get(services::ron::recipe::get_titles)) + .route("/recipe/set_title", put(services::ron::recipe::set_title)) .route( "/recipe/set_description", - put(services::ron::set_recipe_description), + put(services::ron::recipe::set_description), + ) + .route( + "/recipe/set_servings", + put(services::ron::recipe::set_servings), ) - .route("/recipe/set_servings", put(services::ron::set_servings)) .route( "/recipe/set_estimated_time", - put(services::ron::set_estimated_time), + put(services::ron::recipe::set_estimated_time), + ) + .route("/recipe/get_tags", get(services::ron::recipe::get_tags)) + .route("/recipe/add_tags", post(services::ron::recipe::add_tags)) + .route("/recipe/rm_tags", delete(services::ron::recipe::rm_tags)) + .route( + "/recipe/set_difficulty", + put(services::ron::recipe::set_difficulty), + ) + .route( + "/recipe/set_language", + put(services::ron::recipe::set_language), ) - .route("/recipe/get_tags", get(services::ron::get_tags)) - .route("/recipe/add_tags", post(services::ron::add_tags)) - .route("/recipe/rm_tags", delete(services::ron::rm_tags)) - .route("/recipe/set_difficulty", put(services::ron::set_difficulty)) - .route("/recipe/set_language", put(services::ron::set_language)) .route( "/recipe/set_is_published", - put(services::ron::set_is_published), + put(services::ron::recipe::set_is_published), + ) + .route("/recipe/remove", delete(services::ron::recipe::rm)) + .route("/recipe/get_groups", get(services::ron::recipe::get_groups)) + .route("/recipe/add_group", post(services::ron::recipe::add_group)) + .route( + "/recipe/remove_group", + delete(services::ron::recipe::rm_group), + ) + .route( + "/recipe/set_group_name", + put(services::ron::recipe::set_group_name), ) - .route("/recipe/remove", delete(services::ron::rm)) - .route("/recipe/get_groups", get(services::ron::get_groups)) - .route("/recipe/add_group", post(services::ron::add_group)) - .route("/recipe/remove_group", delete(services::ron::rm_group)) - .route("/recipe/set_group_name", put(services::ron::set_group_name)) .route( "/recipe/set_group_comment", - put(services::ron::set_group_comment), + put(services::ron::recipe::set_group_comment), ) .route( "/recipe/set_groups_order", - put(services::ron::set_groups_order), + put(services::ron::recipe::set_groups_order), + ) + .route("/recipe/add_step", post(services::ron::recipe::add_step)) + .route( + "/recipe/remove_step", + delete(services::ron::recipe::rm_step), ) - .route("/recipe/add_step", post(services::ron::add_step)) - .route("/recipe/remove_step", delete(services::ron::rm_step)) .route( "/recipe/set_step_action", - put(services::ron::set_step_action), + put(services::ron::recipe::set_step_action), ) .route( "/recipe/set_steps_order", - put(services::ron::set_steps_order), + put(services::ron::recipe::set_steps_order), ) .route( "/recipe/add_ingredient", - post(services::ron::add_ingredient), + post(services::ron::recipe::add_ingredient), ) .route( "/recipe/remove_ingredient", - delete(services::ron::rm_ingredient), + delete(services::ron::recipe::rm_ingredient), ) .route( "/recipe/set_ingredient_name", - put(services::ron::set_ingredient_name), + put(services::ron::recipe::set_ingredient_name), ) .route( "/recipe/set_ingredient_comment", - put(services::ron::set_ingredient_comment), + put(services::ron::recipe::set_ingredient_comment), ) .route( "/recipe/set_ingredient_quantity", - put(services::ron::set_ingredient_quantity), + put(services::ron::recipe::set_ingredient_quantity), ) .route( "/recipe/set_ingredient_unit", - put(services::ron::set_ingredient_unit), + put(services::ron::recipe::set_ingredient_unit), ) .route( "/recipe/set_ingredients_order", - put(services::ron::set_ingredients_order), + put(services::ron::recipe::set_ingredients_order), ) .route( "/calendar/get_scheduled_recipes", - get(services::ron::get_scheduled_recipes), + get(services::ron::calendar::get_scheduled_recipes), ) .route( "/calendar/schedule_recipe", - post(services::ron::schedule_recipe), + post(services::ron::calendar::schedule_recipe), ) .route( "/calendar/remove_scheduled_recipe", - delete(services::ron::rm_scheduled_recipe), + delete(services::ron::calendar::rm_scheduled_recipe), ) .route( "/shopping_list/get_list", - get(services::ron::get_shopping_list), + get(services::ron::shopping_list::get), + ) + .route( + "/shopping_list/set_checked", + put(services::ron::shopping_list::set_entry_checked), ) .fallback(services::ron::not_found); diff --git a/backend/src/services/ron.rs b/backend/src/services/ron.rs deleted file mode 100644 index 7090ddb..0000000 --- a/backend/src/services/ron.rs +++ /dev/null @@ -1,744 +0,0 @@ -use axum::{ - debug_handler, - extract::{Extension, State}, - http::{HeaderMap, StatusCode}, - response::{ErrorResponse, IntoResponse, Response, Result}, -}; -use axum_extra::extract::{ - cookie::{Cookie, CookieJar}, - Query, -}; -use serde::{Deserialize, Serialize}; -// use tracing::{event, Level}; - -use crate::{ - consts, - data::{self, db}, - model, - ron_extractor::ExtractRon, - ron_utils::{ron_error, ron_response_ok}, -}; - -const NOT_AUTHORIZED_MESSAGE: &str = "Action not authorized"; - -#[derive(Deserialize)] -pub struct Id { - id: i64, -} - -#[derive(Deserialize, Serialize)] -pub struct Ids { - ids: Vec, -} - -// #[allow(dead_code)] -// #[debug_handler] -// pub async fn update_user( -// State(connection): State, -// Extension(user): Extension>, -// ExtractRon(ron): ExtractRon, -// ) -> Result { -// if let Some(user) = user { -// connection -// .update_user( -// user.id, -// ron.email.as_deref().map(str::trim), -// ron.name.as_deref(), -// ron.password.as_deref(), -// ) -// .await?; -// } else { -// return Err(ErrorResponse::from(ron_error( -// StatusCode::UNAUTHORIZED, -// NOT_AUTHORIZED_MESSAGE, -// ))); -// } -// Ok(StatusCode::OK) -// } - -#[debug_handler] -pub async fn set_lang( - State(connection): State, - Extension(user): Extension>, - headers: HeaderMap, - ExtractRon(ron): ExtractRon, -) -> Result<(CookieJar, StatusCode)> { - let mut jar = CookieJar::from_headers(&headers); - if let Some(user) = user { - connection.set_user_lang(user.id, &ron.lang).await?; - } else { - let cookie = Cookie::build((consts::COOKIE_LANG_NAME, ron.lang)).path("/"); - jar = jar.add(cookie); - } - Ok((jar, StatusCode::OK)) -} - -/*** Rights ***/ - -async fn check_user_rights_recipe( - connection: &db::Connection, - user: &Option, - recipe_id: i64, -) -> Result<()> { - if user.is_none() - || !connection - .can_edit_recipe(user.as_ref().unwrap().id, recipe_id) - .await? - { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } else { - Ok(()) - } -} - -async fn check_user_rights_recipe_group( - connection: &db::Connection, - user: &Option, - group_id: i64, -) -> Result<()> { - if user.is_none() - || !connection - .can_edit_recipe_group(user.as_ref().unwrap().id, group_id) - .await? - { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } else { - Ok(()) - } -} - -async fn check_user_rights_recipe_groups( - connection: &db::Connection, - user: &Option, - group_ids: &[i64], -) -> Result<()> { - if user.is_none() - || !connection - .can_edit_recipe_all_groups(user.as_ref().unwrap().id, group_ids) - .await? - { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } else { - Ok(()) - } -} - -async fn check_user_rights_recipe_step( - connection: &db::Connection, - user: &Option, - step_id: i64, -) -> Result<()> { - if user.is_none() - || !connection - .can_edit_recipe_step(user.as_ref().unwrap().id, step_id) - .await? - { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } else { - Ok(()) - } -} - -async fn check_user_rights_recipe_steps( - connection: &db::Connection, - user: &Option, - step_ids: &[i64], -) -> Result<()> { - if user.is_none() - || !connection - .can_edit_recipe_all_steps(user.as_ref().unwrap().id, step_ids) - .await? - { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } else { - Ok(()) - } -} - -async fn check_user_rights_recipe_ingredient( - connection: &db::Connection, - user: &Option, - ingredient_id: i64, -) -> Result<()> { - if user.is_none() - || !connection - .can_edit_recipe_ingredient(user.as_ref().unwrap().id, ingredient_id) - .await? - { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } else { - Ok(()) - } -} - -async fn check_user_rights_recipe_ingredients( - connection: &db::Connection, - user: &Option, - ingredient_ids: &[i64], -) -> Result<()> { - if user.is_none() - || !connection - .can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, ingredient_ids) - .await? - { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } else { - Ok(()) - } -} - -/*** Recipe ***/ - -/// Ask recipe titles associated with each given id. The returned titles are in the same order -/// as the given ids. -#[debug_handler] -pub async fn get_titles( - State(connection): State, - recipe_ids: Query, -) -> Result { - Ok(ron_response_ok(common::ron_api::Strings { - strs: connection.get_recipe_titles(&recipe_ids.ids).await?, - })) -} - -#[debug_handler] -pub async fn set_recipe_title( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection - .set_recipe_title(ron.recipe_id, &ron.title) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_recipe_description( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection - .set_recipe_description(ron.recipe_id, &ron.description) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_servings( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection - .set_recipe_servings(ron.recipe_id, ron.servings) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_estimated_time( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection - .set_recipe_estimated_time(ron.recipe_id, ron.estimated_time) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn get_tags( - State(connection): State, - recipe_id: Query, -) -> Result { - Ok(ron_response_ok(common::ron_api::Tags { - recipe_id: recipe_id.id, - tags: connection.get_recipes_tags(recipe_id.id).await?, - })) -} - -#[debug_handler] -pub async fn add_tags( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection - .add_recipe_tags( - ron.recipe_id, - &ron.tags - .into_iter() - .map(|tag| tag.to_lowercase()) - .collect::>(), - ) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn rm_tags( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection.rm_recipe_tags(ron.recipe_id, &ron.tags).await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_difficulty( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection - .set_recipe_difficulty(ron.recipe_id, ron.difficulty) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_language( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - if !crate::translation::available_codes() - .iter() - .any(|&l| l == ron.lang) - { - // TODO: log? - return Ok(StatusCode::BAD_REQUEST); - } - - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection - .set_recipe_language(ron.recipe_id, &ron.lang) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_is_published( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - connection - .set_recipe_is_published(ron.recipe_id, ron.is_published) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn rm( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.id).await?; - connection.rm_recipe(ron.id).await?; - Ok(StatusCode::OK) -} - -impl From for common::ron_api::Group { - fn from(group: model::Group) -> Self { - Self { - id: group.id, - name: group.name, - comment: group.comment, - steps: group - .steps - .into_iter() - .map(common::ron_api::Step::from) - .collect(), - } - } -} - -impl From for common::ron_api::Step { - fn from(step: model::Step) -> Self { - Self { - id: step.id, - action: step.action, - ingredients: step - .ingredients - .into_iter() - .map(common::ron_api::Ingredient::from) - .collect(), - } - } -} - -impl From for common::ron_api::Ingredient { - fn from(ingredient: model::Ingredient) -> Self { - Self { - id: ingredient.id, - name: ingredient.name, - comment: ingredient.comment, - quantity_value: ingredient.quantity_value, - quantity_unit: ingredient.quantity_unit, - } - } -} - -#[debug_handler] -pub async fn get_groups( - State(connection): State, - recipe_id: Query, -) -> Result { - // Here we don't check user rights on purpose. - Ok(ron_response_ok( - connection - .get_groups(recipe_id.id) - .await? - .into_iter() - .map(common::ron_api::Group::from) - .collect::>(), - )) -} - -#[debug_handler] -pub async fn add_group( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.id).await?; - let id = connection.add_recipe_group(ron.id).await?; - - Ok(ron_response_ok(common::ron_api::Id { id })) -} - -#[debug_handler] -pub async fn rm_group( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_group(&connection, &user, ron.id).await?; - connection.rm_recipe_group(ron.id).await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_group_name( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_group(&connection, &user, ron.group_id).await?; - connection.set_group_name(ron.group_id, &ron.name).await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_group_comment( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_group(&connection, &user, ron.group_id).await?; - connection - .set_group_comment(ron.group_id, &ron.comment) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_groups_order( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_groups(&connection, &user, &ron.ids).await?; - connection.set_groups_order(&ron.ids).await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn add_step( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_group(&connection, &user, ron.id).await?; - let id = connection.add_recipe_step(ron.id).await?; - - Ok(ron_response_ok(common::ron_api::Id { id })) -} - -#[debug_handler] -pub async fn rm_step( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_step(&connection, &user, ron.id).await?; - connection.rm_recipe_step(ron.id).await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_step_action( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_step(&connection, &user, ron.step_id).await?; - connection.set_step_action(ron.step_id, &ron.action).await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_steps_order( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_steps(&connection, &user, &ron.ids).await?; - connection.set_steps_order(&ron.ids).await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn add_ingredient( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_step(&connection, &user, ron.id).await?; - let id = connection.add_recipe_ingredient(ron.id).await?; - Ok(ron_response_ok(common::ron_api::Id { id })) -} - -#[debug_handler] -pub async fn rm_ingredient( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_ingredient(&connection, &user, ron.id).await?; - connection.rm_recipe_ingredient(ron.id).await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_ingredient_name( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?; - connection - .set_ingredient_name(ron.ingredient_id, &ron.name) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_ingredient_comment( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?; - connection - .set_ingredient_comment(ron.ingredient_id, &ron.comment) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_ingredient_quantity( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?; - connection - .set_ingredient_quantity(ron.ingredient_id, ron.quantity) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_ingredient_unit( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?; - connection - .set_ingredient_unit(ron.ingredient_id, &ron.unit) - .await?; - Ok(StatusCode::OK) -} - -#[debug_handler] -pub async fn set_ingredients_order( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe_ingredients(&connection, &user, &ron.ids).await?; - connection.set_ingredients_order(&ron.ids).await?; - Ok(StatusCode::OK) -} - -/*** Calendar ***/ - -#[debug_handler] -pub async fn get_scheduled_recipes( - State(connection): State, - Extension(user): Extension>, - date_range: Query, -) -> Result { - if let Some(user) = user { - Ok(ron_response_ok(common::ron_api::ScheduledRecipes { - recipes: connection - .get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date) - .await?, - })) - } else { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } -} - -impl From for common::ron_api::ScheduleRecipeResult { - fn from(db_res: data::db::recipe::AddScheduledRecipeResult) -> Self { - match db_res { - db::recipe::AddScheduledRecipeResult::Ok => Self::Ok, - db::recipe::AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate => { - Self::RecipeAlreadyScheduledAtThisDate - } - } - } -} - -#[debug_handler] -pub async fn schedule_recipe( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - if let Some(user) = user { - connection - .add_scheduled_recipe( - user.id, - ron.recipe_id, - ron.date, - ron.servings, - ron.add_ingredients_to_shopping_list, - ) - .await - .map(|res| { - ron_response_ok(common::ron_api::ScheduleRecipeResult::from(res)).into_response() - }) - .map_err(ErrorResponse::from) - } else { - Ok(StatusCode::OK.into_response()) - } -} - -#[debug_handler] -pub async fn rm_scheduled_recipe( - State(connection): State, - Extension(user): Extension>, - ExtractRon(ron): ExtractRon, -) -> Result { - check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; - if let Some(user) = user { - connection - .rm_scheduled_recipe( - user.id, - ron.recipe_id, - ron.date, - ron.remove_ingredients_from_shopping_list, - ) - .await?; - } - Ok(StatusCode::OK) -} - -/*** Shopping list ***/ - -impl From for common::ron_api::ShoppingListItem { - fn from(item: model::ShoppingListItem) -> Self { - Self { - id: item.id, - name: item.name, - quantity_value: item.quantity_value, - quantity_unit: item.quantity_unit, - recipe_id: item.recipe_id, - recipe_title: item.recipe_title, - date: item.date, - is_checked: item.is_checked, - } - } -} - -#[debug_handler] -pub async fn get_shopping_list( - State(connection): State, - Extension(user): Extension>, -) -> Result { - if let Some(user) = user { - Ok(ron_response_ok( - connection - .get_shopping_list(user.id) - .await? - .into_iter() - .map(common::ron_api::ShoppingListItem::from) - .collect::>(), - )) - } else { - Err(ErrorResponse::from(ron_error( - StatusCode::UNAUTHORIZED, - NOT_AUTHORIZED_MESSAGE, - ))) - } -} - -/*** 404 ***/ - -#[debug_handler] -pub async fn not_found(Extension(_user): Extension>) -> impl IntoResponse { - ron_error(StatusCode::NOT_FOUND, "Not found") -} diff --git a/backend/src/services/ron/calendar.rs b/backend/src/services/ron/calendar.rs new file mode 100644 index 0000000..2f3a6a2 --- /dev/null +++ b/backend/src/services/ron/calendar.rs @@ -0,0 +1,94 @@ +use axum::{ + debug_handler, + extract::{Extension, State}, + http::StatusCode, + response::{ErrorResponse, IntoResponse, Response, Result}, +}; +use axum_extra::extract::Query; +// use tracing::{event, Level}; + +use crate::{ + data::{self, db}, + model, + ron_extractor::ExtractRon, + ron_utils::{ron_error, ron_response_ok}, +}; + +use super::rights::*; + +#[debug_handler] +pub async fn get_scheduled_recipes( + State(connection): State, + Extension(user): Extension>, + date_range: Query, +) -> Result { + if let Some(user) = user { + Ok(ron_response_ok(common::ron_api::ScheduledRecipes { + recipes: connection + .get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date) + .await?, + })) + } else { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } +} + +impl From for common::ron_api::ScheduleRecipeResult { + fn from(db_res: data::db::recipe::AddScheduledRecipeResult) -> Self { + match db_res { + db::recipe::AddScheduledRecipeResult::Ok => Self::Ok, + db::recipe::AddScheduledRecipeResult::RecipeAlreadyScheduledAtThisDate => { + Self::RecipeAlreadyScheduledAtThisDate + } + } + } +} + +#[debug_handler] +pub async fn schedule_recipe( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + if let Some(user) = user { + connection + .add_scheduled_recipe( + user.id, + ron.recipe_id, + ron.date, + ron.servings, + ron.add_ingredients_to_shopping_list, + ) + .await + .map(|res| { + ron_response_ok(common::ron_api::ScheduleRecipeResult::from(res)).into_response() + }) + .map_err(ErrorResponse::from) + } else { + Ok(StatusCode::OK.into_response()) + } +} + +#[debug_handler] +pub async fn rm_scheduled_recipe( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + if let Some(user) = user { + connection + .rm_scheduled_recipe( + user.id, + ron.recipe_id, + ron.date, + ron.remove_ingredients_from_shopping_list, + ) + .await?; + } + Ok(StatusCode::OK) +} diff --git a/backend/src/services/ron/mod.rs b/backend/src/services/ron/mod.rs new file mode 100644 index 0000000..1092f76 --- /dev/null +++ b/backend/src/services/ron/mod.rs @@ -0,0 +1,41 @@ +use axum::{ + debug_handler, + extract::{Extension, State}, + http::{HeaderMap, StatusCode}, + response::{IntoResponse, Result}, +}; +use axum_extra::extract::cookie::{Cookie, CookieJar}; +// use tracing::{event, Level}; + +use crate::{consts, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error}; + +pub mod calendar; +pub mod recipe; +mod rights; +pub mod shopping_list; + +const NOT_AUTHORIZED_MESSAGE: &str = "Action not authorized"; + +#[debug_handler] +pub async fn set_lang( + State(connection): State, + Extension(user): Extension>, + headers: HeaderMap, + ExtractRon(ron): ExtractRon, +) -> Result<(CookieJar, StatusCode)> { + let mut jar = CookieJar::from_headers(&headers); + if let Some(user) = user { + connection.set_user_lang(user.id, &ron.lang).await?; + } else { + let cookie = Cookie::build((consts::COOKIE_LANG_NAME, ron.lang)).path("/"); + jar = jar.add(cookie); + } + Ok((jar, StatusCode::OK)) +} + +/*** 404 ***/ + +#[debug_handler] +pub async fn not_found(Extension(_user): Extension>) -> impl IntoResponse { + ron_error(StatusCode::NOT_FOUND, "Not found") +} diff --git a/backend/src/services/ron/recipe.rs b/backend/src/services/ron/recipe.rs new file mode 100644 index 0000000..9cd7db4 --- /dev/null +++ b/backend/src/services/ron/recipe.rs @@ -0,0 +1,417 @@ +use axum::{ + debug_handler, + extract::{Extension, State}, + http::StatusCode, + response::{IntoResponse, Result}, +}; +use axum_extra::extract::Query; +use common::ron_api; +// use tracing::{event, Level}; + +use crate::{data::db, model, ron_extractor::ExtractRon, ron_utils::ron_response_ok}; + +use super::rights::*; + +/// Ask recipe titles associated with each given id. The returned titles are in the same order +/// as the given ids. +#[debug_handler] +pub async fn get_titles( + State(connection): State, + recipe_ids: Query, +) -> Result { + Ok(ron_response_ok(ron_api::Strings { + strs: connection.get_recipe_titles(&recipe_ids.ids).await?, + })) +} + +#[debug_handler] +pub async fn set_title( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection + .set_recipe_title(ron.recipe_id, &ron.title) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_description( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection + .set_recipe_description(ron.recipe_id, &ron.description) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_servings( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection + .set_recipe_servings(ron.recipe_id, ron.servings) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_estimated_time( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection + .set_recipe_estimated_time(ron.recipe_id, ron.estimated_time) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn get_tags( + State(connection): State, + recipe_id: Query, +) -> Result { + Ok(ron_response_ok(ron_api::Tags { + recipe_id: recipe_id.id, + tags: connection.get_recipes_tags(recipe_id.id).await?, + })) +} + +#[debug_handler] +pub async fn add_tags( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection + .add_recipe_tags( + ron.recipe_id, + &ron.tags + .into_iter() + .map(|tag| tag.to_lowercase()) + .collect::>(), + ) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn rm_tags( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection.rm_recipe_tags(ron.recipe_id, &ron.tags).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_difficulty( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection + .set_recipe_difficulty(ron.recipe_id, ron.difficulty) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_language( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + if !crate::translation::available_codes() + .iter() + .any(|&l| l == ron.lang) + { + // TODO: log? + return Ok(StatusCode::BAD_REQUEST); + } + + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection + .set_recipe_language(ron.recipe_id, &ron.lang) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_is_published( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.recipe_id).await?; + connection + .set_recipe_is_published(ron.recipe_id, ron.is_published) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn rm( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.id).await?; + connection.rm_recipe(ron.id).await?; + Ok(StatusCode::OK) +} + +impl From for ron_api::Group { + fn from(group: model::Group) -> Self { + Self { + id: group.id, + name: group.name, + comment: group.comment, + steps: group.steps.into_iter().map(ron_api::Step::from).collect(), + } + } +} + +impl From for ron_api::Step { + fn from(step: model::Step) -> Self { + Self { + id: step.id, + action: step.action, + ingredients: step + .ingredients + .into_iter() + .map(ron_api::Ingredient::from) + .collect(), + } + } +} + +impl From for ron_api::Ingredient { + fn from(ingredient: model::Ingredient) -> Self { + Self { + id: ingredient.id, + name: ingredient.name, + comment: ingredient.comment, + quantity_value: ingredient.quantity_value, + quantity_unit: ingredient.quantity_unit, + } + } +} + +#[debug_handler] +pub async fn get_groups( + State(connection): State, + recipe_id: Query, +) -> Result { + // Here we don't check user rights on purpose. + Ok(ron_response_ok( + connection + .get_groups(recipe_id.id) + .await? + .into_iter() + .map(ron_api::Group::from) + .collect::>(), + )) +} + +#[debug_handler] +pub async fn add_group( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe(&connection, &user, ron.id).await?; + let id = connection.add_recipe_group(ron.id).await?; + + Ok(ron_response_ok(ron_api::Id { id })) +} + +#[debug_handler] +pub async fn rm_group( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_group(&connection, &user, ron.id).await?; + connection.rm_recipe_group(ron.id).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_group_name( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_group(&connection, &user, ron.group_id).await?; + connection.set_group_name(ron.group_id, &ron.name).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_group_comment( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_group(&connection, &user, ron.group_id).await?; + connection + .set_group_comment(ron.group_id, &ron.comment) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_groups_order( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_groups(&connection, &user, &ron.ids).await?; + connection.set_groups_order(&ron.ids).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn add_step( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_group(&connection, &user, ron.id).await?; + let id = connection.add_recipe_step(ron.id).await?; + + Ok(ron_response_ok(ron_api::Id { id })) +} + +#[debug_handler] +pub async fn rm_step( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_step(&connection, &user, ron.id).await?; + connection.rm_recipe_step(ron.id).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_step_action( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_step(&connection, &user, ron.step_id).await?; + connection.set_step_action(ron.step_id, &ron.action).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_steps_order( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_steps(&connection, &user, &ron.ids).await?; + connection.set_steps_order(&ron.ids).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn add_ingredient( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_step(&connection, &user, ron.id).await?; + let id = connection.add_recipe_ingredient(ron.id).await?; + Ok(ron_response_ok(ron_api::Id { id })) +} + +#[debug_handler] +pub async fn rm_ingredient( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_ingredient(&connection, &user, ron.id).await?; + connection.rm_recipe_ingredient(ron.id).await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_ingredient_name( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?; + connection + .set_ingredient_name(ron.ingredient_id, &ron.name) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_ingredient_comment( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?; + connection + .set_ingredient_comment(ron.ingredient_id, &ron.comment) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_ingredient_quantity( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?; + connection + .set_ingredient_quantity(ron.ingredient_id, ron.quantity) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_ingredient_unit( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?; + connection + .set_ingredient_unit(ron.ingredient_id, &ron.unit) + .await?; + Ok(StatusCode::OK) +} + +#[debug_handler] +pub async fn set_ingredients_order( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon, +) -> Result { + check_user_rights_recipe_ingredients(&connection, &user, &ron.ids).await?; + connection.set_ingredients_order(&ron.ids).await?; + Ok(StatusCode::OK) +} diff --git a/backend/src/services/ron/rights.rs b/backend/src/services/ron/rights.rs new file mode 100644 index 0000000..2554c80 --- /dev/null +++ b/backend/src/services/ron/rights.rs @@ -0,0 +1,158 @@ +use axum::{ + http::StatusCode, + response::{ErrorResponse, Result}, +}; + +use crate::{data::db, model, ron_utils::ron_error}; + +pub async fn check_user_rights_recipe( + connection: &db::Connection, + user: &Option, + recipe_id: i64, +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe(user.as_ref().unwrap().id, recipe_id) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + +pub async fn check_user_rights_recipe_group( + connection: &db::Connection, + user: &Option, + group_id: i64, +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_group(user.as_ref().unwrap().id, group_id) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + +pub async fn check_user_rights_recipe_groups( + connection: &db::Connection, + user: &Option, + group_ids: &[i64], +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_all_groups(user.as_ref().unwrap().id, group_ids) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + +pub async fn check_user_rights_recipe_step( + connection: &db::Connection, + user: &Option, + step_id: i64, +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_step(user.as_ref().unwrap().id, step_id) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + +pub async fn check_user_rights_recipe_steps( + connection: &db::Connection, + user: &Option, + step_ids: &[i64], +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_all_steps(user.as_ref().unwrap().id, step_ids) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + +pub async fn check_user_rights_recipe_ingredient( + connection: &db::Connection, + user: &Option, + ingredient_id: i64, +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_ingredient(user.as_ref().unwrap().id, ingredient_id) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + +pub async fn check_user_rights_recipe_ingredients( + connection: &db::Connection, + user: &Option, + ingredient_ids: &[i64], +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_recipe_all_ingredients(user.as_ref().unwrap().id, ingredient_ids) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} + +pub async fn check_user_rights_shopping_list_entry( + connection: &db::Connection, + user: &Option, + entry_id: i64, +) -> Result<()> { + if user.is_none() + || !connection + .can_edit_shopping_list_entry(user.as_ref().unwrap().id, entry_id) + .await? + { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } else { + Ok(()) + } +} diff --git a/backend/src/services/ron/shopping_list.rs b/backend/src/services/ron/shopping_list.rs new file mode 100644 index 0000000..4a785ee --- /dev/null +++ b/backend/src/services/ron/shopping_list.rs @@ -0,0 +1,65 @@ +use axum::{ + debug_handler, + extract::{Extension, State}, + http::StatusCode, + response::{ErrorResponse, IntoResponse, Result}, +}; +use common::ron_api; + +use crate::{ + data::db, + model, + ron_extractor::ExtractRon, + ron_utils::{ron_error, ron_response_ok}, +}; + +use super::rights::*; + +impl From for common::ron_api::ShoppingListItem { + fn from(item: model::ShoppingListItem) -> Self { + Self { + id: item.id, + name: item.name, + quantity_value: item.quantity_value, + quantity_unit: item.quantity_unit, + recipe_id: item.recipe_id, + recipe_title: item.recipe_title, + date: item.date, + is_checked: item.is_checked, + } + } +} + +#[debug_handler] +pub async fn get( + State(connection): State, + Extension(user): Extension>, +) -> Result { + if let Some(user) = user { + Ok(ron_response_ok( + connection + .get_shopping_list(user.id) + .await? + .into_iter() + .map(common::ron_api::ShoppingListItem::from) + .collect::>(), + )) + } else { + Err(ErrorResponse::from(ron_error( + StatusCode::UNAUTHORIZED, + super::NOT_AUTHORIZED_MESSAGE, + ))) + } +} + +#[debug_handler] +pub async fn set_entry_checked( + State(connection): State, + Extension(user): Extension>, + ExtractRon(ron): ExtractRon>, +) -> Result { + check_user_rights_shopping_list_entry(&connection, &user, ron.id).await?; + Ok(ron_response_ok( + connection.set_entry_checked(ron.id, ron.value).await?, + )) +} diff --git a/backend/templates/home.html b/backend/templates/home.html index 3bd50a8..426c9e3 100644 --- a/backend/templates/home.html +++ b/backend/templates/home.html @@ -7,6 +7,9 @@
+ +
+
diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index 0e308ce..50f9ef0 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -19,6 +19,13 @@ pub struct Id { pub id: i64, } +// A value associated with an id. +#[derive(Serialize, Deserialize, Clone)] +pub struct Value { + pub id: i64, + pub value: T, +} + #[derive(Serialize, Deserialize, Clone)] pub struct Strings { pub strs: Vec, diff --git a/frontend/src/home.rs b/frontend/src/home.rs index 3a4f660..2d0737e 100644 --- a/frontend/src/home.rs +++ b/frontend/src/home.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use chrono::Locale; use common::{ron_api, utils::substitute_with_names}; +use futures::TryFutureExt; use gloo::events::EventListener; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; @@ -33,10 +34,14 @@ pub fn setup_page(is_user_logged: bool) -> Result<(), JsValue> { spawn_local(async move { let item_template: Element = selector("#hidden-templates .shopping-item"); let container: Element = by_id("shopping-list"); + let container_checked: Element = by_id("shopping-list-checked"); let date_format = selector::("#hidden-templates .calendar-date-format").inner_html(); for item in shopping_list.get_items().await.unwrap() { let item_element = item_template.deep_clone(); + + // item_element.set_id(format!("shopping-item-{}", )); + item_element .selector::(".item-name") .set_inner_html(&item.name); @@ -62,7 +67,48 @@ pub fn setup_page(is_user_logged: bool) -> Result<(), JsValue> { .unwrap(); } - container.append_child(&item_element).unwrap(); + EventListener::new( + &item_element.selector(".item-is-checked"), + "change", + move |event| { + let input: HtmlInputElement = event.target().unwrap().dyn_into().unwrap(); + spawn_local(async move { + shopping_list + .set_item_checked(item.id, input.checked()) + .await + .unwrap(); + let item_element = input.parent_element().unwrap(); + item_element.remove(); + // TODO: Find the correct place to insert the element. + if input.checked() { + by_id::("shopping-list-checked") + .append_child(&item_element) + .unwrap(); + } else { + by_id::("shopping-list") + .append_child(&item_element) + .unwrap(); + } + }); + }, + ) + .forget(); + + EventListener::new(&item_element, "click", move |event| { + let target: Element = event.target().unwrap().dyn_into().unwrap(); + + // if target.class_name() == "item-is-checked" + }) + .forget(); + + if item.is_checked { + item_element + .selector::(".item-is-checked") + .set_checked(true); + container_checked.append_child(&item_element).unwrap(); + } else { + container.append_child(&item_element).unwrap(); + } } }); diff --git a/frontend/src/shopping_list.rs b/frontend/src/shopping_list.rs index 155a1d9..452c98a 100644 --- a/frontend/src/shopping_list.rs +++ b/frontend/src/shopping_list.rs @@ -32,4 +32,20 @@ impl ShoppingList { Ok(request::get("shopping_list/get_list", ()).await?) } } + + pub async fn set_item_checked(&self, item_id: i64, is_checked: bool) -> Result<()> { + if self.is_local { + todo!(); + } else { + request::put( + "shopping_list/set_checked", + ron_api::Value { + id: item_id, + value: is_checked, + }, + ) + .await + .map_err(Error::from) + } + } } -- 2.49.0