Add a toggle between dark and light theme

This commit is contained in:
Greg Burri 2025-03-31 15:31:06 +02:00
parent d22617538e
commit 559ed139aa
34 changed files with 640 additions and 469 deletions

View file

@ -1,6 +1,7 @@
use askama::Template;
use crate::{
Context,
data::model,
translation::{self, Sentence, Tr},
};
@ -20,8 +21,7 @@ impl Recipes {
#[derive(Template)]
#[template(path = "home.html")]
pub struct HomeTemplate {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub recipes: Recipes,
}
@ -29,8 +29,7 @@ pub struct HomeTemplate {
#[derive(Template)]
#[template(path = "message.html")]
pub struct MessageTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub message: &'a str,
pub as_code: bool, // Display the message in <pre> markup.
@ -39,8 +38,11 @@ pub struct MessageTemplate<'a> {
impl<'a> MessageTemplate<'a> {
pub fn new(message: &'a str, tr: Tr) -> MessageTemplate<'a> {
MessageTemplate {
user: None,
tr,
context: Context {
user: None,
tr,
dark_theme: false,
},
message,
as_code: false,
}
@ -52,8 +54,11 @@ impl<'a> MessageTemplate<'a> {
user: Option<model::User>,
) -> MessageTemplate<'a> {
MessageTemplate {
user,
tr,
context: Context {
user,
tr,
dark_theme: false,
},
message,
as_code: false,
}
@ -63,8 +68,7 @@ impl<'a> MessageTemplate<'a> {
#[derive(Template)]
#[template(path = "sign_up_form.html")]
pub struct SignUpFormTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub email: String,
pub message: &'a str,
@ -75,8 +79,7 @@ pub struct SignUpFormTemplate<'a> {
#[derive(Template)]
#[template(path = "sign_in_form.html")]
pub struct SignInFormTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub email: &'a str,
pub message: &'a str,
@ -85,8 +88,7 @@ pub struct SignInFormTemplate<'a> {
#[derive(Template)]
#[template(path = "ask_reset_password.html")]
pub struct AskResetPasswordTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub email: &'a str,
pub message: &'a str,
@ -96,8 +98,7 @@ pub struct AskResetPasswordTemplate<'a> {
#[derive(Template)]
#[template(path = "reset_password.html")]
pub struct ResetPasswordTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub reset_token: &'a str,
pub message: &'a str,
@ -107,8 +108,7 @@ pub struct ResetPasswordTemplate<'a> {
#[derive(Template)]
#[template(path = "profile.html")]
pub struct ProfileTemplate<'a> {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub username: &'a str,
pub email: &'a str,
@ -121,8 +121,7 @@ pub struct ProfileTemplate<'a> {
#[derive(Template)]
#[template(path = "recipe_view.html")]
pub struct RecipeViewTemplate {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub recipes: Recipes,
@ -132,8 +131,7 @@ pub struct RecipeViewTemplate {
#[derive(Template)]
#[template(path = "recipe_edit.html")]
pub struct RecipeEditTemplate {
pub user: Option<model::User>,
pub tr: Tr,
pub context: Context,
pub recipes: Recipes,
@ -143,7 +141,7 @@ pub struct RecipeEditTemplate {
#[derive(Template)]
#[template(path = "recipes_list_fragment.html")]
pub struct RecipesListFragmentTemplate {
pub tr: Tr,
pub context: Context,
pub recipes: Recipes,
}

View file

@ -85,6 +85,13 @@ const TRACING_LEVEL: tracing::Level = tracing::Level::DEBUG;
#[cfg(not(debug_assertions))]
const TRACING_LEVEL: tracing::Level = tracing::Level::INFO;
#[derive(Debug, Clone)]
pub struct Context {
pub user: Option<model::User>,
pub tr: Tr,
pub dark_theme: bool,
}
// TODO: Should main returns 'Result'?
#[tokio::main]
async fn main() {
@ -294,11 +301,7 @@ async fn main() {
.merge(html_routes)
.nest("/ron-api", ron_api_routes)
.fallback(services::not_found)
.layer(middleware::from_fn(translation))
.layer(middleware::from_fn_with_state(
state.clone(),
user_authentication,
))
.layer(middleware::from_fn_with_state(state.clone(), context))
.with_state(state)
.nest_service("/favicon.ico", ServeFile::new("static/favicon.ico"))
.nest_service("/static", ServeDir::new("static"))
@ -321,19 +324,6 @@ async fn main() {
event!(Level::INFO, "Recipes stopped");
}
async fn user_authentication(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
State(connection): State<db::Connection>,
mut req: Request,
next: Next,
) -> Result<Response> {
let jar = CookieJar::from_headers(req.headers());
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(req.headers(), addr);
let user = get_current_user(connection, &jar, &client_ip, &client_user_agent).await;
req.extensions_mut().insert(user);
Ok(next.run(req).await)
}
#[derive(Debug, Clone)]
struct Lang(Option<String>);
@ -384,16 +374,21 @@ fn url_rewriting(mut req: Request) -> Request {
/// - Get from the cookie.
/// - Get from the HTTP header `accept-language`.
/// - Set as `translation::DEFAULT_LANGUAGE_CODE`.
async fn translation(
Extension(lang): Extension<Lang>,
Extension(user): Extension<Option<model::User>>,
async fn context(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
State(connection): State<db::Connection>,
Extension(lang_from_url): Extension<Lang>,
mut req: Request,
next: Next,
) -> Result<Response> {
let language = if let Some(lang) = lang.0 {
let jar = CookieJar::from_headers(req.headers());
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(req.headers(), addr);
let user = get_current_user(connection, &jar, &client_ip, &client_user_agent).await;
let language = if let Some(lang) = lang_from_url.0 {
lang
} else if let Some(user) = user {
user.lang
} else if let Some(ref user) = user {
user.lang.clone()
} else {
let available_codes = translation::available_codes();
let jar = CookieJar::from_headers(req.headers());
@ -420,7 +415,17 @@ async fn translation(
let tr = Tr::new(&language);
req.extensions_mut().insert(tr);
let dark_theme = match jar.get(common::consts::COOKIE_DARK_THEME) {
Some(dark_theme_cookie) => dark_theme_cookie.value().parse().unwrap_or_default(),
None => false,
};
req.extensions_mut().insert(Context {
user,
tr,
dark_theme,
});
Ok(next.run(req).await)
}

View file

@ -7,12 +7,7 @@ use axum::{
use serde::Deserialize;
// use tracing::{event, Level};
use crate::{
Result,
data::{db, model},
html_templates::*,
translation,
};
use crate::{Context, Result, data::db, html_templates::*};
#[derive(Deserialize)]
pub struct CurrentRecipeId {
@ -23,14 +18,16 @@ pub struct CurrentRecipeId {
pub async fn recipes_list_fragments(
State(connection): State<db::Connection>,
current_recipe: Query<CurrentRecipeId>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
) -> Result<impl IntoResponse> {
let recipes = Recipes {
published: connection
.get_all_published_recipe_titles(tr.current_lang_code(), user.as_ref().map(|u| u.id))
.get_all_published_recipe_titles(
context.tr.current_lang_code(),
context.user.as_ref().map(|u| u.id),
)
.await?,
unpublished: if let Some(user) = user.as_ref() {
unpublished: if let Some(user) = context.user.as_ref() {
connection
.get_all_unpublished_recipe_titles(user.id)
.await?
@ -39,5 +36,7 @@ pub async fn recipes_list_fragments(
},
current_id: current_recipe.current_recipe_id,
};
Ok(Html(RecipesListFragmentTemplate { tr, recipes }.render()?))
Ok(Html(
RecipesListFragmentTemplate { context, recipes }.render()?,
))
}

View file

@ -7,12 +7,7 @@ use axum::{
response::{Html, IntoResponse, Response},
};
use crate::{
Result,
data::{db, model},
html_templates::*,
ron_utils, translation,
};
use crate::{Context, Result, data::db, html_templates::*, ron_utils};
pub mod fragments;
pub mod recipe;
@ -21,7 +16,7 @@ pub mod user;
// Will embed RON error in HTML page.
pub async fn ron_error_to_html(
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
req: Request,
next: Next,
) -> Result<Response> {
@ -35,10 +30,9 @@ pub async fn ron_error_to_html(
};
return Ok(Html(
MessageTemplate {
user: None,
context,
message: &message,
as_code: true,
tr,
}
.render()?,
)
@ -54,14 +48,16 @@ pub async fn ron_error_to_html(
#[debug_handler]
pub async fn home_page(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
) -> Result<impl IntoResponse> {
let recipes = Recipes {
published: connection
.get_all_published_recipe_titles(tr.current_lang_code(), user.as_ref().map(|u| u.id))
.get_all_published_recipe_titles(
context.tr.current_lang_code(),
context.user.as_ref().map(|u| u.id),
)
.await?,
unpublished: if let Some(user) = user.as_ref() {
unpublished: if let Some(user) = context.user.as_ref() {
connection
.get_all_unpublished_recipe_titles(user.id)
.await?
@ -71,18 +67,15 @@ pub async fn home_page(
current_id: None,
};
Ok(Html(HomeTemplate { user, recipes, tr }.render()?))
Ok(Html(HomeTemplate { context, recipes }.render()?))
}
///// 404 /////
#[debug_handler]
pub async fn not_found(
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
) -> Result<impl IntoResponse> {
pub async fn not_found(Extension(context): Extension<Context>) -> Result<impl IntoResponse> {
Ok((
StatusCode::NOT_FOUND,
Html(MessageTemplate::new_with_user("404: Not found", tr, user).render()?),
Html(MessageTemplate::new_with_user("404: Not found", context.tr, context.user).render()?),
))
}

View file

@ -7,44 +7,48 @@ use axum::{
// use tracing::{event, Level};
use crate::{
Result,
Context, Result,
data::{db, model},
html_templates::*,
translation::{self, Sentence},
translation::Sentence,
};
#[debug_handler]
pub async fn create(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
) -> Result<Response> {
if let Some(user) = user {
if let Some(user) = context.user {
let recipe_id = connection.create_recipe(user.id).await?;
Ok(Redirect::to(&format!(
"/{}/recipe/edit/{}",
tr.current_lang_code(),
context.tr.current_lang_code(),
recipe_id
))
.into_response())
} else {
Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
Ok(
Html(MessageTemplate::new(context.tr.t(Sentence::NotLoggedIn), context.tr).render()?)
.into_response(),
)
}
}
#[debug_handler]
pub async fn edit(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
Path(recipe_id): Path<i64>,
) -> Result<Response> {
if let Some(user) = user {
if let Some(ref user) = context.user {
if let Some(recipe) = connection.get_recipe(recipe_id, false).await? {
if model::can_user_edit_recipe(&user, &recipe) {
let recipes = Recipes {
published: connection
.get_all_published_recipe_titles(tr.current_lang_code(), Some(user.id))
.get_all_published_recipe_titles(
context.tr.current_lang_code(),
Some(user.id),
)
.await?,
unpublished: connection
.get_all_unpublished_recipe_titles(user.id)
@ -54,8 +58,7 @@ pub async fn edit(
Ok(Html(
RecipeEditTemplate {
user: Some(user),
tr,
context,
recipes,
recipe,
}
@ -63,42 +66,48 @@ pub async fn edit(
)
.into_response())
} else {
Ok(
Html(
MessageTemplate::new(tr.t(Sentence::RecipeNotAllowedToEdit), tr)
.render()?,
Ok(Html(
MessageTemplate::new(
context.tr.t(Sentence::RecipeNotAllowedToEdit),
context.tr,
)
.into_response(),
.render()?,
)
.into_response())
}
} else {
Ok(
Html(MessageTemplate::new(tr.t(Sentence::RecipeNotFound), tr).render()?)
.into_response(),
Ok(Html(
MessageTemplate::new(context.tr.t(Sentence::RecipeNotFound), context.tr)
.render()?,
)
.into_response())
}
} else {
Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
Ok(
Html(MessageTemplate::new(context.tr.t(Sentence::NotLoggedIn), context.tr).render()?)
.into_response(),
)
}
}
#[debug_handler]
pub async fn view(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
Path(recipe_id): Path<i64>,
) -> Result<Response> {
match connection.get_recipe(recipe_id, true).await? {
Some(recipe) => {
if !recipe.is_published
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
&& (context.user.is_none() || recipe.user_id != context.user.as_ref().unwrap().id)
{
return Ok(Html(
MessageTemplate::new_with_user(
&tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
tr,
user,
&context
.tr
.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
context.tr,
context.user,
)
.render()?,
)
@ -108,11 +117,11 @@ pub async fn view(
let recipes = Recipes {
published: connection
.get_all_published_recipe_titles(
tr.current_lang_code(),
user.as_ref().map(|u| u.id),
context.tr.current_lang_code(),
context.user.as_ref().map(|u| u.id),
)
.await?,
unpublished: if let Some(user) = user.as_ref() {
unpublished: if let Some(user) = context.user.as_ref() {
connection
.get_all_unpublished_recipe_titles(user.id)
.await?
@ -124,8 +133,7 @@ pub async fn view(
Ok(Html(
RecipeViewTemplate {
user,
tr,
context,
recipes,
recipe,
}
@ -134,7 +142,12 @@ pub async fn view(
.into_response())
}
None => Ok(Html(
MessageTemplate::new_with_user(tr.t(Sentence::RecipeNotFound), tr, user).render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::RecipeNotFound),
context.tr,
context.user,
)
.render()?,
)
.into_response()),
}

View file

@ -8,8 +8,8 @@ use axum_extra::extract::Query;
// use tracing::{event, Level};
use crate::{
Context,
data::{self, db},
model,
ron_extractor::ExtractRon,
ron_utils::{ron_error, ron_response_ok},
};
@ -19,10 +19,10 @@ use super::rights::*;
#[debug_handler]
pub async fn get_scheduled_recipes(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
date_range: Query<common::ron_api::DateRange>,
) -> Result<impl IntoResponse> {
if let Some(user) = user {
if let Some(user) = context.user {
Ok(ron_response_ok(common::ron_api::ScheduledRecipes {
recipes: connection
.get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date)
@ -50,11 +50,11 @@ impl From<data::db::recipe::AddScheduledRecipeResult> for common::ron_api::Sched
#[debug_handler]
pub async fn schedule_recipe(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<common::ron_api::ScheduleRecipe>,
) -> Result<Response> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
if let Some(user) = user {
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
if let Some(user) = context.user {
connection
.add_scheduled_recipe(
user.id,
@ -76,11 +76,11 @@ pub async fn schedule_recipe(
#[debug_handler]
pub async fn rm_scheduled_recipe(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<common::ron_api::RemoveScheduledRecipe>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
if let Some(user) = user {
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
if let Some(user) = context.user {
connection
.rm_scheduled_recipe(
user.id,

View file

@ -7,7 +7,7 @@ use axum::{
use axum_extra::extract::cookie::{Cookie, CookieJar};
// use tracing::{event, Level};
use crate::{consts, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error};
use crate::{Context, consts, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error};
pub mod calendar;
pub mod recipe;
@ -19,12 +19,12 @@ const NOT_AUTHORIZED_MESSAGE: &str = "Action not authorized";
#[debug_handler]
pub async fn set_lang(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
headers: HeaderMap,
ExtractRon(ron): ExtractRon<common::ron_api::SetLang>,
) -> Result<(CookieJar, StatusCode)> {
let mut jar = CookieJar::from_headers(&headers);
if let Some(user) = user {
if let Some(user) = context.user {
connection.set_user_lang(user.id, &ron.lang).await?;
}

View file

@ -8,7 +8,7 @@ use axum_extra::extract::Query;
use common::ron_api;
// use tracing::{event, Level};
use crate::{data::db, model, ron_extractor::ExtractRon, ron_utils::ron_response_ok};
use crate::{Context, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_response_ok};
use super::rights::*;
@ -27,10 +27,10 @@ pub async fn get_titles(
#[debug_handler]
pub async fn set_title(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetRecipeTitle>,
) -> Result<StatusCode> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection
.set_recipe_title(ron.recipe_id, &ron.title)
.await?;
@ -40,10 +40,10 @@ pub async fn set_title(
#[debug_handler]
pub async fn set_description(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetRecipeDescription>,
) -> Result<StatusCode> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection
.set_recipe_description(ron.recipe_id, &ron.description)
.await?;
@ -53,10 +53,10 @@ pub async fn set_description(
#[debug_handler]
pub async fn set_servings(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetRecipeServings>,
) -> Result<StatusCode> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection
.set_recipe_servings(ron.recipe_id, ron.servings)
.await?;
@ -66,10 +66,10 @@ pub async fn set_servings(
#[debug_handler]
pub async fn set_estimated_time(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetRecipeEstimatedTime>,
) -> Result<StatusCode> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection
.set_recipe_estimated_time(ron.recipe_id, ron.estimated_time)
.await?;
@ -90,10 +90,10 @@ pub async fn get_tags(
#[debug_handler]
pub async fn add_tags(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Tags>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection
.add_recipe_tags(
ron.recipe_id,
@ -109,10 +109,10 @@ pub async fn add_tags(
#[debug_handler]
pub async fn rm_tags(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Tags>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection.rm_recipe_tags(ron.recipe_id, &ron.tags).await?;
Ok(StatusCode::OK)
}
@ -120,10 +120,10 @@ pub async fn rm_tags(
#[debug_handler]
pub async fn set_difficulty(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetRecipeDifficulty>,
) -> Result<StatusCode> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection
.set_recipe_difficulty(ron.recipe_id, ron.difficulty)
.await?;
@ -133,7 +133,7 @@ pub async fn set_difficulty(
#[debug_handler]
pub async fn set_language(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetRecipeLanguage>,
) -> Result<StatusCode> {
if !crate::translation::available_codes()
@ -144,7 +144,7 @@ pub async fn set_language(
return Ok(StatusCode::BAD_REQUEST);
}
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection
.set_recipe_language(ron.recipe_id, &ron.lang)
.await?;
@ -154,10 +154,10 @@ pub async fn set_language(
#[debug_handler]
pub async fn set_is_published(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetIsPublished>,
) -> Result<StatusCode> {
check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
connection
.set_recipe_is_published(ron.recipe_id, ron.is_published)
.await?;
@ -167,10 +167,10 @@ pub async fn set_is_published(
#[debug_handler]
pub async fn rm(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Id>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe(&connection, &user, ron.id).await?;
check_user_rights_recipe(&connection, &context.user, ron.id).await?;
connection.rm_recipe(ron.id).await?;
Ok(StatusCode::OK)
}
@ -231,10 +231,10 @@ pub async fn get_groups(
#[debug_handler]
pub async fn add_group(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Id>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe(&connection, &user, ron.id).await?;
check_user_rights_recipe(&connection, &context.user, ron.id).await?;
let id = connection.add_recipe_group(ron.id).await?;
Ok(ron_response_ok(ron_api::Id { id }))
@ -243,10 +243,10 @@ pub async fn add_group(
#[debug_handler]
pub async fn rm_group(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Id>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_group(&connection, &user, ron.id).await?;
check_user_rights_recipe_group(&connection, &context.user, ron.id).await?;
connection.rm_recipe_group(ron.id).await?;
Ok(StatusCode::OK)
}
@ -254,10 +254,10 @@ pub async fn rm_group(
#[debug_handler]
pub async fn set_group_name(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetGroupName>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_group(&connection, &user, ron.group_id).await?;
check_user_rights_recipe_group(&connection, &context.user, ron.group_id).await?;
connection.set_group_name(ron.group_id, &ron.name).await?;
Ok(StatusCode::OK)
}
@ -265,10 +265,10 @@ pub async fn set_group_name(
#[debug_handler]
pub async fn set_group_comment(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetGroupComment>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_group(&connection, &user, ron.group_id).await?;
check_user_rights_recipe_group(&connection, &context.user, ron.group_id).await?;
connection
.set_group_comment(ron.group_id, &ron.comment)
.await?;
@ -278,10 +278,10 @@ pub async fn set_group_comment(
#[debug_handler]
pub async fn set_groups_order(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Ids>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_groups(&connection, &user, &ron.ids).await?;
check_user_rights_recipe_groups(&connection, &context.user, &ron.ids).await?;
connection.set_groups_order(&ron.ids).await?;
Ok(StatusCode::OK)
}
@ -289,10 +289,10 @@ pub async fn set_groups_order(
#[debug_handler]
pub async fn add_step(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Id>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_group(&connection, &user, ron.id).await?;
check_user_rights_recipe_group(&connection, &context.user, ron.id).await?;
let id = connection.add_recipe_step(ron.id).await?;
Ok(ron_response_ok(ron_api::Id { id }))
@ -301,10 +301,10 @@ pub async fn add_step(
#[debug_handler]
pub async fn rm_step(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Id>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_step(&connection, &user, ron.id).await?;
check_user_rights_recipe_step(&connection, &context.user, ron.id).await?;
connection.rm_recipe_step(ron.id).await?;
Ok(StatusCode::OK)
}
@ -312,10 +312,10 @@ pub async fn rm_step(
#[debug_handler]
pub async fn set_step_action(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetStepAction>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_step(&connection, &user, ron.step_id).await?;
check_user_rights_recipe_step(&connection, &context.user, ron.step_id).await?;
connection.set_step_action(ron.step_id, &ron.action).await?;
Ok(StatusCode::OK)
}
@ -323,10 +323,10 @@ pub async fn set_step_action(
#[debug_handler]
pub async fn set_steps_order(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Ids>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_steps(&connection, &user, &ron.ids).await?;
check_user_rights_recipe_steps(&connection, &context.user, &ron.ids).await?;
connection.set_steps_order(&ron.ids).await?;
Ok(StatusCode::OK)
}
@ -334,10 +334,10 @@ pub async fn set_steps_order(
#[debug_handler]
pub async fn add_ingredient(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Id>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_step(&connection, &user, ron.id).await?;
check_user_rights_recipe_step(&connection, &context.user, ron.id).await?;
let id = connection.add_recipe_ingredient(ron.id).await?;
Ok(ron_response_ok(ron_api::Id { id }))
}
@ -345,10 +345,10 @@ pub async fn add_ingredient(
#[debug_handler]
pub async fn rm_ingredient(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Id>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.id).await?;
check_user_rights_recipe_ingredient(&connection, &context.user, ron.id).await?;
connection.rm_recipe_ingredient(ron.id).await?;
Ok(StatusCode::OK)
}
@ -356,10 +356,10 @@ pub async fn rm_ingredient(
#[debug_handler]
pub async fn set_ingredient_name(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetIngredientName>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
check_user_rights_recipe_ingredient(&connection, &context.user, ron.ingredient_id).await?;
connection
.set_ingredient_name(ron.ingredient_id, &ron.name)
.await?;
@ -369,10 +369,10 @@ pub async fn set_ingredient_name(
#[debug_handler]
pub async fn set_ingredient_comment(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetIngredientComment>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
check_user_rights_recipe_ingredient(&connection, &context.user, ron.ingredient_id).await?;
connection
.set_ingredient_comment(ron.ingredient_id, &ron.comment)
.await?;
@ -382,10 +382,10 @@ pub async fn set_ingredient_comment(
#[debug_handler]
pub async fn set_ingredient_quantity(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetIngredientQuantity>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
check_user_rights_recipe_ingredient(&connection, &context.user, ron.ingredient_id).await?;
connection
.set_ingredient_quantity(ron.ingredient_id, ron.quantity)
.await?;
@ -395,10 +395,10 @@ pub async fn set_ingredient_quantity(
#[debug_handler]
pub async fn set_ingredient_unit(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::SetIngredientUnit>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
check_user_rights_recipe_ingredient(&connection, &context.user, ron.ingredient_id).await?;
connection
.set_ingredient_unit(ron.ingredient_id, &ron.unit)
.await?;
@ -408,10 +408,10 @@ pub async fn set_ingredient_unit(
#[debug_handler]
pub async fn set_ingredients_order(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Ids>,
) -> Result<impl IntoResponse> {
check_user_rights_recipe_ingredients(&connection, &user, &ron.ids).await?;
check_user_rights_recipe_ingredients(&connection, &context.user, &ron.ids).await?;
connection.set_ingredients_order(&ron.ids).await?;
Ok(StatusCode::OK)
}

View file

@ -7,6 +7,7 @@ use axum::{
use common::ron_api;
use crate::{
Context,
data::db,
model,
ron_extractor::ExtractRon,
@ -33,9 +34,9 @@ impl From<model::ShoppingListItem> for common::ron_api::ShoppingListItem {
#[debug_handler]
pub async fn get(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
) -> Result<impl IntoResponse> {
if let Some(user) = user {
if let Some(user) = context.user {
Ok(ron_response_ok(
connection
.get_shopping_list(user.id)
@ -55,10 +56,10 @@ pub async fn get(
#[debug_handler]
pub async fn set_entry_checked(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(context): Extension<Context>,
ExtractRon(ron): ExtractRon<ron_api::Value<bool>>,
) -> Result<impl IntoResponse> {
check_user_rights_shopping_list_entry(&connection, &user, ron.id).await?;
check_user_rights_shopping_list_entry(&connection, &context.user, ron.id).await?;
Ok(ron_response_ok(
connection.set_entry_checked(ron.id, ron.value).await?,
))

View file

@ -11,7 +11,7 @@ use axum::{
};
use axum_extra::extract::{
Host, Query,
cookie::{Cookie, CookieJar},
cookie::{self, Cookie, CookieJar},
};
use chrono::Duration;
use lettre::Address;
@ -19,14 +19,8 @@ use serde::Deserialize;
use tracing::{Level, event};
use crate::{
AppState, Result,
config::Config,
consts,
data::{db, model},
email,
html_templates::*,
translation::{self, Sentence},
utils,
AppState, Context, Result, config::Config, consts, data::db, email, html_templates::*,
translation::Sentence, utils,
};
/// SIGN UP ///
@ -34,14 +28,12 @@ use crate::{
#[debug_handler]
pub async fn sign_up_get(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
) -> Result<Response> {
if connection.get_new_user_registration_enabled().await? {
Ok(Html(
SignUpFormTemplate {
user,
tr,
context,
email: String::new(),
message: "",
message_email: "",
@ -51,10 +43,15 @@ pub async fn sign_up_get(
)
.into_response())
} else {
Ok(
Html(MessageTemplate::new_with_user(tr.t(Sentence::SignUpClosed), tr, user).render()?)
.into_response(),
Ok(Html(
MessageTemplate::new_with_user(
context.tr.t(Sentence::SignUpClosed),
context.tr,
context.user,
)
.render()?,
)
.into_response())
}
}
@ -79,40 +76,37 @@ pub async fn sign_up_post(
Host(host): Host,
State(connection): State<db::Connection>,
State(config): State<Config>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
Form(form_data): Form<SignUpFormData>,
) -> Result<Response> {
fn error_response(
error: SignUpError,
form_data: &SignUpFormData,
user: Option<model::User>,
tr: translation::Tr,
context: Context,
) -> Result<Response> {
let invalid_password_mess = &tr.tp(
let invalid_password_mess = &context.tr.tp(
Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
);
Ok(Html(
SignUpFormTemplate {
user,
email: form_data.email.clone(),
message_email: match error {
SignUpError::InvalidEmail => tr.t(Sentence::InvalidEmail),
SignUpError::InvalidEmail => context.tr.t(Sentence::InvalidEmail),
_ => "",
},
message_password: match error {
SignUpError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
SignUpError::PasswordsNotEqual => context.tr.t(Sentence::PasswordDontMatch),
SignUpError::InvalidPassword => invalid_password_mess,
_ => "",
},
message: match error {
SignUpError::UserAlreadyExists => tr.t(Sentence::EmailAlreadyTaken),
SignUpError::DatabaseError => tr.t(Sentence::DatabaseError),
SignUpError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
SignUpError::UserAlreadyExists => context.tr.t(Sentence::EmailAlreadyTaken),
SignUpError::DatabaseError => context.tr.t(Sentence::DatabaseError),
SignUpError::UnableSendEmail => context.tr.t(Sentence::UnableToSendEmail),
_ => "",
},
tr,
context,
}
.render()?,
)
@ -121,24 +115,29 @@ pub async fn sign_up_post(
if !connection.get_new_user_registration_enabled().await? {
return Ok(Html(
MessageTemplate::new_with_user(tr.t(Sentence::SignUpClosed), tr, user).render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::SignUpClosed),
context.tr,
context.user,
)
.render()?,
)
.into_response());
}
// Validation of email and password.
if form_data.email.parse::<Address>().is_err() {
return error_response(SignUpError::InvalidEmail, &form_data, user, tr);
return error_response(SignUpError::InvalidEmail, &form_data, context);
}
if form_data.password_1 != form_data.password_2 {
return error_response(SignUpError::PasswordsNotEqual, &form_data, user, tr);
return error_response(SignUpError::PasswordsNotEqual, &form_data, context);
}
if let common::utils::PasswordValidation::TooShort =
common::utils::validate_password(&form_data.password_1)
{
return error_response(SignUpError::InvalidPassword, &form_data, user, tr);
return error_response(SignUpError::InvalidPassword, &form_data, context);
}
match connection
@ -146,14 +145,14 @@ pub async fn sign_up_post(
.await
{
Ok(db::user::SignUpResult::UserAlreadyExists) => {
error_response(SignUpError::UserAlreadyExists, &form_data, user, tr)
error_response(SignUpError::UserAlreadyExists, &form_data, context)
}
Ok(db::user::SignUpResult::UserCreatedWaitingForValidation(token)) => {
let url = utils::get_url_from_host(&host);
let email = form_data.email.clone();
match email::send_email(
&email,
&tr.tp(
&context.tr.tp(
Sentence::SignUpFollowEmailLink,
&[Box::new(format!(
"{}/validation?validation_token={}",
@ -167,19 +166,23 @@ pub async fn sign_up_post(
.await
{
Ok(()) => Ok(Html(
MessageTemplate::new_with_user(tr.t(Sentence::SignUpEmailSent), tr, user)
.render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::SignUpEmailSent),
context.tr,
context.user,
)
.render()?,
)
.into_response()),
Err(_) => {
// error!("Email validation error: {}", error); // TODO: log
error_response(SignUpError::UnableSendEmail, &form_data, user, tr)
error_response(SignUpError::UnableSendEmail, &form_data, context)
}
}
}
Err(_) => {
// error!("Signup database error: {}", error); // TODO: log
error_response(SignUpError::DatabaseError, &form_data, user, tr)
error_response(SignUpError::DatabaseError, &form_data, context)
}
}
}
@ -187,21 +190,20 @@ pub async fn sign_up_post(
#[debug_handler]
pub async fn sign_up_validation(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Query(query): Query<HashMap<String, String>>,
headers: HeaderMap,
) -> Result<(CookieJar, impl IntoResponse)> {
let mut jar = CookieJar::from_headers(&headers);
if user.is_some() {
if context.user.is_some() {
return Ok((
jar,
Html(
MessageTemplate::new_with_user(
tr.t(Sentence::ValidationUserAlreadyExists),
tr,
user,
context.tr.t(Sentence::ValidationUserAlreadyExists),
context.tr,
context.user,
)
.render()?,
),
@ -221,15 +223,16 @@ pub async fn sign_up_validation(
.await?
{
db::user::ValidationResult::Ok(token, user_id) => {
let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token))
.same_site(cookie::SameSite::Strict);
jar = jar.add(cookie);
let user = connection.load_user(user_id).await?;
Ok((
jar,
Html(
MessageTemplate::new_with_user(
tr.t(Sentence::SignUpEmailValidationSuccess),
tr,
context.tr.t(Sentence::SignUpEmailValidationSuccess),
context.tr,
user,
)
.render()?,
@ -240,9 +243,9 @@ pub async fn sign_up_validation(
jar,
Html(
MessageTemplate::new_with_user(
tr.t(Sentence::SignUpValidationExpired),
tr,
user,
context.tr.t(Sentence::SignUpValidationExpired),
context.tr,
context.user,
)
.render()?,
),
@ -251,9 +254,9 @@ pub async fn sign_up_validation(
jar,
Html(
MessageTemplate::new_with_user(
tr.t(Sentence::SignUpValidationErrorTryAgain),
tr,
user,
context.tr.t(Sentence::SignUpValidationErrorTryAgain),
context.tr,
context.user,
)
.render()?,
),
@ -263,8 +266,12 @@ pub async fn sign_up_validation(
None => Ok((
jar,
Html(
MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user)
.render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::ValidationError),
context.tr,
context.user,
)
.render()?,
),
)),
}
@ -273,14 +280,10 @@ pub async fn sign_up_validation(
/// SIGN IN ///
#[debug_handler]
pub async fn sign_in_get(
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
) -> Result<impl IntoResponse> {
pub async fn sign_in_get(Extension(context): Extension<Context>) -> Result<impl IntoResponse> {
Ok(Html(
SignInFormTemplate {
user,
tr,
context,
email: "",
message: "",
}
@ -298,8 +301,7 @@ pub struct SignInFormData {
pub async fn sign_in_post(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
headers: HeaderMap,
Form(form_data): Form<SignInFormData>,
) -> Result<(CookieJar, Response)> {
@ -319,10 +321,9 @@ pub async fn sign_in_post(
jar,
Html(
SignInFormTemplate {
user,
email: &form_data.email,
message: tr.t(Sentence::AccountMustBeValidatedFirst),
tr,
message: context.tr.t(Sentence::AccountMustBeValidatedFirst),
context,
}
.render()?,
)
@ -332,20 +333,20 @@ pub async fn sign_in_post(
jar,
Html(
SignInFormTemplate {
user,
email: &form_data.email,
message: tr.t(Sentence::WrongEmailOrPassword),
tr,
message: context.tr.t(Sentence::WrongEmailOrPassword),
context,
}
.render()?,
)
.into_response(),
)),
db::user::SignInResult::Ok(token, _user_id) => {
let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token))
.same_site(cookie::SameSite::Strict);
Ok((
jar.add(cookie),
Redirect::to(&format!("/{}/", tr.current_lang_code())).into_response(),
Redirect::to(&format!("/{}/", context.tr.current_lang_code())).into_response(),
))
}
}
@ -356,7 +357,7 @@ pub async fn sign_in_post(
#[debug_handler]
pub async fn sign_out(
State(connection): State<db::Connection>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
req: Request<Body>,
) -> Result<(CookieJar, Redirect)> {
let mut jar = CookieJar::from_headers(req.headers());
@ -365,27 +366,30 @@ pub async fn sign_out(
jar = jar.remove(consts::COOKIE_AUTH_TOKEN_NAME);
connection.sign_out(&token).await?;
}
Ok((jar, Redirect::to(&format!("/{}/", tr.current_lang_code()))))
Ok((
jar,
Redirect::to(&format!("/{}/", context.tr.current_lang_code())),
))
}
/// RESET PASSWORD ///
#[debug_handler]
pub async fn ask_reset_password_get(
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
) -> Result<Response> {
if user.is_some() {
pub async fn ask_reset_password_get(Extension(context): Extension<Context>) -> Result<Response> {
if context.user.is_some() {
Ok(Html(
MessageTemplate::new_with_user(tr.t(Sentence::AskResetAlreadyLoggedInError), tr, user)
.render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::AskResetAlreadyLoggedInError),
context.tr,
context.user,
)
.render()?,
)
.into_response())
} else {
Ok(Html(
AskResetPasswordTemplate {
user,
tr,
context,
email: "",
message: "",
message_email: "",
@ -414,36 +418,33 @@ pub async fn ask_reset_password_post(
Host(host): Host,
State(connection): State<db::Connection>,
State(config): State<Config>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
Form(form_data): Form<AskResetPasswordForm>,
) -> Result<Response> {
fn error_response(
error: AskResetPasswordError,
email: &str,
user: Option<model::User>,
tr: translation::Tr,
context: Context,
) -> Result<Response> {
Ok(Html(
AskResetPasswordTemplate {
user,
email,
message_email: match error {
AskResetPasswordError::InvalidEmail => tr.t(Sentence::InvalidEmail),
AskResetPasswordError::InvalidEmail => context.tr.t(Sentence::InvalidEmail),
_ => "",
},
message: match error {
AskResetPasswordError::EmailAlreadyReset => {
tr.t(Sentence::AskResetEmailAlreadyResetError)
context.tr.t(Sentence::AskResetEmailAlreadyResetError)
}
AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown),
AskResetPasswordError::EmailUnknown => context.tr.t(Sentence::EmailUnknown),
AskResetPasswordError::UnableSendEmail => {
tr.t(Sentence::UnableToSendResetEmail)
context.tr.t(Sentence::UnableToSendResetEmail)
}
AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
AskResetPasswordError::DatabaseError => context.tr.t(Sentence::DatabaseError),
_ => "",
},
tr,
context,
}
.render()?,
)
@ -455,8 +456,7 @@ pub async fn ask_reset_password_post(
return error_response(
AskResetPasswordError::InvalidEmail,
&form_data.email,
user,
tr,
context,
);
}
@ -470,20 +470,18 @@ pub async fn ask_reset_password_post(
Ok(db::user::GetTokenResetPasswordResult::PasswordAlreadyReset) => error_response(
AskResetPasswordError::EmailAlreadyReset,
&form_data.email,
user,
tr,
context,
),
Ok(db::user::GetTokenResetPasswordResult::EmailUnknown) => error_response(
AskResetPasswordError::EmailUnknown,
&form_data.email,
user,
tr,
context,
),
Ok(db::user::GetTokenResetPasswordResult::Ok(token)) => {
let url = utils::get_url_from_host(&host);
match email::send_email(
&form_data.email,
&tr.tp(
&context.tr.tp(
Sentence::AskResetFollowEmailLink,
&[Box::new(format!(
"{}/reset_password?reset_token={}",
@ -497,8 +495,12 @@ pub async fn ask_reset_password_post(
.await
{
Ok(()) => Ok(Html(
MessageTemplate::new_with_user(tr.t(Sentence::AskResetEmailSent), tr, user)
.render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::AskResetEmailSent),
context.tr,
context.user,
)
.render()?,
)
.into_response()),
Err(_) => {
@ -506,8 +508,7 @@ pub async fn ask_reset_password_post(
error_response(
AskResetPasswordError::UnableSendEmail,
&form_data.email,
user,
tr,
context,
)
}
}
@ -517,8 +518,7 @@ pub async fn ask_reset_password_post(
error_response(
AskResetPasswordError::DatabaseError,
&form_data.email,
user,
tr,
context,
)
}
}
@ -527,8 +527,7 @@ pub async fn ask_reset_password_post(
#[debug_handler]
pub async fn reset_password_get(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
Query(query): Query<HashMap<String, String>>,
) -> Result<Response> {
if let Some(reset_token) = query.get("reset_token") {
@ -542,8 +541,7 @@ pub async fn reset_password_get(
{
Ok(Html(
ResetPasswordTemplate {
user,
tr,
context,
reset_token,
message: "",
message_password: "",
@ -553,15 +551,23 @@ pub async fn reset_password_get(
.into_response())
} else {
Ok(Html(
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
.render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::AskResetTokenMissing),
context.tr,
context.user,
)
.render()?,
)
.into_response())
}
} else {
Ok(Html(
MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
.render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::AskResetTokenMissing),
context.tr,
context.user,
)
.render()?,
)
.into_response())
}
@ -584,35 +590,36 @@ enum ResetPasswordError {
#[debug_handler]
pub async fn reset_password_post(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
Form(form_data): Form<ResetPasswordForm>,
) -> Result<Response> {
fn error_response(
error: ResetPasswordError,
form_data: &ResetPasswordForm,
user: Option<model::User>,
tr: translation::Tr,
context: Context,
) -> Result<Response> {
let reset_password_mess = &tr.tp(
let reset_password_mess = &context.tr.tp(
Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
);
Ok(Html(
ResetPasswordTemplate {
user,
reset_token: &form_data.reset_token,
message_password: match error {
ResetPasswordError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
ResetPasswordError::PasswordsNotEqual => {
context.tr.t(Sentence::PasswordDontMatch)
}
ResetPasswordError::InvalidPassword => reset_password_mess,
_ => "",
},
message: match error {
ResetPasswordError::TokenExpired => tr.t(Sentence::AskResetTokenExpired),
ResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
ResetPasswordError::TokenExpired => {
context.tr.t(Sentence::AskResetTokenExpired)
}
ResetPasswordError::DatabaseError => context.tr.t(Sentence::DatabaseError),
_ => "",
},
tr,
context,
}
.render()?,
)
@ -620,13 +627,13 @@ pub async fn reset_password_post(
}
if form_data.password_1 != form_data.password_2 {
return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, user, tr);
return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, context);
}
if let common::utils::PasswordValidation::TooShort =
common::utils::validate_password(&form_data.password_1)
{
return error_response(ResetPasswordError::InvalidPassword, &form_data, user, tr);
return error_response(ResetPasswordError::InvalidPassword, &form_data, context);
}
match connection
@ -638,24 +645,26 @@ pub async fn reset_password_post(
.await
{
Ok(db::user::ResetPasswordResult::Ok) => Ok(Html(
MessageTemplate::new_with_user(tr.t(Sentence::PasswordReset), tr, user).render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::PasswordReset),
context.tr,
context.user,
)
.render()?,
)
.into_response()),
Ok(db::user::ResetPasswordResult::ResetTokenExpired) => {
error_response(ResetPasswordError::TokenExpired, &form_data, user, tr)
error_response(ResetPasswordError::TokenExpired, &form_data, context)
}
Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user, tr),
Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, context),
}
}
/// EDIT PROFILE ///
#[debug_handler]
pub async fn edit_user_get(
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
) -> Result<Response> {
Ok(if let Some(user) = user {
pub async fn edit_user_get(Extension(context): Extension<Context>) -> Result<Response> {
Ok(if let Some(ref user) = context.user {
Html(
ProfileTemplate {
username: &user.name,
@ -664,14 +673,14 @@ pub async fn edit_user_get(
message: "",
message_email: "",
message_password: "",
user: Some(user.clone()),
tr,
context: context.clone(),
}
.render()?,
)
.into_response()
} else {
Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response()
Html(MessageTemplate::new(context.tr.t(Sentence::NotLoggedIn), context.tr).render()?)
.into_response()
})
}
@ -699,43 +708,46 @@ pub async fn edit_user_post(
Host(host): Host,
State(connection): State<db::Connection>,
State(config): State<Config>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
Form(form_data): Form<EditUserForm>,
) -> Result<Response> {
if let Some(user) = user {
if let Some(ref user) = context.user {
fn error_response(
error: ProfileUpdateError,
form_data: &EditUserForm,
user: model::User,
tr: translation::Tr,
context: Context,
) -> Result<Response> {
let invalid_password_mess = &tr.tp(
let invalid_password_mess = &context.tr.tp(
Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
);
Ok(Html(
ProfileTemplate {
user: Some(user),
username: &form_data.name,
email: &form_data.email,
default_servings: form_data.default_servings,
message_email: match error {
ProfileUpdateError::InvalidEmail => tr.t(Sentence::InvalidEmail),
ProfileUpdateError::EmailAlreadyTaken => tr.t(Sentence::EmailAlreadyTaken),
ProfileUpdateError::InvalidEmail => context.tr.t(Sentence::InvalidEmail),
ProfileUpdateError::EmailAlreadyTaken => {
context.tr.t(Sentence::EmailAlreadyTaken)
}
_ => "",
},
message_password: match error {
ProfileUpdateError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
ProfileUpdateError::PasswordsNotEqual => {
context.tr.t(Sentence::PasswordDontMatch)
}
ProfileUpdateError::InvalidPassword => invalid_password_mess,
_ => "",
},
message: match error {
ProfileUpdateError::DatabaseError => tr.t(Sentence::DatabaseError),
ProfileUpdateError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
ProfileUpdateError::DatabaseError => context.tr.t(Sentence::DatabaseError),
ProfileUpdateError::UnableSendEmail => {
context.tr.t(Sentence::UnableToSendEmail)
}
_ => "",
},
tr,
context,
}
.render()?,
)
@ -743,17 +755,17 @@ pub async fn edit_user_post(
}
if form_data.email.parse::<Address>().is_err() {
return error_response(ProfileUpdateError::InvalidEmail, &form_data, user, tr);
return error_response(ProfileUpdateError::InvalidEmail, &form_data, context);
}
let new_password = if !form_data.password_1.is_empty() || !form_data.password_2.is_empty() {
if form_data.password_1 != form_data.password_2 {
return error_response(ProfileUpdateError::PasswordsNotEqual, &form_data, user, tr);
return error_response(ProfileUpdateError::PasswordsNotEqual, &form_data, context);
}
if let common::utils::PasswordValidation::TooShort =
common::utils::validate_password(&form_data.password_1)
{
return error_response(ProfileUpdateError::InvalidPassword, &form_data, user, tr);
return error_response(ProfileUpdateError::InvalidPassword, &form_data, context);
}
Some(form_data.password_1.as_ref())
} else {
@ -774,14 +786,14 @@ pub async fn edit_user_post(
.await
{
Ok(db::user::UpdateUserResult::EmailAlreadyTaken) => {
return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, user, tr);
return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, context);
}
Ok(db::user::UpdateUserResult::UserUpdatedWaitingForRevalidation(token)) => {
let url = utils::get_url_from_host(&host);
let email = form_data.email.clone();
match email::send_email(
&email,
&tr.tp(
&context.tr.tp(
Sentence::ProfileFollowEmailLink,
&[Box::new(format!(
"{}/revalidation?validation_token={}",
@ -795,24 +807,23 @@ pub async fn edit_user_post(
.await
{
Ok(()) => {
message = tr.t(Sentence::ProfileEmailSent);
message = context.tr.t(Sentence::ProfileEmailSent);
}
Err(_) => {
// error!("Email validation error: {}", error); // TODO: log
return error_response(
ProfileUpdateError::UnableSendEmail,
&form_data,
user,
tr,
context,
);
}
}
}
Ok(db::user::UpdateUserResult::Ok) => {
message = tr.t(Sentence::ProfileSaved);
message = context.tr.t(Sentence::ProfileSaved);
}
Err(_) => {
return error_response(ProfileUpdateError::DatabaseError, &form_data, user, tr);
return error_response(ProfileUpdateError::DatabaseError, &form_data, context);
}
}
@ -821,41 +832,42 @@ pub async fn edit_user_post(
Ok(Html(
ProfileTemplate {
user,
username: &form_data.name,
email: &form_data.email,
default_servings: form_data.default_servings,
message,
message_email: "",
message_password: "",
tr,
context: Context { user, ..context },
}
.render()?,
)
.into_response())
} else {
Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
Ok(
Html(MessageTemplate::new(context.tr.t(Sentence::NotLoggedIn), context.tr).render()?)
.into_response(),
)
}
}
#[debug_handler]
pub async fn email_revalidation(
State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
Extension(context): Extension<Context>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Query(query): Query<HashMap<String, String>>,
headers: HeaderMap,
) -> Result<(CookieJar, impl IntoResponse)> {
let mut jar = CookieJar::from_headers(&headers);
if user.is_some() {
if context.user.is_some() {
return Ok((
jar,
Html(
MessageTemplate::new_with_user(
tr.t(Sentence::ValidationUserAlreadyExists),
tr,
user,
context.tr.t(Sentence::ValidationUserAlreadyExists),
context.tr,
context.user,
)
.render()?,
),
@ -875,15 +887,16 @@ pub async fn email_revalidation(
.await?
{
db::user::ValidationResult::Ok(token, user_id) => {
let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token))
.same_site(cookie::SameSite::Strict);
jar = jar.add(cookie);
let user = connection.load_user(user_id).await?;
Ok((
jar,
Html(
MessageTemplate::new_with_user(
tr.t(Sentence::ValidationSuccessful),
tr,
context.tr.t(Sentence::ValidationSuccessful),
context.tr,
user,
)
.render()?,
@ -893,17 +906,21 @@ pub async fn email_revalidation(
db::user::ValidationResult::ValidationExpired => Ok((
jar,
Html(
MessageTemplate::new_with_user(tr.t(Sentence::ValidationExpired), tr, user)
.render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::ValidationExpired),
context.tr,
context.user,
)
.render()?,
),
)),
db::user::ValidationResult::UnknownUser => Ok((
jar,
Html(
MessageTemplate::new_with_user(
tr.t(Sentence::ValidationErrorTryToSignUpAgain),
tr,
user,
context.tr.t(Sentence::ValidationErrorTryToSignUpAgain),
context.tr,
context.user,
)
.render()?,
),
@ -913,8 +930,12 @@ pub async fn email_revalidation(
None => Ok((
jar,
Html(
MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user)
.render()?,
MessageTemplate::new_with_user(
context.tr.t(Sentence::ValidationError),
context.tr,
context.user,
)
.render()?,
),
)),
}

View file

@ -62,6 +62,7 @@ pub enum Sentence {
// Reset password page.
LostPassword,
AskResetChooseNewPassword,
AskResetButton,
AskResetAlreadyLoggedInError,
AskResetEmailAlreadyResetError,
@ -153,7 +154,7 @@ pub enum Sentence {
pub const DEFAULT_LANGUAGE_CODE: &str = "en";
pub const PLACEHOLDER_SUBSTITUTE: &str = "{}";
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct Tr {
lang: &'static Language,
}