Service for editing/creating recipe

Other stuff...
This commit is contained in:
Greg Burri 2022-12-15 01:13:57 +01:00
parent adcf4a5a5d
commit cc2e5b6893
15 changed files with 323 additions and 146 deletions

3
.gitignore vendored
View file

@ -1,7 +1,6 @@
target target
**/*.rs.bk **/*.rs.bk
backend/data/recipes.sqlite backend/data
backend/data/recipes.sqlite-journal
/deploy-to-pi.nu /deploy-to-pi.nu
style.css.map style.css.map
backend/static/style.css backend/static/style.css

View file

@ -2,7 +2,17 @@ INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [val
VALUES ( VALUES (
1, 1,
'paul@atreides.com', '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', '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
0, 0,
NULL NULL
@ -16,3 +26,6 @@ VALUES (1, 'Gratin de thon aux olives');
INSERT INTO [Recipe] ([user_id], [title]) INSERT INTO [Recipe] ([user_id], [title])
VALUES (1, 'Saumon en croute'); VALUES (1, 'Saumon en croute');
INSERT INTO [Recipe] ([user_id], [title])
VALUES (2, 'Ouiche lorraine');

View file

@ -2,70 +2,74 @@
CREATE TABLE [Version] ( CREATE TABLE [Version] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[version] INTEGER NOT NULL UNIQUE, [version] INTEGER NOT NULL UNIQUE,
[datetime] DATETIME [datetime] TEXT
); ) STRICT;
CREATE TABLE [User] ( CREATE TABLE [User] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[email] TEXT NOT NULL, [email] TEXT NOT NULL,
[name] TEXT, [name] TEXT NOT NULL DEFAULT '',
[default_servings] INTEGER DEFAULT 4, [default_servings] INTEGER DEFAULT 4,
[password] TEXT NOT NULL, -- argon2(password_plain, salt). [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. [validation_token] TEXT, -- If not null then the user has not validated his account yet.
[is_admin] INTEGER NOT NULL DEFAULT FALSE [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] ( CREATE TABLE [UserLoginToken] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[user_id] INTEGER NOT NULL, [user_id] INTEGER NOT NULL,
[last_login_datetime] DATETIME, [last_login_datetime] TEXT,
[token] TEXT NOT NULL, -- 24 alphanumeric character token. Can be stored in a cookie to be able to authenticate without a password.
-- 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 [ip] TEXT, -- Can be ipv4 or ipv6
[user_agent] TEXT, [user_agent] TEXT,
FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE CASCADE 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] ( CREATE TABLE [Recipe] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[user_id] INTEGER, -- Can be null if a user is deleted. [user_id] INTEGER, -- Can be null if a user is deleted.
[title] TEXT NOT NULL, [title] TEXT NOT NULL,
[estimate_time] INTEGER, [estimate_time] INTEGER,
[description] TEXT, [description] TEXT NOT NULL DEFAULT '',
[difficulty] INTEGER NOT NULL DEFAULT 0,
[servings] INTEGER DEFAULT 4, [servings] INTEGER DEFAULT 4,
[is_published] INTEGER NOT NULL DEFAULT FALSE, [is_published] INTEGER NOT NULL DEFAULT FALSE,
FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL
); ) STRICT;
CREATE TABLE [Image] ( CREATE TABLE [Image] (
[Id] INTEGER PRIMARY KEY, [Id] INTEGER PRIMARY KEY,
[recipe_id] INTEGER NOT NULL, [recipe_id] INTEGER NOT NULL,
[name] TEXT, [name] TEXT NOT NULL DEFAULT '',
[description] TEXT, [description] TEXT NOT NULL DEFAULT '',
[image] BLOB, [image] BLOB,
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
); ) STRICT;
CREATE TABLE [RecipeTag] ( CREATE TABLE [RecipeTag] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[recipe_id] INTEGER NOT NULL, [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([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE,
FOREIGN KEY([tag_id]) REFERENCES [Tag]([id]) ON DELETE CASCADE FOREIGN KEY([tag_id]) REFERENCES [Tag]([id]) ON DELETE CASCADE
); ) STRICT;
CREATE TABLE [Tag] ( CREATE TABLE [Tag] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
@ -73,56 +77,50 @@ CREATE TABLE [Tag] (
[name] TEXT NOT NULL, [name] TEXT NOT NULL,
FOREIGN KEY([recipe_tag_id]) REFERENCES [RecipeTag]([id]) ON DELETE SET NULL FOREIGN KEY([recipe_tag_id]) REFERENCES [RecipeTag]([id]) ON DELETE SET NULL
); ) STRICT;
CREATE UNIQUE INDEX [Tag_name_index] ON [Tag] ([name]); CREATE UNIQUE INDEX [Tag_name_index] ON [Tag] ([name]);
CREATE TABLE [Quantity] (
[id] INTEGER PRIMARY KEY,
[value] REAL,
[unit] TEXT
);
CREATE TABLE [Ingredient] ( CREATE TABLE [Ingredient] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[name] TEXT NOT NULL, [name] TEXT NOT NULL,
[quantity_id] INTEGER, [quantity_value] REAL,
[input_step_id] INTEGER NOT NULL, [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_group_id]) REFERENCES [Group]([id]) ON DELETE CASCADE
FOREIGN KEY([input_step_id]) REFERENCES Step([id]) ON DELETE CASCADE ) STRICT;
);
CREATE TABLE [Group] ( CREATE TABLE [Group] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[order] INTEGER NOT NULL DEFAULT 0, [order] INTEGER NOT NULL DEFAULT 0,
[recipe_id] INTEGER, [recipe_id] INTEGER,
name TEXT, [name] TEXT NOT NULL DEFAULT '',
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE 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] ( CREATE TABLE [Step] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[order] INTEGER NOT NULL DEFAULT 0, [order] INTEGER NOT NULL DEFAULT 0,
[action] TEXT NOT NULL, [action] TEXT NOT NULL DEFAULT '',
[group_id] INTEGER NOT NULL, [group_id] INTEGER NOT NULL,
FOREIGN KEY(group_id) REFERENCES [Group](id) ON DELETE CASCADE 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] ( CREATE TABLE [IntermediateSubstance] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[name] TEXT NOT NULL, [name] TEXT NOT NULL DEFAULT '',
[quantity_id] INTEGER, [quantity_value] REAL,
[output_step_id] INTEGER NOT NULL, [quantity_unit] TEXT NOT NULL DEFAULT '',
[input_step_id] INTEGER NOT NULL, [output_group_id] INTEGER NOT NULL,
[input_group_id] INTEGER NOT NULL,
FOREIGN KEY([quantity_id]) REFERENCES [Quantity]([id]) ON DELETE CASCADE, FOREIGN KEY([output_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE,
FOREIGN KEY([output_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE, FOREIGN KEY([input_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE
FOREIGN KEY([input_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE ) STRICT;
);

View file

@ -7,7 +7,6 @@ use chrono::{prelude::*, Duration};
use super::db::*; use super::db::*;
use crate::model; use crate::model;
use crate::user::User;
#[derive(Debug)] #[derive(Debug)]
pub enum DBAsyncError { pub enum DBAsyncError {
@ -65,7 +64,7 @@ impl Connection {
) )
} }
pub async fn load_user_async(&self, user_id: i64) -> Result<User> { pub async fn load_user_async(&self, user_id: i64) -> Result<model::User> {
let self_copy = self.clone(); let self_copy = self.clone();
combine_errors( combine_errors(
web::block(move || self_copy.load_user(user_id).map_err(DBAsyncError::from)).await, web::block(move || self_copy.load_user(user_id).map_err(DBAsyncError::from)).await,

View file

@ -12,10 +12,11 @@ use r2d2_sqlite::SqliteConnectionManager;
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::{Alphanumeric, DistString};
use rusqlite::{named_params, params, OptionalExtension, Params}; use rusqlite::{named_params, params, OptionalExtension, Params};
use crate::hash::{hash, verify_password}; use crate::{
use crate::model; hash::{hash, verify_password},
use crate::user::*; model,
use crate::{consts, user}; consts,
};
const CURRENT_DB_VERSION: u32 = 1; const CURRENT_DB_VERSION: u32 = 1;
@ -221,11 +222,12 @@ impl Connection {
pub fn get_recipe(&self, id: i64) -> Result<model::Recipe> { pub fn get_recipe(&self, id: i64) -> Result<model::Recipe> {
let con = self.get()?; let con = self.get()?;
con.query_row( con.query_row(
"SELECT [id], [title], [description] FROM [Recipe] WHERE [id] = ?1", "SELECT [id], [user_id], [title], [description] FROM [Recipe] WHERE [id] = ?1",
[id], [id],
|row| { |row| {
Ok(model::Recipe::new( Ok(model::Recipe::new(
row.get("id")?, row.get("id")?,
row.get("user_id")?,
row.get("title")?, row.get("title")?,
row.get("description")?, row.get("description")?,
)) ))
@ -234,10 +236,10 @@ impl Connection {
.map_err(DBError::from) .map_err(DBError::from)
} }
pub fn get_user_login_info(&self, token: &str) -> Result<UserLoginInfo> { pub fn get_user_login_info(&self, token: &str) -> Result<model::UserLoginInfo> {
let con = self.get()?; let con = self.get()?;
con.query_row("SELECT [last_login_datetime], [ip], [user_agent] FROM [UserLoginToken] WHERE [token] = ?1", [token], |r| { 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")?, last_login_datetime: r.get("last_login_datetime")?,
ip: r.get("ip")?, ip: r.get("ip")?,
user_agent: r.get("user_agent")?, user_agent: r.get("user_agent")?,
@ -245,13 +247,14 @@ impl Connection {
}).map_err(DBError::from) }).map_err(DBError::from)
} }
pub fn load_user(&self, user_id: i64) -> Result<User> { pub fn load_user(&self, user_id: i64) -> Result<model::User> {
let con = self.get()?; let con = self.get()?;
con.query_row( con.query_row(
"SELECT [email] FROM [User] WHERE [id] = ?1", "SELECT [email] FROM [User] WHERE [id] = ?1",
[user_id], [user_id],
|r| { |r| {
Ok(User { Ok(model::User {
id: user_id,
email: r.get("email")?, email: r.get("email")?,
}) })
}, },
@ -290,13 +293,23 @@ impl Connection {
} }
let token = generate_token(); let token = generate_token();
let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?; 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 token
} }
None => { None => {
let token = generate_token(); let token = generate_token();
let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?; 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 token
} }
}; };
@ -400,7 +413,12 @@ impl Connection {
.optional()? .optional()?
{ {
Some((login_id, user_id)) => { 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()?; tx.commit()?;
Ok(AuthenticationResult::Ok(user_id)) Ok(AuthenticationResult::Ok(user_id))
} }
@ -435,21 +453,27 @@ impl Connection {
let con = self.get()?; let con = self.get()?;
// Verify if an empty recipe already exists. Returns its id if one exists. // Verify if an empty recipe already exists. Returns its id if one exists.
match con.query_row( match con
"SELECT [Recipe].[id] FROM [Recipe] .query_row(
"SELECT [Recipe].[id] FROM [Recipe]
INNER JOIN [Image] ON [Image].[recipe_id] = [Recipe].[id] INNER JOIN [Image] ON [Image].[recipe_id] = [Recipe].[id]
INNER JOIN [Group] ON [Group].[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", WHERE [Recipe].[user_id] = ?1
[user_id], AND [Recipe].[estimate_time] = NULL
|r| { AND [Recipe].[description] = NULL",
Ok(r.get::<&str, i64>("id")?) [user_id],
} |r| Ok(r.get::<&str, i64>("id")?),
).optional()? { )
.optional()?
{
Some(recipe_id) => Ok(recipe_id), Some(recipe_id) => Ok(recipe_id),
None => { 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()) Ok(con.last_insert_rowid())
}, }
} }
} }
@ -495,7 +519,12 @@ impl Connection {
user_agent: &str, user_agent: &str,
) -> Result<String> { ) -> Result<String> {
let token = generate_token(); 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) Ok(token)
} }
} }
@ -542,7 +571,8 @@ mod tests {
fn sign_up_to_an_already_existing_user() -> Result<()> { fn sign_up_to_an_already_existing_user() -> Result<()> {
let connection = Connection::new_in_memory()?; let connection = Connection::new_in_memory()?;
connection.execute_sql(" 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 ( VALUES (
1, 1,
'paul@atreides.com', 'paul@atreides.com',
@ -583,7 +613,8 @@ mod tests {
let connection = Connection::new_in_memory()?; let connection = Connection::new_in_memory()?;
let token = generate_token(); let token = generate_token();
connection.execute_sql(" 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 ( VALUES (
1, 1,
'paul@atreides.com', 'paul@atreides.com',
@ -794,7 +825,9 @@ mod tests {
let connection = Connection::new_in_memory()?; let connection = Connection::new_in_memory()?;
connection.execute_sql( 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![ params![
1, 1,
"paul@atreides.com", "paul@atreides.com",

View file

@ -14,7 +14,6 @@ mod email;
mod hash; mod hash;
mod model; mod model;
mod services; mod services;
mod user;
mod utils; mod utils;
#[actix_web::main] #[actix_web::main]

View file

@ -1,22 +1,41 @@
use chrono::prelude::*;
pub struct User {
pub id: i64,
pub email: String,
}
pub struct UserLoginInfo {
pub last_login_datetime: DateTime<Utc>,
pub ip: String,
pub user_agent: String,
}
pub struct Recipe { pub struct Recipe {
pub id: i64, pub id: i64,
pub user_id: i64,
pub title: String, pub title: String,
pub description: Option<String>, pub description: String,
pub estimate_time: Option<i32>, // [min]. pub estimate_time: Option<i32>, // [min].
pub difficulty: Option<Difficulty>, pub difficulty: Difficulty,
//ingredients: Vec<Ingredient>, // For four people. //ingredients: Vec<Ingredient>, // For four people.
pub process: Vec<Group>, pub process: Vec<Group>,
} }
impl Recipe { impl Recipe {
pub fn new(id: i64, title: String, description: Option<String>) -> 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 { Recipe {
id, id,
user_id,
title, title,
description, description,
estimate_time: None, estimate_time: None,
difficulty: None, difficulty: Difficulty::Unknown,
process: Vec::new(), process: Vec::new(),
} }
} }
@ -34,13 +53,13 @@ pub struct Quantity {
pub struct Group { pub struct Group {
pub name: Option<String>, pub name: Option<String>,
pub input: Vec<StepInput>,
pub output: Vec<IntermediateSubstance>,
pub steps: Vec<Step>, pub steps: Vec<Step>,
} }
pub struct Step { pub struct Step {
pub action: String, pub action: String,
pub input: Vec<StepInput>,
pub output: Vec<IntermediateSubstance>,
} }
pub struct IntermediateSubstance { pub struct IntermediateSubstance {

View file

@ -11,13 +11,14 @@ use chrono::Duration;
use log::{debug, error, info, log_enabled, Level}; use log::{debug, error, info, log_enabled, Level};
use serde::Deserialize; use serde::Deserialize;
use crate::config::Config; use crate::{
use crate::consts; config::Config,
use crate::data::{asynchronous, db}; consts,
use crate::email; data::{asynchronous, db},
use crate::model; email,
use crate::user::User; model,
use crate::utils; utils,
};
mod api; mod api;
@ -45,7 +46,7 @@ fn get_ip_and_user_agent(req: &HttpRequest) -> (String, String) {
async fn get_current_user( async fn get_current_user(
req: &HttpRequest, req: &HttpRequest,
connection: web::Data<db::Connection>, connection: web::Data<db::Connection>,
) -> Option<User> { ) -> Option<model::User> {
let (client_ip, client_user_agent) = get_ip_and_user_agent(req); let (client_ip, client_user_agent) = get_ip_and_user_agent(req);
match req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) { match req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) {
@ -149,7 +150,7 @@ impl actix_web::error::ResponseError for ServiceError {
#[derive(Template)] #[derive(Template)]
#[template(path = "home.html")] #[template(path = "home.html")]
struct HomeTemplate { struct HomeTemplate {
user: Option<User>, user: Option<model::User>,
recipes: Vec<(i64, String)>, recipes: Vec<(i64, String)>,
current_recipe_id: Option<i64>, current_recipe_id: Option<i64>,
} }
@ -175,9 +176,10 @@ pub async fn home_page(
#[derive(Template)] #[derive(Template)]
#[template(path = "view_recipe.html")] #[template(path = "view_recipe.html")]
struct ViewRecipeTemplate { struct ViewRecipeTemplate {
user: Option<User>, user: Option<model::User>,
recipes: Vec<(i64, String)>, recipes: Vec<(i64, String)>,
current_recipe_id: Option<i64>, current_recipe_id: Option<i64>,
current_recipe: model::Recipe, current_recipe: model::Recipe,
} }
@ -201,14 +203,15 @@ pub async fn view_recipe(
.to_response()) .to_response())
} }
///// EDIT RECIPE ///// ///// EDIT/NEW RECIPE /////
#[derive(Template)] #[derive(Template)]
#[template(path = "edit_recipe.html")] #[template(path = "edit_recipe.html")]
struct EditRecipeTemplate { struct EditRecipeTemplate {
user: Option<User>, user: Option<model::User>,
recipes: Vec<(i64, String)>, recipes: Vec<(i64, String)>,
current_recipe_id: Option<i64>, current_recipe_id: Option<i64>,
current_recipe: model::Recipe, current_recipe: model::Recipe,
} }
@ -219,12 +222,28 @@ pub async fn edit_recipe(
connection: web::Data<db::Connection>, connection: web::Data<db::Connection>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let (id,) = path.into_inner(); let (id,) = path.into_inner();
let user = get_current_user(&req, connection.clone()).await; let user = match get_current_user(&req, connection.clone()).await {
let recipes = connection.get_all_recipe_titles_async().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?; 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 { Ok(EditRecipeTemplate {
user, user: Some(user),
current_recipe_id: Some(recipe.id), current_recipe_id: Some(recipe.id),
recipes, recipes,
current_recipe: recipe, current_recipe: recipe,
@ -232,6 +251,34 @@ pub async fn edit_recipe(
.to_response()) .to_response())
} }
#[get("/recipe/new")]
pub async fn new_recipe(
req: HttpRequest,
path: web::Path<(i64,)>,
connection: web::Data<db::Connection>,
) -> Result<HttpResponse> {
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 ///// ///// MESSAGE /////
#[derive(Template)] #[derive(Template)]
@ -243,7 +290,7 @@ struct MessageBaseTemplate<'a> {
#[derive(Template)] #[derive(Template)]
#[template(path = "message.html")] #[template(path = "message.html")]
struct MessageTemplate<'a> { struct MessageTemplate<'a> {
user: Option<User>, user: Option<model::User>,
message: &'a str, message: &'a str,
} }
@ -252,7 +299,7 @@ struct MessageTemplate<'a> {
#[derive(Template)] #[derive(Template)]
#[template(path = "sign_up_form.html")] #[template(path = "sign_up_form.html")]
struct SignUpFormTemplate { struct SignUpFormTemplate {
user: Option<User>, user: Option<model::User>,
email: String, email: String,
message: String, message: String,
message_email: String, message_email: String,
@ -300,7 +347,7 @@ pub async fn sign_up_post(
fn error_response( fn error_response(
error: SignUpError, error: SignUpError,
form: &web::Form<SignUpFormData>, form: &web::Form<SignUpFormData>,
user: Option<User>, user: Option<model::User>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
Ok(SignUpFormTemplate { Ok(SignUpFormTemplate {
user, user,
@ -486,7 +533,7 @@ pub async fn sign_up_validation(
#[derive(Template)] #[derive(Template)]
#[template(path = "sign_in_form.html")] #[template(path = "sign_in_form.html")]
struct SignInFormTemplate { struct SignInFormTemplate {
user: Option<User>, user: Option<model::User>,
email: String, email: String,
message: String, message: String,
} }
@ -524,7 +571,7 @@ pub async fn sign_in_post(
fn error_response( fn error_response(
error: SignInError, error: SignInError,
form: &web::Form<SignInFormData>, form: &web::Form<SignInFormData>,
user: Option<User>, user: Option<model::User>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
Ok(SignInFormTemplate { Ok(SignInFormTemplate {
user, user,

View file

@ -1,22 +1,12 @@
use actix_web::{ use actix_web::{
cookie::Cookie,
get,
http::{header, header::ContentType, StatusCode}, http::{header, header::ContentType, StatusCode},
post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder, post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder,
}; };
use chrono::Duration;
use futures::TryFutureExt;
use log::{debug, error, info, log_enabled, Level}; use log::{debug, error, info, log_enabled, Level};
use ron::de::from_bytes; use ron::de::from_bytes;
use serde::Deserialize;
use super::Result; use super::Result;
use crate::config::Config;
use crate::consts;
use crate::data::{asynchronous, db}; use crate::data::{asynchronous, db};
use crate::model;
use crate::user::User;
use crate::utils;
#[put("/ron-api/recipe/set-title")] #[put("/ron-api/recipe/set-title")]
pub async fn set_recipe_title( pub async fn set_recipe_title(
@ -43,3 +33,15 @@ pub async fn set_recipe_description(
.await?; .await?;
Ok(HttpResponse::Ok().finish()) 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")]

View file

@ -1,11 +0,0 @@
use chrono::prelude::*;
pub struct User {
pub email: String,
}
pub struct UserLoginInfo {
pub last_login_datetime: DateTime<Utc>,
pub ip: String,
pub user_agent: String,
}

View file

@ -4,10 +4,9 @@
<div class="header-container"> <div class="header-container">
<a class="title" href="/">~~ Recettes de cuisine ~~</a> <a class="title" href="/">~~ Recettes de cuisine ~~</a>
<span class="create-recipe">Create a new recipe</span>
{% match user %} {% match user %}
{% when Some with (user) %} {% when Some with (user) %}
<a class="create-recipe" href="/recipe/new" >Create a new recipe</a>
<span>{{ user.email }} / <a href="/signout" />Sign out</a></span> <span>{{ user.email }} / <a href="/signout" />Sign out</a></span>
{% when None %} {% when None %}
<span><a href="/signin" >Sign in</a> / <a href="/signup">Sign up</a></span> <span><a href="/signin" >Sign in</a> / <a href="/signup">Sign up</a></span>

View file

@ -2,17 +2,14 @@
{% block content %} {% block content %}
<h2 class="recipe-title" >{{ current_recipe.title }}</h2>
<label for="title_field">Title</label> <label for="title_field">Title</label>
<input id="title_field" type="text" name="title" value="{{ current_recipe.title }}" autocapitalize="none" autocomplete="title" autofocus="autofocus" /> <input
id="title_field"
{% match current_recipe.description %} type="text"
{% when Some with (description) %} name="title"
<div class="recipe-description" > value="{{ current_recipe.title }}"
{{ description|markdown }} autocapitalize="none"
</div> autocomplete="title"
{% when None %} autofocus="autofocus" />
{% endmatch %}
{% endblock %} {% endblock %}

View file

@ -4,12 +4,15 @@
<h2 class="recipe-title" >{{ current_recipe.title }}</h2> <h2 class="recipe-title" >{{ current_recipe.title }}</h2>
{% match current_recipe.description %}
{% when Some with (description) %} {% if user.is_some() && current_recipe.user_id == user.as_ref().unwrap().id %}
<div class="recipe-description" > <a class="edit-recipe" href="/recipe/edit/{{ current_recipe.id }}" >Edit</a>
{{ description|markdown }} {% endif %}
</div>
{% when None %} {% if !current_recipe.description.is_empty() %}
{% endmatch %} <div class="recipe-description" >
{{ current_recipe.description.clone()|markdown }}
</div>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -1,14 +1,94 @@
use ron::de::from_reader; use ron::de::from_reader;
use serde::Deserialize; use serde::{Deserialize, Serialize};
#[derive(Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct SetRecipeTitle { pub struct SetRecipeTitle {
pub recipe_id: i64, pub recipe_id: i64,
pub title: String, pub title: String,
} }
#[derive(Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct SetRecipeDescription { pub struct SetRecipeDescription {
pub recipe_id: i64, pub recipe_id: i64,
pub description: String, pub description: String,
} }
#[derive(Serialize, Deserialize, Clone)]
pub struct AddRecipeImage {
pub recipe_id: i64,
pub image: Vec<u8>,
}
#[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<f64>,
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<i64>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct AddRecipeGroup {
pub recipe_id: i64,
pub name: String,
pub quantity_value: Option<f64>,
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<i64>,
}
#[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,
}

View file

@ -33,7 +33,7 @@ pub fn main() -> Result<(), JsValue> {
let path: Vec<&str> = location.split('/').skip(1).collect(); let path: Vec<&str> = location.split('/').skip(1).collect();
/* /*
* Todo: * TODO:
* [ok] get url (/recipe/edit/{id}) and extract the id * [ok] get url (/recipe/edit/{id}) and extract the id
* - Add a handle (event?) to the title field (when edited/changed?): * - 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 * - Call (as AJAR) /ron-api/set-title and set the body to a serialized RON of the type common::ron_api::SetTitle