Split db::Connection implementation in submodules (db::user and db::recipe).
This commit is contained in:
parent
4248d11aa9
commit
fce4eade73
17 changed files with 1307 additions and 1234 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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
125
backend/src/data/db/recipe.rs
Normal file
125
backend/src/data/db/recipe.rs
Normal 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
945
backend/src/data/db/user.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod model;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@
|
||||||
{% 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) %}
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
18
backend/templates/recipe_view.html
Normal file
18
backend/templates/recipe_view.html
Normal 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 %}
|
||||||
|
|
@ -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 }}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue