-INSERT INTO [User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
+INSERT INTO [User] ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES (
1,
'paul@atreides.com',
'Paul',
+ '2025-01-07T10:41:05.697884837+00:00',
'$argon2id$v=19$m=4096,t=4,p=2$l1fAMRc0VfkNzqpEfFEReg$/gsUsY2aML8EbKjPeCxucenxkxhiFSXDmizWZPLvNuo',
0,
NULL
);
-INSERT INTO [User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
+INSERT INTO [User] ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES (
2,
'alia@atreides.com',
'Alia',
+ '2025-01-07T10:41:05.697884837+00:00',
'$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
0,
NULL
CREATE TABLE [User] (
[id] INTEGER PRIMARY KEY,
+
[email] TEXT NOT NULL,
[name] TEXT NOT NULL DEFAULT '',
+ [creation_datetime] TEXT NOT NULL,
+
[default_servings] INTEGER DEFAULT 4,
[lang] TEXT NOT NULL DEFAULT 'en',
+++ /dev/null
-use std::{
- fmt,
- fs::{self, File},
- io::Read,
- path::Path,
- str::FromStr,
- time::Duration,
-};
-
-use sqlx::{
- sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
- Pool, Sqlite, Transaction,
-};
-use thiserror::Error;
-use tracing::{event, Level};
-
-use crate::consts;
-
-pub mod recipe;
-pub mod user;
-
-const CURRENT_DB_VERSION: u32 = 1;
-
-#[derive(Error, Debug)]
-pub enum DBError {
- #[error("Sqlx error: {0}")]
- Sqlx(#[from] sqlx::Error),
-
- #[error(
- "Unsupported database version: {0} (application version: {current})",
- current = CURRENT_DB_VERSION
- )]
- UnsupportedVersion(u32),
-
- #[error("Unknown error: {0}")]
- Other(String),
-}
-
-impl DBError {
- fn from_dyn_error(error: Box<dyn std::error::Error>) -> Self {
- DBError::Other(error.to_string())
- }
-}
-
-type Result<T> = std::result::Result<T, DBError>;
-
-#[derive(Clone)]
-pub struct Connection {
- pool: Pool<Sqlite>,
-}
-
-impl Connection {
- pub async fn new() -> Result<Connection> {
- let path = Path::new(consts::DB_DIRECTORY).join(consts::DB_FILENAME);
- Self::new_from_file(path).await
- }
-
- #[cfg(test)]
- pub async fn new_in_memory() -> Result<Connection> {
- Self::create_connection(SqlitePoolOptions::new().connect("sqlite::memory:").await?).await
- }
-
- pub async fn new_from_file<P: AsRef<Path>>(file: P) -> Result<Connection> {
- if let Some(data_dir) = file.as_ref().parent() {
- if !data_dir.exists() {
- fs::DirBuilder::new().create(data_dir).unwrap();
- }
- }
-
- let options = SqliteConnectOptions::from_str(&format!(
- "sqlite://{}",
- file.as_ref().to_str().unwrap()
- ))?
- .journal_mode(SqliteJournalMode::Wal) // TODO: use 'Wal2' when available.
- .create_if_missing(true)
- .busy_timeout(Duration::from_secs(10))
- .foreign_keys(true)
- .synchronous(SqliteSynchronous::Normal);
-
- Self::create_connection(
- SqlitePoolOptions::new()
- .max_connections(consts::MAX_DB_CONNECTION)
- .connect_with(options)
- .await?,
- )
- .await
- }
-
- async fn create_connection(pool: Pool<Sqlite>) -> Result<Connection> {
- let connection = Connection { pool };
- connection.create_or_update_db().await?;
- Ok(connection)
- }
-
- async fn tx(&self) -> Result<Transaction<Sqlite>> {
- self.pool.begin().await.map_err(DBError::from)
- }
-
- /// Called after the connection has been established for creating or updating the database.
- /// The 'Version' table tracks the current state of the database.
- async fn create_or_update_db(&self) -> Result<()> {
- let mut tx = self.tx().await?; //con.transaction()?;
-
- // Check current database version. (Version 0 corresponds to an empty database).
- let mut version = match sqlx::query(
- r#"
-SELECT [name] FROM [sqlite_master]
-WHERE [type] = 'table' AND [name] = 'Version'
- "#,
- )
- .fetch_one(&mut *tx)
- .await
- {
- Ok(_) => sqlx::query_scalar("SELECT [version] FROM [Version] ORDER BY [id] DESC")
- .fetch_optional(&mut *tx)
- .await?
- .unwrap_or(0),
- Err(_) => 0, // If the database doesn't exist.
- };
-
- while Self::update_to_next_version(version, &mut tx).await? {
- version += 1;
- }
-
- tx.commit().await?;
-
- Ok(())
- }
-
- async fn update_to_next_version(
- current_version: u32,
- tx: &mut Transaction<'_, Sqlite>,
- ) -> Result<bool> {
- let next_version = current_version + 1;
-
- if next_version <= CURRENT_DB_VERSION {
- event!(Level::INFO, "Update to version {}...", next_version);
- }
-
- async fn update_version(to_version: u32, tx: &mut Transaction<'_, Sqlite>) -> Result<()> {
- sqlx::query(
- "INSERT INTO [Version] ([version], [datetime]) VALUES ($1, datetime('now'))",
- )
- .bind(to_version)
- .execute(&mut **tx)
- .await?;
- Ok(())
- }
-
- fn ok(updated: bool) -> Result<bool> {
- if updated {
- event!(Level::INFO, "Version updated");
- }
- Ok(updated)
- }
-
- match next_version {
- 1 => {
- let sql_file = consts::SQL_FILENAME.replace("{VERSION}", &next_version.to_string());
- sqlx::query(&load_sql_file(&sql_file)?)
- .execute(&mut **tx)
- .await?;
- update_version(next_version, tx).await?;
-
- ok(true)
- }
-
- // Version 2 doesn't exist yet.
- 2 => ok(false),
-
- v => Err(DBError::UnsupportedVersion(v)),
- }
- }
-
- /// Execute a given SQL file.
- pub async fn execute_file<P: AsRef<Path> + fmt::Display>(&self, file: P) -> Result<()> {
- let sql = load_sql_file(file)?;
- sqlx::query(&sql)
- .execute(&self.pool)
- .await
- .map(|_| ())
- .map_err(DBError::from)
- }
-
- pub async fn execute_sql<'a>(
- &self,
- query: sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments<'a>>,
- ) -> Result<u64> {
- query
- .execute(&self.pool)
- .await
- .map(|db_result| db_result.rows_affected())
- .map_err(DBError::from)
- }
-
- // pub async fn execute_sql_and_fetch_all<'a>(
- // &self,
- // query: sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments<'a>>,
- // ) -> Result<Vec<SqliteRow>> {
- // query.fetch_all(&self.pool).await.map_err(DBError::from)
- // }
-}
-
-fn load_sql_file<P: AsRef<Path> + fmt::Display>(sql_file: P) -> Result<String> {
- let mut file = File::open(&sql_file)
- .map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err)))?;
- let mut sql = String::new();
- file.read_to_string(&mut sql)
- .map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err)))?;
- Ok(sql)
-}
--- /dev/null
+use std::{
+ fmt,
+ fs::{self, File},
+ io::Read,
+ path::Path,
+ str::FromStr,
+ time::Duration,
+};
+
+use sqlx::{
+ sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous},
+ Pool, Sqlite, Transaction,
+};
+use thiserror::Error;
+use tracing::{event, Level};
+
+use crate::consts;
+
+pub mod recipe;
+pub mod user;
+
+const CURRENT_DB_VERSION: u32 = 1;
+
+#[derive(Error, Debug)]
+pub enum DBError {
+ #[error("Sqlx error: {0}")]
+ Sqlx(#[from] sqlx::Error),
+
+ #[error(
+ "Unsupported database version: {0} (application version: {current})",
+ current = CURRENT_DB_VERSION
+ )]
+ UnsupportedVersion(u32),
+
+ #[error("Unknown error: {0}")]
+ Other(String),
+}
+
+impl DBError {
+ fn from_dyn_error(error: Box<dyn std::error::Error>) -> Self {
+ DBError::Other(error.to_string())
+ }
+}
+
+type Result<T> = std::result::Result<T, DBError>;
+
+#[derive(Clone)]
+pub struct Connection {
+ pool: Pool<Sqlite>,
+}
+
+impl Connection {
+ pub async fn new() -> Result<Connection> {
+ let path = Path::new(consts::DB_DIRECTORY).join(consts::DB_FILENAME);
+ Self::new_from_file(path).await
+ }
+
+ #[cfg(test)]
+ pub async fn new_in_memory() -> Result<Connection> {
+ Self::create_connection(SqlitePoolOptions::new().connect("sqlite::memory:").await?).await
+ }
+
+ pub async fn new_from_file<P: AsRef<Path>>(file: P) -> Result<Connection> {
+ if let Some(data_dir) = file.as_ref().parent() {
+ if !data_dir.exists() {
+ fs::DirBuilder::new().create(data_dir).unwrap();
+ }
+ }
+
+ let options = SqliteConnectOptions::from_str(&format!(
+ "sqlite://{}",
+ file.as_ref().to_str().unwrap()
+ ))?
+ .journal_mode(SqliteJournalMode::Wal) // TODO: use 'Wal2' when available.
+ .create_if_missing(true)
+ .busy_timeout(Duration::from_secs(10))
+ .foreign_keys(true)
+ .synchronous(SqliteSynchronous::Normal);
+
+ Self::create_connection(
+ SqlitePoolOptions::new()
+ .max_connections(consts::MAX_DB_CONNECTION)
+ .connect_with(options)
+ .await?,
+ )
+ .await
+ }
+
+ async fn create_connection(pool: Pool<Sqlite>) -> Result<Connection> {
+ let connection = Connection { pool };
+ connection.create_or_update_db().await?;
+ Ok(connection)
+ }
+
+ async fn tx(&self) -> Result<Transaction<Sqlite>> {
+ self.pool.begin().await.map_err(DBError::from)
+ }
+
+ /// Called after the connection has been established for creating or updating the database.
+ /// The 'Version' table tracks the current state of the database.
+ async fn create_or_update_db(&self) -> Result<()> {
+ let mut tx = self.tx().await?; //con.transaction()?;
+
+ // Check current database version. (Version 0 corresponds to an empty database).
+ let mut version = match sqlx::query(
+ r#"
+SELECT [name] FROM [sqlite_master]
+WHERE [type] = 'table' AND [name] = 'Version'
+ "#,
+ )
+ .fetch_one(&mut *tx)
+ .await
+ {
+ Ok(_) => sqlx::query_scalar("SELECT [version] FROM [Version] ORDER BY [id] DESC")
+ .fetch_optional(&mut *tx)
+ .await?
+ .unwrap_or(0),
+ Err(_) => 0, // If the database doesn't exist.
+ };
+
+ while Self::update_to_next_version(version, &mut tx).await? {
+ version += 1;
+ }
+
+ tx.commit().await?;
+
+ Ok(())
+ }
+
+ async fn update_to_next_version(
+ current_version: u32,
+ tx: &mut Transaction<'_, Sqlite>,
+ ) -> Result<bool> {
+ let next_version = current_version + 1;
+
+ if next_version <= CURRENT_DB_VERSION {
+ event!(Level::INFO, "Update to version {}...", next_version);
+ }
+
+ async fn update_version(to_version: u32, tx: &mut Transaction<'_, Sqlite>) -> Result<()> {
+ sqlx::query(
+ "INSERT INTO [Version] ([version], [datetime]) VALUES ($1, datetime('now'))",
+ )
+ .bind(to_version)
+ .execute(&mut **tx)
+ .await?;
+ Ok(())
+ }
+
+ fn ok(updated: bool) -> Result<bool> {
+ if updated {
+ event!(Level::INFO, "Version updated");
+ }
+ Ok(updated)
+ }
+
+ match next_version {
+ 1 => {
+ let sql_file = consts::SQL_FILENAME.replace("{VERSION}", &next_version.to_string());
+ sqlx::query(&load_sql_file(&sql_file)?)
+ .execute(&mut **tx)
+ .await?;
+ update_version(next_version, tx).await?;
+
+ ok(true)
+ }
+
+ // Version 2 doesn't exist yet.
+ 2 => ok(false),
+
+ v => Err(DBError::UnsupportedVersion(v)),
+ }
+ }
+
+ /// Execute a given SQL file.
+ pub async fn execute_file<P: AsRef<Path> + fmt::Display>(&self, file: P) -> Result<()> {
+ let sql = load_sql_file(file)?;
+ sqlx::query(&sql)
+ .execute(&self.pool)
+ .await
+ .map(|_| ())
+ .map_err(DBError::from)
+ }
+
+ pub async fn execute_sql<'a>(
+ &self,
+ query: sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments<'a>>,
+ ) -> Result<u64> {
+ query
+ .execute(&self.pool)
+ .await
+ .map(|db_result| db_result.rows_affected())
+ .map_err(DBError::from)
+ }
+
+ // pub async fn execute_sql_and_fetch_all<'a>(
+ // &self,
+ // query: sqlx::query::Query<'a, Sqlite, sqlx::sqlite::SqliteArguments<'a>>,
+ // ) -> Result<Vec<SqliteRow>> {
+ // query.fetch_all(&self.pool).await.map_err(DBError::from)
+ // }
+}
+
+fn load_sql_file<P: AsRef<Path> + fmt::Display>(sql_file: P) -> Result<String> {
+ let mut file = File::open(&sql_file)
+ .map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err)))?;
+ let mut sql = String::new();
+ file.read_to_string(&mut sql)
+ .map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err)))?;
+ Ok(sql)
+}
sqlx::query(
r#"
INSERT INTO [User]
- ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
+ ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES
- ($1, $2, $3, $4, $5, $6)
+ ($1, $2, $3, $4, $5, $6, $7)
"#
)
.bind(user_id)
.bind("paul@atreides.com")
.bind("paul")
+ .bind("")
.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'.
sqlx::query(
r#"
INSERT INTO [User]
-([email], [validation_token], [validation_token_datetime], [password])
-VALUES ($1, $2, $3, $4)
+([email], [creation_datetime], [validation_token], [validation_token_datetime], [password])
+VALUES ($1, $2, $3, $4, $5)
"#,
)
.bind(email)
+ .bind(Utc::now())
.bind(&token)
.bind(datetime)
.bind(hashed_password)
sqlx::query(
r#"
INSERT INTO
- [User] ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
+ [User] ([id], [email], [name], [creation_datetime], [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
sqlx::query(
r#"
INSERT INTO [User]
- ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
+ ([id], [email], [creation_datetime], [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,
sqlx::query(
r#"
INSERT INTO [User]
- ([id], [email], [name], [password], [validation_token_datetime], [validation_token])
+ ([id], [email], [name], [creation_datetime], [password], [validation_token_datetime], [validation_token])
VALUES
- ($1, $2, $3, $4, $5, $6)
+ (1, 'paul@atreides.com', 'paul', '', '$argon2id$v=19$m=4096,t=3,p=1$G4fjepS05MkRbTqEImUdYg$GGziE8uVQe1L1oFHk37lBno10g4VISnVqynSkLCH3Lc', '2022-11-29 22:05:04.121407300+00:00', NULL)
"#
)
- .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();
#[derive(Template)]
#[template(path = "message.html")]
-pub struct MessageTemplate {
+pub struct MessageTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
- pub message: String,
+ pub message: &'a str,
pub as_code: bool, // Display the message in <pre> markup.
}
-impl MessageTemplate {
- pub fn new(message: String, tr: Tr) -> MessageTemplate {
+impl<'a> MessageTemplate<'a> {
+ pub fn new(message: &'a str, tr: Tr) -> MessageTemplate<'a> {
MessageTemplate {
user: None,
tr,
}
}
- pub fn new_with_user(message: String, tr: Tr, user: Option<model::User>) -> MessageTemplate {
+ pub fn new_with_user(
+ message: &'a str,
+ tr: Tr,
+ user: Option<model::User>,
+ ) -> MessageTemplate<'a> {
MessageTemplate {
user,
tr,
#[derive(Template)]
#[template(path = "sign_up_form.html")]
-pub struct SignUpFormTemplate {
+pub struct SignUpFormTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
pub email: String,
- pub message: String,
- pub message_email: String,
- pub message_password: String,
+ pub message: &'a str,
+ pub message_email: &'a str,
+ pub message_password: &'a str,
}
#[derive(Template)]
#[template(path = "sign_in_form.html")]
-pub struct SignInFormTemplate {
+pub struct SignInFormTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
- pub email: String,
- pub message: String,
+ pub email: &'a str,
+ pub message: &'a str,
}
#[derive(Template)]
#[template(path = "ask_reset_password.html")]
-pub struct AskResetPasswordTemplate {
+pub struct AskResetPasswordTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
- pub email: String,
- pub message: String,
- pub message_email: String,
+ pub email: &'a str,
+ pub message: &'a str,
+ pub message_email: &'a str,
}
#[derive(Template)]
#[template(path = "reset_password.html")]
-pub struct ResetPasswordTemplate {
+pub struct ResetPasswordTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
- pub reset_token: String,
- pub message: String,
- pub message_password: String,
+ pub reset_token: &'a str,
+ pub message: &'a str,
+ pub message_password: &'a str,
}
#[derive(Template)]
#[template(path = "profile.html")]
-pub struct ProfileTemplate {
+pub struct ProfileTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
- pub username: String,
- pub email: String,
- pub message: String,
- pub message_email: String,
- pub message_password: String,
+ pub username: &'a str,
+ pub email: &'a str,
+ pub message: &'a str,
+ pub message_email: &'a str,
+ pub message_password: &'a str,
}
#[derive(Template)]
.map(|l| l.split('-').next().unwrap_or_default())
.find_or_first(|l| available_codes.contains(l));
- accept_language.unwrap_or("en").to_string()
+ match accept_language {
+ Some(lang) if !lang.is_empty() => lang,
+ _ => translation::DEFAULT_LANGUAGE_CODE,
+ }
+ .to_string()
}
}
};
+++ /dev/null
-use axum::{
- body, debug_handler,
- extract::{Extension, Request, State},
- http::{header, StatusCode},
- middleware::Next,
- response::{IntoResponse, Response, Result},
-};
-
-use crate::{
- data::{db, model},
- html_templates::*,
- ron_utils, translation,
-};
-
-pub mod fragments;
-pub mod recipe;
-pub mod ron;
-pub mod user;
-
-// Will embed RON error in HTML page.
-pub async fn ron_error_to_html(
- Extension(tr): Extension<translation::Tr>,
- req: Request,
- next: Next,
-) -> Result<Response> {
- let response = next.run(req).await;
-
- if let Some(content_type) = response.headers().get(header::CONTENT_TYPE) {
- if content_type == ron_utils::RON_CONTENT_TYPE {
- let message = match body::to_bytes(response.into_body(), usize::MAX).await {
- Ok(bytes) => String::from_utf8(bytes.to_vec()).unwrap_or_default(),
- Err(error) => error.to_string(),
- };
- return Ok(MessageTemplate {
- user: None,
- message,
- as_code: true,
- tr,
- }
- .into_response());
- }
- }
-
- Ok(response)
-}
-
-///// HOME /////
-
-#[debug_handler]
-pub async fn home_page(
- State(connection): State<db::Connection>,
- Extension(user): Extension<Option<model::User>>,
- Extension(tr): Extension<translation::Tr>,
-) -> Result<impl IntoResponse> {
- let recipes = Recipes {
- published: connection
- .get_all_published_recipe_titles(tr.current_lang_code(), user.as_ref().map(|u| u.id))
- .await?,
- unpublished: if let Some(user) = user.as_ref() {
- connection
- .get_all_unpublished_recipe_titles(user.id)
- .await?
- } else {
- vec![]
- },
- current_id: None,
- };
-
- Ok(HomeTemplate { user, recipes, tr })
-}
-
-///// 404 /////
-
-#[debug_handler]
-pub async fn not_found(
- Extension(user): Extension<Option<model::User>>,
- Extension(tr): Extension<translation::Tr>,
-) -> impl IntoResponse {
- (
- StatusCode::NOT_FOUND,
- MessageTemplate::new_with_user("404: Not found".to_string(), tr, user),
- )
-}
--- /dev/null
+use axum::{
+ body, debug_handler,
+ extract::{Extension, Request, State},
+ http::{header, StatusCode},
+ middleware::Next,
+ response::{IntoResponse, Response, Result},
+};
+
+use crate::{
+ data::{db, model},
+ html_templates::*,
+ ron_utils, translation,
+};
+
+pub mod fragments;
+pub mod recipe;
+pub mod ron;
+pub mod user;
+
+// Will embed RON error in HTML page.
+pub async fn ron_error_to_html(
+ Extension(tr): Extension<translation::Tr>,
+ req: Request,
+ next: Next,
+) -> Result<Response> {
+ let response = next.run(req).await;
+
+ if let Some(content_type) = response.headers().get(header::CONTENT_TYPE) {
+ if content_type == ron_utils::RON_CONTENT_TYPE {
+ let message = match body::to_bytes(response.into_body(), usize::MAX).await {
+ Ok(bytes) => String::from_utf8(bytes.to_vec()).unwrap_or_default(),
+ Err(error) => error.to_string(),
+ };
+ return Ok(MessageTemplate {
+ user: None,
+ message: &message,
+ as_code: true,
+ tr,
+ }
+ .into_response());
+ }
+ }
+
+ Ok(response)
+}
+
+///// HOME /////
+
+#[debug_handler]
+pub async fn home_page(
+ State(connection): State<db::Connection>,
+ Extension(user): Extension<Option<model::User>>,
+ Extension(tr): Extension<translation::Tr>,
+) -> Result<impl IntoResponse> {
+ let recipes = Recipes {
+ published: connection
+ .get_all_published_recipe_titles(tr.current_lang_code(), user.as_ref().map(|u| u.id))
+ .await?,
+ unpublished: if let Some(user) = user.as_ref() {
+ connection
+ .get_all_unpublished_recipe_titles(user.id)
+ .await?
+ } else {
+ vec![]
+ },
+ current_id: None,
+ };
+
+ Ok(HomeTemplate { user, recipes, tr })
+}
+
+///// 404 /////
+
+#[debug_handler]
+pub async fn not_found(
+ Extension(user): Extension<Option<model::User>>,
+ Extension(tr): Extension<translation::Tr>,
+) -> impl IntoResponse {
+ (
+ StatusCode::NOT_FOUND,
+ MessageTemplate::new_with_user("404: Not found", tr, user),
+ )
+}
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
{
return Ok(MessageTemplate::new_with_user(
- tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
+ &tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
tr,
user,
)
user,
tr,
email: String::new(),
- message: String::new(),
- message_email: String::new(),
- message_password: String::new(),
+ message: "",
+ message_email: "",
+ message_password: "",
})
}
user: Option<model::User>,
tr: translation::Tr,
) -> Result<Response> {
+ let invalid_password_mess = &tr.tp(
+ Sentence::InvalidPassword,
+ &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
+ );
Ok(SignUpFormTemplate {
user,
email: form_data.email.clone(),
message_email: match error {
SignUpError::InvalidEmail => tr.t(Sentence::InvalidEmail),
- _ => String::new(),
+ _ => "",
},
message_password: match error {
SignUpError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
- SignUpError::InvalidPassword => tr.tp(
- Sentence::InvalidPassword,
- &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
- ),
- _ => String::new(),
+ SignUpError::InvalidPassword => invalid_password_mess,
+ _ => "",
},
message: match error {
SignUpError::UserAlreadyExists => tr.t(Sentence::EmailAlreadyTaken),
- SignUpError::DatabaseError => "Database error".to_string(),
+ SignUpError::DatabaseError => tr.t(Sentence::DatabaseError),
SignUpError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
- _ => String::new(),
+ _ => "",
},
tr,
}
Ok(SignInFormTemplate {
user,
tr,
- email: String::new(),
- message: String::new(),
+ email: "",
+ message: "",
})
}
jar,
SignInFormTemplate {
user,
- email: form_data.email,
+ email: &form_data.email,
message: tr.t(Sentence::AccountMustBeValidatedFirst),
tr,
}
jar,
SignInFormTemplate {
user,
- email: form_data.email,
+ email: &form_data.email,
message: tr.t(Sentence::WrongEmailOrPassword),
tr,
}
Ok(AskResetPasswordTemplate {
user,
tr,
- email: String::new(),
- message: String::new(),
- message_email: String::new(),
+ email: "",
+ message: "",
+ message_email: "",
}
.into_response())
}
) -> Result<Response> {
Ok(AskResetPasswordTemplate {
user,
- email: email.to_string(),
+ email,
message_email: match error {
AskResetPasswordError::InvalidEmail => tr.t(Sentence::InvalidEmail),
- _ => String::new(),
+ _ => "",
},
message: match error {
AskResetPasswordError::EmailAlreadyReset => {
AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown),
AskResetPasswordError::UnableSendEmail => tr.t(Sentence::UnableToSendResetEmail),
AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
- _ => String::new(),
+ _ => "",
},
tr,
}
Ok(ResetPasswordTemplate {
user,
tr,
- reset_token: reset_token.to_string(),
- message: String::new(),
- message_password: String::new(),
+ reset_token,
+ message: "",
+ message_password: "",
}
.into_response())
} else {
user: Option<model::User>,
tr: translation::Tr,
) -> Result<Response> {
+ let reset_password_mess = &tr.tp(
+ Sentence::InvalidPassword,
+ &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
+ );
Ok(ResetPasswordTemplate {
user,
- reset_token: form_data.reset_token.clone(),
+ reset_token: &form_data.reset_token,
message_password: match error {
ResetPasswordError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
- ResetPasswordError::InvalidPassword => tr.tp(
- Sentence::InvalidPassword,
- &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
- ),
- _ => String::new(),
+ ResetPasswordError::InvalidPassword => reset_password_mess,
+ _ => "",
},
message: match error {
ResetPasswordError::TokenExpired => tr.t(Sentence::AskResetTokenExpired),
ResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
- _ => String::new(),
+ _ => "",
},
tr,
}
) -> Response {
if let Some(user) = user {
ProfileTemplate {
- username: user.name.clone(),
- email: user.email.clone(),
- message: String::new(),
- message_email: String::new(),
- message_password: String::new(),
- user: Some(user),
+ username: &user.name,
+ email: &user.email,
+ message: "",
+ message_email: "",
+ message_password: "",
+ user: Some(user.clone()),
tr,
}
.into_response()
password_1: String,
password_2: String,
}
+
enum ProfileUpdateError {
InvalidEmail,
EmailAlreadyTaken,
user: model::User,
tr: translation::Tr,
) -> Result<Response> {
+ let invalid_password_mess = &tr.tp(
+ Sentence::InvalidPassword,
+ &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
+ );
Ok(ProfileTemplate {
user: Some(user),
- username: form_data.name.clone(),
- email: form_data.email.clone(),
+ username: &form_data.name,
+ email: &form_data.email,
message_email: match error {
ProfileUpdateError::InvalidEmail => tr.t(Sentence::InvalidEmail),
ProfileUpdateError::EmailAlreadyTaken => tr.t(Sentence::EmailAlreadyTaken),
- _ => String::new(),
+ _ => "",
},
message_password: match error {
ProfileUpdateError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
- ProfileUpdateError::InvalidPassword => tr.tp(
- Sentence::InvalidPassword,
- &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
- ),
- _ => String::new(),
+ ProfileUpdateError::InvalidPassword => invalid_password_mess,
+ _ => "",
},
message: match error {
ProfileUpdateError::DatabaseError => tr.t(Sentence::DatabaseError),
ProfileUpdateError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
- _ => String::new(),
+ _ => "",
},
tr,
}
};
let email_trimmed = form_data.email.trim();
- let message: String;
+ let message: &str;
match connection
.update_user(
Ok(ProfileTemplate {
user,
- username: form_data.name,
- email: form_data.email,
+ username: &form_data.name,
+ email: &form_data.email,
message,
- message_email: String::new(),
- message_password: String::new(),
+ message_email: "",
+ message_password: "",
tr,
}
.into_response())
use strum_macros::EnumCount;
use tracing::{event, Level};
-use crate::consts;
+use crate::{consts, utils};
#[derive(Debug, Clone, EnumCount, Deserialize)]
pub enum Sentence {
RecipeIngredientComment,
}
-const DEFAULT_LANGUAGE_CODE: &str = "en";
+pub const DEFAULT_LANGUAGE_CODE: &str = "en";
+pub const PLACEHOLDER_SUBSTITUTE: &str = "{}";
#[derive(Clone)]
pub struct Tr {
}
}
- pub fn t(&self, sentence: Sentence) -> String {
- //&'static str {
- self.lang.get(sentence).to_string()
- // match self.lang.translation.get(&sentence) {
- // Some(str) => str.clone(),
- // None => format!(
- // "Translation missing, lang: {}/{}, element: {:?}",
- // self.lang.name, self.lang.code, sentence
- // ),
- // }
+ pub fn t(&self, sentence: Sentence) -> &'static str {
+ self.lang.get(sentence)
}
pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString + Send>]) -> String {
- // match self.lang.translation.get(&sentence) {
- // Some(str) => {
- // let mut result = str.clone();
- // for p in params {
- // result = result.replacen("{}", &p.to_string(), 1);
- // }
- // result
- // }
- // None => format!(
- // "Translation missing, lang: {}/{}, element: {:?}",
- // self.lang.name, self.lang.code, sentence
- // ),
- // }
let text = self.lang.get(sentence);
- let mut result = text.to_string();
- for p in params {
- result = result.replacen("{}", &p.to_string(), 1);
- }
- result
+ let params_as_string: Vec<String> = params.iter().map(|p| p.to_string()).collect();
+ utils::substitute(
+ text,
+ PLACEHOLDER_SUBSTITUTE,
+ ¶ms_as_string
+ .iter()
+ .map(AsRef::as_ref)
+ .collect::<Vec<_>>(),
+ )
}
pub fn current_lang_code(&self) -> &str {
impl Language {
pub fn from_stored_language(stored_language: StoredLanguage) -> Self {
- println!("!!!!!!!!!!!! {:?}", &stored_language.code);
Self {
code: stored_language.code,
name: stored_language.name,
host
)
}
+
+pub fn substitute(str: &str, pattern: &str, replacements: &[&str]) -> String {
+ let mut result = String::with_capacity(
+ (str.len() + replacements.iter().map(|s| s.len()).sum::<usize>())
+ .saturating_sub(pattern.len() * replacements.len()),
+ );
+
+ let mut i = 0;
+ for s in str.split(pattern) {
+ result.push_str(s);
+ if i < replacements.len() {
+ result.push_str(replacements[i]);
+ }
+ i += 1;
+ }
+
+ if i == 1 {
+ return str.to_string();
+ }
+
+ result
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_substitute() {
+ assert_eq!(substitute("", "", &[]), "");
+ assert_eq!(substitute("", "", &[""]), "");
+ assert_eq!(substitute("", "{}", &["a"]), "");
+ assert_eq!(substitute("a", "{}", &["b"]), "a");
+ assert_eq!(substitute("a{}", "{}", &["b"]), "ab");
+ assert_eq!(substitute("{}c", "{}", &["b"]), "bc");
+ assert_eq!(substitute("a{}c", "{}", &["b"]), "abc");
+ assert_eq!(substitute("{}b{}", "{}", &["a", "c"]), "abc");
+ assert_eq!(substitute("{}{}{}", "{}", &["a", "bc", "def"]), "abcdef");
+ assert_eq!(substitute("{}{}{}", "{}", &["a"]), "a");
+ }
+}
pub fn recipe_edit(recipe_id: i64) -> Result<(), JsValue> {
// Title.
{
- let title: HtmlInputElement = by_id("input-title");
+ let Some(title) = document().get_element_by_id("input-title") else {
+ return Err(JsValue::from_str("Unable to find 'input-title' element"));
+ };
+
+ let title: HtmlInputElement = title.dyn_into().unwrap();
+
+ // Check if the recipe has been loaded.
+
let mut current_title = title.value();
EventListener::new(&title.clone(), "blur", move |_event| {
if title.value() != current_title {
mod toast;
mod utils;
-use gloo::{events::EventListener, utils::window};
+use gloo::{console::log, events::EventListener, utils::window};
use utils::by_id;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
if let ["recipe", "edit", id] = path[..] {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap.
- handles::recipe_edit(id)?;
+ if let Err(error) = handles::recipe_edit(id) {
+ log!(error);
+ }
// Disable: user editing data are now submitted as classic form data.
// ["user", "edit"] => {