From: Greg Burri Date: Thu, 15 Dec 2022 00:13:57 +0000 (+0100) Subject: Service for editing/creating recipe X-Git-Url: http://git.euphorik.ch/?p=recipes.git;a=commitdiff_plain;h=cc2e5b6893b582b4b5c4e7a93e914a189f6a959b Service for editing/creating recipe Other stuff... --- diff --git a/.gitignore b/.gitignore index 8d38471..deef046 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ target **/*.rs.bk -backend/data/recipes.sqlite -backend/data/recipes.sqlite-journal +backend/data /deploy-to-pi.nu style.css.map backend/static/style.css diff --git a/backend/sql/data_test.sql b/backend/sql/data_test.sql index 890baa5..72ccb3d 100644 --- a/backend/sql/data_test.sql +++ b/backend/sql/data_test.sql @@ -2,7 +2,17 @@ INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [val VALUES ( 1, 'paul@atreides.com', - 'paul', + 'Paul', + '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY', + 0, + NULL +); + +INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) +VALUES ( + 2, + 'alia@atreides.com', + 'Alia', '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY', 0, NULL @@ -16,3 +26,6 @@ VALUES (1, 'Gratin de thon aux olives'); INSERT INTO [Recipe] ([user_id], [title]) VALUES (1, 'Saumon en croute'); + +INSERT INTO [Recipe] ([user_id], [title]) +VALUES (2, 'Ouiche lorraine'); diff --git a/backend/sql/version_1.sql b/backend/sql/version_1.sql index c3488db..b1f231f 100644 --- a/backend/sql/version_1.sql +++ b/backend/sql/version_1.sql @@ -2,70 +2,74 @@ CREATE TABLE [Version] ( [id] INTEGER PRIMARY KEY, [version] INTEGER NOT NULL UNIQUE, - [datetime] DATETIME -); + [datetime] TEXT +) STRICT; CREATE TABLE [User] ( [id] INTEGER PRIMARY KEY, [email] TEXT NOT NULL, - [name] TEXT, + [name] TEXT NOT NULL DEFAULT '', [default_servings] INTEGER DEFAULT 4, [password] TEXT NOT NULL, -- argon2(password_plain, salt). - [creation_datetime] DATETIME NOT NULL, -- Updated when the validation email is sent. + [creation_datetime] TEXT NOT NULL, -- Updated when the validation email is sent. [validation_token] TEXT, -- If not null then the user has not validated his account yet. [is_admin] INTEGER NOT NULL DEFAULT FALSE -); +) STRICT; -CREATE UNIQUE INDEX [User_email_index] ON [User] ([email]); +CREATE UNIQUE INDEX [User_email_index] ON [User]([email]); CREATE TABLE [UserLoginToken] ( [id] INTEGER PRIMARY KEY, [user_id] INTEGER NOT NULL, - [last_login_datetime] DATETIME, - [token] TEXT NOT NULL, -- 24 alphanumeric character token. Can be stored in a cookie to be able to authenticate without a password. + [last_login_datetime] TEXT, + + -- 24 alphanumeric character token. + -- Can be stored in a cookie to be able to authenticate without a password. + [token] TEXT NOT NULL, [ip] TEXT, -- Can be ipv4 or ipv6 [user_agent] TEXT, FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE CASCADE -); +) STRICT; -CREATE INDEX [UserLoginToken_token_index] ON [UserLoginToken] ([token]); +CREATE INDEX [UserLoginToken_token_index] ON [UserLoginToken]([token]); CREATE TABLE [Recipe] ( [id] INTEGER PRIMARY KEY, [user_id] INTEGER, -- Can be null if a user is deleted. [title] TEXT NOT NULL, [estimate_time] INTEGER, - [description] TEXT, + [description] TEXT NOT NULL DEFAULT '', + [difficulty] INTEGER NOT NULL DEFAULT 0, [servings] INTEGER DEFAULT 4, [is_published] INTEGER NOT NULL DEFAULT FALSE, FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL -); +) STRICT; CREATE TABLE [Image] ( [Id] INTEGER PRIMARY KEY, [recipe_id] INTEGER NOT NULL, - [name] TEXT, - [description] TEXT, + [name] TEXT NOT NULL DEFAULT '', + [description] TEXT NOT NULL DEFAULT '', [image] BLOB, FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE -); +) STRICT; CREATE TABLE [RecipeTag] ( [id] INTEGER PRIMARY KEY, [recipe_id] INTEGER NOT NULL, - [tag_id] INTEGER NO NULL, + [tag_id] INTEGER NOT NULL, FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE, FOREIGN KEY([tag_id]) REFERENCES [Tag]([id]) ON DELETE CASCADE -); +) STRICT; CREATE TABLE [Tag] ( [id] INTEGER PRIMARY KEY, @@ -73,56 +77,50 @@ CREATE TABLE [Tag] ( [name] TEXT NOT NULL, FOREIGN KEY([recipe_tag_id]) REFERENCES [RecipeTag]([id]) ON DELETE SET NULL -); +) STRICT; CREATE UNIQUE INDEX [Tag_name_index] ON [Tag] ([name]); -CREATE TABLE [Quantity] ( - [id] INTEGER PRIMARY KEY, - [value] REAL, - [unit] TEXT -); - CREATE TABLE [Ingredient] ( [id] INTEGER PRIMARY KEY, [name] TEXT NOT NULL, - [quantity_id] INTEGER, - [input_step_id] INTEGER NOT NULL, + [quantity_value] REAL, + [quantity_unit] TEXT NOT NULL DEFAULT '', + [input_group_id] INTEGER NOT NULL, - FOREIGN KEY([quantity_id]) REFERENCES Quantity([id]) ON DELETE CASCADE, - FOREIGN KEY([input_step_id]) REFERENCES Step([id]) ON DELETE CASCADE -); + FOREIGN KEY([input_group_id]) REFERENCES [Group]([id]) ON DELETE CASCADE +) STRICT; CREATE TABLE [Group] ( [id] INTEGER PRIMARY KEY, [order] INTEGER NOT NULL DEFAULT 0, [recipe_id] INTEGER, - name TEXT, + [name] TEXT NOT NULL DEFAULT '', FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE -); +) STRICT; -CREATE INDEX [Group_order_index] ON [Group] ([order]); +CREATE INDEX [Group_order_index] ON [Group]([order]); CREATE TABLE [Step] ( [id] INTEGER PRIMARY KEY, [order] INTEGER NOT NULL DEFAULT 0, - [action] TEXT NOT NULL, + [action] TEXT NOT NULL DEFAULT '', [group_id] INTEGER NOT NULL, FOREIGN KEY(group_id) REFERENCES [Group](id) ON DELETE CASCADE -); +) STRICT; -CREATE INDEX [Step_order_index] ON [Group] ([order]); +CREATE INDEX [Step_order_index] ON [Group]([order]); CREATE TABLE [IntermediateSubstance] ( [id] INTEGER PRIMARY KEY, - [name] TEXT NOT NULL, - [quantity_id] INTEGER, - [output_step_id] INTEGER NOT NULL, - [input_step_id] INTEGER NOT NULL, - - FOREIGN KEY([quantity_id]) REFERENCES [Quantity]([id]) ON DELETE CASCADE, - FOREIGN KEY([output_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE, - FOREIGN KEY([input_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE -); + [name] TEXT NOT NULL DEFAULT '', + [quantity_value] REAL, + [quantity_unit] TEXT NOT NULL DEFAULT '', + [output_group_id] INTEGER NOT NULL, + [input_group_id] INTEGER NOT NULL, + + FOREIGN KEY([output_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE, + FOREIGN KEY([input_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE +) STRICT; diff --git a/backend/src/data/asynchronous.rs b/backend/src/data/asynchronous.rs index 7ef77b8..5d27388 100644 --- a/backend/src/data/asynchronous.rs +++ b/backend/src/data/asynchronous.rs @@ -7,7 +7,6 @@ use chrono::{prelude::*, Duration}; use super::db::*; use crate::model; -use crate::user::User; #[derive(Debug)] pub enum DBAsyncError { @@ -65,7 +64,7 @@ impl Connection { ) } - pub async fn load_user_async(&self, user_id: i64) -> Result { + pub async fn load_user_async(&self, user_id: i64) -> Result { let self_copy = self.clone(); combine_errors( web::block(move || self_copy.load_user(user_id).map_err(DBAsyncError::from)).await, diff --git a/backend/src/data/db.rs b/backend/src/data/db.rs index 10b6f31..0358d20 100644 --- a/backend/src/data/db.rs +++ b/backend/src/data/db.rs @@ -12,10 +12,11 @@ use r2d2_sqlite::SqliteConnectionManager; use rand::distributions::{Alphanumeric, DistString}; use rusqlite::{named_params, params, OptionalExtension, Params}; -use crate::hash::{hash, verify_password}; -use crate::model; -use crate::user::*; -use crate::{consts, user}; +use crate::{ + hash::{hash, verify_password}, + model, + consts, +}; const CURRENT_DB_VERSION: u32 = 1; @@ -221,11 +222,12 @@ impl Connection { pub fn get_recipe(&self, id: i64) -> Result { let con = self.get()?; con.query_row( - "SELECT [id], [title], [description] FROM [Recipe] WHERE [id] = ?1", + "SELECT [id], [user_id], [title], [description] FROM [Recipe] WHERE [id] = ?1", [id], |row| { Ok(model::Recipe::new( row.get("id")?, + row.get("user_id")?, row.get("title")?, row.get("description")?, )) @@ -234,10 +236,10 @@ impl Connection { .map_err(DBError::from) } - pub fn get_user_login_info(&self, token: &str) -> Result { + pub fn get_user_login_info(&self, token: &str) -> Result { let con = self.get()?; con.query_row("SELECT [last_login_datetime], [ip], [user_agent] FROM [UserLoginToken] WHERE [token] = ?1", [token], |r| { - Ok(UserLoginInfo { + Ok(model::UserLoginInfo { last_login_datetime: r.get("last_login_datetime")?, ip: r.get("ip")?, user_agent: r.get("user_agent")?, @@ -245,13 +247,14 @@ impl Connection { }).map_err(DBError::from) } - pub fn load_user(&self, user_id: i64) -> Result { + pub fn load_user(&self, user_id: i64) -> Result { let con = self.get()?; con.query_row( "SELECT [email] FROM [User] WHERE [id] = ?1", [user_id], |r| { - Ok(User { + Ok(model::User { + id: user_id, email: r.get("email")?, }) }, @@ -290,13 +293,23 @@ impl Connection { } let token = generate_token(); let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?; - tx.execute("UPDATE [User] SET [validation_token] = ?2, [creation_datetime] = ?3, [password] = ?4 WHERE [id] = ?1", params![id, token, datetime, hashed_password])?; + tx.execute( + "UPDATE [User] + SET [validation_token] = ?2, [creation_datetime] = ?3, [password] = ?4 + WHERE [id] = ?1", + params![id, token, datetime, hashed_password], + )?; token } None => { let token = generate_token(); let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?; - tx.execute("INSERT INTO [User] ([email], [validation_token], [creation_datetime], [password]) VALUES (?1, ?2, ?3, ?4)", params![email, token, datetime, hashed_password])?; + tx.execute( + "INSERT INTO [User] + ([email], [validation_token], [creation_datetime], [password]) + VALUES (?1, ?2, ?3, ?4)", + params![email, token, datetime, hashed_password], + )?; token } }; @@ -400,7 +413,12 @@ impl Connection { .optional()? { Some((login_id, user_id)) => { - tx.execute("UPDATE [UserLoginToken] SET [last_login_datetime] = ?2, [ip] = ?3, [user_agent] = ?4 WHERE [id] = ?1", params![login_id, Utc::now(), ip, user_agent])?; + tx.execute( + "UPDATE [UserLoginToken] + SET [last_login_datetime] = ?2, [ip] = ?3, [user_agent] = ?4 + WHERE [id] = ?1", + params![login_id, Utc::now(), ip, user_agent], + )?; tx.commit()?; Ok(AuthenticationResult::Ok(user_id)) } @@ -435,21 +453,27 @@ impl Connection { let con = self.get()?; // Verify if an empty recipe already exists. Returns its id if one exists. - match con.query_row( - "SELECT [Recipe].[id] FROM [Recipe] + match con + .query_row( + "SELECT [Recipe].[id] FROM [Recipe] INNER JOIN [Image] ON [Image].[recipe_id] = [Recipe].[id] INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id] - WHERE [Recipe].[user_id] = ?1 AND [Recipe].[estimate_time] = NULL AND [Recipe].[description] = NULL", - [user_id], - |r| { - Ok(r.get::<&str, i64>("id")?) - } - ).optional()? { + WHERE [Recipe].[user_id] = ?1 + AND [Recipe].[estimate_time] = NULL + AND [Recipe].[description] = NULL", + [user_id], + |r| Ok(r.get::<&str, i64>("id")?), + ) + .optional()? + { Some(recipe_id) => Ok(recipe_id), None => { - con.execute("INSERT INTO [Recipe] ([user_id], [title]) VALUES (?1, '')", [user_id])?; + con.execute( + "INSERT INTO [Recipe] ([user_id], [title]) VALUES (?1, '')", + [user_id], + )?; Ok(con.last_insert_rowid()) - }, + } } } @@ -495,7 +519,12 @@ impl Connection { user_agent: &str, ) -> Result { let token = generate_token(); - tx.execute("INSERT INTO [UserLoginToken] ([user_id], [last_login_datetime], [token], [ip], [user_agent]) VALUES (?1, ?2, ?3, ?4, ?5)", params![user_id, Utc::now(), token, ip, user_agent])?; + tx.execute( + "INSERT INTO [UserLoginToken] + ([user_id], [last_login_datetime], [token], [ip], [user_agent]) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![user_id, Utc::now(), token, ip, user_agent], + )?; Ok(token) } } @@ -542,7 +571,8 @@ mod tests { fn sign_up_to_an_already_existing_user() -> Result<()> { let connection = Connection::new_in_memory()?; connection.execute_sql(" - INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) + INSERT INTO + [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) VALUES ( 1, 'paul@atreides.com', @@ -583,7 +613,8 @@ mod tests { let connection = Connection::new_in_memory()?; let token = generate_token(); connection.execute_sql(" - INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) + INSERT INTO + [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) VALUES ( 1, 'paul@atreides.com', @@ -794,7 +825,9 @@ mod tests { let connection = Connection::new_in_memory()?; connection.execute_sql( - "INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + "INSERT INTO [User] + ([id], [email], [name], [password], [creation_datetime], [validation_token]) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ 1, "paul@atreides.com", diff --git a/backend/src/main.rs b/backend/src/main.rs index 951fd78..74e0383 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -14,7 +14,6 @@ mod email; mod hash; mod model; mod services; -mod user; mod utils; #[actix_web::main] diff --git a/backend/src/model.rs b/backend/src/model.rs index d0f95d2..0ed4825 100644 --- a/backend/src/model.rs +++ b/backend/src/model.rs @@ -1,22 +1,41 @@ +use chrono::prelude::*; + +pub struct User { + pub id: i64, + pub email: String, +} + +pub struct UserLoginInfo { + pub last_login_datetime: DateTime, + pub ip: String, + pub user_agent: String, +} + pub struct Recipe { pub id: i64, + pub user_id: i64, pub title: String, - pub description: Option, + pub description: String, pub estimate_time: Option, // [min]. - pub difficulty: Option, + pub difficulty: Difficulty, //ingredients: Vec, // For four people. pub process: Vec, } impl Recipe { - pub fn new(id: i64, title: String, description: Option) -> Recipe { + pub fn empty(id: i64, user_id: i64) -> Recipe { + Self::new(id, user_id, String::new(), String::new()) + } + + pub fn new(id: i64, user_id: i64, title: String, description: String) -> Recipe { Recipe { id, + user_id, title, description, estimate_time: None, - difficulty: None, + difficulty: Difficulty::Unknown, process: Vec::new(), } } @@ -34,13 +53,13 @@ pub struct Quantity { pub struct Group { pub name: Option, + pub input: Vec, + pub output: Vec, pub steps: Vec, } pub struct Step { pub action: String, - pub input: Vec, - pub output: Vec, } pub struct IntermediateSubstance { diff --git a/backend/src/services.rs b/backend/src/services.rs index afa20a0..c283868 100644 --- a/backend/src/services.rs +++ b/backend/src/services.rs @@ -11,13 +11,14 @@ use chrono::Duration; use log::{debug, error, info, log_enabled, Level}; use serde::Deserialize; -use crate::config::Config; -use crate::consts; -use crate::data::{asynchronous, db}; -use crate::email; -use crate::model; -use crate::user::User; -use crate::utils; +use crate::{ + config::Config, + consts, + data::{asynchronous, db}, + email, + model, + utils, +}; mod api; @@ -45,7 +46,7 @@ fn get_ip_and_user_agent(req: &HttpRequest) -> (String, String) { async fn get_current_user( req: &HttpRequest, connection: web::Data, -) -> Option { +) -> Option { let (client_ip, client_user_agent) = get_ip_and_user_agent(req); match req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) { @@ -149,7 +150,7 @@ impl actix_web::error::ResponseError for ServiceError { #[derive(Template)] #[template(path = "home.html")] struct HomeTemplate { - user: Option, + user: Option, recipes: Vec<(i64, String)>, current_recipe_id: Option, } @@ -175,9 +176,10 @@ pub async fn home_page( #[derive(Template)] #[template(path = "view_recipe.html")] struct ViewRecipeTemplate { - user: Option, + user: Option, recipes: Vec<(i64, String)>, current_recipe_id: Option, + current_recipe: model::Recipe, } @@ -201,14 +203,15 @@ pub async fn view_recipe( .to_response()) } -///// EDIT RECIPE ///// +///// EDIT/NEW RECIPE ///// #[derive(Template)] #[template(path = "edit_recipe.html")] struct EditRecipeTemplate { - user: Option, + user: Option, recipes: Vec<(i64, String)>, current_recipe_id: Option, + current_recipe: model::Recipe, } @@ -219,12 +222,28 @@ pub async fn edit_recipe( connection: web::Data, ) -> Result { let (id,) = path.into_inner(); - let user = get_current_user(&req, connection.clone()).await; - let recipes = connection.get_all_recipe_titles_async().await?; + let user = match get_current_user(&req, connection.clone()).await { + Some(u) => u, + None => + return Ok(MessageTemplate { + user: None, + message: "Cannot edit a recipe without being logged in", + }.to_response()) + }; + let recipe = connection.get_recipe_async(id).await?; + if recipe.user_id != user.id { + return Ok(MessageTemplate { + message: "Cannot edit a recipe you don't own", + user: Some(user) + }.to_response()) + } + + let recipes = connection.get_all_recipe_titles_async().await?; + Ok(EditRecipeTemplate { - user, + user: Some(user), current_recipe_id: Some(recipe.id), recipes, current_recipe: recipe, @@ -232,6 +251,34 @@ pub async fn edit_recipe( .to_response()) } +#[get("/recipe/new")] +pub async fn new_recipe( + req: HttpRequest, + path: web::Path<(i64,)>, + connection: web::Data, +) -> Result { + let user = match get_current_user(&req, connection.clone()).await { + Some(u) => u, + None => + return Ok(MessageTemplate { + message: "Cannot create a recipe without being logged in", + user: None + }.to_response()) + }; + + let recipe_id = connection.create_recipe_async(user.id).await?; + let recipes = connection.get_all_recipe_titles_async().await?; + let user_id = user.id; + + Ok(EditRecipeTemplate { + user: Some(user), + current_recipe_id: Some(recipe_id), + recipes, + current_recipe: model::Recipe::empty(recipe_id, user_id), + } + .to_response()) +} + ///// MESSAGE ///// #[derive(Template)] @@ -243,7 +290,7 @@ struct MessageBaseTemplate<'a> { #[derive(Template)] #[template(path = "message.html")] struct MessageTemplate<'a> { - user: Option, + user: Option, message: &'a str, } @@ -252,7 +299,7 @@ struct MessageTemplate<'a> { #[derive(Template)] #[template(path = "sign_up_form.html")] struct SignUpFormTemplate { - user: Option, + user: Option, email: String, message: String, message_email: String, @@ -300,7 +347,7 @@ pub async fn sign_up_post( fn error_response( error: SignUpError, form: &web::Form, - user: Option, + user: Option, ) -> Result { Ok(SignUpFormTemplate { user, @@ -486,7 +533,7 @@ pub async fn sign_up_validation( #[derive(Template)] #[template(path = "sign_in_form.html")] struct SignInFormTemplate { - user: Option, + user: Option, email: String, message: String, } @@ -524,7 +571,7 @@ pub async fn sign_in_post( fn error_response( error: SignInError, form: &web::Form, - user: Option, + user: Option, ) -> Result { Ok(SignInFormTemplate { user, diff --git a/backend/src/services/api.rs b/backend/src/services/api.rs index 88dc048..c72d96d 100644 --- a/backend/src/services/api.rs +++ b/backend/src/services/api.rs @@ -1,22 +1,12 @@ use actix_web::{ - cookie::Cookie, - get, http::{header, header::ContentType, StatusCode}, post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder, }; -use chrono::Duration; -use futures::TryFutureExt; use log::{debug, error, info, log_enabled, Level}; use ron::de::from_bytes; -use serde::Deserialize; use super::Result; -use crate::config::Config; -use crate::consts; use crate::data::{asynchronous, db}; -use crate::model; -use crate::user::User; -use crate::utils; #[put("/ron-api/recipe/set-title")] pub async fn set_recipe_title( @@ -43,3 +33,15 @@ pub async fn set_recipe_description( .await?; Ok(HttpResponse::Ok().finish()) } + +// #[put("/ron-api/recipe/add-image)] +// #[put("/ron-api/recipe/rm-photo")] +// #[put("/ron-api/recipe/add-ingredient")] +// #[put("/ron-api/recipe/rm-ingredient")] +// #[put("/ron-api/recipe/set-ingredients-order")] +// #[put("/ron-api/recipe/add-group")] +// #[put("/ron-api/recipe/rm-group")] +// #[put("/ron-api/recipe/set-groups-order")] +// #[put("/ron-api/recipe/add-step")] +// #[put("/ron-api/recipe/rm-step")] +// #[put("/ron-api/recipe/set-steps-order")] diff --git a/backend/src/user.rs b/backend/src/user.rs deleted file mode 100644 index 753d714..0000000 --- a/backend/src/user.rs +++ /dev/null @@ -1,11 +0,0 @@ -use chrono::prelude::*; - -pub struct User { - pub email: String, -} - -pub struct UserLoginInfo { - pub last_login_datetime: DateTime, - pub ip: String, - pub user_agent: String, -} diff --git a/backend/templates/base_with_header.html b/backend/templates/base_with_header.html index e8ed659..9cff88c 100644 --- a/backend/templates/base_with_header.html +++ b/backend/templates/base_with_header.html @@ -4,10 +4,9 @@
~~ Recettes de cuisine ~~ - Create a new recipe - {% match user %} {% when Some with (user) %} + Create a new recipe {{ user.email }} / Sign out {% when None %} Sign in / Sign up diff --git a/backend/templates/edit_recipe.html b/backend/templates/edit_recipe.html index 81d9f2c..5569e6a 100644 --- a/backend/templates/edit_recipe.html +++ b/backend/templates/edit_recipe.html @@ -2,17 +2,14 @@ {% block content %} -

{{ current_recipe.title }}

- - - -{% match current_recipe.description %} - {% when Some with (description) %} -
- {{ description|markdown }} -
- {% when None %} -{% endmatch %} + {% endblock %} \ No newline at end of file diff --git a/backend/templates/view_recipe.html b/backend/templates/view_recipe.html index 1a5ba52..aba5c69 100644 --- a/backend/templates/view_recipe.html +++ b/backend/templates/view_recipe.html @@ -4,12 +4,15 @@

{{ current_recipe.title }}

-{% match current_recipe.description %} - {% when Some with (description) %} -
- {{ description|markdown }} -
- {% when None %} -{% endmatch %} + +{% if user.is_some() && current_recipe.user_id == user.as_ref().unwrap().id %} + Edit +{% endif %} + +{% if !current_recipe.description.is_empty() %} +
+ {{ current_recipe.description.clone()|markdown }} +
+{% endif %} {% endblock %} \ No newline at end of file diff --git a/common/src/ron_api.rs b/common/src/ron_api.rs index 84b1619..566e430 100644 --- a/common/src/ron_api.rs +++ b/common/src/ron_api.rs @@ -1,14 +1,94 @@ use ron::de::from_reader; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone)] pub struct SetRecipeTitle { pub recipe_id: i64, pub title: String, } -#[derive(Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone)] pub struct SetRecipeDescription { pub recipe_id: i64, pub description: String, } + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeImage { + pub recipe_id: i64, + pub image: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeImageReply { + pub image_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct RemoveRecipeImage { + pub image_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeIngredient { + pub group_id: i64, + pub name: String, + pub quantity_value: Option, + pub quantity_unit: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeIngredientReply { + pub ingredient_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct RemoveRecipeIngredient { + pub group_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct SetRecipeIngredientsOrder { + pub group_id: i64, + pub ingredient_ids: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeGroup { + pub recipe_id: i64, + pub name: String, + pub quantity_value: Option, + pub quantity_unit: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeGroupReply { + pub group_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct RemoveRecipeGroupReply { + pub group_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct SetRecipeGroupsOrder { + pub recipe_id: i64, + pub group_ids: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeStep { + pub group_id: i64, + pub name: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddRecipeStepReply { + pub step_id: i64, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct RemoveRecipeStep { + pub step_id: i64, +} diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index aea6567..65fbac1 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -33,7 +33,7 @@ pub fn main() -> Result<(), JsValue> { let path: Vec<&str> = location.split('/').skip(1).collect(); /* - * Todo: + * TODO: * [ok] get url (/recipe/edit/{id}) and extract the id * - Add a handle (event?) to the title field (when edited/changed?): * - Call (as AJAR) /ron-api/set-title and set the body to a serialized RON of the type common::ron_api::SetTitle