Service for editing/creating recipe
Other stuff...
This commit is contained in:
parent
adcf4a5a5d
commit
cc2e5b6893
15 changed files with 323 additions and 146 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
.query_row(
|
||||||
"SELECT [Recipe].[id] FROM [Recipe]
|
"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
|
||||||
|
AND [Recipe].[estimate_time] = NULL
|
||||||
|
AND [Recipe].[description] = NULL",
|
||||||
[user_id],
|
[user_id],
|
||||||
|r| {
|
|r| Ok(r.get::<&str, i64>("id")?),
|
||||||
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",
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
@ -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 %}
|
||||||
|
<a class="edit-recipe" href="/recipe/edit/{{ current_recipe.id }}" >Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if !current_recipe.description.is_empty() %}
|
||||||
<div class="recipe-description" >
|
<div class="recipe-description" >
|
||||||
{{ description|markdown }}
|
{{ current_recipe.description.clone()|markdown }}
|
||||||
</div>
|
</div>
|
||||||
{% when None %}
|
{% endif %}
|
||||||
{% endmatch %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue