Split db::Connection implementation in submodules (db::user and db::recipe).

This commit is contained in:
Greg Burri 2024-12-18 23:10:19 +01:00
parent 4248d11aa9
commit fce4eade73
17 changed files with 1307 additions and 1234 deletions

12
Cargo.lock generated
View file

@ -2006,7 +2006,7 @@ dependencies = [
"ron", "ron",
"serde", "serde",
"sqlx", "sqlx",
"thiserror 2.0.7", "thiserror 2.0.8",
"tokio", "tokio",
"tower", "tower",
"tower-http", "tower-http",
@ -2672,11 +2672,11 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.7" version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a"
dependencies = [ dependencies = [
"thiserror-impl 2.0.7", "thiserror-impl 2.0.8",
] ]
[[package]] [[package]]
@ -2692,9 +2692,9 @@ dependencies = [
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.7" version = "2.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -1,4 +1,4 @@
INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) INSERT INTO [User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
VALUES ( VALUES (
1, 1,
'paul@atreides.com', 'paul@atreides.com',
@ -8,7 +8,7 @@ VALUES (
NULL NULL
); );
INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) INSERT INTO [User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
VALUES ( VALUES (
2, 2,
'alia@atreides.com', 'alia@atreides.com',

View file

@ -47,7 +47,9 @@ 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, -- https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
[lang] TEXT NOT NULL DEFAULT 'en',
[estimate_time] INTEGER, -- in [s].
[description] TEXT NOT NULL DEFAULT '', [description] TEXT NOT NULL DEFAULT '',
[difficulty] INTEGER NOT NULL DEFAULT 0, [difficulty] INTEGER NOT NULL DEFAULT 0,
[servings] INTEGER DEFAULT 4, [servings] INTEGER DEFAULT 4,
@ -61,7 +63,7 @@ CREATE TABLE [Image] (
[recipe_id] INTEGER NOT NULL, [recipe_id] INTEGER NOT NULL,
[name] TEXT NOT NULL DEFAULT '', [name] TEXT NOT NULL DEFAULT '',
[description] TEXT NOT NULL DEFAULT '', [description] TEXT NOT NULL DEFAULT '',
[image] BLOB, [image] BLOB NOT NULL,
FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
) STRICT; ) STRICT;
@ -80,26 +82,30 @@ CREATE TABLE [RecipeTag] (
CREATE TABLE [Tag] ( CREATE TABLE [Tag] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[name] TEXT NOT NULL [name] TEXT NOT NULL,
-- https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
[lang] TEXT NOT NULL DEFAULT 'en'
) STRICT; ) STRICT;
CREATE UNIQUE INDEX [Tag_name_index] ON [Tag] ([name]); CREATE UNIQUE INDEX [Tag_name_lang_index] ON [Tag] ([name], [lang]);
CREATE TABLE [Ingredient] ( CREATE TABLE [Ingredient] (
[id] INTEGER PRIMARY KEY, [id] INTEGER PRIMARY KEY,
[name] TEXT NOT NULL, [name] TEXT NOT NULL,
[comment] TEXT NOT NULL DEFAULT '',
[quantity_value] REAL, [quantity_value] REAL,
[quantity_unit] TEXT NOT NULL DEFAULT '', [quantity_unit] TEXT NOT NULL DEFAULT '',
[input_group_id] INTEGER NOT NULL, [input_step_id] INTEGER NOT NULL,
FOREIGN KEY([input_group_id]) REFERENCES [Group]([id]) ON DELETE CASCADE FOREIGN KEY([input_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE
) STRICT; ) 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 NOT NULL,
[name] TEXT NOT NULL DEFAULT '', [name] TEXT NOT NULL DEFAULT '',
[comment] 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; ) STRICT;
@ -117,14 +123,14 @@ CREATE TABLE [Step] (
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 DEFAULT '', -- [name] TEXT NOT NULL DEFAULT '',
[quantity_value] REAL, -- [quantity_value] REAL,
[quantity_unit] TEXT NOT NULL DEFAULT '', -- [quantity_unit] TEXT NOT NULL DEFAULT '',
[output_group_id] INTEGER NOT NULL, -- [output_group_id] INTEGER NOT NULL,
[input_group_id] INTEGER NOT NULL, -- [input_group_id] INTEGER NOT NULL,
FOREIGN KEY([output_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE, -- FOREIGN KEY([output_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE,
FOREIGN KEY([input_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE -- FOREIGN KEY([input_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE
) STRICT; -- ) STRICT;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,125 @@
use super::{model, Connection, DBError, Result};
impl Connection {
pub async fn get_all_recipe_titles(&self) -> Result<Vec<(i64, String)>> {
sqlx::query_as("SELECT [id], [title] FROM [Recipe] ORDER BY [title]")
.fetch_all(&self.pool)
.await
.map_err(DBError::from)
}
pub async fn get_recipe(&self, id: i64) -> Result<Option<model::Recipe>> {
sqlx::query_as(
r#"
SELECT [id], [user_id], [title], [description]
FROM [Recipe] WHERE [id] = $1
"#,
)
.bind(id)
.fetch_optional(&self.pool)
.await
.map_err(DBError::from)
}
pub async fn create_recipe(&self, user_id: i64) -> Result<i64> {
let mut tx = self.tx().await?;
match sqlx::query_scalar::<_, i64>(
r#"
SELECT [Recipe].[id] FROM [Recipe]
LEFT JOIN [Image] ON [Image].[recipe_id] = [Recipe].[id]
LEFT JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
WHERE [Recipe].[user_id] = $1
AND [Recipe].[title] = ''
AND [Recipe].[estimate_time] IS NULL
AND [Recipe].[description] = ''
AND [Image].[id] IS NULL
AND [Group].[id] IS NULL
"#,
)
.bind(user_id)
.fetch_optional(&mut *tx)
.await?
{
Some(recipe_id) => Ok(recipe_id),
None => {
let db_result =
sqlx::query("INSERT INTO [Recipe] ([user_id], [title]) VALUES ($1, '')")
.bind(user_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(db_result.last_insert_rowid())
}
}
}
pub async fn set_recipe_title(&self, recipe_id: i64, title: &str) -> Result<()> {
sqlx::query("UPDATE [Recipe] SET [title] = $2 WHERE [id] = $1")
.bind(recipe_id)
.bind(title)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
pub async fn set_recipe_description(&self, recipe_id: i64, description: &str) -> Result<()> {
sqlx::query("UPDATE [Recipe] SET [description] = $2 WHERE [id] = $1")
.bind(recipe_id)
.bind(description)
.execute(&self.pool)
.await
.map(|_| ())
.map_err(DBError::from)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn create_a_new_recipe_then_update_its_title() -> Result<()> {
let connection = Connection::new_in_memory().await?;
connection.execute_sql(
sqlx::query(
r#"
INSERT INTO [User]
([id], [email], [name], [password], [validation_token_datetime], [validation_token])
VALUES
($1, $2, $3, $4, $5, $6)
"#
)
.bind(1)
.bind("paul@atreides.com")
.bind("paul")
.bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc")
.bind("2022-11-29 22:05:04.121407300+00:00")
.bind(None::<&str>) // 'null'.
).await?;
match connection.create_recipe(2).await {
Err(DBError::Sqlx(sqlx::Error::Database(err))) => {
// SQLITE_CONSTRAINT_FOREIGNKEY
// https://www.sqlite.org/rescode.html#constraint_foreignkey
assert_eq!(err.code(), Some(std::borrow::Cow::from("787")));
} // Nominal case. TODO: check 'err' value.
other => panic!(
"Creating a recipe with an inexistant user must fail: {:?}",
other
),
}
let recipe_id = connection.create_recipe(1).await?;
assert_eq!(recipe_id, 1);
connection.set_recipe_title(recipe_id, "Crêpe").await?;
let recipe = connection.get_recipe(recipe_id).await?.unwrap();
assert_eq!(recipe.title, "Crêpe".to_string());
Ok(())
}
}

945
backend/src/data/db/user.rs Normal file
View file

@ -0,0 +1,945 @@
use chrono::{prelude::*, Duration};
use rand::distributions::{Alphanumeric, DistString};
use sqlx::Sqlite;
use super::{model, Connection, DBError, Result};
use crate::{
consts,
hash::{hash, verify_password},
};
#[derive(Debug)]
pub enum SignUpResult {
UserAlreadyExists,
UserCreatedWaitingForValidation(String), // Validation token.
}
#[derive(Debug)]
pub enum UpdateUserResult {
EmailAlreadyTaken,
UserUpdatedWaitingForRevalidation(String), // Validation token.
Ok,
}
#[derive(Debug)]
pub enum ValidationResult {
UnknownUser,
ValidationExpired,
Ok(String, i64), // Returns token and user id.
}
#[derive(Debug)]
pub enum SignInResult {
UserNotFound,
WrongPassword,
AccountNotValidated,
Ok(String, i64), // Returns token and user id.
}
#[derive(Debug)]
pub enum AuthenticationResult {
NotValidToken,
Ok(i64), // Returns user id.
}
#[derive(Debug)]
pub enum GetTokenResetPasswordResult {
PasswordAlreadyReset,
EmailUnknown,
Ok(String),
}
#[derive(Debug)]
pub enum ResetPasswordResult {
ResetTokenExpired,
Ok,
}
fn generate_token() -> String {
Alphanumeric.sample_string(&mut rand::thread_rng(), consts::TOKEN_SIZE)
}
impl Connection {
#[cfg(test)]
pub async fn get_user_login_info(&self, token: &str) -> Result<model::UserLoginInfo> {
sqlx::query_as(
r#"
SELECT [last_login_datetime], [ip], [user_agent]
FROM [UserLoginToken] WHERE [token] = $1
"#,
)
.bind(token)
.fetch_one(&self.pool)
.await
.map_err(DBError::from)
}
pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> {
sqlx::query_as("SELECT [id], [email], [name] FROM [User] WHERE [id] = $1")
.bind(user_id)
.fetch_optional(&self.pool)
.await
.map_err(DBError::from)
}
/// If a new email is given and it doesn't match the current one then it has to be
/// Revalidated.
pub async fn update_user(
&self,
user_id: i64,
new_email: Option<&str>,
new_name: Option<&str>,
new_password: Option<&str>,
) -> Result<UpdateUserResult> {
let mut tx = self.tx().await?;
let hashed_new_password = new_password.map(|p| hash(p).unwrap());
let (email, name, hashed_password) = sqlx::query_as::<_, (String, String, String)>(
"SELECT [email], [name], [password] FROM [User] WHERE [id] = $1",
)
.bind(user_id)
.fetch_one(&mut *tx)
.await?;
let email_changed = new_email.is_some_and(|new_email| new_email != email);
// Check if email not already taken.
let validation_token = if email_changed {
if sqlx::query_scalar::<_, i64>(
r#"
SELECT COUNT(*)
FROM [User]
WHERE [email] = $1
"#,
)
.bind(new_email.unwrap())
.fetch_one(&mut *tx)
.await?
> 0
{
return Ok(UpdateUserResult::EmailAlreadyTaken);
}
let token = Some(generate_token());
sqlx::query(
r#"
UPDATE [User]
SET [validation_token] = $2, [validation_token_datetime] = $3
WHERE [id] = $1
"#,
)
.bind(user_id)
.bind(&token)
.bind(Utc::now())
.execute(&mut *tx)
.await?;
token
} else {
None
};
sqlx::query(
r#"
UPDATE [User]
SET [email] = $2, [name] = $3, [password] = $4
WHERE [id] = $1
"#,
)
.bind(user_id)
.bind(new_email.unwrap_or(&email))
.bind(new_name.unwrap_or(&name))
.bind(hashed_new_password.unwrap_or(hashed_password))
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(if let Some(validation_token) = validation_token {
UpdateUserResult::UserUpdatedWaitingForRevalidation(validation_token)
} else {
UpdateUserResult::Ok
})
}
pub async fn sign_up(&self, email: &str, password: &str) -> Result<SignUpResult> {
self.sign_up_with_given_time(email, password, Utc::now())
.await
}
async fn sign_up_with_given_time(
&self,
email: &str,
password: &str,
datetime: DateTime<Utc>,
) -> Result<SignUpResult> {
let mut tx = self.tx().await?;
let token = match sqlx::query_as::<_, (i64, Option<String>)>(
r#"
SELECT [id], [validation_token]
FROM [User] WHERE [email] = $1
"#,
)
.bind(email)
.fetch_optional(&mut *tx)
.await?
{
Some((id, validation_token)) => {
if validation_token.is_none() {
return Ok(SignUpResult::UserAlreadyExists);
}
let token = generate_token();
let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?;
sqlx::query(
r#"
UPDATE [User]
SET [validation_token] = $2, [validation_token_datetime] = $3, [password] = $4
WHERE [id] = $1
"#,
)
.bind(id)
.bind(&token)
.bind(datetime)
.bind(hashed_password)
.execute(&mut *tx)
.await?;
token
}
None => {
let token = generate_token();
let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?;
sqlx::query(
r#"
INSERT INTO [User]
([email], [validation_token], [validation_token_datetime], [password])
VALUES ($1, $2, $3, $4)
"#,
)
.bind(email)
.bind(&token)
.bind(datetime)
.bind(hashed_password)
.execute(&mut *tx)
.await?;
token
}
};
tx.commit().await?;
Ok(SignUpResult::UserCreatedWaitingForValidation(token))
}
pub async fn validation(
&self,
token: &str,
validation_time: Duration,
ip: &str,
user_agent: &str,
) -> Result<ValidationResult> {
let mut tx = self.tx().await?;
// There is no index on [validation_token]. Is it useful?
let user_id = match sqlx::query_as::<_, (i64, DateTime<Utc>)>(
"SELECT [id], [validation_token_datetime] FROM [User] WHERE [validation_token] = $1",
)
.bind(token)
.fetch_optional(&mut *tx)
.await?
{
Some((id, validation_token_datetime)) => {
if Utc::now() - validation_token_datetime > validation_time {
return Ok(ValidationResult::ValidationExpired);
}
sqlx::query("UPDATE [User] SET [validation_token] = NULL WHERE [id] = $1")
.bind(id)
.execute(&mut *tx)
.await?;
id
}
None => return Ok(ValidationResult::UnknownUser),
};
let token = Self::create_login_token(&mut tx, user_id, ip, user_agent).await?;
tx.commit().await?;
Ok(ValidationResult::Ok(token, user_id))
}
pub async fn sign_in(
&self,
email: &str,
password: &str,
ip: &str,
user_agent: &str,
) -> Result<SignInResult> {
let mut tx = self.tx().await?;
match sqlx::query_as::<_, (i64, String, Option<String>)>(
"SELECT [id], [password], [validation_token] FROM [User] WHERE [email] = $1",
)
.bind(email)
.fetch_optional(&mut *tx)
.await?
{
Some((id, stored_password, validation_token)) => {
if validation_token.is_some() {
Ok(SignInResult::AccountNotValidated)
} else if verify_password(password, &stored_password)
.map_err(DBError::from_dyn_error)?
{
let token = Self::create_login_token(&mut tx, id, ip, user_agent).await?;
tx.commit().await?;
Ok(SignInResult::Ok(token, id))
} else {
Ok(SignInResult::WrongPassword)
}
}
None => Ok(SignInResult::UserNotFound),
}
}
pub async fn authentication(
&self,
token: &str,
ip: &str,
user_agent: &str,
) -> Result<AuthenticationResult> {
let mut tx = self.tx().await?;
match sqlx::query_as::<_, (i64, i64)>(
"SELECT [id], [user_id] FROM [UserLoginToken] WHERE [token] = $1",
)
.bind(token)
.fetch_optional(&mut *tx)
.await?
{
Some((login_id, user_id)) => {
sqlx::query(
r#"
UPDATE [UserLoginToken]
SET [last_login_datetime] = $2, [ip] = $3, [user_agent] = $4
WHERE [id] = $1
"#,
)
.bind(login_id)
.bind(Utc::now())
.bind(ip)
.bind(user_agent)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(AuthenticationResult::Ok(user_id))
}
None => Ok(AuthenticationResult::NotValidToken),
}
}
pub async fn sign_out(&self, token: &str) -> Result<()> {
let mut tx = self.tx().await?;
match sqlx::query_scalar::<_, i64>("SELECT [id] FROM [UserLoginToken] WHERE [token] = $1")
.bind(token)
.fetch_optional(&mut *tx)
.await?
{
Some(login_id) => {
sqlx::query("DELETE FROM [UserLoginToken] WHERE [id] = $1")
.bind(login_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
}
None => (),
}
Ok(())
}
pub async fn get_token_reset_password(
&self,
email: &str,
validation_time: Duration,
) -> Result<GetTokenResetPasswordResult> {
let mut tx = self.tx().await?;
if let Some(db_datetime_nullable) = sqlx::query_scalar::<_, Option<DateTime<Utc>>>(
r#"
SELECT [password_reset_datetime]
FROM [User]
WHERE [email] = $1
"#,
)
.bind(email)
.fetch_optional(&mut *tx)
.await?
{
if let Some(db_datetime) = db_datetime_nullable {
if Utc::now() - db_datetime <= validation_time {
return Ok(GetTokenResetPasswordResult::PasswordAlreadyReset);
}
}
} else {
return Ok(GetTokenResetPasswordResult::EmailUnknown);
}
let token = generate_token();
sqlx::query(
r#"
UPDATE [User]
SET [password_reset_token] = $2, [password_reset_datetime] = $3
WHERE [email] = $1
"#,
)
.bind(email)
.bind(&token)
.bind(Utc::now())
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(GetTokenResetPasswordResult::Ok(token))
}
pub async fn reset_password(
&self,
new_password: &str,
token: &str,
validation_time: Duration,
) -> Result<ResetPasswordResult> {
let mut tx = self.tx().await?;
// There is no index on [password_reset_token]. Is it useful?
if let (user_id, Some(db_datetime)) = sqlx::query_as::<_, (i64, Option<DateTime<Utc>>)>(
r#"
SELECT [id], [password_reset_datetime]
FROM [User]
WHERE [password_reset_token] = $1
"#,
)
.bind(token)
.fetch_one(&mut *tx)
.await?
{
if Utc::now() - db_datetime > validation_time {
return Ok(ResetPasswordResult::ResetTokenExpired);
}
// Remove all login tokens (for security reasons).
sqlx::query("DELETE FROM [UserLoginToken] WHERE [user_id] = $1")
.bind(user_id)
.execute(&mut *tx)
.await?;
let hashed_new_password = hash(new_password).map_err(|e| DBError::from_dyn_error(e))?;
sqlx::query(
r#"
UPDATE [User]
SET [password] = $2, [password_reset_token] = NULL, [password_reset_datetime] = NULL
WHERE [id] = $1
"#,
)
.bind(user_id)
.bind(hashed_new_password)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(ResetPasswordResult::Ok)
} else {
Err(DBError::Other(
"Can't reset password: stored token or datetime not set (NULL)".to_string(),
))
}
}
// Return the token.
async fn create_login_token(
tx: &mut sqlx::Transaction<'_, Sqlite>,
user_id: i64,
ip: &str,
user_agent: &str,
) -> Result<String> {
let token = generate_token();
sqlx::query(
r#"
INSERT INTO [UserLoginToken]
([user_id], [last_login_datetime], [token], [ip], [user_agent])
VALUES ($1, $2, $3, $4, $5)
"#,
)
.bind(user_id)
.bind(Utc::now())
.bind(&token)
.bind(ip)
.bind(user_agent)
.execute(&mut **tx)
.await?;
Ok(token)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn sign_up() -> Result<()> {
let connection = Connection::new_in_memory().await?;
match connection.sign_up("paul@atreides.com", "12345").await? {
SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
other => panic!("{:?}", other),
}
Ok(())
}
#[tokio::test]
async fn sign_up_to_an_already_existing_user() -> Result<()> {
let connection = Connection::new_in_memory().await?;
connection.execute_sql(
sqlx::query(
r#"
INSERT INTO
[User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
VALUES (
1,
'paul@atreides.com',
'paul',
'$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
0,
NULL
);
"#)).await?;
match connection.sign_up("paul@atreides.com", "12345").await? {
SignUpResult::UserAlreadyExists => (), // Nominal case.
other => panic!("{:?}", other),
}
Ok(())
}
#[tokio::test]
async fn sign_up_and_sign_in_without_validation() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let email = "paul@atreides.com";
let password = "12345";
match connection.sign_up(email, password).await? {
SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
other => panic!("{:?}", other),
}
match connection
.sign_in(email, password, "127.0.0.1", "Mozilla/5.0")
.await?
{
SignInResult::AccountNotValidated => (), // Nominal case.
other => panic!("{:?}", other),
}
Ok(())
}
#[tokio::test]
async fn sign_up_to_an_unvalidated_already_existing_user() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let token = generate_token();
connection.execute_sql(
sqlx::query(
r#"
INSERT INTO [User]
([id], [email], [name], [password], [validation_token_datetime], [validation_token])
VALUES (
1,
'paul@atreides.com',
'paul',
'$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
0,
$1
)
"#
).bind(token)).await?;
match connection.sign_up("paul@atreides.com", "12345").await? {
SignUpResult::UserCreatedWaitingForValidation(_) => (), // Nominal case.
other => panic!("{:?}", other),
}
Ok(())
}
#[tokio::test]
async fn sign_up_then_send_validation_at_time() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let validation_token = match connection.sign_up("paul@atreides.com", "12345").await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other),
};
match connection
.validation(
&validation_token,
Duration::hours(1),
"127.0.0.1",
"Mozilla/5.0",
)
.await?
{
ValidationResult::Ok(_, _) => (), // Nominal case.
other => panic!("{:?}", other),
}
Ok(())
}
#[tokio::test]
async fn sign_up_then_send_validation_too_late() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let validation_token = match connection
.sign_up_with_given_time("paul@atreides.com", "12345", Utc::now() - Duration::days(1))
.await?
{
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other),
};
match connection
.validation(
&validation_token,
Duration::hours(1),
"127.0.0.1",
"Mozilla/5.0",
)
.await?
{
ValidationResult::ValidationExpired => (), // Nominal case.
other => panic!("{:?}", other),
}
Ok(())
}
#[tokio::test]
async fn sign_up_then_send_validation_with_bad_token() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let _validation_token = match connection.sign_up("paul@atreides.com", "12345").await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other),
};
let random_token = generate_token();
match connection
.validation(
&random_token,
Duration::hours(1),
"127.0.0.1",
"Mozilla/5.0",
)
.await?
{
ValidationResult::UnknownUser => (), // Nominal case.
other => panic!("{:?}", other),
}
Ok(())
}
#[tokio::test]
async fn sign_up_then_send_validation_then_sign_in() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let email = "paul@atreides.com";
let password = "12345";
// Sign up.
let validation_token = match connection.sign_up(email, password).await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other),
};
// Validation.
match connection
.validation(
&validation_token,
Duration::hours(1),
"127.0.0.1",
"Mozilla/5.0",
)
.await?
{
ValidationResult::Ok(_, _) => (),
other => panic!("{:?}", other),
};
// Sign in.
match connection
.sign_in(email, password, "127.0.0.1", "Mozilla/5.0")
.await?
{
SignInResult::Ok(_, _) => (), // Nominal case.
other => panic!("{:?}", other),
}
Ok(())
}
#[tokio::test]
async fn sign_up_then_send_validation_then_authentication() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let email = "paul@atreides.com";
let password = "12345";
// Sign up.
let validation_token = match connection.sign_up(email, password).await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other),
};
// Validation.
let (authentication_token, _user_id) = match connection
.validation(
&validation_token,
Duration::hours(1),
"127.0.0.1",
"Mozilla",
)
.await?
{
ValidationResult::Ok(token, user_id) => (token, user_id),
other => panic!("{:?}", other),
};
// Check user login information.
let user_login_info_1 = connection
.get_user_login_info(&authentication_token)
.await?;
assert_eq!(user_login_info_1.ip, "127.0.0.1");
assert_eq!(user_login_info_1.user_agent, "Mozilla");
// Authentication.
let _user_id = match connection
.authentication(&authentication_token, "192.168.1.1", "Chrome")
.await?
{
AuthenticationResult::Ok(user_id) => user_id, // Nominal case.
other => panic!("{:?}", other),
};
// Check user login information.
let user_login_info_2 = connection
.get_user_login_info(&authentication_token)
.await?;
assert_eq!(user_login_info_2.ip, "192.168.1.1");
assert_eq!(user_login_info_2.user_agent, "Chrome");
Ok(())
}
#[tokio::test]
async fn sign_up_then_send_validation_then_sign_out_then_sign_in() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let email = "paul@atreides.com";
let password = "12345";
// Sign up.
let validation_token = match connection.sign_up(email, password).await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other),
};
// Validation.
let (authentication_token_1, user_id_1) = match connection
.validation(
&validation_token,
Duration::hours(1),
"127.0.0.1",
"Mozilla",
)
.await?
{
ValidationResult::Ok(token, user_id) => (token, user_id),
other => panic!("{:?}", other),
};
// Check user login information.
let user_login_info_1 = connection
.get_user_login_info(&authentication_token_1)
.await?;
assert_eq!(user_login_info_1.ip, "127.0.0.1");
assert_eq!(user_login_info_1.user_agent, "Mozilla");
// Sign out.
connection.sign_out(&authentication_token_1).await?;
// Sign in.
let (authentication_token_2, user_id_2) = match connection
.sign_in(email, password, "192.168.1.1", "Chrome")
.await?
{
SignInResult::Ok(token, user_id) => (token, user_id),
other => panic!("{:?}", other),
};
assert_eq!(user_id_1, user_id_2);
assert_ne!(authentication_token_1, authentication_token_2);
// Check user login information.
let user_login_info_2 = connection
.get_user_login_info(&authentication_token_2)
.await?;
assert_eq!(user_login_info_2.ip, "192.168.1.1");
assert_eq!(user_login_info_2.user_agent, "Chrome");
Ok(())
}
#[tokio::test]
async fn ask_to_reset_password_for_unknown_email() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let email = "paul@atreides.com";
// Ask for password reset.
match connection
.get_token_reset_password(email, Duration::hours(1))
.await?
{
GetTokenResetPasswordResult::EmailUnknown => Ok(()), // Nominal case.
other => panic!("{:?}", other),
}
}
#[tokio::test]
async fn sign_up_then_send_validation_then_sign_out_then_ask_to_reset_password() -> Result<()> {
let connection = Connection::new_in_memory().await?;
let email = "paul@atreides.com";
let password = "12345";
let new_password = "54321";
// Sign up.
let validation_token = match connection.sign_up(email, password).await? {
SignUpResult::UserCreatedWaitingForValidation(token) => token, // Nominal case.
other => panic!("{:?}", other),
};
// Validation.
let (authentication_token_1, user_id_1) = match connection
.validation(
&validation_token,
Duration::hours(1),
"127.0.0.1",
"Mozilla",
)
.await?
{
ValidationResult::Ok(token, user_id) => (token, user_id),
other => panic!("{:?}", other),
};
// Check user login information.
let user_login_info_1 = connection
.get_user_login_info(&authentication_token_1)
.await?;
assert_eq!(user_login_info_1.ip, "127.0.0.1");
assert_eq!(user_login_info_1.user_agent, "Mozilla");
// Sign out.
connection.sign_out(&authentication_token_1).await?;
// Ask for password reset.
let token = match connection
.get_token_reset_password(email, Duration::hours(1))
.await?
{
GetTokenResetPasswordResult::Ok(token) => token,
other => panic!("{:?}", other),
};
connection
.reset_password(&new_password, &token, Duration::hours(1))
.await?;
// Sign in.
let (authentication_token_2, user_id_2) = match connection
.sign_in(email, new_password, "192.168.1.1", "Chrome")
.await?
{
SignInResult::Ok(token, user_id) => (token, user_id),
other => panic!("{:?}", other),
};
assert_eq!(user_id_1, user_id_2);
assert_ne!(authentication_token_1, authentication_token_2);
// Check user login information.
let user_login_info_2 = connection
.get_user_login_info(&authentication_token_2)
.await?;
assert_eq!(user_login_info_2.ip, "192.168.1.1");
assert_eq!(user_login_info_2.user_agent, "Chrome");
Ok(())
}
#[tokio::test]
async fn update_user() -> Result<()> {
let connection = Connection::new_in_memory().await?;
connection.execute_sql(
sqlx::query(
r#"
INSERT INTO [User]
([id], [email], [name], [password], [validation_token_datetime], [validation_token])
VALUES
($1, $2, $3, $4, $5, $6)
"#
)
.bind(1)
.bind("paul@atreides.com")
.bind("paul")
.bind("$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc")
.bind("2022-11-29 22:05:04.121407300+00:00")
.bind(None::<&str>) // 'null'.
).await?;
let user = connection.load_user(1).await?.unwrap();
assert_eq!(user.name, "paul");
assert_eq!(user.email, "paul@atreides.com");
if let UpdateUserResult::UserUpdatedWaitingForRevalidation(token) = connection
.update_user(
1,
Some("muaddib@fremen.com"),
Some("muaddib"),
Some("Chani"),
)
.await?
{
let (_authentication_token_1, user_id_1) = match connection
.validation(&token, Duration::hours(1), "127.0.0.1", "Mozilla/5.0")
.await?
{
ValidationResult::Ok(token, user_id) => (token, user_id),
other => panic!("{:?}", other),
};
assert_eq!(user_id_1, 1);
} else {
panic!("A revalidation token must be created when changin e-mail");
}
let user = connection.load_user(1).await?.unwrap();
assert_eq!(user.name, "muaddib");
assert_eq!(user.email, "muaddib@fremen.com");
// Tests if password has been updated correctly.
if let SignInResult::Ok(_token, id) = connection
.sign_in("muaddib@fremen.com", "Chani", "127.0.0.1", "Mozilla/5.0")
.await?
{
assert_eq!(id, 1);
} else {
panic!("Can't sign in");
}
Ok(())
}
}

View file

@ -1,2 +1,3 @@
pub mod db; pub mod db;
pub mod model;
mod utils; mod utils;

View file

@ -18,8 +18,9 @@ pub struct Recipe {
pub user_id: i64, pub user_id: i64,
pub title: String, pub title: String,
pub description: String, pub description: String,
pub estimate_time: Option<i32>, // [min]. pub estimate_time: Option<i32>, // [s].
pub difficulty: Difficulty, pub difficulty: Difficulty,
pub lang: String,
//ingredients: Vec<Ingredient>, // For four people. //ingredients: Vec<Ingredient>, // For four people.
pub process: Vec<Group>, pub process: Vec<Group>,
@ -38,6 +39,7 @@ impl Recipe {
description, description,
estimate_time: None, estimate_time: None,
difficulty: Difficulty::Unknown, difficulty: Difficulty::Unknown,
lang: "en".to_string(),
process: Vec::new(), process: Vec::new(),
} }
} }
@ -56,7 +58,6 @@ pub struct Quantity {
pub struct Group { pub struct Group {
pub name: Option<String>, pub name: Option<String>,
pub input: Vec<StepInput>, pub input: Vec<StepInput>,
pub output: Vec<IntermediateSubstance>,
pub steps: Vec<Step>, pub steps: Vec<Step>,
} }
@ -64,19 +65,13 @@ pub struct Step {
pub action: String, pub action: String,
} }
pub struct IntermediateSubstance {
pub name: String,
pub quantity: Option<Quantity>,
}
pub enum StepInput { pub enum StepInput {
Ingredient(Ingredient), Ingredient(Ingredient),
IntermediateSubstance(IntermediateSubstance),
} }
pub enum Difficulty { pub enum Difficulty {
Unknown, Unknown = 0,
Easy, Easy = 1,
Medium, Medium = 2,
Hard, Hard = 3,
} }

View file

@ -1,6 +1,6 @@
use sqlx::{sqlite::SqliteRow, FromRow, Row}; use sqlx::{sqlite::SqliteRow, FromRow, Row};
use crate::model; use super::model;
impl FromRow<'_, SqliteRow> for model::Recipe { impl FromRow<'_, SqliteRow> for model::Recipe {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> { fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {

View file

@ -1,22 +1,17 @@
use askama::Template; use askama::Template;
use crate::model; use crate::data::model;
pub struct Recipes {
pub list: Vec<(i64, String)>,
pub current_id: Option<i64>,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "home.html")] #[template(path = "home.html")]
pub struct HomeTemplate { pub struct HomeTemplate {
pub user: Option<model::User>, pub user: Option<model::User>,
pub recipes: Vec<(i64, String)>, pub recipes: Recipes,
pub current_recipe_id: Option<i64>,
}
#[derive(Template)]
#[template(path = "view_recipe.html")]
pub struct ViewRecipeTemplate {
pub user: Option<model::User>,
pub recipes: Vec<(i64, String)>,
pub current_recipe_id: Option<i64>,
pub current_recipe: model::Recipe,
} }
#[derive(Template)] #[derive(Template)]
@ -91,3 +86,19 @@ pub struct ProfileTemplate {
pub message_email: String, pub message_email: String,
pub message_password: String, pub message_password: String,
} }
#[derive(Template)]
#[template(path = "recipe_view.html")]
pub struct RecipeViewTemplate {
pub user: Option<model::User>,
pub recipes: Recipes,
pub recipe: model::Recipe,
}
#[derive(Template)]
#[template(path = "recipe_edit.html")]
pub struct RecipeEditTemplate {
pub user: Option<model::User>,
pub recipes: Recipes,
pub recipe: model::Recipe,
}

View file

@ -15,7 +15,7 @@ use config::Config;
use tower_http::{services::ServeDir, trace::TraceLayer}; use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing::{event, Level}; use tracing::{event, Level};
use data::db; use data::{db, model};
mod config; mod config;
mod consts; mod consts;
@ -23,7 +23,6 @@ mod data;
mod email; mod email;
mod hash; mod hash;
mod html_templates; mod html_templates;
mod model;
mod ron_extractor; mod ron_extractor;
mod ron_utils; mod ron_utils;
mod services; mod services;
@ -111,6 +110,8 @@ async fn main() {
get(services::reset_password_get).post(services::reset_password_post), get(services::reset_password_get).post(services::reset_password_post),
) )
// Recipes. // Recipes.
.route("/recipe/new", get(services::create_recipe))
// .route("/recipe/edit/:id", get(services::edit_recipe))
.route("/recipe/view/:id", get(services::view_recipe)) .route("/recipe/view/:id", get(services::view_recipe))
// User. // User.
.route( .route(
@ -163,8 +164,8 @@ async fn get_current_user(
.authentication(token_cookie.value(), &client_ip, &client_user_agent) .authentication(token_cookie.value(), &client_ip, &client_user_agent)
.await .await
{ {
Ok(db::AuthenticationResult::NotValidToken) => None, Ok(db::user::AuthenticationResult::NotValidToken) => None,
Ok(db::AuthenticationResult::Ok(user_id)) => { Ok(db::user::AuthenticationResult::Ok(user_id)) => {
match connection.load_user(user_id).await { match connection.load_user(user_id).await {
Ok(user) => user, Ok(user) => user,
Err(error) => { Err(error) => {
@ -227,7 +228,7 @@ async fn process_args() -> bool {
// Set the creation datetime to 'now'. // Set the creation datetime to 'now'.
con.execute_sql( con.execute_sql(
sqlx::query( sqlx::query(
"UPDATE [User] SET [creation_datetime] = ?1 WHERE [email] = 'paul@test.org'") "UPDATE [User] SET [validation_token_datetime] = $1 WHERE [email] = 'paul@test.org'")
.bind(Utc::now()) .bind(Utc::now())
) )
.await .await

View file

@ -15,7 +15,12 @@ use serde::Deserialize;
use tracing::{event, Level}; use tracing::{event, Level};
use crate::{ use crate::{
config::Config, consts, data::db, email, html_templates::*, model, ron_utils, utils, AppState, config::Config,
consts,
data::{db, model},
email,
html_templates::*,
ron_utils, utils, AppState,
}; };
pub mod ron; pub mod ron;
@ -53,12 +58,41 @@ pub async fn home_page(
Ok(HomeTemplate { Ok(HomeTemplate {
user, user,
current_recipe_id: None, recipes: Recipes {
recipes, list: recipes,
current_id: None,
}, // current_recipe_id: None,
// recipes,
}) })
} }
///// VIEW RECIPE ///// ///// RECIPE /////
#[debug_handler]
pub async fn create_recipe(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
) -> Result<Response> {
if let Some(user) = user {
let recipe_id = connection.create_recipe(user.id).await?;
Ok(Redirect::to(&format!("/recipe/edit/{}", recipe_id)).into_response())
} else {
Ok(MessageTemplate::new("Not logged in").into_response())
}
}
// #[debug_handler]
// pub async fn edit_recipe(
// State(connection): State<db::Connection>,
// Extension(user): Extension<Option<model::User>>,
// Path(recipe_id): Path<i64>,
// ) -> Result<Response> {
// if let Some(user) = user {
// Ok(RecipeEditTemplate { user }.into_response())
// } else {
// Ok(MessageTemplate::new("Not logged in").into_response())
// }
// }
#[debug_handler] #[debug_handler]
pub async fn view_recipe( pub async fn view_recipe(
@ -68,11 +102,13 @@ pub async fn view_recipe(
) -> Result<Response> { ) -> Result<Response> {
let recipes = connection.get_all_recipe_titles().await?; let recipes = connection.get_all_recipe_titles().await?;
match connection.get_recipe(recipe_id).await? { match connection.get_recipe(recipe_id).await? {
Some(recipe) => Ok(ViewRecipeTemplate { Some(recipe) => Ok(RecipeViewTemplate {
user, user,
current_recipe_id: Some(recipe.id), recipes: Recipes {
recipes, list: recipes,
current_recipe: recipe, current_id: Some(recipe.id),
},
recipe,
} }
.into_response()), .into_response()),
None => Ok(MessageTemplate::new_with_user( None => Ok(MessageTemplate::new_with_user(
@ -173,10 +209,10 @@ pub async fn sign_up_post(
.sign_up(&form_data.email, &form_data.password_1) .sign_up(&form_data.email, &form_data.password_1)
.await .await
{ {
Ok(db::SignUpResult::UserAlreadyExists) => { Ok(db::user::SignUpResult::UserAlreadyExists) => {
error_response(SignUpError::UserAlreadyExists, &form_data, user) error_response(SignUpError::UserAlreadyExists, &form_data, user)
} }
Ok(db::SignUpResult::UserCreatedWaitingForValidation(token)) => { Ok(db::user::SignUpResult::UserCreatedWaitingForValidation(token)) => {
let url = utils::get_url_from_host(&host); let url = utils::get_url_from_host(&host);
let email = form_data.email.clone(); let email = form_data.email.clone();
match email::send_email( match email::send_email(
@ -236,7 +272,7 @@ pub async fn sign_up_validation(
) )
.await? .await?
{ {
db::ValidationResult::Ok(token, user_id) => { db::user::ValidationResult::Ok(token, user_id) => {
let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token); let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
jar = jar.add(cookie); jar = jar.add(cookie);
let user = connection.load_user(user_id).await?; let user = connection.load_user(user_id).await?;
@ -248,14 +284,14 @@ pub async fn sign_up_validation(
), ),
)) ))
} }
db::ValidationResult::ValidationExpired => Ok(( db::user::ValidationResult::ValidationExpired => Ok((
jar, jar,
MessageTemplate::new_with_user( MessageTemplate::new_with_user(
"The validation has expired. Try to sign up again", "The validation has expired. Try to sign up again",
user, user,
), ),
)), )),
db::ValidationResult::UnknownUser => Ok(( db::user::ValidationResult::UnknownUser => Ok((
jar, jar,
MessageTemplate::new_with_user("Validation error. Try to sign up again", user), MessageTemplate::new_with_user("Validation error. Try to sign up again", user),
)), )),
@ -307,7 +343,7 @@ pub async fn sign_in_post(
) )
.await? .await?
{ {
db::SignInResult::AccountNotValidated => Ok(( db::user::SignInResult::AccountNotValidated => Ok((
jar, jar,
SignInFormTemplate { SignInFormTemplate {
user, user,
@ -316,7 +352,7 @@ pub async fn sign_in_post(
} }
.into_response(), .into_response(),
)), )),
db::SignInResult::UserNotFound | db::SignInResult::WrongPassword => Ok(( db::user::SignInResult::UserNotFound | db::user::SignInResult::WrongPassword => Ok((
jar, jar,
SignInFormTemplate { SignInFormTemplate {
user, user,
@ -325,7 +361,7 @@ pub async fn sign_in_post(
} }
.into_response(), .into_response(),
)), )),
db::SignInResult::Ok(token, _user_id) => { db::user::SignInResult::Ok(token, _user_id) => {
let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token); let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
Ok((jar.add(cookie), Redirect::to("/").into_response())) Ok((jar.add(cookie), Redirect::to("/").into_response()))
} }
@ -433,15 +469,15 @@ pub async fn ask_reset_password_post(
) )
.await .await
{ {
Ok(db::GetTokenResetPasswordResult::PasswordAlreadyReset) => error_response( Ok(db::user::GetTokenResetPasswordResult::PasswordAlreadyReset) => error_response(
AskResetPasswordError::EmailAlreadyReset, AskResetPasswordError::EmailAlreadyReset,
&form_data.email, &form_data.email,
user, user,
), ),
Ok(db::GetTokenResetPasswordResult::EmailUnknown) => { Ok(db::user::GetTokenResetPasswordResult::EmailUnknown) => {
error_response(AskResetPasswordError::EmailUnknown, &form_data.email, user) error_response(AskResetPasswordError::EmailUnknown, &form_data.email, user)
} }
Ok(db::GetTokenResetPasswordResult::Ok(token)) => { Ok(db::user::GetTokenResetPasswordResult::Ok(token)) => {
let url = utils::get_url_from_host(&host); let url = utils::get_url_from_host(&host);
match email::send_email( match email::send_email(
&form_data.email, &form_data.email,
@ -559,12 +595,12 @@ pub async fn reset_password_post(
) )
.await .await
{ {
Ok(db::ResetPasswordResult::Ok) => Ok(MessageTemplate::new_with_user( Ok(db::user::ResetPasswordResult::Ok) => Ok(MessageTemplate::new_with_user(
"Your password has been reset", "Your password has been reset",
user, user,
) )
.into_response()), .into_response()),
Ok(db::ResetPasswordResult::ResetTokenExpired) => { Ok(db::user::ResetPasswordResult::ResetTokenExpired) => {
error_response(ResetPasswordError::TokenExpired, &form_data, user) error_response(ResetPasswordError::TokenExpired, &form_data, user)
} }
Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user), Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user),
@ -681,10 +717,10 @@ pub async fn edit_user_post(
) )
.await .await
{ {
Ok(db::UpdateUserResult::EmailAlreadyTaken) => { Ok(db::user::UpdateUserResult::EmailAlreadyTaken) => {
return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, user); return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, user);
} }
Ok(db::UpdateUserResult::UserUpdatedWaitingForRevalidation(token)) => { Ok(db::user::UpdateUserResult::UserUpdatedWaitingForRevalidation(token)) => {
let url = utils::get_url_from_host(&host); let url = utils::get_url_from_host(&host);
let email = form_data.email.clone(); let email = form_data.email.clone();
match email::send_email( match email::send_email(
@ -709,7 +745,7 @@ pub async fn edit_user_post(
} }
} }
} }
Ok(db::UpdateUserResult::Ok) => { Ok(db::user::UpdateUserResult::Ok) => {
message = "Profile saved"; message = "Profile saved";
} }
Err(_) => return error_response(ProfileUpdateError::DatabaseError, &form_data, user), Err(_) => return error_response(ProfileUpdateError::DatabaseError, &form_data, user),
@ -760,7 +796,7 @@ pub async fn email_revalidation(
) )
.await? .await?
{ {
db::ValidationResult::Ok(token, user_id) => { db::user::ValidationResult::Ok(token, user_id) => {
let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token); let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
jar = jar.add(cookie); jar = jar.add(cookie);
let user = connection.load_user(user_id).await?; let user = connection.load_user(user_id).await?;
@ -769,14 +805,14 @@ pub async fn email_revalidation(
MessageTemplate::new_with_user("Email validation successful", user), MessageTemplate::new_with_user("Email validation successful", user),
)) ))
} }
db::ValidationResult::ValidationExpired => Ok(( db::user::ValidationResult::ValidationExpired => Ok((
jar, jar,
MessageTemplate::new_with_user( MessageTemplate::new_with_user(
"The validation has expired. Try to sign up again with the same email", "The validation has expired. Try to sign up again with the same email",
user, user,
), ),
)), )),
db::ValidationResult::UnknownUser => Ok(( db::user::ValidationResult::UnknownUser => Ok((
jar, jar,
MessageTemplate::new_with_user( MessageTemplate::new_with_user(
"Validation error. Try to sign up again with the same email", "Validation error. Try to sign up again with the same email",

View file

@ -7,14 +7,14 @@
{% block main_container %} {% block main_container %}
<nav class="recipes-list"> <nav class="recipes-list">
<ul> <ul>
{% for (id, title) in recipes %} {% for (id, title) in recipes.list %}
<li> <li>
{% match current_recipe_id %} {% match recipes.current_id %}
{# Don't know how to avoid {# Don't know how to avoid
repetition: comparing (using '==' or .eq()) current_recipe_id.unwrap() and id doesn't work. repetition: comparing (using '==' or .eq()) recipes.current_recipe_id.unwrap() and id doesn't work.
Guards for match don't exist. Guards for match don't exist.
See: https://github.com/djc/askama/issues/752 #} See: https://github.com/djc/askama/issues/752 #}
{% when Some (current_id) %} {% when Some(current_id) %}
{% if current_id == id %} {% if current_id == id %}
{% call recipe_item(id, title, "recipe-item-current") %} {% call recipe_item(id, title, "recipe-item-current") %}
{% else %} {% else %}

View file

@ -6,7 +6,7 @@
id="title_field" id="title_field"
type="text" type="text"
name="title" name="title"
value="{{ current_recipe.title }}" value="{{ recipe.title }}"
autocapitalize="none" autocapitalize="none"
autocomplete="title" autocomplete="title"
autofocus="autofocus" /> autofocus="autofocus" />
@ -16,7 +16,7 @@
id="title_field" id="title_field"
type="text" type="text"
name="title" name="title"
value="{{ current_recipe.description }}" value="{{ recipe.description }}"
autocapitalize="none" autocapitalize="none"
autocomplete="title" autocomplete="title"
autofocus="autofocus" /> autofocus="autofocus" />

View file

@ -0,0 +1,18 @@
{% extends "base_with_list.html" %}
{% block content %}
<h2 class="recipe-title" >{{ recipe.title }}</h2>
{% if user.is_some() && recipe.user_id == user.as_ref().unwrap().id %}
<a class="edit-recipe" href="/recipe/edit/{{ recipe.id }}" >Edit</a>
{% endif %}
{% if !recipe.description.is_empty() %}
<div class="recipe-description" >
{{ recipe.description.clone()|markdown }}
</div>
{% endif %}
{% endblock %}

View file

@ -12,10 +12,10 @@
{{ message_email }} {{ message_email }}
<label for="input-password-1">Choose a password (minimum 8 characters)</label> <label for="input-password-1">Choose a password (minimum 8 characters)</label>
<input id="input-password-1" type="password" name="password_1" /> <input id="input-password-1" type="password" name="password_1" autocomplete="new-password" />
<label for="input-password-2">Re-enter password</label> <label for="input-password-2">Re-enter password</label>
<input id="input-password-2" type="password" name="password_2" /> <input id="input-password-2" type="password" name="password_2" autocomplete="new-password" />
{{ message_password }} {{ message_password }}

View file

@ -1,18 +0,0 @@
{% extends "base_with_list.html" %}
{% block content %}
<h2 class="recipe-title" >{{ current_recipe.title }}</h2>
{% 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" >
{{ current_recipe.description.clone()|markdown }}
</div>
{% endif %}
{% endblock %}