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

2
.gitignore vendored
View file

@ -4,7 +4,7 @@ backend/data
/deploy-to-pi.nu /deploy-to-pi.nu
style.css.map style.css.map
backend/static/wasm/* backend/static/wasm/*
backend/static/style.css backend/static/*.css
backend/file.db backend/file.db
backend/.sass-cache/* backend/.sass-cache/*
frontend/dist/ frontend/dist/

View file

@ -9,7 +9,7 @@ common = { path = "../common" }
axum = { version = "0.8", features = ["macros"] } axum = { version = "0.8", features = ["macros"] }
axum-extra = { version = "0.10", features = ["cookie", "query"] } axum-extra = { version = "0.10", features = ["cookie", "query"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["signal", "rt-multi-thread"] }
tower = { version = "0.5", features = ["util", "limit", "buffer"] } tower = { version = "0.5", features = ["util", "limit", "buffer"] }
tower-http = { version = "0.6", features = ["fs", "trace"] } tower-http = { version = "0.6", features = ["fs", "trace"] }
@ -44,5 +44,4 @@ lettre = { version = "0.11", default-features = false, features = [
"tokio1-rustls-tls", "tokio1-rustls-tls",
] } ] }
derive_more = { version = "2", features = ["full"] }
thiserror = "2" thiserror = "2"

View file

@ -26,25 +26,36 @@ where
fn main() { fn main() {
println!("cargo:rerun-if-changed=style.scss"); println!("cargo:rerun-if-changed=style.scss");
fn run_sass(command: &mut Command) -> Output { fn run_sass(filename_without_extension: &str) {
command fn run_sass_command(command: &mut Command, name: &str) -> Output {
.arg("--no-source-map") command
.arg("scss/style.scss") .arg("--no-source-map")
.arg("static/style.css") .arg(format!("scss/{}.scss", name))
.output() .arg(format!("static/{}.css", name))
.expect("Unable to compile SASS file, install SASS, see https://sass-lang.com/") .output()
.expect("Unable to compile SASS file, install SASS, see https://sass-lang.com/")
}
let output = if exists_in_path("sass.bat") {
run_sass_command(
Command::new("cmd").args(["/C", "sass.bat"]),
filename_without_extension,
)
} else {
run_sass_command(&mut Command::new("sass"), filename_without_extension)
};
if !output.status.success() {
// SASS will put the error in the file.
let error =
std::fs::read_to_string(format!("./static/{}.css", filename_without_extension))
.unwrap_or_else(|_| {
panic!("unable to read {}.css", filename_without_extension)
});
panic!("{}", error);
}
} }
let output = if exists_in_path("sass.bat") { run_sass("style_light");
run_sass(Command::new("cmd").args(["/C", "sass.bat"])) run_sass("style_dark");
} else {
run_sass(&mut Command::new("sass"))
};
if !output.status.success() {
// SASS will put the error in the file.
let error =
std::fs::read_to_string("./static/style.css").expect("unable to read style.css");
panic!("{}", error);
}
} }

View file

@ -4,6 +4,8 @@
@use 'modal-dialog.scss'; @use 'modal-dialog.scss';
@use 'calendar.scss'; @use 'calendar.scss';
$dark-theme: false !default;
$color-1: #B29B89; $color-1: #B29B89;
$color-2: #89B29B; $color-2: #89B29B;
$color-3: #9B89B2; $color-3: #9B89B2;
@ -13,6 +15,17 @@ $text-highlight: color.adjust($color-1, $lightness: +30%);
$link-color: color.adjust($color-3, $lightness: -25%); $link-color: color.adjust($color-3, $lightness: -25%);
$link-hover-color: color.adjust($color-3, $lightness: +20%); $link-hover-color: color.adjust($color-3, $lightness: +20%);
@if $dark-theme {
$text-color: color.adjust($color-1, $lightness: -10%);
$text-highlight: color.adjust($color-1, $lightness: +10%);
$link-color: color.adjust($color-3, $lightness: -5%);
$link-hover-color: color.adjust($color-3, $lightness: +10%);
$color-1: color.adjust($color-1, $lightness: -47%);
$color-2: color.adjust($color-2, $lightness: -47%);
$color-3: color.adjust($color-2, $lightness: -47%);
}
* { * {
margin: 5px; margin: 5px;
padding: 0px; padding: 0px;
@ -271,4 +284,82 @@ body {
// } // }
// } // }
} }
}
// Customize some form elements.
#toggle-theme {
// font-size: 17px;
position: relative;
display: inline-block;
width: 3.5em;
height: 2em;
// Hide default HTML checkbox
input {
opacity: 0;
width: 0;
height: 0;
}
// The slider.
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #223243;
transition: .4s;
border-radius: 30px;
}
.slider:before {
position: absolute;
content: "";
height: 1.4em;
width: 1.4em;
border-radius: 20px;
left: 0.3em;
bottom: 0.3em;
background-color: #223243;
box-shadow: inset 2px -2px 0 1.8px #fff;
transition: .4s;
animation: anima1 0.3s linear;
}
@keyframes anima1 {
0% {
transform: translateX(1.5em);
}
80% {
transform: translateX(-0.3em);
}
100% {
transform: translateX(0em);
}
}
input:checked+.slider:before {
background-color: yellow;
box-shadow: none;
transform: translateX(1.5em);
animation: anima 0.3s linear;
}
@keyframes anima {
0% {
transform: translateX(0px)
}
80% {
transform: translateX(1.6em)
}
100% {
transform: translateX(1.4em)
}
}
} }

View file

@ -0,0 +1 @@
@use 'main.scss' with ($dark-theme: true);

View file

@ -0,0 +1 @@
@use 'main.scss' with ($dark-theme: false);

View file

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

View file

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

View file

@ -7,12 +7,7 @@ use axum::{
use serde::Deserialize; use serde::Deserialize;
// use tracing::{event, Level}; // use tracing::{event, Level};
use crate::{ use crate::{Context, Result, data::db, html_templates::*};
Result,
data::{db, model},
html_templates::*,
translation,
};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CurrentRecipeId { pub struct CurrentRecipeId {
@ -23,14 +18,16 @@ pub struct CurrentRecipeId {
pub async fn recipes_list_fragments( pub async fn recipes_list_fragments(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
current_recipe: Query<CurrentRecipeId>, current_recipe: Query<CurrentRecipeId>,
Extension(user): Extension<Option<model::User>>, Extension(context): Extension<Context>,
Extension(tr): Extension<translation::Tr>,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
let recipes = Recipes { let recipes = Recipes {
published: connection 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?, .await?,
unpublished: if let Some(user) = user.as_ref() { unpublished: if let Some(user) = context.user.as_ref() {
connection connection
.get_all_unpublished_recipe_titles(user.id) .get_all_unpublished_recipe_titles(user.id)
.await? .await?
@ -39,5 +36,7 @@ pub async fn recipes_list_fragments(
}, },
current_id: current_recipe.current_recipe_id, 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}, response::{Html, IntoResponse, Response},
}; };
use crate::{ use crate::{Context, Result, data::db, html_templates::*, ron_utils};
Result,
data::{db, model},
html_templates::*,
ron_utils, translation,
};
pub mod fragments; pub mod fragments;
pub mod recipe; pub mod recipe;
@ -21,7 +16,7 @@ pub mod user;
// Will embed RON error in HTML page. // Will embed RON error in HTML page.
pub async fn ron_error_to_html( pub async fn ron_error_to_html(
Extension(tr): Extension<translation::Tr>, Extension(context): Extension<Context>,
req: Request, req: Request,
next: Next, next: Next,
) -> Result<Response> { ) -> Result<Response> {
@ -35,10 +30,9 @@ pub async fn ron_error_to_html(
}; };
return Ok(Html( return Ok(Html(
MessageTemplate { MessageTemplate {
user: None, context,
message: &message, message: &message,
as_code: true, as_code: true,
tr,
} }
.render()?, .render()?,
) )
@ -54,14 +48,16 @@ pub async fn ron_error_to_html(
#[debug_handler] #[debug_handler]
pub async fn home_page( pub async fn home_page(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>, Extension(context): Extension<Context>,
Extension(tr): Extension<translation::Tr>,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
let recipes = Recipes { let recipes = Recipes {
published: connection 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?, .await?,
unpublished: if let Some(user) = user.as_ref() { unpublished: if let Some(user) = context.user.as_ref() {
connection connection
.get_all_unpublished_recipe_titles(user.id) .get_all_unpublished_recipe_titles(user.id)
.await? .await?
@ -71,18 +67,15 @@ pub async fn home_page(
current_id: None, current_id: None,
}; };
Ok(Html(HomeTemplate { user, recipes, tr }.render()?)) Ok(Html(HomeTemplate { context, recipes }.render()?))
} }
///// 404 ///// ///// 404 /////
#[debug_handler] #[debug_handler]
pub async fn not_found( pub async fn not_found(Extension(context): Extension<Context>) -> Result<impl IntoResponse> {
Extension(user): Extension<Option<model::User>>,
Extension(tr): Extension<translation::Tr>,
) -> Result<impl IntoResponse> {
Ok(( Ok((
StatusCode::NOT_FOUND, 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 tracing::{event, Level};
use crate::{ use crate::{
Result, Context, Result,
data::{db, model}, data::{db, model},
html_templates::*, html_templates::*,
translation::{self, Sentence}, translation::Sentence,
}; };
#[debug_handler] #[debug_handler]
pub async fn create( pub async fn create(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>, Extension(context): Extension<Context>,
Extension(tr): Extension<translation::Tr>,
) -> Result<Response> { ) -> Result<Response> {
if let Some(user) = user { if let Some(user) = context.user {
let recipe_id = connection.create_recipe(user.id).await?; let recipe_id = connection.create_recipe(user.id).await?;
Ok(Redirect::to(&format!( Ok(Redirect::to(&format!(
"/{}/recipe/edit/{}", "/{}/recipe/edit/{}",
tr.current_lang_code(), context.tr.current_lang_code(),
recipe_id recipe_id
)) ))
.into_response()) .into_response())
} else { } 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] #[debug_handler]
pub async fn edit( pub async fn edit(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>, Extension(context): Extension<Context>,
Extension(tr): Extension<translation::Tr>,
Path(recipe_id): Path<i64>, Path(recipe_id): Path<i64>,
) -> Result<Response> { ) -> 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 let Some(recipe) = connection.get_recipe(recipe_id, false).await? {
if model::can_user_edit_recipe(&user, &recipe) { if model::can_user_edit_recipe(&user, &recipe) {
let recipes = Recipes { let recipes = Recipes {
published: connection 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?, .await?,
unpublished: connection unpublished: connection
.get_all_unpublished_recipe_titles(user.id) .get_all_unpublished_recipe_titles(user.id)
@ -54,8 +58,7 @@ pub async fn edit(
Ok(Html( Ok(Html(
RecipeEditTemplate { RecipeEditTemplate {
user: Some(user), context,
tr,
recipes, recipes,
recipe, recipe,
} }
@ -63,42 +66,48 @@ pub async fn edit(
) )
.into_response()) .into_response())
} else { } else {
Ok( Ok(Html(
Html( MessageTemplate::new(
MessageTemplate::new(tr.t(Sentence::RecipeNotAllowedToEdit), tr) context.tr.t(Sentence::RecipeNotAllowedToEdit),
.render()?, context.tr,
) )
.into_response(), .render()?,
) )
.into_response())
} }
} else { } else {
Ok( Ok(Html(
Html(MessageTemplate::new(tr.t(Sentence::RecipeNotFound), tr).render()?) MessageTemplate::new(context.tr.t(Sentence::RecipeNotFound), context.tr)
.into_response(), .render()?,
) )
.into_response())
} }
} else { } 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] #[debug_handler]
pub async fn view( pub async fn view(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
Extension(user): Extension<Option<model::User>>, Extension(context): Extension<Context>,
Extension(tr): Extension<translation::Tr>,
Path(recipe_id): Path<i64>, Path(recipe_id): Path<i64>,
) -> Result<Response> { ) -> Result<Response> {
match connection.get_recipe(recipe_id, true).await? { match connection.get_recipe(recipe_id, true).await? {
Some(recipe) => { Some(recipe) => {
if !recipe.is_published 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( return Ok(Html(
MessageTemplate::new_with_user( MessageTemplate::new_with_user(
&tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]), &context
tr, .tr
user, .tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
context.tr,
context.user,
) )
.render()?, .render()?,
) )
@ -108,11 +117,11 @@ pub async fn view(
let recipes = Recipes { let recipes = Recipes {
published: connection published: connection
.get_all_published_recipe_titles( .get_all_published_recipe_titles(
tr.current_lang_code(), context.tr.current_lang_code(),
user.as_ref().map(|u| u.id), context.user.as_ref().map(|u| u.id),
) )
.await?, .await?,
unpublished: if let Some(user) = user.as_ref() { unpublished: if let Some(user) = context.user.as_ref() {
connection connection
.get_all_unpublished_recipe_titles(user.id) .get_all_unpublished_recipe_titles(user.id)
.await? .await?
@ -124,8 +133,7 @@ pub async fn view(
Ok(Html( Ok(Html(
RecipeViewTemplate { RecipeViewTemplate {
user, context,
tr,
recipes, recipes,
recipe, recipe,
} }
@ -134,7 +142,12 @@ pub async fn view(
.into_response()) .into_response())
} }
None => Ok(Html( 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()), .into_response()),
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,15 +2,15 @@
{% block main_container %} {% block main_container %}
<div class="content" id="ask-reset-password"> <div class="content" id="ask-reset-password">
<h1>{{ tr.t(Sentence::LostPassword) }}</h1> <h1>{{ context.tr.t(Sentence::LostPassword) }}</h1>
<form action="/ask_reset_password" method="post"> <form action="/ask_reset_password" method="post">
<label for="email_field">{{ tr.t(Sentence::EmailAddress) }}</label> <label for="email_field">{{ context.tr.t(Sentence::EmailAddress) }}</label>
<input id="email_field" type="email" <input id="email_field" type="email"
name="email" value="{{ email }}" name="email" value="{{ email }}"
autocapitalize="none" autocomplete="email" autofocus="autofocus"> autocapitalize="none" autocomplete="email" autofocus="autofocus">
<span class="user-message">{{ message_email }}</span> <span class="user-message">{{ message_email }}</span>
<input type="submit" name="commit" value="{{ tr.t(Sentence::AskResetButton) }}"> <input type="submit" name="commit" value="{{ context.tr.t(Sentence::AskResetButton) }}">
</form> </form>
<span class="user-message">{{ message }}</span> <span class="user-message">{{ message }}</span>

View file

@ -1,10 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ tr.current_lang_and_territory_code() }}" data-user-logged="{{ user.is_some() }}" > <html lang="{{ context.tr.current_lang_and_territory_code() }}" data-user-logged="{{ context.user.is_some() }}" >
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Recettes de cuisine</title> <title>Recettes de cuisine</title>
<link rel="stylesheet" type="text/css" href="/static/style.css"> <link rel="stylesheet" type="text/css" href="/static/
{% if context.dark_theme %}
style_dark.css
{% else %}
style_light.css
{% endif %}">
<link rel="modulepreload" href="/static/wasm/frontend.js" crossorigin="anonymous" as="fetch" type="application/wasm"> <link rel="modulepreload" href="/static/wasm/frontend.js" crossorigin="anonymous" as="fetch" type="application/wasm">
</head> </head>

View file

@ -5,29 +5,37 @@
{% include "title.html" %} {% include "title.html" %}
<span class="header-menu"> <span class="header-menu">
{% match user %} {% match context.user %}
{% when Some with (user) %} {% when Some with (user) %}
<a class="create-recipe" href="/recipe/new" >{{ tr.t(Sentence::CreateNewRecipe) }}</a> <a class="create-recipe" href="/recipe/new" >{{ context.tr.t(Sentence::CreateNewRecipe) }}</a>
<a href="/{{ tr.current_lang_code() }}/user/edit"> <a href="/{{ context.tr.current_lang_code() }}/user/edit">
{% if user.name == "" %} {% if user.name == "" %}
{{ user.email }} {{ user.email }}
{% else %} {% else %}
{{ user.name }} {{ user.name }}
{% endif %} {% endif %}
</a> / <a href="/signout">{{ tr.t(Sentence::SignOut) }}</a> </a> / <a href="/signout">{{ context.tr.t(Sentence::SignOut) }}</a>
{% when None %} {% when None %}
<a href="/{{ tr.current_lang_code() }}/signin" >{{ tr.t(Sentence::SignInMenu) }}</a>/<a href="/{{ tr.current_lang_code() }}/signup">{{ tr.t(Sentence::SignUpMenu) }}</a>/<a href="/{{ tr.current_lang_code() }}/ask_reset_password">{{ tr.t(Sentence::LostPassword) }}</a> <a href="/{{ context.tr.current_lang_code() }}/signin" >{{ context.tr.t(Sentence::SignInMenu) }}</a>/<a href="/{{ context.tr.current_lang_code() }}/signup">{{ context.tr.t(Sentence::SignUpMenu) }}</a>/<a href="/{{ context.tr.current_lang_code() }}/ask_reset_password">{{ context.tr.t(Sentence::LostPassword) }}</a>
{% endmatch %} {% endmatch %}
<select id="select-website-language"> <select id="select-website-language">
{% for lang in translation::available_languages() %} {% for lang in translation::available_languages() %}
<option value="{{ lang.0 }}" <option value="{{ lang.0 }}"
{%~ if tr.current_lang_code() == lang.0 %} {%~ if context.tr.current_lang_code() == lang.0 %}
selected selected
{% endif %} {% endif %}
>{{ lang.1 }}</option> >{{ lang.1 }}</option>
{% endfor %} {% endfor %}
</select> </select>
<label id="toggle-theme">
<input type="checkbox"
{%~ if !context.dark_theme %}
checked
{% endif %} >
<span class="slider"></span>
</label>
</span> </span>
</div> </div>

View file

@ -17,7 +17,7 @@
Sentence::CalendarNovember, Sentence::CalendarNovember,
Sentence::CalendarDecember, Sentence::CalendarDecember,
] %} ] %}
<span class="month">{{ tr.t(*month) }}</span> <span class="month">{{ context.tr.t(*month) }}</span>
{% endfor %} {% endfor %}
<span class="next">NEXT</span> <span class="next">NEXT</span>
@ -32,7 +32,7 @@
Sentence::CalendarSaturdayAbbreviation, Sentence::CalendarSaturdayAbbreviation,
Sentence::CalendarSundayAbbreviation, Sentence::CalendarSundayAbbreviation,
] %} ] %}
<li class="weekday">{{ tr.t(*day) }}</li> <li class="weekday">{{ context.tr.t(*day) }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
@ -52,17 +52,17 @@
<div class="scheduled-recipe"></div> <div class="scheduled-recipe"></div>
<div class="unschedule-confirmation"> <div class="unschedule-confirmation">
<div>{{ tr.t(Sentence::CalendarUnscheduleConfirmation) }}</div> <div>{{ context.tr.t(Sentence::CalendarUnscheduleConfirmation) }}</div>
<input <input
id="input-remove-ingredients-from-shopping-list" id="input-remove-ingredients-from-shopping-list"
type="checkbox" type="checkbox"
checked checked
> >
<label for="input-remove-ingredients-from-shopping-list"> <label for="input-remove-ingredients-from-shopping-list">
{{ tr.t(Sentence::CalendarRemoveIngredientsFromShoppingList) }} {{ context.tr.t(Sentence::CalendarRemoveIngredientsFromShoppingList) }}
</label> </label>
</div> </div>
<span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span> <span class="calendar-date-format">{{ context.tr.t(Sentence::CalendarDateFormat) }}</span>
</div> </div>
</div> </div>

View file

@ -21,7 +21,7 @@
<div class="item-delete"></div> <div class="item-delete"></div>
</div> </div>
<span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span> <span class="calendar-date-format">{{ context.tr.t(Sentence::CalendarDateFormat) }}</span>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -2,14 +2,14 @@
{% block main_container %} {% block main_container %}
{% if let Some(user) = user %} {% if let Some(user) = context.user %}
<div class="content" id="user-edit"> <div class="content" id="user-edit">
<h1>{{ tr.t(Sentence::ProfileTitle) }}</h1> <h1>{{ context.tr.t(Sentence::ProfileTitle) }}</h1>
<form action="/{{ tr.current_lang_code() }}/user/edit" method="post"> <form action="/{{ context.tr.current_lang_code() }}/user/edit" method="post">
<label for="input-name">{{ tr.t(Sentence::Name) }}</label> <label for="input-name">{{ context.tr.t(Sentence::Name) }}</label>
<input <input
id="input-name" id="input-name"
type="text" type="text"
@ -20,13 +20,13 @@
autofocus="autofocus"> autofocus="autofocus">
<span></span> <span></span>
<label for="input-email">{{ tr.t(Sentence::ProfileEmail) }}</label> <label for="input-email">{{ context.tr.t(Sentence::ProfileEmail) }}</label>
<input id="input-email" type="email" <input id="input-email" type="email"
name="email" value="{{ email }}" name="email" value="{{ email }}"
autocapitalize="none" autocomplete="email" autofocus="autofocus"> autocapitalize="none" autocomplete="email" autofocus="autofocus">
<span class="user-message">{{ message_email }}</span> <span class="user-message">{{ message_email }}</span>
<label for="input-servings">{{ tr.t(Sentence::ProfileDefaultServings) }}</label> <label for="input-servings">{{ context.tr.t(Sentence::ProfileDefaultServings) }}</label>
<input <input
id="input-servings" id="input-servings"
type="number" type="number"
@ -35,15 +35,15 @@
value="{{ default_servings }}"> value="{{ default_servings }}">
<span></span> <span></span>
<label for="input-password-1">{{ tr.tp(Sentence::ProfileNewPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }}</label> <label for="input-password-1">{{ context.tr.tp(Sentence::ProfileNewPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }}</label>
<input id="input-password-1" type="password" name="password_1" autocomplete="new-password"> <input id="input-password-1" type="password" name="password_1" autocomplete="new-password">
<span></span> <span></span>
<label for="input-password-2">{{ tr.t(Sentence::ReEnterPassword) }}</label> <label for="input-password-2">{{ context.tr.t(Sentence::ReEnterPassword) }}</label>
<input id="input-password-2" type="password" name="password_2" autocomplete="new-password"> <input id="input-password-2" type="password" name="password_2" autocomplete="new-password">
<span class="user-message">{{ message_password }}</span> <span class="user-message">{{ message_password }}</span>
<input type="submit" name="commit" value="{{ tr.t(Sentence::Save) }}"> <input type="submit" name="commit" value="{{ context.tr.t(Sentence::Save) }}">
</form> </form>
<span class="user-message">{{ message }}</span> <span class="user-message">{{ message }}</span>

View file

@ -9,18 +9,18 @@
{% block content %} {% block content %}
<div class="content" id="recipe-edit"> <div class="content" id="recipe-edit">
<label for="input-title">{{ tr.t(Sentence::RecipeTitle) }}</label> <label for="input-title">{{ context.tr.t(Sentence::RecipeTitle) }}</label>
<input <input
id="input-title" id="input-title"
type="text" type="text"
value="{{ recipe.title }}" value="{{ recipe.title }}"
autofocus="true"> autofocus="true">
<label for="text-area-description">{{ tr.t(Sentence::RecipeDescription) }}</label> <label for="text-area-description">{{ context.tr.t(Sentence::RecipeDescription) }}</label>
<textarea <textarea
id="text-area-description">{{ recipe.description }}</textarea> id="text-area-description">{{ recipe.description }}</textarea>
<label for="input-servings">{{ tr.t(Sentence::RecipeServings) }}</label> <label for="input-servings">{{ context.tr.t(Sentence::RecipeServings) }}</label>
<input <input
id="input-servings" id="input-servings"
type="number" type="number"
@ -30,7 +30,7 @@
{{ s }} {{ s }}
{% endif %}"> {% endif %}">
<label for="input-estimated-time">{{ tr.t(Sentence::RecipeEstimatedTime) }}</label> <label for="input-estimated-time">{{ context.tr.t(Sentence::RecipeEstimatedTime) }}</label>
<input <input
id="input-estimated-time" id="input-estimated-time"
type="number" type="number"
@ -40,16 +40,16 @@
{{ t }} {{ t }}
{% endif %}"> {% endif %}">
<label for="select-difficulty">{{ tr.t(Sentence::RecipeDifficulty) }}</label> <label for="select-difficulty">{{ context.tr.t(Sentence::RecipeDifficulty) }}</label>
<select id="select-difficulty"> <select id="select-difficulty">
<option value="0" {%~ call is_difficulty(common::ron_api::Difficulty::Unknown) %}> - </option> <option value="0" {%~ call is_difficulty(common::ron_api::Difficulty::Unknown) %}> - </option>
<option value="1" {%~ call is_difficulty(common::ron_api::Difficulty::Easy) %}>{{ tr.t(Sentence::RecipeDifficultyEasy) }}</option> <option value="1" {%~ call is_difficulty(common::ron_api::Difficulty::Easy) %}>{{ context.tr.t(Sentence::RecipeDifficultyEasy) }}</option>
<option value="2" {%~ call is_difficulty(common::ron_api::Difficulty::Medium) %}>{{ tr.t(Sentence::RecipeDifficultyMedium) }}</option> <option value="2" {%~ call is_difficulty(common::ron_api::Difficulty::Medium) %}>{{ context.tr.t(Sentence::RecipeDifficultyMedium) }}</option>
<option value="3" {%~ call is_difficulty(common::ron_api::Difficulty::Hard) %}>{{ tr.t(Sentence::RecipeDifficultyHard) }}</option> <option value="3" {%~ call is_difficulty(common::ron_api::Difficulty::Hard) %}>{{ context.tr.t(Sentence::RecipeDifficultyHard) }}</option>
</select> </select>
<div id="container-tags"> <div id="container-tags">
<label for="input-tags" >{{ tr.t(Sentence::RecipeTags) }}</label> <label for="input-tags" >{{ context.tr.t(Sentence::RecipeTags) }}</label>
<span class="tags"></span> <span class="tags"></span>
<input <input
id="input-tags" id="input-tags"
@ -57,7 +57,7 @@
value=""> value="">
</div> </div>
<label for="select-language">{{ tr.t(Sentence::RecipeLanguage) }}</label> <label for="select-language">{{ context.tr.t(Sentence::RecipeLanguage) }}</label>
<select id="select-language"> <select id="select-language">
{% for lang in translation::available_languages() %} {% for lang in translation::available_languages() %}
<option value="{{ lang.0 }}" <option value="{{ lang.0 }}"
@ -75,71 +75,71 @@
checked checked
{% endif %} {% endif %}
> >
<label for="input-is-published">{{ tr.t(Sentence::RecipeIsPublished) }}</label> <label for="input-is-published">{{ context.tr.t(Sentence::RecipeIsPublished) }}</label>
<input id="input-delete" type="button" value="{{ tr.t(Sentence::RecipeDelete) }}"> <input id="input-delete" type="button" value="{{ context.tr.t(Sentence::RecipeDelete) }}">
<div id="groups-container"> <div id="groups-container">
</div> </div>
<input id="input-add-group" type="button" value="{{ tr.t(Sentence::RecipeAddAGroup) }}"> <input id="input-add-group" type="button" value="{{ context.tr.t(Sentence::RecipeAddAGroup) }}">
</div> </div>
<div id="hidden-templates"> <div id="hidden-templates">
<div class="group"> <div class="group">
<span class="drag-handle"></span> <span class="drag-handle"></span>
<label for="input-group-name">{{ tr.t(Sentence::RecipeGroupName) }}</label> <label for="input-group-name">{{ context.tr.t(Sentence::RecipeGroupName) }}</label>
<input class="input-group-name" type="text"> <input class="input-group-name" type="text">
<label for="input-group-comment">{{ tr.t(Sentence::RecipeGroupComment) }}</label> <label for="input-group-comment">{{ context.tr.t(Sentence::RecipeGroupComment) }}</label>
<input class="input-group-comment" type="text"> <input class="input-group-comment" type="text">
<input class="input-group-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveGroup) }}"> <input class="input-group-delete" type="button" value="{{ context.tr.t(Sentence::RecipeRemoveGroup) }}">
<div class="steps"> <div class="steps">
</div> </div>
<input class="input-add-step" type="button" value="{{ tr.t(Sentence::RecipeAddAStep) }}"> <input class="input-add-step" type="button" value="{{ context.tr.t(Sentence::RecipeAddAStep) }}">
</div> </div>
<div class="step"> <div class="step">
<span class="drag-handle"></span> <span class="drag-handle"></span>
<label for="text-area-step-action">{{ tr.t(Sentence::RecipeStepAction) }}</label> <label for="text-area-step-action">{{ context.tr.t(Sentence::RecipeStepAction) }}</label>
<textarea class="text-area-step-action"></textarea> <textarea class="text-area-step-action"></textarea>
<input class="input-step-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveStep) }}"> <input class="input-step-delete" type="button" value="{{ context.tr.t(Sentence::RecipeRemoveStep) }}">
<div class="ingredients"></div> <div class="ingredients"></div>
<input class="input-add-ingredient" type="button" value="{{ tr.t(Sentence::RecipeAddAnIngredient) }}"> <input class="input-add-ingredient" type="button" value="{{ context.tr.t(Sentence::RecipeAddAnIngredient) }}">
</div> </div>
<div class="ingredient"> <div class="ingredient">
<span class="drag-handle"></span> <span class="drag-handle"></span>
<label for="input-ingredient-name">{{ tr.t(Sentence::RecipeIngredientName) }}</label> <label for="input-ingredient-name">{{ context.tr.t(Sentence::RecipeIngredientName) }}</label>
<input class="input-ingredient-name" type="text"> <input class="input-ingredient-name" type="text">
<label for="input-ingredient-quantity">{{ tr.t(Sentence::RecipeIngredientQuantity) }}</label> <label for="input-ingredient-quantity">{{ context.tr.t(Sentence::RecipeIngredientQuantity) }}</label>
<input class="input-ingredient-quantity" type="number" step="0.1" min="0" max="10000"> <input class="input-ingredient-quantity" type="number" step="0.1" min="0" max="10000">
<label for="input-ingredient-unit">{{ tr.t(Sentence::RecipeIngredientUnit) }}</label> <label for="input-ingredient-unit">{{ context.tr.t(Sentence::RecipeIngredientUnit) }}</label>
<input class="input-ingredient-unit" type="text"> <input class="input-ingredient-unit" type="text">
<label for="input-ingredient-comment">{{ tr.t(Sentence::RecipeIngredientComment) }}</label> <label for="input-ingredient-comment">{{ context.tr.t(Sentence::RecipeIngredientComment) }}</label>
<input class="input-ingredient-comment" type="text"> <input class="input-ingredient-comment" type="text">
<input class="input-ingredient-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveIngredient) }}"> <input class="input-ingredient-delete" type="button" value="{{ context.tr.t(Sentence::RecipeRemoveIngredient) }}">
</div> </div>
<div class="dropzone"></div> <div class="dropzone"></div>
<span class="recipe-delete-confirmation">{{ tr.t(Sentence::RecipeDeleteConfirmation) }}</span> <span class="recipe-delete-confirmation">{{ context.tr.t(Sentence::RecipeDeleteConfirmation) }}</span>
<span class="recipe-group-delete-confirmation">{{ tr.t(Sentence::RecipeGroupDeleteConfirmation) }}</span> <span class="recipe-group-delete-confirmation">{{ context.tr.t(Sentence::RecipeGroupDeleteConfirmation) }}</span>
<span class="recipe-step-delete-confirmation">{{ tr.t(Sentence::RecipeStepDeleteConfirmation) }}</span> <span class="recipe-step-delete-confirmation">{{ context.tr.t(Sentence::RecipeStepDeleteConfirmation) }}</span>
<span class="recipe-ingredient-delete-confirmation">{{ tr.t(Sentence::RecipeIngredientDeleteConfirmation) }}</span> <span class="recipe-ingredient-delete-confirmation">{{ context.tr.t(Sentence::RecipeIngredientDeleteConfirmation) }}</span>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -5,13 +5,13 @@
<div class="content" id="recipe-view"> <div class="content" id="recipe-view">
<h2 class="recipe-title" >{{ recipe.title }}</h2> <h2 class="recipe-title" >{{ recipe.title }}</h2>
{% if let Some(user) = user %} {% if let Some(user) = context.user %}
{% if crate::data::model::can_user_edit_recipe(user, recipe) %} {% if crate::data::model::can_user_edit_recipe(user, recipe) %}
<a class="edit-recipe" href="/{{ tr.current_lang_code() }}/recipe/edit/{{ recipe.id }}" >Edit</a> <a class="edit-recipe" href="/{{ context.tr.current_lang_code() }}/recipe/edit/{{ recipe.id }}" >Edit</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<span class="add-to-planner">{{ tr.t(Sentence::CalendarAddToPlanner) }}</span> <span class="add-to-planner">{{ context.tr.t(Sentence::CalendarAddToPlanner) }}</span>
<div class="tags"> <div class="tags">
{% for tag in recipe.tags %} {% for tag in recipe.tags %}
@ -23,9 +23,9 @@
{% when Some(servings) %} {% when Some(servings) %}
<span class="servings"> <span class="servings">
{% if *servings == 1 %} {% if *servings == 1 %}
{{ tr.t(Sentence::RecipeOneServing) }} {{ context.tr.t(Sentence::RecipeOneServing) }}
{% else %} {% else %}
{{ tr.tp(Sentence::RecipeSomeServings, [Box::new(**servings)]) }} {{ context.tr.tp(Sentence::RecipeSomeServings, [Box::new(**servings)]) }}
{% endif %} {% endif %}
</span> </span>
{% else %} {% else %}
@ -33,7 +33,7 @@
{% match recipe.estimated_time %} {% match recipe.estimated_time %}
{% when Some(time) %} {% when Some(time) %}
{{ time ~}} {{~ tr.t(Sentence::RecipeEstimatedTimeMinAbbreviation) }} {{ time ~}} {{~ context.tr.t(Sentence::RecipeEstimatedTimeMinAbbreviation) }}
{% else %} {% else %}
{% endmatch %} {% endmatch %}
@ -41,11 +41,11 @@
{% match recipe.difficulty %} {% match recipe.difficulty %}
{% when common::ron_api::Difficulty::Unknown %} {% when common::ron_api::Difficulty::Unknown %}
{% when common::ron_api::Difficulty::Easy %} {% when common::ron_api::Difficulty::Easy %}
{{ tr.t(Sentence::RecipeDifficultyEasy) }} {{ context.tr.t(Sentence::RecipeDifficultyEasy) }}
{% when common::ron_api::Difficulty::Medium %} {% when common::ron_api::Difficulty::Medium %}
{{ tr.t(Sentence::RecipeDifficultyMedium) }} {{ context.tr.t(Sentence::RecipeDifficultyMedium) }}
{% when common::ron_api::Difficulty::Hard %} {% when common::ron_api::Difficulty::Hard %}
{{ tr.t(Sentence::RecipeDifficultyHard) }} {{ context.tr.t(Sentence::RecipeDifficultyHard) }}
{% endmatch %} {% endmatch %}
</span> </span>
@ -86,13 +86,13 @@
<div class="date-and-servings" > <div class="date-and-servings" >
{% include "calendar.html" %} {% include "calendar.html" %}
<label for="input-servings">{{ tr.t(Sentence::RecipeServings) }}</label> <label for="input-servings">{{ context.tr.t(Sentence::RecipeServings) }}</label>
<input <input
id="input-servings" id="input-servings"
type="number" type="number"
step="1" min="1" max="100" step="1" min="1" max="100"
value=" value="
{% if let Some(user) = user %} {% if let Some(user) = context.user %}
{{ user.default_servings }} {{ user.default_servings }}
{% else %} {% else %}
4 4
@ -106,13 +106,13 @@
checked checked
> >
<label for="input-add-ingredients-to-shopping-list"> <label for="input-add-ingredients-to-shopping-list">
{{ tr.t(Sentence::CalendarAddIngredientsToShoppingList) }} {{ context.tr.t(Sentence::CalendarAddIngredientsToShoppingList) }}
</label> </label>
</div> </div>
<span class="calendar-add-to-planner-success">{{ tr.t(Sentence::CalendarAddToPlannerSuccess) }}</span> <span class="calendar-add-to-planner-success">{{ context.tr.t(Sentence::CalendarAddToPlannerSuccess) }}</span>
<span class="calendar-add-to-planner-already-exists">{{ tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }}</span> <span class="calendar-add-to-planner-already-exists">{{ context.tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }}</span>
<span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span> <span class="calendar-date-format">{{ context.tr.t(Sentence::CalendarDateFormat) }}</span>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,12 +1,12 @@
{% macro recipe_item(id, title, is_current) %} {% macro recipe_item(id, title, is_current) %}
<li> <li>
<a href="/{{ tr.current_lang_code() }}/recipe/view/{{ id }}" class="recipe-item <a href="/{{ context.tr.current_lang_code() }}/recipe/view/{{ id }}" class="recipe-item
{%~ if is_current %} {%~ if is_current %}
current current
{% endif %}" id="recipe-{{ id }}" {% endif %}" id="recipe-{{ id }}"
> >
{% if title == "" %} {% if title == "" %}
{{ tr.t(Sentence::UntitledRecipe) }} {{ context.tr.t(Sentence::UntitledRecipe) }}
{% else %} {% else %}
{{ title }} {{ title }}
{% endif %} {% endif %}
@ -16,7 +16,7 @@
<div id="recipes-list"> <div id="recipes-list">
{% if !recipes.unpublished.is_empty() %} {% if !recipes.unpublished.is_empty() %}
{{ tr.t(Sentence::UnpublishedRecipes) }} {{ context.tr.t(Sentence::UnpublishedRecipes) }}
{% endif %} {% endif %}
<nav class="recipes-list-unpublished"> <nav class="recipes-list-unpublished">

View file

@ -4,10 +4,10 @@
<div class="content" id="reset-password"> <div class="content" id="reset-password">
<form action="/reset_password" method="post"> <form action="/reset_password" method="post">
<label for="password_field_1">Choose a new password (minimum 8 characters)</label> <label for="password_field_1">{{ context.tr.tp(Sentence::AskResetChooseNewPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }}</label>
<input id="password_field_1" type="password" name="password_1"> <input id="password_field_1" type="password" name="password_1">
<label for="password_field_1">Re-enter password</label> <label for="password_field_1">{{ context.tr.t(Sentence::ReEnterPassword) }}</label>
<input id="password_field_2" type="password" name="password_2"> <input id="password_field_2" type="password" name="password_2">
{{ message_password }} {{ message_password }}

View file

@ -4,17 +4,17 @@
<div class="content" id="sign-in"> <div class="content" id="sign-in">
<h1>{{ tr.t(Sentence::SignInTitle) }}</h1> <h1>{{ context.tr.t(Sentence::SignInTitle) }}</h1>
<form action="/signin" method="post"> <form action="/signin" method="post">
<label for="input-email">{{ tr.t(Sentence::EmailAddress) }}</label> <label for="input-email">{{ context.tr.t(Sentence::EmailAddress) }}</label>
<input id="input-email" type="email" name="email" value="{{ email }}" <input id="input-email" type="email" name="email" value="{{ email }}"
autocapitalize="none" autocomplete="email" autofocus="autofocus"> autocapitalize="none" autocomplete="email" autofocus="autofocus">
<label for="input-password">{{ tr.t(Sentence::Password) }}</label> <label for="input-password">{{ context.tr.t(Sentence::Password) }}</label>
<input id="input-password" type="password" name="password" autocomplete="current-password"> <input id="input-password" type="password" name="password" autocomplete="current-password">
<input type="submit" value="{{ tr.t(Sentence::SignInMenu) }}"> <input type="submit" value="{{ context.tr.t(Sentence::SignInMenu) }}">
</form> </form>
<span class="user-message">{{ message }}</span> <span class="user-message">{{ message }}</span>

View file

@ -4,10 +4,10 @@
<div class="content" id="sign-up"> <div class="content" id="sign-up">
<h1>{{ tr.t(Sentence::SignUpTitle) }}</h1> <h1>{{ context.tr.t(Sentence::SignUpTitle) }}</h1>
<form action="/signup" method="post"> <form action="/signup" method="post">
<label for="input-email">{{ tr.t(Sentence::EmailAddress) }}</label> <label for="input-email">{{ context.tr.t(Sentence::EmailAddress) }}</label>
<input id="input-email" type="email" <input id="input-email" type="email"
name="email" value="{{ email }}" name="email" value="{{ email }}"
autocapitalize="none" autocomplete="email" autofocus="autofocus" autocapitalize="none" autocomplete="email" autofocus="autofocus"
@ -15,16 +15,16 @@
<span class="user-message">{{ message_email }}</span> <span class="user-message">{{ message_email }}</span>
<label for="input-password-1"> <label for="input-password-1">
{{ tr.tp(Sentence::ChooseAPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }} {{ context.tr.tp(Sentence::ChooseAPassword, [Box::new(common::consts::MIN_PASSWORD_SIZE)]) }}
</label> </label>
<input id="input-password-1" type="password" name="password_1" autocomplete="new-password"> <input id="input-password-1" type="password" name="password_1" autocomplete="new-password">
<span></span> <span></span>
<label for="input-password-2">{{ tr.t(Sentence::ReEnterPassword) }}</label> <label for="input-password-2">{{ context.tr.t(Sentence::ReEnterPassword) }}</label>
<input id="input-password-2" type="password" name="password_2" autocomplete="new-password"> <input id="input-password-2" type="password" name="password_2" autocomplete="new-password">
<span class="user-message">{{ message_password }}</span> <span class="user-message">{{ message_password }}</span>
<input type="submit" name="commit" value="{{ tr.t(Sentence::SignUpButton) }}"> <input type="submit" name="commit" value="{{ context.tr.t(Sentence::SignUpButton) }}">
</form> </form>
<span class="user-message">{{ message }}</span> <span class="user-message">{{ message }}</span>

View file

@ -1 +1 @@
<a class="title" href="/{{ tr.current_lang_code() }}/"><img class="logo" src="/static/logo.svg" alt="logo">{{ tr.t(Sentence::MainTitle) }}</a> <a class="title" href="/{{ context.tr.current_lang_code() }}/"><img class="logo" src="/static/logo.svg" alt="logo">{{ context.tr.t(Sentence::MainTitle) }}</a>

View file

@ -50,6 +50,7 @@
(ReEnterPassword, "Re-enter password"), (ReEnterPassword, "Re-enter password"),
(LostPassword, "Lost password"), (LostPassword, "Lost password"),
(AskResetChooseNewPassword, "Choose a new password (minimum {} characters)"),
(AskResetButton, "Ask reset"), (AskResetButton, "Ask reset"),
(AskResetAlreadyLoggedInError, "Can't ask to reset password when already logged in"), (AskResetAlreadyLoggedInError, "Can't ask to reset password when already logged in"),
(AskResetEmailAlreadyResetError, "The password has already been reset for this email"), (AskResetEmailAlreadyResetError, "The password has already been reset for this email"),
@ -185,6 +186,7 @@
(ReEnterPassword, "Entrez à nouveau le mot de passe"), (ReEnterPassword, "Entrez à nouveau le mot de passe"),
(LostPassword, "Mot de passe perdu"), (LostPassword, "Mot de passe perdu"),
(AskResetChooseNewPassword, "Choisir un nouveau mot de passe (minimum {} caractères)"),
(AskResetButton, "Demander la réinitialisation"), (AskResetButton, "Demander la réinitialisation"),
(AskResetAlreadyLoggedInError, "Impossible de demander une réinitialisation du mot de passe lorsque déjà connecté"), (AskResetAlreadyLoggedInError, "Impossible de demander une réinitialisation du mot de passe lorsque déjà connecté"),
(AskResetEmailAlreadyResetError, "Le mot de passe a déjà été réinitialisé pour cette adresse email"), (AskResetEmailAlreadyResetError, "Le mot de passe a déjà été réinitialisé pour cette adresse email"),

View file

@ -1 +1,2 @@
pub const MIN_PASSWORD_SIZE: usize = 8; pub const MIN_PASSWORD_SIZE: usize = 8;
pub const COOKIE_DARK_THEME: &str = "dark_theme";

View file

@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

View file

@ -3,7 +3,7 @@ use gloo::{console::log, events::EventListener, utils::window};
use utils::by_id; use utils::by_id;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::spawn_local;
use web_sys::{HtmlElement, HtmlSelectElement}; use web_sys::{HtmlElement, HtmlInputElement, HtmlSelectElement};
use crate::utils::selector; use crate::utils::selector;
@ -58,6 +58,7 @@ pub fn main() -> Result<(), JsValue> {
_ => log!("Path unknown: ", location), _ => log!("Path unknown: ", location),
} }
// Language handling.
let select_language: HtmlSelectElement = by_id("select-website-language"); let select_language: HtmlSelectElement = by_id("select-website-language");
EventListener::new(&select_language.clone(), "input", move |_event| { EventListener::new(&select_language.clone(), "input", move |_event| {
let lang = select_language.value(); let lang = select_language.value();
@ -79,5 +80,23 @@ pub fn main() -> Result<(), JsValue> {
}) })
.forget(); .forget();
// Dark/light theme handling.
let toggle_theme: HtmlInputElement = selector("#toggle-theme input");
EventListener::new(&toggle_theme.clone(), "change", move |_event| {
wasm_cookies::set(
common::consts::COOKIE_DARK_THEME,
&(!toggle_theme.checked()).to_string(),
&wasm_cookies::CookieOptions {
path: Some("/"),
domain: None,
expires: None,
secure: false,
same_site: wasm_cookies::SameSite::Strict,
},
);
window().location().reload().unwrap();
})
.forget();
Ok(()) Ok(())
} }