Add a toggle between dark and light theme
authorGreg Burri <greg.burri@gmail.com>
Mon, 31 Mar 2025 13:31:06 +0000 (15:31 +0200)
committerGreg Burri <greg.burri@gmail.com>
Mon, 31 Mar 2025 13:31:06 +0000 (15:31 +0200)
35 files changed:
.gitignore
backend/Cargo.toml
backend/build.rs
backend/scss/main.scss [new file with mode: 0644]
backend/scss/style.scss [deleted file]
backend/scss/style_dark.scss [new file with mode: 0644]
backend/scss/style_light.scss [new file with mode: 0644]
backend/src/html_templates.rs
backend/src/main.rs
backend/src/services/fragments.rs
backend/src/services/mod.rs
backend/src/services/recipe.rs
backend/src/services/ron/calendar.rs
backend/src/services/ron/mod.rs
backend/src/services/ron/recipe.rs
backend/src/services/ron/shopping_list.rs
backend/src/services/user.rs
backend/src/translation.rs
backend/templates/ask_reset_password.html
backend/templates/base.html
backend/templates/base_with_header.html
backend/templates/calendar.html
backend/templates/home.html
backend/templates/profile.html
backend/templates/recipe_edit.html
backend/templates/recipe_view.html
backend/templates/recipes_list_fragment.html
backend/templates/reset_password.html
backend/templates/sign_in_form.html
backend/templates/sign_up_form.html
backend/templates/title.html
backend/translation.ron
common/src/consts.rs
frontend/.cargo/config.toml [new file with mode: 0644]
frontend/src/lib.rs

index d849085..3ff887c 100644 (file)
@@ -4,7 +4,7 @@ backend/data
 /deploy-to-pi.nu
 style.css.map
 backend/static/wasm/*
-backend/static/style.css
+backend/static/*.css
 backend/file.db
 backend/.sass-cache/*
 frontend/dist/
index 92f288c..6eaacf9 100644 (file)
@@ -9,7 +9,7 @@ common = { path = "../common" }
 \r
 axum = { version = "0.8", features = ["macros"] }\r
 axum-extra = { version = "0.10", features = ["cookie", "query"] }\r
-tokio = { version = "1", features = ["full"] }\r
+tokio = { version = "1", features = ["signal", "rt-multi-thread"] }\r
 tower = { version = "0.5", features = ["util", "limit", "buffer"] }\r
 tower-http = { version = "0.6", features = ["fs", "trace"] }\r
 \r
@@ -44,5 +44,4 @@ lettre = { version = "0.11", default-features = false, features = [
     "tokio1-rustls-tls",\r
 ] }\r
 \r
-derive_more = { version = "2", features = ["full"] }\r
 thiserror = "2"\r
index b30077f..7a04912 100644 (file)
@@ -26,25 +26,36 @@ where
 fn main() {
     println!("cargo:rerun-if-changed=style.scss");
 
-    fn run_sass(command: &mut Command) -> Output {
-        command
-            .arg("--no-source-map")
-            .arg("scss/style.scss")
-            .arg("static/style.css")
-            .output()
-            .expect("Unable to compile SASS file, install SASS, see https://sass-lang.com/")
-    }
+    fn run_sass(filename_without_extension: &str) {
+        fn run_sass_command(command: &mut Command, name: &str) -> Output {
+            command
+                .arg("--no-source-map")
+                .arg(format!("scss/{}.scss", name))
+                .arg(format!("static/{}.css", name))
+                .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)
+        };
 
-    let output = if exists_in_path("sass.bat") {
-        run_sass(Command::new("cmd").args(["/C", "sass.bat"]))
-    } 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);
+        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);
+        }
     }
+
+    run_sass("style_light");
+    run_sass("style_dark");
 }
diff --git a/backend/scss/main.scss b/backend/scss/main.scss
new file mode 100644 (file)
index 0000000..89cc732
--- /dev/null
@@ -0,0 +1,365 @@
+@use 'sass:color';
+
+@use 'toast.scss';
+@use 'modal-dialog.scss';
+@use 'calendar.scss';
+
+$dark-theme: false !default;
+
+$color-1: #B29B89;
+$color-2: #89B29B;
+$color-3: #9B89B2;
+
+$text-color: color.adjust($color-1, $lightness: -30%);
+$text-highlight: color.adjust($color-1, $lightness: +30%);
+$link-color: color.adjust($color-3, $lightness: -25%);
+$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;
+    padding: 0px;
+}
+
+a {
+    color: $link-color;
+    text-decoration: none;
+
+    &:hover {
+        color: $link-hover-color;
+    }
+}
+
+body {
+    display: flex;
+    flex-direction: column;
+
+    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+    color: $text-color;
+    background-color: $color-1;
+    margin: 0px;
+
+    .user-message {
+        font-weight: bold;
+    }
+
+    .footer-container {
+        align-self: center;
+        font-size: 0.7em;
+    }
+
+    .drag-handle {
+        width: 20px;
+        height: 20px;
+        display: inline-block;
+        vertical-align: bottom;
+        background-color: blue;
+    }
+
+    img {
+        border: 0px;
+    }
+
+    .header-container {
+        display: flex;
+        flex-direction: row;
+        justify-content: space-between;
+
+        .title {
+            font-size: 180%;
+            font-style: italic;
+
+            .logo {
+                width: 50px;
+                height: 50px;
+                vertical-align: bottom;
+                margin: 0px 10px 0px 0px;
+            }
+        }
+
+        .header-menu {
+            align-self: flex-end;
+
+            .create-recipe {
+                border: 0.1em solid $color-3;
+                padding: 2px 8px 2px 8px;
+                border-radius: 0.5em;
+            }
+        }
+
+        #select-website-language {
+            padding: 5px;
+        }
+    }
+
+    .main-container {
+        display: flex;
+        flex-direction: row;
+
+        #recipes-list {
+            hr {
+                margin-top: 10px;
+                margin-bottom: 10px;
+                height: 1px;
+                color: $color-3;
+                background: $color-3;
+                font-size: 0;
+                border: 0;
+            }
+
+            .recipe-item {
+                white-space: preserve nowrap;
+                padding: 4px;
+                // Transparent border: to keep same size than '.recipe-item-current'.
+                border: 0.1em solid rgba(0, 0, 0, 0);
+
+                &.current {
+                    white-space: preserve nowrap;
+                    padding: 4px;
+                    border: 0.1em solid $color-3;
+
+                    border-radius: 0.5em;
+                    color: $text-highlight;
+                    background-color: $color-2;
+                }
+            }
+        }
+
+        .content {
+            flex-grow: 1;
+
+            margin-left: 0px;
+
+            background-color: $color-2;
+            border: 0.1em solid $color-3;
+            border-radius: 1em;
+            padding: 0.8em;
+
+            h1 {
+                text-align: center;
+            }
+        }
+
+        #hidden-templates {
+            display: none;
+        }
+
+        #recipe-edit {
+            display: grid;
+
+            .drag-handle {
+                cursor: move;
+            }
+
+            .group {
+                border: 0.1em solid color.adjust($color-3, $lightness: +30%);
+                margin-top: 0px;
+                margin-bottom: 0px;
+            }
+
+            .step {
+                border: 0.1em solid color.adjust($color-3, $lightness: +30%);
+                margin-top: 0px;
+                margin-bottom: 0px;
+            }
+
+            .ingredient {
+                border: 0.1em solid color.adjust($color-3, $lightness: +30%);
+                margin-top: 0px;
+                margin-bottom: 0px;
+            }
+
+            .dropzone {
+                height: 10px;
+                margin-top: 0px;
+                margin-bottom: 0px;
+
+                background-color: white;
+
+                &.active {
+                    background-color: blue;
+                }
+
+                &.hover {
+                    background-color: red;
+                }
+            }
+        }
+
+        form {
+            display: grid;
+            grid-template-columns: auto 1fr;
+            gap: 3px;
+
+            input,
+            button {
+                // background-color: rgb(52, 40, 85);
+                border-width: 1px;
+                // border-color: white;
+                // color: white;
+            }
+        }
+
+        #sign-up form {
+            display: grid;
+            grid-template-columns: auto 1fr auto;
+
+            input[type="submit"] {
+                grid-column: 2
+            }
+        }
+
+        #sign-in form {
+            input[type="submit"] {
+                grid-column: 2
+            }
+        }
+
+        #ask-reset-password form {
+            grid-template-columns: auto 1fr auto;
+
+            input[type="submit"] {
+                grid-column: 2
+            }
+        }
+
+        #user-edit form {
+            grid-template-columns: auto 1fr auto;
+
+            input[type="submit"] {
+                grid-column: 2
+            }
+        }
+
+        // #sign-in {
+
+        // }
+
+        // #user-edit {
+        //     .label-name {
+        //         grid-column: 1;
+        //         grid-row: 1;
+        //     }
+
+        //     .input-name {
+        //         grid-column: 2;
+        //         grid-row: 1;
+        //     }
+
+        //     .label-password-1 {
+        //         grid-column: 1;
+        //         grid-row: 2;
+        //     }
+
+        //     .input-password-1 {
+        //         grid-column: 2;
+        //         grid-row: 2;
+        //     }
+
+        //     .label-password-2 {
+        //         grid-column: 1;
+        //         grid-row: 3;
+        //     }
+
+        //     .input-password-2 {
+        //         grid-column: 2;
+        //         grid-row: 3;
+        //     }
+
+        //     .button-save {
+        //         grid-column: 2;
+        //         grid-row: 4;
+        //         width: fit-content;
+        //         justify-self: flex-end;
+        //     }
+        // }
+    }
+}
+
+// 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)
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/scss/style.scss b/backend/scss/style.scss
deleted file mode 100644 (file)
index 4855638..0000000
+++ /dev/null
@@ -1,274 +0,0 @@
-@use 'sass:color';
-
-@use 'toast.scss';
-@use 'modal-dialog.scss';
-@use 'calendar.scss';
-
-$color-1: #B29B89;
-$color-2: #89B29B;
-$color-3: #9B89B2;
-
-$text-color: color.adjust($color-1, $lightness: -30%);
-$text-highlight: color.adjust($color-1, $lightness: +30%);
-$link-color: color.adjust($color-3, $lightness: -25%);
-$link-hover-color: color.adjust($color-3, $lightness: +20%);
-
-* {
-    margin: 5px;
-    padding: 0px;
-}
-
-a {
-    color: $link-color;
-    text-decoration: none;
-
-    &:hover {
-        color: $link-hover-color;
-    }
-}
-
-body {
-    display: flex;
-    flex-direction: column;
-
-    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
-    color: $text-color;
-    background-color: $color-1;
-    margin: 0px;
-
-    .user-message {
-        font-weight: bold;
-    }
-
-    .footer-container {
-        align-self: center;
-        font-size: 0.7em;
-    }
-
-    .drag-handle {
-        width: 20px;
-        height: 20px;
-        display: inline-block;
-        vertical-align: bottom;
-        background-color: blue;
-    }
-
-    img {
-        border: 0px;
-    }
-
-    .header-container {
-        display: flex;
-        flex-direction: row;
-        justify-content: space-between;
-
-        .title {
-            font-size: 180%;
-            font-style: italic;
-
-            .logo {
-                width: 50px;
-                height: 50px;
-                vertical-align: bottom;
-                margin: 0px 10px 0px 0px;
-            }
-        }
-
-        .header-menu {
-            align-self: flex-end;
-
-            .create-recipe {
-                border: 0.1em solid $color-3;
-                padding: 2px 8px 2px 8px;
-                border-radius: 0.5em;
-            }
-        }
-
-        #select-website-language {
-            padding: 5px;
-        }
-    }
-
-    .main-container {
-        display: flex;
-        flex-direction: row;
-
-        #recipes-list {
-            hr {
-                margin-top: 10px;
-                margin-bottom: 10px;
-                height: 1px;
-                color: $color-3;
-                background: $color-3;
-                font-size: 0;
-                border: 0;
-            }
-
-            .recipe-item {
-                white-space: preserve nowrap;
-                padding: 4px;
-                // Transparent border: to keep same size than '.recipe-item-current'.
-                border: 0.1em solid rgba(0, 0, 0, 0);
-
-                &.current {
-                    white-space: preserve nowrap;
-                    padding: 4px;
-                    border: 0.1em solid $color-3;
-
-                    border-radius: 0.5em;
-                    color: $text-highlight;
-                    background-color: $color-2;
-                }
-            }
-        }
-
-        .content {
-            flex-grow: 1;
-
-            margin-left: 0px;
-
-            background-color: $color-2;
-            border: 0.1em solid $color-3;
-            border-radius: 1em;
-            padding: 0.8em;
-
-            h1 {
-                text-align: center;
-            }
-        }
-
-        #hidden-templates {
-            display: none;
-        }
-
-        #recipe-edit {
-            display: grid;
-
-            .drag-handle {
-                cursor: move;
-            }
-
-            .group {
-                border: 0.1em solid color.adjust($color-3, $lightness: +30%);
-                margin-top: 0px;
-                margin-bottom: 0px;
-            }
-
-            .step {
-                border: 0.1em solid color.adjust($color-3, $lightness: +30%);
-                margin-top: 0px;
-                margin-bottom: 0px;
-            }
-
-            .ingredient {
-                border: 0.1em solid color.adjust($color-3, $lightness: +30%);
-                margin-top: 0px;
-                margin-bottom: 0px;
-            }
-
-            .dropzone {
-                height: 10px;
-                margin-top: 0px;
-                margin-bottom: 0px;
-
-                background-color: white;
-
-                &.active {
-                    background-color: blue;
-                }
-
-                &.hover {
-                    background-color: red;
-                }
-            }
-        }
-
-        form {
-            display: grid;
-            grid-template-columns: auto 1fr;
-            gap: 3px;
-
-            input,
-            button {
-                // background-color: rgb(52, 40, 85);
-                border-width: 1px;
-                // border-color: white;
-                // color: white;
-            }
-        }
-
-        #sign-up form {
-            display: grid;
-            grid-template-columns: auto 1fr auto;
-
-            input[type="submit"] {
-                grid-column: 2
-            }
-        }
-
-        #sign-in form {
-            input[type="submit"] {
-                grid-column: 2
-            }
-        }
-
-        #ask-reset-password form {
-            grid-template-columns: auto 1fr auto;
-
-            input[type="submit"] {
-                grid-column: 2
-            }
-        }
-
-        #user-edit form {
-            grid-template-columns: auto 1fr auto;
-
-            input[type="submit"] {
-                grid-column: 2
-            }
-        }
-
-        // #sign-in {
-
-        // }
-
-        // #user-edit {
-        //     .label-name {
-        //         grid-column: 1;
-        //         grid-row: 1;
-        //     }
-
-        //     .input-name {
-        //         grid-column: 2;
-        //         grid-row: 1;
-        //     }
-
-        //     .label-password-1 {
-        //         grid-column: 1;
-        //         grid-row: 2;
-        //     }
-
-        //     .input-password-1 {
-        //         grid-column: 2;
-        //         grid-row: 2;
-        //     }
-
-        //     .label-password-2 {
-        //         grid-column: 1;
-        //         grid-row: 3;
-        //     }
-
-        //     .input-password-2 {
-        //         grid-column: 2;
-        //         grid-row: 3;
-        //     }
-
-        //     .button-save {
-        //         grid-column: 2;
-        //         grid-row: 4;
-        //         width: fit-content;
-        //         justify-self: flex-end;
-        //     }
-        // }
-    }
-}
\ No newline at end of file
diff --git a/backend/scss/style_dark.scss b/backend/scss/style_dark.scss
new file mode 100644 (file)
index 0000000..c658e94
--- /dev/null
@@ -0,0 +1 @@
+@use 'main.scss' with ($dark-theme: true);
\ No newline at end of file
diff --git a/backend/scss/style_light.scss b/backend/scss/style_light.scss
new file mode 100644 (file)
index 0000000..44fdfde
--- /dev/null
@@ -0,0 +1 @@
+@use 'main.scss' with ($dark-theme: false);
\ No newline at end of file
index 51d3a7d..cca6afc 100644 (file)
@@ -1,6 +1,7 @@
 use askama::Template;
 
 use crate::{
+    Context,
     data::model,
     translation::{self, Sentence, Tr},
 };
@@ -20,8 +21,7 @@ impl Recipes {
 #[derive(Template)]
 #[template(path = "home.html")]
 pub struct HomeTemplate {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub recipes: Recipes,
 }
@@ -29,8 +29,7 @@ pub struct HomeTemplate {
 #[derive(Template)]
 #[template(path = "message.html")]
 pub struct MessageTemplate<'a> {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub message: &'a str,
     pub as_code: bool, // Display the message in <pre> markup.
@@ -39,8 +38,11 @@ pub struct MessageTemplate<'a> {
 impl<'a> MessageTemplate<'a> {
     pub fn new(message: &'a str, tr: Tr) -> MessageTemplate<'a> {
         MessageTemplate {
-            user: None,
-            tr,
+            context: Context {
+                user: None,
+                tr,
+                dark_theme: false,
+            },
             message,
             as_code: false,
         }
@@ -52,8 +54,11 @@ impl<'a> MessageTemplate<'a> {
         user: Option<model::User>,
     ) -> MessageTemplate<'a> {
         MessageTemplate {
-            user,
-            tr,
+            context: Context {
+                user,
+                tr,
+                dark_theme: false,
+            },
             message,
             as_code: false,
         }
@@ -63,8 +68,7 @@ impl<'a> MessageTemplate<'a> {
 #[derive(Template)]
 #[template(path = "sign_up_form.html")]
 pub struct SignUpFormTemplate<'a> {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub email: String,
     pub message: &'a str,
@@ -75,8 +79,7 @@ pub struct SignUpFormTemplate<'a> {
 #[derive(Template)]
 #[template(path = "sign_in_form.html")]
 pub struct SignInFormTemplate<'a> {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub email: &'a str,
     pub message: &'a str,
@@ -85,8 +88,7 @@ pub struct SignInFormTemplate<'a> {
 #[derive(Template)]
 #[template(path = "ask_reset_password.html")]
 pub struct AskResetPasswordTemplate<'a> {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub email: &'a str,
     pub message: &'a str,
@@ -96,8 +98,7 @@ pub struct AskResetPasswordTemplate<'a> {
 #[derive(Template)]
 #[template(path = "reset_password.html")]
 pub struct ResetPasswordTemplate<'a> {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub reset_token: &'a str,
     pub message: &'a str,
@@ -107,8 +108,7 @@ pub struct ResetPasswordTemplate<'a> {
 #[derive(Template)]
 #[template(path = "profile.html")]
 pub struct ProfileTemplate<'a> {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub username: &'a str,
     pub email: &'a str,
@@ -121,8 +121,7 @@ pub struct ProfileTemplate<'a> {
 #[derive(Template)]
 #[template(path = "recipe_view.html")]
 pub struct RecipeViewTemplate {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub recipes: Recipes,
 
@@ -132,8 +131,7 @@ pub struct RecipeViewTemplate {
 #[derive(Template)]
 #[template(path = "recipe_edit.html")]
 pub struct RecipeEditTemplate {
-    pub user: Option<model::User>,
-    pub tr: Tr,
+    pub context: Context,
 
     pub recipes: Recipes,
 
@@ -143,7 +141,7 @@ pub struct RecipeEditTemplate {
 #[derive(Template)]
 #[template(path = "recipes_list_fragment.html")]
 pub struct RecipesListFragmentTemplate {
-    pub tr: Tr,
+    pub context: Context,
 
     pub recipes: Recipes,
 }
index 4588630..81d2ed3 100644 (file)
@@ -85,6 +85,13 @@ const TRACING_LEVEL: tracing::Level = tracing::Level::DEBUG;
 #[cfg(not(debug_assertions))]
 const TRACING_LEVEL: tracing::Level = tracing::Level::INFO;
 
+#[derive(Debug, Clone)]
+pub struct Context {
+    pub user: Option<model::User>,
+    pub tr: Tr,
+    pub dark_theme: bool,
+}
+
 // TODO: Should main returns 'Result'?
 #[tokio::main]
 async fn main() {
@@ -294,11 +301,7 @@ async fn main() {
         .merge(html_routes)
         .nest("/ron-api", ron_api_routes)
         .fallback(services::not_found)
-        .layer(middleware::from_fn(translation))
-        .layer(middleware::from_fn_with_state(
-            state.clone(),
-            user_authentication,
-        ))
+        .layer(middleware::from_fn_with_state(state.clone(), context))
         .with_state(state)
         .nest_service("/favicon.ico", ServeFile::new("static/favicon.ico"))
         .nest_service("/static", ServeDir::new("static"))
@@ -321,19 +324,6 @@ async fn main() {
     event!(Level::INFO, "Recipes stopped");
 }
 
-async fn user_authentication(
-    ConnectInfo(addr): ConnectInfo<SocketAddr>,
-    State(connection): State<db::Connection>,
-    mut req: Request,
-    next: Next,
-) -> Result<Response> {
-    let jar = CookieJar::from_headers(req.headers());
-    let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(req.headers(), addr);
-    let user = get_current_user(connection, &jar, &client_ip, &client_user_agent).await;
-    req.extensions_mut().insert(user);
-    Ok(next.run(req).await)
-}
-
 #[derive(Debug, Clone)]
 struct Lang(Option<String>);
 
@@ -384,16 +374,21 @@ fn url_rewriting(mut req: Request) -> Request {
 /// - Get from the cookie.
 /// - Get from the HTTP header `accept-language`.
 /// - Set as `translation::DEFAULT_LANGUAGE_CODE`.
-async fn translation(
-    Extension(lang): Extension<Lang>,
-    Extension(user): Extension<Option<model::User>>,
+async fn context(
+    ConnectInfo(addr): ConnectInfo<SocketAddr>,
+    State(connection): State<db::Connection>,
+    Extension(lang_from_url): Extension<Lang>,
     mut req: Request,
     next: Next,
 ) -> Result<Response> {
-    let language = if let Some(lang) = lang.0 {
+    let jar = CookieJar::from_headers(req.headers());
+    let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(req.headers(), addr);
+    let user = get_current_user(connection, &jar, &client_ip, &client_user_agent).await;
+
+    let language = if let Some(lang) = lang_from_url.0 {
         lang
-    } else if let Some(user) = user {
-        user.lang
+    } else if let Some(ref user) = user {
+        user.lang.clone()
     } else {
         let available_codes = translation::available_codes();
         let jar = CookieJar::from_headers(req.headers());
@@ -420,7 +415,17 @@ async fn translation(
 
     let tr = Tr::new(&language);
 
-    req.extensions_mut().insert(tr);
+    let dark_theme = match jar.get(common::consts::COOKIE_DARK_THEME) {
+        Some(dark_theme_cookie) => dark_theme_cookie.value().parse().unwrap_or_default(),
+        None => false,
+    };
+
+    req.extensions_mut().insert(Context {
+        user,
+        tr,
+        dark_theme,
+    });
+
     Ok(next.run(req).await)
 }
 
index 874c5fb..4fa12af 100644 (file)
@@ -7,12 +7,7 @@ use axum::{
 use serde::Deserialize;
 // use tracing::{event, Level};
 
-use crate::{
-    Result,
-    data::{db, model},
-    html_templates::*,
-    translation,
-};
+use crate::{Context, Result, data::db, html_templates::*};
 
 #[derive(Deserialize)]
 pub struct CurrentRecipeId {
@@ -23,14 +18,16 @@ pub struct CurrentRecipeId {
 pub async fn recipes_list_fragments(
     State(connection): State<db::Connection>,
     current_recipe: Query<CurrentRecipeId>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
 ) -> Result<impl IntoResponse> {
     let recipes = Recipes {
         published: connection
-            .get_all_published_recipe_titles(tr.current_lang_code(), user.as_ref().map(|u| u.id))
+            .get_all_published_recipe_titles(
+                context.tr.current_lang_code(),
+                context.user.as_ref().map(|u| u.id),
+            )
             .await?,
-        unpublished: if let Some(user) = user.as_ref() {
+        unpublished: if let Some(user) = context.user.as_ref() {
             connection
                 .get_all_unpublished_recipe_titles(user.id)
                 .await?
@@ -39,5 +36,7 @@ pub async fn recipes_list_fragments(
         },
         current_id: current_recipe.current_recipe_id,
     };
-    Ok(Html(RecipesListFragmentTemplate { tr, recipes }.render()?))
+    Ok(Html(
+        RecipesListFragmentTemplate { context, recipes }.render()?,
+    ))
 }
index d9b7c86..67091b3 100644 (file)
@@ -7,12 +7,7 @@ use axum::{
     response::{Html, IntoResponse, Response},
 };
 
-use crate::{
-    Result,
-    data::{db, model},
-    html_templates::*,
-    ron_utils, translation,
-};
+use crate::{Context, Result, data::db, html_templates::*, ron_utils};
 
 pub mod fragments;
 pub mod recipe;
@@ -21,7 +16,7 @@ pub mod user;
 
 // Will embed RON error in HTML page.
 pub async fn ron_error_to_html(
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     req: Request,
     next: Next,
 ) -> Result<Response> {
@@ -35,10 +30,9 @@ pub async fn ron_error_to_html(
             };
             return Ok(Html(
                 MessageTemplate {
-                    user: None,
+                    context,
                     message: &message,
                     as_code: true,
-                    tr,
                 }
                 .render()?,
             )
@@ -54,14 +48,16 @@ pub async fn ron_error_to_html(
 #[debug_handler]
 pub async fn home_page(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
 ) -> Result<impl IntoResponse> {
     let recipes = Recipes {
         published: connection
-            .get_all_published_recipe_titles(tr.current_lang_code(), user.as_ref().map(|u| u.id))
+            .get_all_published_recipe_titles(
+                context.tr.current_lang_code(),
+                context.user.as_ref().map(|u| u.id),
+            )
             .await?,
-        unpublished: if let Some(user) = user.as_ref() {
+        unpublished: if let Some(user) = context.user.as_ref() {
             connection
                 .get_all_unpublished_recipe_titles(user.id)
                 .await?
@@ -71,18 +67,15 @@ pub async fn home_page(
         current_id: None,
     };
 
-    Ok(Html(HomeTemplate { user, recipes, tr }.render()?))
+    Ok(Html(HomeTemplate { context, recipes }.render()?))
 }
 
 ///// 404 /////
 
 #[debug_handler]
-pub async fn not_found(
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
-) -> Result<impl IntoResponse> {
+pub async fn not_found(Extension(context): Extension<Context>) -> Result<impl IntoResponse> {
     Ok((
         StatusCode::NOT_FOUND,
-        Html(MessageTemplate::new_with_user("404: Not found", tr, user).render()?),
+        Html(MessageTemplate::new_with_user("404: Not found", context.tr, context.user).render()?),
     ))
 }
index e812f0b..b9b5f4d 100644 (file)
@@ -7,44 +7,48 @@ use axum::{
 // use tracing::{event, Level};
 
 use crate::{
-    Result,
+    Context, Result,
     data::{db, model},
     html_templates::*,
-    translation::{self, Sentence},
+    translation::Sentence,
 };
 
 #[debug_handler]
 pub async fn create(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
 ) -> Result<Response> {
-    if let Some(user) = user {
+    if let Some(user) = context.user {
         let recipe_id = connection.create_recipe(user.id).await?;
         Ok(Redirect::to(&format!(
             "/{}/recipe/edit/{}",
-            tr.current_lang_code(),
+            context.tr.current_lang_code(),
             recipe_id
         ))
         .into_response())
     } else {
-        Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
+        Ok(
+            Html(MessageTemplate::new(context.tr.t(Sentence::NotLoggedIn), context.tr).render()?)
+                .into_response(),
+        )
     }
 }
 
 #[debug_handler]
 pub async fn edit(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     Path(recipe_id): Path<i64>,
 ) -> Result<Response> {
-    if let Some(user) = user {
+    if let Some(ref user) = context.user {
         if let Some(recipe) = connection.get_recipe(recipe_id, false).await? {
             if model::can_user_edit_recipe(&user, &recipe) {
                 let recipes = Recipes {
                     published: connection
-                        .get_all_published_recipe_titles(tr.current_lang_code(), Some(user.id))
+                        .get_all_published_recipe_titles(
+                            context.tr.current_lang_code(),
+                            Some(user.id),
+                        )
                         .await?,
                     unpublished: connection
                         .get_all_unpublished_recipe_titles(user.id)
@@ -54,8 +58,7 @@ pub async fn edit(
 
                 Ok(Html(
                     RecipeEditTemplate {
-                        user: Some(user),
-                        tr,
+                        context,
                         recipes,
                         recipe,
                     }
@@ -63,42 +66,48 @@ pub async fn edit(
                 )
                 .into_response())
             } else {
-                Ok(
-                    Html(
-                        MessageTemplate::new(tr.t(Sentence::RecipeNotAllowedToEdit), tr)
-                            .render()?,
+                Ok(Html(
+                    MessageTemplate::new(
+                        context.tr.t(Sentence::RecipeNotAllowedToEdit),
+                        context.tr,
                     )
-                    .into_response(),
+                    .render()?,
                 )
+                .into_response())
             }
         } else {
-            Ok(
-                Html(MessageTemplate::new(tr.t(Sentence::RecipeNotFound), tr).render()?)
-                    .into_response(),
+            Ok(Html(
+                MessageTemplate::new(context.tr.t(Sentence::RecipeNotFound), context.tr)
+                    .render()?,
             )
+            .into_response())
         }
     } else {
-        Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
+        Ok(
+            Html(MessageTemplate::new(context.tr.t(Sentence::NotLoggedIn), context.tr).render()?)
+                .into_response(),
+        )
     }
 }
 
 #[debug_handler]
 pub async fn view(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     Path(recipe_id): Path<i64>,
 ) -> Result<Response> {
     match connection.get_recipe(recipe_id, true).await? {
         Some(recipe) => {
             if !recipe.is_published
-                && (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
+                && (context.user.is_none() || recipe.user_id != context.user.as_ref().unwrap().id)
             {
                 return Ok(Html(
                     MessageTemplate::new_with_user(
-                        &tr.tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
-                        tr,
-                        user,
+                        &context
+                            .tr
+                            .tp(Sentence::RecipeNotAllowedToView, &[Box::new(recipe_id)]),
+                        context.tr,
+                        context.user,
                     )
                     .render()?,
                 )
@@ -108,11 +117,11 @@ pub async fn view(
             let recipes = Recipes {
                 published: connection
                     .get_all_published_recipe_titles(
-                        tr.current_lang_code(),
-                        user.as_ref().map(|u| u.id),
+                        context.tr.current_lang_code(),
+                        context.user.as_ref().map(|u| u.id),
                     )
                     .await?,
-                unpublished: if let Some(user) = user.as_ref() {
+                unpublished: if let Some(user) = context.user.as_ref() {
                     connection
                         .get_all_unpublished_recipe_titles(user.id)
                         .await?
@@ -124,8 +133,7 @@ pub async fn view(
 
             Ok(Html(
                 RecipeViewTemplate {
-                    user,
-                    tr,
+                    context,
                     recipes,
                     recipe,
                 }
@@ -134,7 +142,12 @@ pub async fn view(
             .into_response())
         }
         None => Ok(Html(
-            MessageTemplate::new_with_user(tr.t(Sentence::RecipeNotFound), tr, user).render()?,
+            MessageTemplate::new_with_user(
+                context.tr.t(Sentence::RecipeNotFound),
+                context.tr,
+                context.user,
+            )
+            .render()?,
         )
         .into_response()),
     }
index 2f3a6a2..8df7ccd 100644 (file)
@@ -8,8 +8,8 @@ use axum_extra::extract::Query;
 // use tracing::{event, Level};
 
 use crate::{
+    Context,
     data::{self, db},
-    model,
     ron_extractor::ExtractRon,
     ron_utils::{ron_error, ron_response_ok},
 };
@@ -19,10 +19,10 @@ use super::rights::*;
 #[debug_handler]
 pub async fn get_scheduled_recipes(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     date_range: Query<common::ron_api::DateRange>,
 ) -> Result<impl IntoResponse> {
-    if let Some(user) = user {
+    if let Some(user) = context.user {
         Ok(ron_response_ok(common::ron_api::ScheduledRecipes {
             recipes: connection
                 .get_scheduled_recipes(user.id, date_range.start_date, date_range.end_date)
@@ -50,11 +50,11 @@ impl From<data::db::recipe::AddScheduledRecipeResult> for common::ron_api::Sched
 #[debug_handler]
 pub async fn schedule_recipe(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<common::ron_api::ScheduleRecipe>,
 ) -> Result<Response> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    if let Some(user) = user {
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
+    if let Some(user) = context.user {
         connection
             .add_scheduled_recipe(
                 user.id,
@@ -76,11 +76,11 @@ pub async fn schedule_recipe(
 #[debug_handler]
 pub async fn rm_scheduled_recipe(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<common::ron_api::RemoveScheduledRecipe>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
-    if let Some(user) = user {
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
+    if let Some(user) = context.user {
         connection
             .rm_scheduled_recipe(
                 user.id,
index fd606c3..87ac32c 100644 (file)
@@ -7,7 +7,7 @@ use axum::{
 use axum_extra::extract::cookie::{Cookie, CookieJar};
 // use tracing::{event, Level};
 
-use crate::{consts, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error};
+use crate::{Context, consts, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_error};
 
 pub mod calendar;
 pub mod recipe;
@@ -19,12 +19,12 @@ const NOT_AUTHORIZED_MESSAGE: &str = "Action not authorized";
 #[debug_handler]
 pub async fn set_lang(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     headers: HeaderMap,
     ExtractRon(ron): ExtractRon<common::ron_api::SetLang>,
 ) -> Result<(CookieJar, StatusCode)> {
     let mut jar = CookieJar::from_headers(&headers);
-    if let Some(user) = user {
+    if let Some(user) = context.user {
         connection.set_user_lang(user.id, &ron.lang).await?;
     }
 
index 9cd7db4..3890e69 100644 (file)
@@ -8,7 +8,7 @@ use axum_extra::extract::Query;
 use common::ron_api;
 // use tracing::{event, Level};
 
-use crate::{data::db, model, ron_extractor::ExtractRon, ron_utils::ron_response_ok};
+use crate::{Context, data::db, model, ron_extractor::ExtractRon, ron_utils::ron_response_ok};
 
 use super::rights::*;
 
@@ -27,10 +27,10 @@ pub async fn get_titles(
 #[debug_handler]
 pub async fn set_title(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetRecipeTitle>,
 ) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection
         .set_recipe_title(ron.recipe_id, &ron.title)
         .await?;
@@ -40,10 +40,10 @@ pub async fn set_title(
 #[debug_handler]
 pub async fn set_description(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetRecipeDescription>,
 ) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection
         .set_recipe_description(ron.recipe_id, &ron.description)
         .await?;
@@ -53,10 +53,10 @@ pub async fn set_description(
 #[debug_handler]
 pub async fn set_servings(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetRecipeServings>,
 ) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection
         .set_recipe_servings(ron.recipe_id, ron.servings)
         .await?;
@@ -66,10 +66,10 @@ pub async fn set_servings(
 #[debug_handler]
 pub async fn set_estimated_time(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetRecipeEstimatedTime>,
 ) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection
         .set_recipe_estimated_time(ron.recipe_id, ron.estimated_time)
         .await?;
@@ -90,10 +90,10 @@ pub async fn get_tags(
 #[debug_handler]
 pub async fn add_tags(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Tags>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection
         .add_recipe_tags(
             ron.recipe_id,
@@ -109,10 +109,10 @@ pub async fn add_tags(
 #[debug_handler]
 pub async fn rm_tags(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Tags>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection.rm_recipe_tags(ron.recipe_id, &ron.tags).await?;
     Ok(StatusCode::OK)
 }
@@ -120,10 +120,10 @@ pub async fn rm_tags(
 #[debug_handler]
 pub async fn set_difficulty(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetRecipeDifficulty>,
 ) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection
         .set_recipe_difficulty(ron.recipe_id, ron.difficulty)
         .await?;
@@ -133,7 +133,7 @@ pub async fn set_difficulty(
 #[debug_handler]
 pub async fn set_language(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetRecipeLanguage>,
 ) -> Result<StatusCode> {
     if !crate::translation::available_codes()
@@ -144,7 +144,7 @@ pub async fn set_language(
         return Ok(StatusCode::BAD_REQUEST);
     }
 
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection
         .set_recipe_language(ron.recipe_id, &ron.lang)
         .await?;
@@ -154,10 +154,10 @@ pub async fn set_language(
 #[debug_handler]
 pub async fn set_is_published(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetIsPublished>,
 ) -> Result<StatusCode> {
-    check_user_rights_recipe(&connection, &user, ron.recipe_id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.recipe_id).await?;
     connection
         .set_recipe_is_published(ron.recipe_id, ron.is_published)
         .await?;
@@ -167,10 +167,10 @@ pub async fn set_is_published(
 #[debug_handler]
 pub async fn rm(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Id>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.id).await?;
     connection.rm_recipe(ron.id).await?;
     Ok(StatusCode::OK)
 }
@@ -231,10 +231,10 @@ pub async fn get_groups(
 #[debug_handler]
 pub async fn add_group(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Id>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe(&connection, &user, ron.id).await?;
+    check_user_rights_recipe(&connection, &context.user, ron.id).await?;
     let id = connection.add_recipe_group(ron.id).await?;
 
     Ok(ron_response_ok(ron_api::Id { id }))
@@ -243,10 +243,10 @@ pub async fn add_group(
 #[debug_handler]
 pub async fn rm_group(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Id>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_group(&connection, &user, ron.id).await?;
+    check_user_rights_recipe_group(&connection, &context.user, ron.id).await?;
     connection.rm_recipe_group(ron.id).await?;
     Ok(StatusCode::OK)
 }
@@ -254,10 +254,10 @@ pub async fn rm_group(
 #[debug_handler]
 pub async fn set_group_name(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetGroupName>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_group(&connection, &user, ron.group_id).await?;
+    check_user_rights_recipe_group(&connection, &context.user, ron.group_id).await?;
     connection.set_group_name(ron.group_id, &ron.name).await?;
     Ok(StatusCode::OK)
 }
@@ -265,10 +265,10 @@ pub async fn set_group_name(
 #[debug_handler]
 pub async fn set_group_comment(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetGroupComment>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_group(&connection, &user, ron.group_id).await?;
+    check_user_rights_recipe_group(&connection, &context.user, ron.group_id).await?;
     connection
         .set_group_comment(ron.group_id, &ron.comment)
         .await?;
@@ -278,10 +278,10 @@ pub async fn set_group_comment(
 #[debug_handler]
 pub async fn set_groups_order(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Ids>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_groups(&connection, &user, &ron.ids).await?;
+    check_user_rights_recipe_groups(&connection, &context.user, &ron.ids).await?;
     connection.set_groups_order(&ron.ids).await?;
     Ok(StatusCode::OK)
 }
@@ -289,10 +289,10 @@ pub async fn set_groups_order(
 #[debug_handler]
 pub async fn add_step(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Id>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_group(&connection, &user, ron.id).await?;
+    check_user_rights_recipe_group(&connection, &context.user, ron.id).await?;
     let id = connection.add_recipe_step(ron.id).await?;
 
     Ok(ron_response_ok(ron_api::Id { id }))
@@ -301,10 +301,10 @@ pub async fn add_step(
 #[debug_handler]
 pub async fn rm_step(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Id>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_step(&connection, &user, ron.id).await?;
+    check_user_rights_recipe_step(&connection, &context.user, ron.id).await?;
     connection.rm_recipe_step(ron.id).await?;
     Ok(StatusCode::OK)
 }
@@ -312,10 +312,10 @@ pub async fn rm_step(
 #[debug_handler]
 pub async fn set_step_action(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetStepAction>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_step(&connection, &user, ron.step_id).await?;
+    check_user_rights_recipe_step(&connection, &context.user, ron.step_id).await?;
     connection.set_step_action(ron.step_id, &ron.action).await?;
     Ok(StatusCode::OK)
 }
@@ -323,10 +323,10 @@ pub async fn set_step_action(
 #[debug_handler]
 pub async fn set_steps_order(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Ids>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_steps(&connection, &user, &ron.ids).await?;
+    check_user_rights_recipe_steps(&connection, &context.user, &ron.ids).await?;
     connection.set_steps_order(&ron.ids).await?;
     Ok(StatusCode::OK)
 }
@@ -334,10 +334,10 @@ pub async fn set_steps_order(
 #[debug_handler]
 pub async fn add_ingredient(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Id>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_step(&connection, &user, ron.id).await?;
+    check_user_rights_recipe_step(&connection, &context.user, ron.id).await?;
     let id = connection.add_recipe_ingredient(ron.id).await?;
     Ok(ron_response_ok(ron_api::Id { id }))
 }
@@ -345,10 +345,10 @@ pub async fn add_ingredient(
 #[debug_handler]
 pub async fn rm_ingredient(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Id>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_ingredient(&connection, &user, ron.id).await?;
+    check_user_rights_recipe_ingredient(&connection, &context.user, ron.id).await?;
     connection.rm_recipe_ingredient(ron.id).await?;
     Ok(StatusCode::OK)
 }
@@ -356,10 +356,10 @@ pub async fn rm_ingredient(
 #[debug_handler]
 pub async fn set_ingredient_name(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetIngredientName>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
+    check_user_rights_recipe_ingredient(&connection, &context.user, ron.ingredient_id).await?;
     connection
         .set_ingredient_name(ron.ingredient_id, &ron.name)
         .await?;
@@ -369,10 +369,10 @@ pub async fn set_ingredient_name(
 #[debug_handler]
 pub async fn set_ingredient_comment(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetIngredientComment>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
+    check_user_rights_recipe_ingredient(&connection, &context.user, ron.ingredient_id).await?;
     connection
         .set_ingredient_comment(ron.ingredient_id, &ron.comment)
         .await?;
@@ -382,10 +382,10 @@ pub async fn set_ingredient_comment(
 #[debug_handler]
 pub async fn set_ingredient_quantity(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetIngredientQuantity>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
+    check_user_rights_recipe_ingredient(&connection, &context.user, ron.ingredient_id).await?;
     connection
         .set_ingredient_quantity(ron.ingredient_id, ron.quantity)
         .await?;
@@ -395,10 +395,10 @@ pub async fn set_ingredient_quantity(
 #[debug_handler]
 pub async fn set_ingredient_unit(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::SetIngredientUnit>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_ingredient(&connection, &user, ron.ingredient_id).await?;
+    check_user_rights_recipe_ingredient(&connection, &context.user, ron.ingredient_id).await?;
     connection
         .set_ingredient_unit(ron.ingredient_id, &ron.unit)
         .await?;
@@ -408,10 +408,10 @@ pub async fn set_ingredient_unit(
 #[debug_handler]
 pub async fn set_ingredients_order(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Ids>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_recipe_ingredients(&connection, &user, &ron.ids).await?;
+    check_user_rights_recipe_ingredients(&connection, &context.user, &ron.ids).await?;
     connection.set_ingredients_order(&ron.ids).await?;
     Ok(StatusCode::OK)
 }
index 4a785ee..2438758 100644 (file)
@@ -7,6 +7,7 @@ use axum::{
 use common::ron_api;
 
 use crate::{
+    Context,
     data::db,
     model,
     ron_extractor::ExtractRon,
@@ -33,9 +34,9 @@ impl From<model::ShoppingListItem> for common::ron_api::ShoppingListItem {
 #[debug_handler]
 pub async fn get(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
 ) -> Result<impl IntoResponse> {
-    if let Some(user) = user {
+    if let Some(user) = context.user {
         Ok(ron_response_ok(
             connection
                 .get_shopping_list(user.id)
@@ -55,10 +56,10 @@ pub async fn get(
 #[debug_handler]
 pub async fn set_entry_checked(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
+    Extension(context): Extension<Context>,
     ExtractRon(ron): ExtractRon<ron_api::Value<bool>>,
 ) -> Result<impl IntoResponse> {
-    check_user_rights_shopping_list_entry(&connection, &user, ron.id).await?;
+    check_user_rights_shopping_list_entry(&connection, &context.user, ron.id).await?;
     Ok(ron_response_ok(
         connection.set_entry_checked(ron.id, ron.value).await?,
     ))
index 86a0dc0..abc094d 100644 (file)
@@ -11,7 +11,7 @@ use axum::{
 };
 use axum_extra::extract::{
     Host, Query,
-    cookie::{Cookie, CookieJar},
+    cookie::{self, Cookie, CookieJar},
 };
 use chrono::Duration;
 use lettre::Address;
@@ -19,14 +19,8 @@ use serde::Deserialize;
 use tracing::{Level, event};
 
 use crate::{
-    AppState, Result,
-    config::Config,
-    consts,
-    data::{db, model},
-    email,
-    html_templates::*,
-    translation::{self, Sentence},
-    utils,
+    AppState, Context, Result, config::Config, consts, data::db, email, html_templates::*,
+    translation::Sentence, utils,
 };
 
 /// SIGN UP ///
@@ -34,14 +28,12 @@ use crate::{
 #[debug_handler]
 pub async fn sign_up_get(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
 ) -> Result<Response> {
     if connection.get_new_user_registration_enabled().await? {
         Ok(Html(
             SignUpFormTemplate {
-                user,
-                tr,
+                context,
                 email: String::new(),
                 message: "",
                 message_email: "",
@@ -51,10 +43,15 @@ pub async fn sign_up_get(
         )
         .into_response())
     } else {
-        Ok(
-            Html(MessageTemplate::new_with_user(tr.t(Sentence::SignUpClosed), tr, user).render()?)
-                .into_response(),
+        Ok(Html(
+            MessageTemplate::new_with_user(
+                context.tr.t(Sentence::SignUpClosed),
+                context.tr,
+                context.user,
+            )
+            .render()?,
         )
+        .into_response())
     }
 }
 
@@ -79,40 +76,37 @@ pub async fn sign_up_post(
     Host(host): Host,
     State(connection): State<db::Connection>,
     State(config): State<Config>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     Form(form_data): Form<SignUpFormData>,
 ) -> Result<Response> {
     fn error_response(
         error: SignUpError,
         form_data: &SignUpFormData,
-        user: Option<model::User>,
-        tr: translation::Tr,
+        context: Context,
     ) -> Result<Response> {
-        let invalid_password_mess = &tr.tp(
+        let invalid_password_mess = &context.tr.tp(
             Sentence::InvalidPassword,
             &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
         );
         Ok(Html(
             SignUpFormTemplate {
-                user,
                 email: form_data.email.clone(),
                 message_email: match error {
-                    SignUpError::InvalidEmail => tr.t(Sentence::InvalidEmail),
+                    SignUpError::InvalidEmail => context.tr.t(Sentence::InvalidEmail),
                     _ => "",
                 },
                 message_password: match error {
-                    SignUpError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
+                    SignUpError::PasswordsNotEqual => context.tr.t(Sentence::PasswordDontMatch),
                     SignUpError::InvalidPassword => invalid_password_mess,
                     _ => "",
                 },
                 message: match error {
-                    SignUpError::UserAlreadyExists => tr.t(Sentence::EmailAlreadyTaken),
-                    SignUpError::DatabaseError => tr.t(Sentence::DatabaseError),
-                    SignUpError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
+                    SignUpError::UserAlreadyExists => context.tr.t(Sentence::EmailAlreadyTaken),
+                    SignUpError::DatabaseError => context.tr.t(Sentence::DatabaseError),
+                    SignUpError::UnableSendEmail => context.tr.t(Sentence::UnableToSendEmail),
                     _ => "",
                 },
-                tr,
+                context,
             }
             .render()?,
         )
@@ -121,24 +115,29 @@ pub async fn sign_up_post(
 
     if !connection.get_new_user_registration_enabled().await? {
         return Ok(Html(
-            MessageTemplate::new_with_user(tr.t(Sentence::SignUpClosed), tr, user).render()?,
+            MessageTemplate::new_with_user(
+                context.tr.t(Sentence::SignUpClosed),
+                context.tr,
+                context.user,
+            )
+            .render()?,
         )
         .into_response());
     }
 
     // Validation of email and password.
     if form_data.email.parse::<Address>().is_err() {
-        return error_response(SignUpError::InvalidEmail, &form_data, user, tr);
+        return error_response(SignUpError::InvalidEmail, &form_data, context);
     }
 
     if form_data.password_1 != form_data.password_2 {
-        return error_response(SignUpError::PasswordsNotEqual, &form_data, user, tr);
+        return error_response(SignUpError::PasswordsNotEqual, &form_data, context);
     }
 
     if let common::utils::PasswordValidation::TooShort =
         common::utils::validate_password(&form_data.password_1)
     {
-        return error_response(SignUpError::InvalidPassword, &form_data, user, tr);
+        return error_response(SignUpError::InvalidPassword, &form_data, context);
     }
 
     match connection
@@ -146,14 +145,14 @@ pub async fn sign_up_post(
         .await
     {
         Ok(db::user::SignUpResult::UserAlreadyExists) => {
-            error_response(SignUpError::UserAlreadyExists, &form_data, user, tr)
+            error_response(SignUpError::UserAlreadyExists, &form_data, context)
         }
         Ok(db::user::SignUpResult::UserCreatedWaitingForValidation(token)) => {
             let url = utils::get_url_from_host(&host);
             let email = form_data.email.clone();
             match email::send_email(
                 &email,
-                &tr.tp(
+                &context.tr.tp(
                     Sentence::SignUpFollowEmailLink,
                     &[Box::new(format!(
                         "{}/validation?validation_token={}",
@@ -167,19 +166,23 @@ pub async fn sign_up_post(
             .await
             {
                 Ok(()) => Ok(Html(
-                    MessageTemplate::new_with_user(tr.t(Sentence::SignUpEmailSent), tr, user)
-                        .render()?,
+                    MessageTemplate::new_with_user(
+                        context.tr.t(Sentence::SignUpEmailSent),
+                        context.tr,
+                        context.user,
+                    )
+                    .render()?,
                 )
                 .into_response()),
                 Err(_) => {
                     // error!("Email validation error: {}", error); // TODO: log
-                    error_response(SignUpError::UnableSendEmail, &form_data, user, tr)
+                    error_response(SignUpError::UnableSendEmail, &form_data, context)
                 }
             }
         }
         Err(_) => {
             // error!("Signup database error: {}", error); // TODO: log
-            error_response(SignUpError::DatabaseError, &form_data, user, tr)
+            error_response(SignUpError::DatabaseError, &form_data, context)
         }
     }
 }
@@ -187,21 +190,20 @@ pub async fn sign_up_post(
 #[debug_handler]
 pub async fn sign_up_validation(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     ConnectInfo(addr): ConnectInfo<SocketAddr>,
     Query(query): Query<HashMap<String, String>>,
     headers: HeaderMap,
 ) -> Result<(CookieJar, impl IntoResponse)> {
     let mut jar = CookieJar::from_headers(&headers);
-    if user.is_some() {
+    if context.user.is_some() {
         return Ok((
             jar,
             Html(
                 MessageTemplate::new_with_user(
-                    tr.t(Sentence::ValidationUserAlreadyExists),
-                    tr,
-                    user,
+                    context.tr.t(Sentence::ValidationUserAlreadyExists),
+                    context.tr,
+                    context.user,
                 )
                 .render()?,
             ),
@@ -221,15 +223,16 @@ pub async fn sign_up_validation(
                 .await?
             {
                 db::user::ValidationResult::Ok(token, user_id) => {
-                    let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
+                    let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token))
+                        .same_site(cookie::SameSite::Strict);
                     jar = jar.add(cookie);
                     let user = connection.load_user(user_id).await?;
                     Ok((
                         jar,
                         Html(
                             MessageTemplate::new_with_user(
-                                tr.t(Sentence::SignUpEmailValidationSuccess),
-                                tr,
+                                context.tr.t(Sentence::SignUpEmailValidationSuccess),
+                                context.tr,
                                 user,
                             )
                             .render()?,
@@ -240,9 +243,9 @@ pub async fn sign_up_validation(
                     jar,
                     Html(
                         MessageTemplate::new_with_user(
-                            tr.t(Sentence::SignUpValidationExpired),
-                            tr,
-                            user,
+                            context.tr.t(Sentence::SignUpValidationExpired),
+                            context.tr,
+                            context.user,
                         )
                         .render()?,
                     ),
@@ -251,9 +254,9 @@ pub async fn sign_up_validation(
                     jar,
                     Html(
                         MessageTemplate::new_with_user(
-                            tr.t(Sentence::SignUpValidationErrorTryAgain),
-                            tr,
-                            user,
+                            context.tr.t(Sentence::SignUpValidationErrorTryAgain),
+                            context.tr,
+                            context.user,
                         )
                         .render()?,
                     ),
@@ -263,8 +266,12 @@ pub async fn sign_up_validation(
         None => Ok((
             jar,
             Html(
-                MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user)
-                    .render()?,
+                MessageTemplate::new_with_user(
+                    context.tr.t(Sentence::ValidationError),
+                    context.tr,
+                    context.user,
+                )
+                .render()?,
             ),
         )),
     }
@@ -273,14 +280,10 @@ pub async fn sign_up_validation(
 /// SIGN IN ///
 
 #[debug_handler]
-pub async fn sign_in_get(
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
-) -> Result<impl IntoResponse> {
+pub async fn sign_in_get(Extension(context): Extension<Context>) -> Result<impl IntoResponse> {
     Ok(Html(
         SignInFormTemplate {
-            user,
-            tr,
+            context,
             email: "",
             message: "",
         }
@@ -298,8 +301,7 @@ pub struct SignInFormData {
 pub async fn sign_in_post(
     ConnectInfo(addr): ConnectInfo<SocketAddr>,
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     headers: HeaderMap,
     Form(form_data): Form<SignInFormData>,
 ) -> Result<(CookieJar, Response)> {
@@ -319,10 +321,9 @@ pub async fn sign_in_post(
             jar,
             Html(
                 SignInFormTemplate {
-                    user,
                     email: &form_data.email,
-                    message: tr.t(Sentence::AccountMustBeValidatedFirst),
-                    tr,
+                    message: context.tr.t(Sentence::AccountMustBeValidatedFirst),
+                    context,
                 }
                 .render()?,
             )
@@ -332,20 +333,20 @@ pub async fn sign_in_post(
             jar,
             Html(
                 SignInFormTemplate {
-                    user,
                     email: &form_data.email,
-                    message: tr.t(Sentence::WrongEmailOrPassword),
-                    tr,
+                    message: context.tr.t(Sentence::WrongEmailOrPassword),
+                    context,
                 }
                 .render()?,
             )
             .into_response(),
         )),
         db::user::SignInResult::Ok(token, _user_id) => {
-            let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
+            let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token))
+                .same_site(cookie::SameSite::Strict);
             Ok((
                 jar.add(cookie),
-                Redirect::to(&format!("/{}/", tr.current_lang_code())).into_response(),
+                Redirect::to(&format!("/{}/", context.tr.current_lang_code())).into_response(),
             ))
         }
     }
@@ -356,7 +357,7 @@ pub async fn sign_in_post(
 #[debug_handler]
 pub async fn sign_out(
     State(connection): State<db::Connection>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     req: Request<Body>,
 ) -> Result<(CookieJar, Redirect)> {
     let mut jar = CookieJar::from_headers(req.headers());
@@ -365,27 +366,30 @@ pub async fn sign_out(
         jar = jar.remove(consts::COOKIE_AUTH_TOKEN_NAME);
         connection.sign_out(&token).await?;
     }
-    Ok((jar, Redirect::to(&format!("/{}/", tr.current_lang_code()))))
+    Ok((
+        jar,
+        Redirect::to(&format!("/{}/", context.tr.current_lang_code())),
+    ))
 }
 
 /// RESET PASSWORD ///
 
 #[debug_handler]
-pub async fn ask_reset_password_get(
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
-) -> Result<Response> {
-    if user.is_some() {
+pub async fn ask_reset_password_get(Extension(context): Extension<Context>) -> Result<Response> {
+    if context.user.is_some() {
         Ok(Html(
-            MessageTemplate::new_with_user(tr.t(Sentence::AskResetAlreadyLoggedInError), tr, user)
-                .render()?,
+            MessageTemplate::new_with_user(
+                context.tr.t(Sentence::AskResetAlreadyLoggedInError),
+                context.tr,
+                context.user,
+            )
+            .render()?,
         )
         .into_response())
     } else {
         Ok(Html(
             AskResetPasswordTemplate {
-                user,
-                tr,
+                context,
                 email: "",
                 message: "",
                 message_email: "",
@@ -414,36 +418,33 @@ pub async fn ask_reset_password_post(
     Host(host): Host,
     State(connection): State<db::Connection>,
     State(config): State<Config>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     Form(form_data): Form<AskResetPasswordForm>,
 ) -> Result<Response> {
     fn error_response(
         error: AskResetPasswordError,
         email: &str,
-        user: Option<model::User>,
-        tr: translation::Tr,
+        context: Context,
     ) -> Result<Response> {
         Ok(Html(
             AskResetPasswordTemplate {
-                user,
                 email,
                 message_email: match error {
-                    AskResetPasswordError::InvalidEmail => tr.t(Sentence::InvalidEmail),
+                    AskResetPasswordError::InvalidEmail => context.tr.t(Sentence::InvalidEmail),
                     _ => "",
                 },
                 message: match error {
                     AskResetPasswordError::EmailAlreadyReset => {
-                        tr.t(Sentence::AskResetEmailAlreadyResetError)
+                        context.tr.t(Sentence::AskResetEmailAlreadyResetError)
                     }
-                    AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown),
+                    AskResetPasswordError::EmailUnknown => context.tr.t(Sentence::EmailUnknown),
                     AskResetPasswordError::UnableSendEmail => {
-                        tr.t(Sentence::UnableToSendResetEmail)
+                        context.tr.t(Sentence::UnableToSendResetEmail)
                     }
-                    AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
+                    AskResetPasswordError::DatabaseError => context.tr.t(Sentence::DatabaseError),
                     _ => "",
                 },
-                tr,
+                context,
             }
             .render()?,
         )
@@ -455,8 +456,7 @@ pub async fn ask_reset_password_post(
         return error_response(
             AskResetPasswordError::InvalidEmail,
             &form_data.email,
-            user,
-            tr,
+            context,
         );
     }
 
@@ -470,20 +470,18 @@ pub async fn ask_reset_password_post(
         Ok(db::user::GetTokenResetPasswordResult::PasswordAlreadyReset) => error_response(
             AskResetPasswordError::EmailAlreadyReset,
             &form_data.email,
-            user,
-            tr,
+            context,
         ),
         Ok(db::user::GetTokenResetPasswordResult::EmailUnknown) => error_response(
             AskResetPasswordError::EmailUnknown,
             &form_data.email,
-            user,
-            tr,
+            context,
         ),
         Ok(db::user::GetTokenResetPasswordResult::Ok(token)) => {
             let url = utils::get_url_from_host(&host);
             match email::send_email(
                 &form_data.email,
-                &tr.tp(
+                &context.tr.tp(
                     Sentence::AskResetFollowEmailLink,
                     &[Box::new(format!(
                         "{}/reset_password?reset_token={}",
@@ -497,8 +495,12 @@ pub async fn ask_reset_password_post(
             .await
             {
                 Ok(()) => Ok(Html(
-                    MessageTemplate::new_with_user(tr.t(Sentence::AskResetEmailSent), tr, user)
-                        .render()?,
+                    MessageTemplate::new_with_user(
+                        context.tr.t(Sentence::AskResetEmailSent),
+                        context.tr,
+                        context.user,
+                    )
+                    .render()?,
                 )
                 .into_response()),
                 Err(_) => {
@@ -506,8 +508,7 @@ pub async fn ask_reset_password_post(
                     error_response(
                         AskResetPasswordError::UnableSendEmail,
                         &form_data.email,
-                        user,
-                        tr,
+                        context,
                     )
                 }
             }
@@ -517,8 +518,7 @@ pub async fn ask_reset_password_post(
             error_response(
                 AskResetPasswordError::DatabaseError,
                 &form_data.email,
-                user,
-                tr,
+                context,
             )
         }
     }
@@ -527,8 +527,7 @@ pub async fn ask_reset_password_post(
 #[debug_handler]
 pub async fn reset_password_get(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     Query(query): Query<HashMap<String, String>>,
 ) -> Result<Response> {
     if let Some(reset_token) = query.get("reset_token") {
@@ -542,8 +541,7 @@ pub async fn reset_password_get(
         {
             Ok(Html(
                 ResetPasswordTemplate {
-                    user,
-                    tr,
+                    context,
                     reset_token,
                     message: "",
                     message_password: "",
@@ -553,15 +551,23 @@ pub async fn reset_password_get(
             .into_response())
         } else {
             Ok(Html(
-                MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
-                    .render()?,
+                MessageTemplate::new_with_user(
+                    context.tr.t(Sentence::AskResetTokenMissing),
+                    context.tr,
+                    context.user,
+                )
+                .render()?,
             )
             .into_response())
         }
     } else {
         Ok(Html(
-            MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user)
-                .render()?,
+            MessageTemplate::new_with_user(
+                context.tr.t(Sentence::AskResetTokenMissing),
+                context.tr,
+                context.user,
+            )
+            .render()?,
         )
         .into_response())
     }
@@ -584,35 +590,36 @@ enum ResetPasswordError {
 #[debug_handler]
 pub async fn reset_password_post(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     Form(form_data): Form<ResetPasswordForm>,
 ) -> Result<Response> {
     fn error_response(
         error: ResetPasswordError,
         form_data: &ResetPasswordForm,
-        user: Option<model::User>,
-        tr: translation::Tr,
+        context: Context,
     ) -> Result<Response> {
-        let reset_password_mess = &tr.tp(
+        let reset_password_mess = &context.tr.tp(
             Sentence::InvalidPassword,
             &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
         );
         Ok(Html(
             ResetPasswordTemplate {
-                user,
                 reset_token: &form_data.reset_token,
                 message_password: match error {
-                    ResetPasswordError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
+                    ResetPasswordError::PasswordsNotEqual => {
+                        context.tr.t(Sentence::PasswordDontMatch)
+                    }
                     ResetPasswordError::InvalidPassword => reset_password_mess,
                     _ => "",
                 },
                 message: match error {
-                    ResetPasswordError::TokenExpired => tr.t(Sentence::AskResetTokenExpired),
-                    ResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError),
+                    ResetPasswordError::TokenExpired => {
+                        context.tr.t(Sentence::AskResetTokenExpired)
+                    }
+                    ResetPasswordError::DatabaseError => context.tr.t(Sentence::DatabaseError),
                     _ => "",
                 },
-                tr,
+                context,
             }
             .render()?,
         )
@@ -620,13 +627,13 @@ pub async fn reset_password_post(
     }
 
     if form_data.password_1 != form_data.password_2 {
-        return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, user, tr);
+        return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, context);
     }
 
     if let common::utils::PasswordValidation::TooShort =
         common::utils::validate_password(&form_data.password_1)
     {
-        return error_response(ResetPasswordError::InvalidPassword, &form_data, user, tr);
+        return error_response(ResetPasswordError::InvalidPassword, &form_data, context);
     }
 
     match connection
@@ -638,24 +645,26 @@ pub async fn reset_password_post(
         .await
     {
         Ok(db::user::ResetPasswordResult::Ok) => Ok(Html(
-            MessageTemplate::new_with_user(tr.t(Sentence::PasswordReset), tr, user).render()?,
+            MessageTemplate::new_with_user(
+                context.tr.t(Sentence::PasswordReset),
+                context.tr,
+                context.user,
+            )
+            .render()?,
         )
         .into_response()),
         Ok(db::user::ResetPasswordResult::ResetTokenExpired) => {
-            error_response(ResetPasswordError::TokenExpired, &form_data, user, tr)
+            error_response(ResetPasswordError::TokenExpired, &form_data, context)
         }
-        Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user, tr),
+        Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, context),
     }
 }
 
 /// EDIT PROFILE ///
 
 #[debug_handler]
-pub async fn edit_user_get(
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
-) -> Result<Response> {
-    Ok(if let Some(user) = user {
+pub async fn edit_user_get(Extension(context): Extension<Context>) -> Result<Response> {
+    Ok(if let Some(ref user) = context.user {
         Html(
             ProfileTemplate {
                 username: &user.name,
@@ -664,14 +673,14 @@ pub async fn edit_user_get(
                 message: "",
                 message_email: "",
                 message_password: "",
-                user: Some(user.clone()),
-                tr,
+                context: context.clone(),
             }
             .render()?,
         )
         .into_response()
     } else {
-        Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response()
+        Html(MessageTemplate::new(context.tr.t(Sentence::NotLoggedIn), context.tr).render()?)
+            .into_response()
     })
 }
 
@@ -699,43 +708,46 @@ pub async fn edit_user_post(
     Host(host): Host,
     State(connection): State<db::Connection>,
     State(config): State<Config>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     Form(form_data): Form<EditUserForm>,
 ) -> Result<Response> {
-    if let Some(user) = user {
+    if let Some(ref user) = context.user {
         fn error_response(
             error: ProfileUpdateError,
             form_data: &EditUserForm,
-            user: model::User,
-            tr: translation::Tr,
+            context: Context,
         ) -> Result<Response> {
-            let invalid_password_mess = &tr.tp(
+            let invalid_password_mess = &context.tr.tp(
                 Sentence::InvalidPassword,
                 &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
             );
             Ok(Html(
                 ProfileTemplate {
-                    user: Some(user),
                     username: &form_data.name,
                     email: &form_data.email,
                     default_servings: form_data.default_servings,
                     message_email: match error {
-                        ProfileUpdateError::InvalidEmail => tr.t(Sentence::InvalidEmail),
-                        ProfileUpdateError::EmailAlreadyTaken => tr.t(Sentence::EmailAlreadyTaken),
+                        ProfileUpdateError::InvalidEmail => context.tr.t(Sentence::InvalidEmail),
+                        ProfileUpdateError::EmailAlreadyTaken => {
+                            context.tr.t(Sentence::EmailAlreadyTaken)
+                        }
                         _ => "",
                     },
                     message_password: match error {
-                        ProfileUpdateError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
+                        ProfileUpdateError::PasswordsNotEqual => {
+                            context.tr.t(Sentence::PasswordDontMatch)
+                        }
                         ProfileUpdateError::InvalidPassword => invalid_password_mess,
                         _ => "",
                     },
                     message: match error {
-                        ProfileUpdateError::DatabaseError => tr.t(Sentence::DatabaseError),
-                        ProfileUpdateError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
+                        ProfileUpdateError::DatabaseError => context.tr.t(Sentence::DatabaseError),
+                        ProfileUpdateError::UnableSendEmail => {
+                            context.tr.t(Sentence::UnableToSendEmail)
+                        }
                         _ => "",
                     },
-                    tr,
+                    context,
                 }
                 .render()?,
             )
@@ -743,17 +755,17 @@ pub async fn edit_user_post(
         }
 
         if form_data.email.parse::<Address>().is_err() {
-            return error_response(ProfileUpdateError::InvalidEmail, &form_data, user, tr);
+            return error_response(ProfileUpdateError::InvalidEmail, &form_data, context);
         }
 
         let new_password = if !form_data.password_1.is_empty() || !form_data.password_2.is_empty() {
             if form_data.password_1 != form_data.password_2 {
-                return error_response(ProfileUpdateError::PasswordsNotEqual, &form_data, user, tr);
+                return error_response(ProfileUpdateError::PasswordsNotEqual, &form_data, context);
             }
             if let common::utils::PasswordValidation::TooShort =
                 common::utils::validate_password(&form_data.password_1)
             {
-                return error_response(ProfileUpdateError::InvalidPassword, &form_data, user, tr);
+                return error_response(ProfileUpdateError::InvalidPassword, &form_data, context);
             }
             Some(form_data.password_1.as_ref())
         } else {
@@ -774,14 +786,14 @@ pub async fn edit_user_post(
             .await
         {
             Ok(db::user::UpdateUserResult::EmailAlreadyTaken) => {
-                return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, user, tr);
+                return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, context);
             }
             Ok(db::user::UpdateUserResult::UserUpdatedWaitingForRevalidation(token)) => {
                 let url = utils::get_url_from_host(&host);
                 let email = form_data.email.clone();
                 match email::send_email(
                     &email,
-                    &tr.tp(
+                    &context.tr.tp(
                         Sentence::ProfileFollowEmailLink,
                         &[Box::new(format!(
                             "{}/revalidation?validation_token={}",
@@ -795,24 +807,23 @@ pub async fn edit_user_post(
                 .await
                 {
                     Ok(()) => {
-                        message = tr.t(Sentence::ProfileEmailSent);
+                        message = context.tr.t(Sentence::ProfileEmailSent);
                     }
                     Err(_) => {
                         // error!("Email validation error: {}", error); // TODO: log
                         return error_response(
                             ProfileUpdateError::UnableSendEmail,
                             &form_data,
-                            user,
-                            tr,
+                            context,
                         );
                     }
                 }
             }
             Ok(db::user::UpdateUserResult::Ok) => {
-                message = tr.t(Sentence::ProfileSaved);
+                message = context.tr.t(Sentence::ProfileSaved);
             }
             Err(_) => {
-                return error_response(ProfileUpdateError::DatabaseError, &form_data, user, tr);
+                return error_response(ProfileUpdateError::DatabaseError, &form_data, context);
             }
         }
 
@@ -821,41 +832,42 @@ pub async fn edit_user_post(
 
         Ok(Html(
             ProfileTemplate {
-                user,
                 username: &form_data.name,
                 email: &form_data.email,
                 default_servings: form_data.default_servings,
                 message,
                 message_email: "",
                 message_password: "",
-                tr,
+                context: Context { user, ..context },
             }
             .render()?,
         )
         .into_response())
     } else {
-        Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response())
+        Ok(
+            Html(MessageTemplate::new(context.tr.t(Sentence::NotLoggedIn), context.tr).render()?)
+                .into_response(),
+        )
     }
 }
 
 #[debug_handler]
 pub async fn email_revalidation(
     State(connection): State<db::Connection>,
-    Extension(user): Extension<Option<model::User>>,
-    Extension(tr): Extension<translation::Tr>,
+    Extension(context): Extension<Context>,
     ConnectInfo(addr): ConnectInfo<SocketAddr>,
     Query(query): Query<HashMap<String, String>>,
     headers: HeaderMap,
 ) -> Result<(CookieJar, impl IntoResponse)> {
     let mut jar = CookieJar::from_headers(&headers);
-    if user.is_some() {
+    if context.user.is_some() {
         return Ok((
             jar,
             Html(
                 MessageTemplate::new_with_user(
-                    tr.t(Sentence::ValidationUserAlreadyExists),
-                    tr,
-                    user,
+                    context.tr.t(Sentence::ValidationUserAlreadyExists),
+                    context.tr,
+                    context.user,
                 )
                 .render()?,
             ),
@@ -875,15 +887,16 @@ pub async fn email_revalidation(
                 .await?
             {
                 db::user::ValidationResult::Ok(token, user_id) => {
-                    let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
+                    let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token))
+                        .same_site(cookie::SameSite::Strict);
                     jar = jar.add(cookie);
                     let user = connection.load_user(user_id).await?;
                     Ok((
                         jar,
                         Html(
                             MessageTemplate::new_with_user(
-                                tr.t(Sentence::ValidationSuccessful),
-                                tr,
+                                context.tr.t(Sentence::ValidationSuccessful),
+                                context.tr,
                                 user,
                             )
                             .render()?,
@@ -893,17 +906,21 @@ pub async fn email_revalidation(
                 db::user::ValidationResult::ValidationExpired => Ok((
                     jar,
                     Html(
-                        MessageTemplate::new_with_user(tr.t(Sentence::ValidationExpired), tr, user)
-                            .render()?,
+                        MessageTemplate::new_with_user(
+                            context.tr.t(Sentence::ValidationExpired),
+                            context.tr,
+                            context.user,
+                        )
+                        .render()?,
                     ),
                 )),
                 db::user::ValidationResult::UnknownUser => Ok((
                     jar,
                     Html(
                         MessageTemplate::new_with_user(
-                            tr.t(Sentence::ValidationErrorTryToSignUpAgain),
-                            tr,
-                            user,
+                            context.tr.t(Sentence::ValidationErrorTryToSignUpAgain),
+                            context.tr,
+                            context.user,
                         )
                         .render()?,
                     ),
@@ -913,8 +930,12 @@ pub async fn email_revalidation(
         None => Ok((
             jar,
             Html(
-                MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user)
-                    .render()?,
+                MessageTemplate::new_with_user(
+                    context.tr.t(Sentence::ValidationError),
+                    context.tr,
+                    context.user,
+                )
+                .render()?,
             ),
         )),
     }
index 5f01870..ceba825 100644 (file)
@@ -62,6 +62,7 @@ pub enum Sentence {
 
     // Reset password page.
     LostPassword,
+    AskResetChooseNewPassword,
     AskResetButton,
     AskResetAlreadyLoggedInError,
     AskResetEmailAlreadyResetError,
@@ -153,7 +154,7 @@ pub enum Sentence {
 pub const DEFAULT_LANGUAGE_CODE: &str = "en";
 pub const PLACEHOLDER_SUBSTITUTE: &str = "{}";
 
-#[derive(Clone)]
+#[derive(Debug, Clone)]
 pub struct Tr {
     lang: &'static Language,
 }
index 679ef97..bfd951d 100644 (file)
@@ -2,15 +2,15 @@
 
 {% block main_container %}
     <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">
-            <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"
                 name="email" value="{{ email }}"
                 autocapitalize="none" autocomplete="email" autofocus="autofocus">
                 <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>
 
         <span class="user-message">{{ message }}</span>
index fdf4ef6..ecef2a9 100644 (file)
@@ -1,10 +1,15 @@
 <!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>
         <meta charset="utf-8">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <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">
     </head>
 
index 7525fab..ed58fbb 100644 (file)
@@ -5,29 +5,37 @@
         {% include "title.html" %}
 
         <span class="header-menu">
-            {% match user %}
+            {% match context.user %}
             {% when Some with (user) %}
-                <a class="create-recipe" href="/recipe/new" >{{ tr.t(Sentence::CreateNewRecipe) }}</a>
-                <a href="/{{ tr.current_lang_code() }}/user/edit">
+                <a class="create-recipe" href="/recipe/new" >{{ context.tr.t(Sentence::CreateNewRecipe) }}</a>
+                <a href="/{{ context.tr.current_lang_code() }}/user/edit">
                     {% if user.name == "" %}
                         {{ user.email }}
                     {% else %}
                         {{ user.name }}
                     {% endif %}
-                </a> / <a href="/signout">{{ tr.t(Sentence::SignOut) }}</a>
+                </a> / <a href="/signout">{{ context.tr.t(Sentence::SignOut) }}</a>
             {% 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 %}
 
             <select id="select-website-language">
             {% for lang in translation::available_languages() %}
                 <option value="{{ lang.0 }}"
-                {%~ if tr.current_lang_code() == lang.0 %}
+                {%~ if context.tr.current_lang_code() == lang.0 %}
                     selected
                 {% endif %}
                 >{{ lang.1 }}</option>
             {% endfor %}
             </select>
+
+            <label id="toggle-theme">
+                <input type="checkbox"
+                {%~ if !context.dark_theme %}
+                    checked
+                {% endif %} >
+                <span class="slider"></span>
+            </label>
         </span>
     </div>
 
index ba29d95..052a7e2 100644 (file)
@@ -17,7 +17,7 @@
             Sentence::CalendarNovember,
             Sentence::CalendarDecember,
         ] %}
-            <span class="month">{{ tr.t(*month) }}</span>
+            <span class="month">{{ context.tr.t(*month) }}</span>
         {% endfor %}
 
         <span class="next">NEXT</span>
@@ -32,7 +32,7 @@
             Sentence::CalendarSaturdayAbbreviation,
             Sentence::CalendarSundayAbbreviation,
         ] %}
-            <li class="weekday">{{ tr.t(*day) }}</li>
+            <li class="weekday">{{ context.tr.t(*day) }}</li>
         {% endfor %}
     </ul>
 
         <div class="scheduled-recipe"></div>
 
         <div class="unschedule-confirmation">
-            <div>{{ tr.t(Sentence::CalendarUnscheduleConfirmation) }}</div>
+            <div>{{ context.tr.t(Sentence::CalendarUnscheduleConfirmation) }}</div>
             <input
                 id="input-remove-ingredients-from-shopping-list"
                 type="checkbox"
                 checked
             >
             <label for="input-remove-ingredients-from-shopping-list">
-                {{ tr.t(Sentence::CalendarRemoveIngredientsFromShoppingList) }}
+                {{ context.tr.t(Sentence::CalendarRemoveIngredientsFromShoppingList) }}
             </label>
         </div>
 
-        <span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span>
+        <span class="calendar-date-format">{{ context.tr.t(Sentence::CalendarDateFormat) }}</span>
     </div>
 </div>
\ No newline at end of file
index e93a1bf..6379c5b 100644 (file)
@@ -21,7 +21,7 @@
         <div class="item-delete"></div>
     </div>
 
-    <span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span>
+    <span class="calendar-date-format">{{ context.tr.t(Sentence::CalendarDateFormat) }}</span>
 </div>
 
 {% endblock %}
\ No newline at end of file
index 4623144..dad1d96 100644 (file)
@@ -2,14 +2,14 @@
 
 {% block main_container %}
 
-{% if let Some(user) = user %}
+{% if let Some(user) = context.user %}
 
 <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
             id="input-name"
             type="text"
             autofocus="autofocus">
         <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"
             name="email" value="{{ email }}"
             autocapitalize="none" autocomplete="email" autofocus="autofocus">
         <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
             id="input-servings"
             type="number"
             value="{{ default_servings }}">
         <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">
         <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">
         <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>
 
     <span class="user-message">{{ message }}</span>
index 22dea8c..195d811 100644 (file)
@@ -9,18 +9,18 @@
 {% block content %}
 
 <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
         id="input-title"
         type="text"
         value="{{ recipe.title }}"
         autofocus="true">
 
-    <label for="text-area-description">{{ tr.t(Sentence::RecipeDescription) }}</label>
+    <label for="text-area-description">{{ context.tr.t(Sentence::RecipeDescription) }}</label>
     <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
         id="input-servings"
         type="number"
@@ -30,7 +30,7 @@
                 {{ s }}
             {% endif %}">
 
-    <label for="input-estimated-time">{{ tr.t(Sentence::RecipeEstimatedTime) }}</label>
+    <label for="input-estimated-time">{{ context.tr.t(Sentence::RecipeEstimatedTime) }}</label>
     <input
         id="input-estimated-time"
         type="number"
                 {{ t }}
             {% endif %}">
 
-    <label for="select-difficulty">{{ tr.t(Sentence::RecipeDifficulty) }}</label>
+    <label for="select-difficulty">{{ context.tr.t(Sentence::RecipeDifficulty) }}</label>
     <select id="select-difficulty">
         <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="2" {%~ call is_difficulty(common::ron_api::Difficulty::Medium) %}>{{ tr.t(Sentence::RecipeDifficultyMedium) }}</option>
-        <option value="3" {%~ call is_difficulty(common::ron_api::Difficulty::Hard) %}>{{ tr.t(Sentence::RecipeDifficultyHard) }}</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) %}>{{ context.tr.t(Sentence::RecipeDifficultyMedium) }}</option>
+        <option value="3" {%~ call is_difficulty(common::ron_api::Difficulty::Hard) %}>{{ context.tr.t(Sentence::RecipeDifficultyHard) }}</option>
     </select>
 
     <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>
         <input
             id="input-tags"
@@ -57,7 +57,7 @@
             value="">
     </div>
 
-    <label for="select-language">{{ tr.t(Sentence::RecipeLanguage) }}</label>
+    <label for="select-language">{{ context.tr.t(Sentence::RecipeLanguage) }}</label>
     <select id="select-language">
     {% for lang in translation::available_languages() %}
         <option value="{{ lang.0 }}"
             checked
         {% 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>
 
-    <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 id="hidden-templates">
     <div class="group">
         <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">
 
-        <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-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>
 
-        <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 class="step">
         <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>
 
-        <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>
 
-        <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 class="ingredient">
         <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">
 
-        <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">
 
-        <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">
 
-        <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-delete" type="button" value="{{ tr.t(Sentence::RecipeRemoveIngredient) }}">
+        <input class="input-ingredient-delete" type="button" value="{{ context.tr.t(Sentence::RecipeRemoveIngredient) }}">
     </div>
 
     <div class="dropzone"></div>
 
-    <span class="recipe-delete-confirmation">{{ tr.t(Sentence::RecipeDeleteConfirmation) }}</span>
-    <span class="recipe-group-delete-confirmation">{{ tr.t(Sentence::RecipeGroupDeleteConfirmation) }}</span>
-    <span class="recipe-step-delete-confirmation">{{ tr.t(Sentence::RecipeStepDeleteConfirmation) }}</span>
-    <span class="recipe-ingredient-delete-confirmation">{{ tr.t(Sentence::RecipeIngredientDeleteConfirmation) }}</span>
+    <span class="recipe-delete-confirmation">{{ context.tr.t(Sentence::RecipeDeleteConfirmation) }}</span>
+    <span class="recipe-group-delete-confirmation">{{ context.tr.t(Sentence::RecipeGroupDeleteConfirmation) }}</span>
+    <span class="recipe-step-delete-confirmation">{{ context.tr.t(Sentence::RecipeStepDeleteConfirmation) }}</span>
+    <span class="recipe-ingredient-delete-confirmation">{{ context.tr.t(Sentence::RecipeIngredientDeleteConfirmation) }}</span>
 </div>
 
 {% endblock %}
\ No newline at end of file
index 86a4ddd..54ce4e3 100644 (file)
@@ -5,13 +5,13 @@
 <div class="content" id="recipe-view">
     <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) %}
-            <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 %}
 
-    <span class="add-to-planner">{{ tr.t(Sentence::CalendarAddToPlanner) }}</span>
+    <span class="add-to-planner">{{ context.tr.t(Sentence::CalendarAddToPlanner) }}</span>
 
     <div class="tags">
     {% for tag in recipe.tags %}
@@ -23,9 +23,9 @@
     {% when Some(servings) %}
         <span class="servings">
             {% if *servings == 1 %}
-                {{ tr.t(Sentence::RecipeOneServing) }}
+                {{ context.tr.t(Sentence::RecipeOneServing) }}
             {% else %}
-                {{ tr.tp(Sentence::RecipeSomeServings, [Box::new(**servings)]) }}
+                {{ context.tr.tp(Sentence::RecipeSomeServings, [Box::new(**servings)]) }}
             {% endif %}
         </span>
     {% else %}
@@ -33,7 +33,7 @@
 
     {% match recipe.estimated_time %}
     {% when Some(time) %}
-        {{ time ~}} {{~ tr.t(Sentence::RecipeEstimatedTimeMinAbbreviation) }}
+        {{ time ~}} {{~ context.tr.t(Sentence::RecipeEstimatedTimeMinAbbreviation) }}
     {% else %}
     {% endmatch %}
 
     {% match recipe.difficulty %}
     {% when common::ron_api::Difficulty::Unknown %}
     {% when common::ron_api::Difficulty::Easy %}
-        {{ tr.t(Sentence::RecipeDifficultyEasy) }}
+        {{ context.tr.t(Sentence::RecipeDifficultyEasy) }}
     {% when common::ron_api::Difficulty::Medium %}
-        {{ tr.t(Sentence::RecipeDifficultyMedium) }}
+        {{ context.tr.t(Sentence::RecipeDifficultyMedium) }}
     {% when common::ron_api::Difficulty::Hard %}
-        {{ tr.t(Sentence::RecipeDifficultyHard) }}
+        {{ context.tr.t(Sentence::RecipeDifficultyHard) }}
     {% endmatch %}
     </span>
 
     <div class="date-and-servings" >
         {% include "calendar.html" %}
 
-        <label for="input-servings">{{ tr.t(Sentence::RecipeServings) }}</label>
+        <label for="input-servings">{{ context.tr.t(Sentence::RecipeServings) }}</label>
         <input
             id="input-servings"
             type="number"
             step="1" min="1" max="100"
             value="
-            {% if let Some(user) = user %}
+            {% if let Some(user) = context.user %}
                 {{ user.default_servings }}
             {% else %}
                 4
             checked
         >
         <label for="input-add-ingredients-to-shopping-list">
-            {{ tr.t(Sentence::CalendarAddIngredientsToShoppingList) }}
+            {{ context.tr.t(Sentence::CalendarAddIngredientsToShoppingList) }}
         </label>
     </div>
 
-    <span class="calendar-add-to-planner-success">{{ tr.t(Sentence::CalendarAddToPlannerSuccess) }}</span>
-    <span class="calendar-add-to-planner-already-exists">{{ tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }}</span>
-    <span class="calendar-date-format">{{ tr.t(Sentence::CalendarDateFormat) }}</span>
+    <span class="calendar-add-to-planner-success">{{ context.tr.t(Sentence::CalendarAddToPlannerSuccess) }}</span>
+    <span class="calendar-add-to-planner-already-exists">{{ context.tr.t(Sentence::CalendarAddToPlannerAlreadyExists) }}</span>
+    <span class="calendar-date-format">{{ context.tr.t(Sentence::CalendarDateFormat) }}</span>
 </div>
 
 {% endblock %}
\ No newline at end of file
index cec2dcc..680c23e 100644 (file)
@@ -1,12 +1,12 @@
 {% macro recipe_item(id, title, is_current) %}
     <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 %}
                 current
             {% endif %}" id="recipe-{{ id }}"
         >
             {% if title == "" %}
-                {{ tr.t(Sentence::UntitledRecipe) }}
+                {{ context.tr.t(Sentence::UntitledRecipe) }}
             {% else %}
                 {{ title }}
             {% endif %}
@@ -16,7 +16,7 @@
 
 <div id="recipes-list">
     {% if !recipes.unpublished.is_empty() %}
-        {{ tr.t(Sentence::UnpublishedRecipes) }}
+        {{ context.tr.t(Sentence::UnpublishedRecipes) }}
     {% endif %}
 
     <nav class="recipes-list-unpublished">
index 08cd9a8..002744b 100644 (file)
@@ -4,10 +4,10 @@
 
 <div class="content" id="reset-password">
     <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">
 
-        <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">
 
         {{ message_password }}
index 88c4536..6dc6244 100644 (file)
@@ -4,17 +4,17 @@
 
 <div class="content" id="sign-in">
 
-    <h1>{{ tr.t(Sentence::SignInTitle) }}</h1>
+    <h1>{{ context.tr.t(Sentence::SignInTitle) }}</h1>
 
     <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 }}"
             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 type="submit" value="{{ tr.t(Sentence::SignInMenu) }}">
+        <input type="submit" value="{{ context.tr.t(Sentence::SignInMenu) }}">
     </form>
 
     <span class="user-message">{{ message }}</span>
index 86dae07..e4d1c28 100644 (file)
@@ -4,10 +4,10 @@
 
 <div class="content" id="sign-up">
 
-    <h1>{{ tr.t(Sentence::SignUpTitle) }}</h1>
+    <h1>{{ context.tr.t(Sentence::SignUpTitle) }}</h1>
 
     <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"
             name="email" value="{{ email }}"
             autocapitalize="none" autocomplete="email" autofocus="autofocus"
         <span class="user-message">{{ message_email }}</span>
 
         <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>
         <input id="input-password-1" type="password" name="password_1" autocomplete="new-password">
         <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">
         <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>
 
     <span class="user-message">{{ message }}</span>
index 334df85..569eb53 100644 (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>
\ No newline at end of file
+<a class="title" href="/{{ context.tr.current_lang_code() }}/"><img class="logo" src="/static/logo.svg" alt="logo">{{ context.tr.t(Sentence::MainTitle) }}</a>
\ No newline at end of file
index 53e68e0..8580417 100644 (file)
@@ -50,6 +50,7 @@
             (ReEnterPassword, "Re-enter password"),
 
             (LostPassword, "Lost password"),
+            (AskResetChooseNewPassword, "Choose a new password (minimum {} characters)"),
             (AskResetButton, "Ask reset"),
             (AskResetAlreadyLoggedInError, "Can't ask to reset password when already logged in"),
             (AskResetEmailAlreadyResetError, "The password has already been reset for this email"),
             (ReEnterPassword, "Entrez à nouveau le mot de passe"),
 
             (LostPassword, "Mot de passe perdu"),
+            (AskResetChooseNewPassword, "Choisir un nouveau mot de passe (minimum {} caractères)"),
             (AskResetButton, "Demander la réinitialisation"),
             (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"),
index 6861e36..f6d3af3 100644 (file)
@@ -1 +1,2 @@
 pub const MIN_PASSWORD_SIZE: usize = 8;
+pub const COOKIE_DARK_THEME: &str = "dark_theme";
diff --git a/frontend/.cargo/config.toml b/frontend/.cargo/config.toml
new file mode 100644 (file)
index 0000000..f4e8c00
--- /dev/null
@@ -0,0 +1,2 @@
+[build]
+target = "wasm32-unknown-unknown"
index 53d1680..42d2e6e 100644 (file)
@@ -3,7 +3,7 @@ use gloo::{console::log, events::EventListener, utils::window};
 use utils::by_id;
 use wasm_bindgen::prelude::*;
 use wasm_bindgen_futures::spawn_local;
-use web_sys::{HtmlElement, HtmlSelectElement};
+use web_sys::{HtmlElement, HtmlInputElement, HtmlSelectElement};
 
 use crate::utils::selector;
 
@@ -58,6 +58,7 @@ pub fn main() -> Result<(), JsValue> {
         _ => log!("Path unknown: ", location),
     }
 
+    // Language handling.
     let select_language: HtmlSelectElement = by_id("select-website-language");
     EventListener::new(&select_language.clone(), "input", move |_event| {
         let lang = select_language.value();
@@ -79,5 +80,23 @@ pub fn main() -> Result<(), JsValue> {
     })
     .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(())
 }