Translation (WIP)
This commit is contained in:
parent
9b0fcec5e2
commit
e9873c1943
29 changed files with 586 additions and 285 deletions
265
Cargo.lock
generated
265
Cargo.lock
generated
|
|
@ -120,67 +120,11 @@ dependencies = [
|
||||||
"password-hash",
|
"password-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
|
|
||||||
dependencies = [
|
|
||||||
"askama_derive",
|
|
||||||
"askama_escape",
|
|
||||||
"comrak",
|
|
||||||
"humansize",
|
|
||||||
"num-traits",
|
|
||||||
"percent-encoding",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama_axum"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163"
|
|
||||||
dependencies = [
|
|
||||||
"askama",
|
|
||||||
"axum-core",
|
|
||||||
"http 1.2.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama_derive"
|
|
||||||
version = "0.12.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
|
|
||||||
dependencies = [
|
|
||||||
"askama_parser",
|
|
||||||
"basic-toml",
|
|
||||||
"mime",
|
|
||||||
"mime_guess",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"serde",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama_escape"
|
|
||||||
version = "0.10.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "askama_parser"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.83"
|
version = "0.1.84"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
|
checksum = "1b1244b10dcd56c92219da4e14caa97e312079e185f04ba3eea25061561dc0a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -391,9 +335,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.6"
|
version = "1.2.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
|
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
@ -483,21 +427,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "comrak"
|
|
||||||
version = "0.18.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894"
|
|
||||||
dependencies = [
|
|
||||||
"entities",
|
|
||||||
"memchr",
|
|
||||||
"once_cell",
|
|
||||||
"regex",
|
|
||||||
"slug",
|
|
||||||
"typed-arena",
|
|
||||||
"unicode_categories",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -640,12 +569,6 @@ dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "deunicode"
|
|
||||||
version = "1.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
|
@ -709,12 +632,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "entities"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -776,6 +693,12 @@ version = "1.0.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "form_urlencoded"
|
name = "form_urlencoded"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
@ -1126,14 +1049,19 @@ name = "hashbrown"
|
||||||
version = "0.15.2"
|
version = "0.15.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
version = "0.9.1"
|
version = "0.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.15.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1473,9 +1401,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.13.0"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
@ -1811,12 +1739,6 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paste"
|
|
||||||
version = "1.0.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem-rfc7468"
|
name = "pem-rfc7468"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -1995,8 +1917,6 @@ name = "recipes"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"askama",
|
|
||||||
"askama_axum",
|
|
||||||
"axum",
|
"axum",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -2007,7 +1927,10 @@ dependencies = [
|
||||||
"lettre",
|
"lettre",
|
||||||
"rand",
|
"rand",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
|
"rinja",
|
||||||
|
"rinja_axum",
|
||||||
"ron",
|
"ron",
|
||||||
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.9",
|
"thiserror 2.0.9",
|
||||||
|
|
@ -2086,6 +2009,58 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rinja"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3dc4940d00595430b3d7d5a01f6222b5e5b51395d1120bdb28d854bb8abb17a5"
|
||||||
|
dependencies = [
|
||||||
|
"humansize",
|
||||||
|
"itoa",
|
||||||
|
"percent-encoding",
|
||||||
|
"rinja_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rinja_axum"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc64d77bb950f6498d0fc64b028d168fcb4e56ac31b66a8ae05f64d3b0c218b6"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core",
|
||||||
|
"http 1.2.0",
|
||||||
|
"rinja",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rinja_derive"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d9ed0146aef6e2825f1b1515f074510549efba38d71f4554eec32eb36ba18b"
|
||||||
|
dependencies = [
|
||||||
|
"basic-toml",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rinja_parser",
|
||||||
|
"rustc-hash",
|
||||||
|
"serde",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rinja_parser"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"nom",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ron"
|
name = "ron"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|
@ -2124,6 +2099,12 @@ version = "0.1.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.42"
|
version = "0.38.42"
|
||||||
|
|
@ -2326,16 +2307,6 @@ dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "slug"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
|
|
||||||
dependencies = [
|
|
||||||
"deunicode",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.13.2"
|
version = "1.13.2"
|
||||||
|
|
@ -2374,21 +2345,11 @@ dependencies = [
|
||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sqlformat"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
|
|
||||||
dependencies = [
|
|
||||||
"nom",
|
|
||||||
"unicode_categories",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx"
|
name = "sqlx"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e"
|
checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"sqlx-macros",
|
"sqlx-macros",
|
||||||
|
|
@ -2399,38 +2360,32 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-core"
|
name = "sqlx-core"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e"
|
checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
|
||||||
"byteorder",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"crc",
|
"crc",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"either",
|
"either",
|
||||||
"event-listener",
|
"event-listener",
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-intrusive",
|
"futures-intrusive",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.15.2",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
"hex",
|
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlformat",
|
"thiserror 2.0.9",
|
||||||
"thiserror 1.0.69",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -2439,9 +2394,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-macros"
|
name = "sqlx-macros"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657"
|
checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2452,9 +2407,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-macros-core"
|
name = "sqlx-macros-core"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
|
checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
|
|
@ -2478,9 +2433,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-mysql"
|
name = "sqlx-mysql"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
|
checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|
@ -2514,16 +2469,16 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 1.0.69",
|
"thiserror 2.0.9",
|
||||||
"tracing",
|
"tracing",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-postgres"
|
name = "sqlx-postgres"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
|
checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
|
@ -2535,7 +2490,6 @@ dependencies = [
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
|
|
@ -2553,16 +2507,16 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"sqlx-core",
|
"sqlx-core",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
"thiserror 1.0.69",
|
"thiserror 2.0.9",
|
||||||
"tracing",
|
"tracing",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlx-sqlite"
|
name = "sqlx-sqlite"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
|
checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atoi",
|
"atoi",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
@ -2626,9 +2580,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.93"
|
version = "2.0.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
|
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -2654,12 +2608,13 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.14.0"
|
version = "3.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
|
"getrandom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
|
|
@ -2966,12 +2921,6 @@ dependencies = [
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typed-arena"
|
|
||||||
version = "2.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
|
|
@ -3023,12 +2972,6 @@ version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode_categories"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,14 @@ chrono = "0.4"
|
||||||
ron = "0.8"
|
ron = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
itertools = "0.13"
|
itertools = "0.14"
|
||||||
|
rustc-hash = "2.1"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
|
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio", "chrono"] }
|
||||||
|
|
||||||
askama = { version = "0.12", features = [
|
rinja = { version = "0.3", features = ["with-axum"] }
|
||||||
"with-axum",
|
rinja_axum = "0.3"
|
||||||
"mime",
|
|
||||||
"mime_guess",
|
|
||||||
"markdown",
|
|
||||||
] }
|
|
||||||
askama_axum = "0.4"
|
|
||||||
|
|
||||||
argon2 = { version = "0.5", features = ["default", "std"] }
|
argon2 = { version = "0.5", features = ["default", "std"] }
|
||||||
rand_core = { version = "0.6", features = ["std"] }
|
rand_core = { version = "0.6", features = ["std"] }
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ CREATE TABLE [User] (
|
||||||
[email] TEXT NOT NULL,
|
[email] TEXT NOT NULL,
|
||||||
[name] TEXT NOT NULL DEFAULT '',
|
[name] TEXT NOT NULL DEFAULT '',
|
||||||
[default_servings] INTEGER DEFAULT 4,
|
[default_servings] INTEGER DEFAULT 4,
|
||||||
|
[lang] TEXT NOT NULL DEFAULT 'en',
|
||||||
|
|
||||||
[password] TEXT NOT NULL, -- argon2(password_plain, salt).
|
[password] TEXT NOT NULL, -- argon2(password_plain, salt).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{sync::LazyLock, time::Duration};
|
use std::{sync::LazyLock, time::Duration};
|
||||||
|
|
||||||
pub const FILE_CONF: &str = "conf.ron";
|
pub const FILE_CONF: &str = "conf.ron";
|
||||||
|
pub const TRANSLATION_FILE: &str = "translation.ron";
|
||||||
pub const DB_DIRECTORY: &str = "data";
|
pub const DB_DIRECTORY: &str = "data";
|
||||||
pub const DB_FILENAME: &str = "recipes.sqlite";
|
pub const DB_FILENAME: &str = "recipes.sqlite";
|
||||||
pub const SQL_FILENAME: &str = "sql/version_{VERSION}.sql";
|
pub const SQL_FILENAME: &str = "sql/version_{VERSION}.sql";
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,8 @@ WHERE [Ingredient].[id] = $1 AND [user_id] = $2
|
||||||
.map_err(DBError::from)
|
.map_err(DBError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_recipe(&self, id: i64) -> Result<Option<model::Recipe>> {
|
pub async fn get_recipe(&self, id: i64, with_groups: bool) -> Result<Option<model::Recipe>> {
|
||||||
sqlx::query_as(
|
match sqlx::query_as::<_, model::Recipe>(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
[id], [user_id], [title], [lang],
|
[id], [user_id], [title], [lang],
|
||||||
|
|
@ -114,8 +114,14 @@ FROM [Recipe] WHERE [id] = $1
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await?
|
||||||
.map_err(DBError::from)
|
{
|
||||||
|
Some(mut recipe) if with_groups => {
|
||||||
|
recipe.groups = self.get_groups(id).await?;
|
||||||
|
Ok(Some(recipe))
|
||||||
|
}
|
||||||
|
recipe => Ok(recipe),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_recipe(&self, user_id: i64) -> Result<i64> {
|
pub async fn create_recipe(&self, user_id: i64) -> Result<i64> {
|
||||||
|
|
@ -543,8 +549,6 @@ ORDER BY [name]
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use axum::routing::connect;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
@ -555,7 +559,7 @@ mod tests {
|
||||||
let recipe_id = connection.create_recipe(user_id).await?;
|
let recipe_id = connection.create_recipe(user_id).await?;
|
||||||
|
|
||||||
connection.set_recipe_title(recipe_id, "Crêpe").await?;
|
connection.set_recipe_title(recipe_id, "Crêpe").await?;
|
||||||
let recipe = connection.get_recipe(recipe_id).await?.unwrap();
|
let recipe = connection.get_recipe(recipe_id, false).await?.unwrap();
|
||||||
assert_eq!(recipe.title, "Crêpe".to_string());
|
assert_eq!(recipe.title, "Crêpe".to_string());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -581,7 +585,7 @@ mod tests {
|
||||||
connection.set_recipe_language(recipe_id, "fr").await?;
|
connection.set_recipe_language(recipe_id, "fr").await?;
|
||||||
connection.set_recipe_is_published(recipe_id, true).await?;
|
connection.set_recipe_is_published(recipe_id, true).await?;
|
||||||
|
|
||||||
let recipe = connection.get_recipe(recipe_id).await?.unwrap();
|
let recipe = connection.get_recipe(recipe_id, false).await?.unwrap();
|
||||||
|
|
||||||
assert_eq!(recipe.id, recipe_id);
|
assert_eq!(recipe.id, recipe_id);
|
||||||
assert_eq!(recipe.title, "Ouiche");
|
assert_eq!(recipe.title, "Ouiche");
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ FROM [UserLoginToken] WHERE [token] = $1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> {
|
pub async fn load_user(&self, user_id: i64) -> Result<Option<model::User>> {
|
||||||
sqlx::query_as("SELECT [id], [email], [name] FROM [User] WHERE [id] = $1")
|
sqlx::query_as("SELECT [id], [email], [name], [lang] FROM [User] WHERE [id] = $1")
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
|
@ -102,13 +102,14 @@ FROM [UserLoginToken] WHERE [token] = $1
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let new_email = new_email.map(str::trim);
|
||||||
let email_changed = new_email.is_some_and(|new_email| new_email != email);
|
let email_changed = new_email.is_some_and(|new_email| new_email != email);
|
||||||
|
|
||||||
// Check if email not already taken.
|
// Check if email not already taken.
|
||||||
let validation_token = if email_changed {
|
let validation_token = if email_changed {
|
||||||
if sqlx::query_scalar::<_, i64>(
|
if sqlx::query_scalar(
|
||||||
r#"
|
r#"
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*) > 0
|
||||||
FROM [User]
|
FROM [User]
|
||||||
WHERE [email] = $1
|
WHERE [email] = $1
|
||||||
"#,
|
"#,
|
||||||
|
|
@ -116,7 +117,6 @@ WHERE [email] = $1
|
||||||
.bind(new_email.unwrap())
|
.bind(new_email.unwrap())
|
||||||
.fetch_one(&mut *tx)
|
.fetch_one(&mut *tx)
|
||||||
.await?
|
.await?
|
||||||
> 0
|
|
||||||
{
|
{
|
||||||
return Ok(UpdateUserResult::EmailAlreadyTaken);
|
return Ok(UpdateUserResult::EmailAlreadyTaken);
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +148,7 @@ WHERE [id] = $1
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(new_email.unwrap_or(&email))
|
.bind(new_email.unwrap_or(&email))
|
||||||
.bind(new_name.unwrap_or(&name))
|
.bind(new_name.map(str::trim).unwrap_or(&name))
|
||||||
.bind(hashed_new_password.unwrap_or(hashed_password))
|
.bind(hashed_new_password.unwrap_or(hashed_password))
|
||||||
.execute(&mut *tx)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub struct User {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
pub lang: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow)]
|
#[derive(FromRow)]
|
||||||
|
|
@ -30,8 +31,9 @@ pub struct Recipe {
|
||||||
|
|
||||||
pub servings: Option<u32>,
|
pub servings: Option<u32>,
|
||||||
pub is_published: bool,
|
pub is_published: bool,
|
||||||
// pub tags: Vec<String>,
|
|
||||||
// pub groups: Vec<Group>,
|
#[sqlx(skip)]
|
||||||
|
pub groups: Vec<Group>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow)]
|
#[derive(FromRow)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
use askama::Template;
|
use rinja_axum::Template;
|
||||||
|
|
||||||
use crate::data::model;
|
use crate::{
|
||||||
|
data::model,
|
||||||
|
translation::{Sentence, Tr},
|
||||||
|
};
|
||||||
|
|
||||||
pub struct Recipes {
|
pub struct Recipes {
|
||||||
pub published: Vec<(i64, String)>,
|
pub published: Vec<(i64, String)>,
|
||||||
|
|
@ -18,6 +21,8 @@ impl Recipes {
|
||||||
#[template(path = "home.html")]
|
#[template(path = "home.html")]
|
||||||
pub struct HomeTemplate {
|
pub struct HomeTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub recipes: Recipes,
|
pub recipes: Recipes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,23 +30,26 @@ pub struct HomeTemplate {
|
||||||
#[template(path = "message.html")]
|
#[template(path = "message.html")]
|
||||||
pub struct MessageTemplate {
|
pub struct MessageTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub as_code: bool, // Display the message in <pre> markup.
|
pub as_code: bool, // Display the message in <pre> markup.
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageTemplate {
|
impl MessageTemplate {
|
||||||
pub fn new(message: &str) -> MessageTemplate {
|
pub fn new(message: &str, tr: Tr) -> MessageTemplate {
|
||||||
MessageTemplate {
|
MessageTemplate {
|
||||||
user: None,
|
user: None,
|
||||||
|
tr,
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
as_code: false,
|
as_code: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_user(message: &str, user: Option<model::User>) -> MessageTemplate {
|
pub fn new_with_user(message: &str, tr: Tr, user: Option<model::User>) -> MessageTemplate {
|
||||||
MessageTemplate {
|
MessageTemplate {
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
as_code: false,
|
as_code: false,
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +60,7 @@ impl MessageTemplate {
|
||||||
#[template(path = "sign_up_form.html")]
|
#[template(path = "sign_up_form.html")]
|
||||||
pub struct SignUpFormTemplate {
|
pub struct SignUpFormTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
|
@ -63,6 +72,7 @@ pub struct SignUpFormTemplate {
|
||||||
#[template(path = "sign_in_form.html")]
|
#[template(path = "sign_in_form.html")]
|
||||||
pub struct SignInFormTemplate {
|
pub struct SignInFormTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
|
@ -72,6 +82,7 @@ pub struct SignInFormTemplate {
|
||||||
#[template(path = "ask_reset_password.html")]
|
#[template(path = "ask_reset_password.html")]
|
||||||
pub struct AskResetPasswordTemplate {
|
pub struct AskResetPasswordTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
|
@ -82,6 +93,7 @@ pub struct AskResetPasswordTemplate {
|
||||||
#[template(path = "reset_password.html")]
|
#[template(path = "reset_password.html")]
|
||||||
pub struct ResetPasswordTemplate {
|
pub struct ResetPasswordTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub reset_token: String,
|
pub reset_token: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
|
@ -92,6 +104,7 @@ pub struct ResetPasswordTemplate {
|
||||||
#[template(path = "profile.html")]
|
#[template(path = "profile.html")]
|
||||||
pub struct ProfileTemplate {
|
pub struct ProfileTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub email: String,
|
pub email: String,
|
||||||
|
|
@ -104,6 +117,8 @@ pub struct ProfileTemplate {
|
||||||
#[template(path = "recipe_view.html")]
|
#[template(path = "recipe_view.html")]
|
||||||
pub struct RecipeViewTemplate {
|
pub struct RecipeViewTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub recipes: Recipes,
|
pub recipes: Recipes,
|
||||||
|
|
||||||
pub recipe: model::Recipe,
|
pub recipe: model::Recipe,
|
||||||
|
|
@ -113,6 +128,8 @@ pub struct RecipeViewTemplate {
|
||||||
#[template(path = "recipe_edit.html")]
|
#[template(path = "recipe_edit.html")]
|
||||||
pub struct RecipeEditTemplate {
|
pub struct RecipeEditTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub recipes: Recipes,
|
pub recipes: Recipes,
|
||||||
|
|
||||||
pub recipe: model::Recipe,
|
pub recipe: model::Recipe,
|
||||||
|
|
@ -123,5 +140,7 @@ pub struct RecipeEditTemplate {
|
||||||
#[template(path = "recipes_list_fragment.html")]
|
#[template(path = "recipes_list_fragment.html")]
|
||||||
pub struct RecipesListFragmentTemplate {
|
pub struct RecipesListFragmentTemplate {
|
||||||
pub user: Option<model::User>,
|
pub user: Option<model::User>,
|
||||||
|
pub tr: Tr,
|
||||||
|
|
||||||
pub recipes: Recipes,
|
pub recipes: Recipes,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{net::SocketAddr, path::Path};
|
use std::{net::SocketAddr, path::Path};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{ConnectInfo, FromRef, Request, State},
|
extract::{ConnectInfo, Extension, FromRef, Request, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::{Response, Result},
|
response::{Response, Result},
|
||||||
|
|
@ -12,10 +12,12 @@ use axum_extra::extract::cookie::CookieJar;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
|
use itertools::Itertools;
|
||||||
use tower_http::{services::ServeDir, trace::TraceLayer};
|
use tower_http::{services::ServeDir, trace::TraceLayer};
|
||||||
use tracing::{event, Level};
|
use tracing::{event, Level};
|
||||||
|
|
||||||
use data::{db, model};
|
use data::{db, model};
|
||||||
|
use translation::Tr;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod consts;
|
mod consts;
|
||||||
|
|
@ -26,6 +28,7 @@ mod html_templates;
|
||||||
mod ron_extractor;
|
mod ron_extractor;
|
||||||
mod ron_utils;
|
mod ron_utils;
|
||||||
mod services;
|
mod services;
|
||||||
|
mod translation;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -191,6 +194,7 @@ async fn main() {
|
||||||
.fallback(services::not_found)
|
.fallback(services::not_found)
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
// FIXME: Should be 'route_layer' but it doesn't work for 'fallback(..)'.
|
// FIXME: Should be 'route_layer' but it doesn't work for 'fallback(..)'.
|
||||||
|
.layer(middleware::from_fn(translation))
|
||||||
.layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
state.clone(),
|
state.clone(),
|
||||||
user_authentication,
|
user_authentication,
|
||||||
|
|
@ -218,6 +222,39 @@ async fn user_authentication(
|
||||||
Ok(next.run(req).await)
|
Ok(next.run(req).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn translation(
|
||||||
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
mut req: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response> {
|
||||||
|
let language = if let Some(user) = user {
|
||||||
|
user.lang
|
||||||
|
} else {
|
||||||
|
let available_codes = Tr::available_codes();
|
||||||
|
|
||||||
|
// TODO: Check cookies before http headers.
|
||||||
|
|
||||||
|
let accept_language = req
|
||||||
|
.headers()
|
||||||
|
.get(axum::http::header::ACCEPT_LANGUAGE)
|
||||||
|
.map(|v| v.to_str().unwrap_or_default())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.split(',')
|
||||||
|
.map(|l| l.split('-').next().unwrap_or_default())
|
||||||
|
.find_or_first(|l| available_codes.contains(l));
|
||||||
|
|
||||||
|
// TODO: Save to cookies.
|
||||||
|
|
||||||
|
accept_language.unwrap_or("en").to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tr = Tr::new(&language);
|
||||||
|
|
||||||
|
// let jar = CookieJar::from_headers(req.headers());
|
||||||
|
req.extensions_mut().insert(tr);
|
||||||
|
Ok(next.run(req).await)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_current_user(
|
async fn get_current_user(
|
||||||
connection: db::Connection,
|
connection: db::Connection,
|
||||||
jar: &CookieJar,
|
jar: &CookieJar,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use axum::{
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{db, model},
|
data::{db, model},
|
||||||
html_templates::*,
|
html_templates::*,
|
||||||
ron_utils,
|
ron_utils, translation,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod fragments;
|
pub mod fragments;
|
||||||
|
|
@ -19,7 +19,11 @@ pub mod ron;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
// Will embed RON error in HTML page.
|
// Will embed RON error in HTML page.
|
||||||
pub async fn ron_error_to_html(req: Request, next: Next) -> Result<Response> {
|
pub async fn ron_error_to_html(
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
|
req: Request,
|
||||||
|
next: Next,
|
||||||
|
) -> Result<Response> {
|
||||||
let response = next.run(req).await;
|
let response = next.run(req).await;
|
||||||
|
|
||||||
if let Some(content_type) = response.headers().get(header::CONTENT_TYPE) {
|
if let Some(content_type) = response.headers().get(header::CONTENT_TYPE) {
|
||||||
|
|
@ -32,6 +36,7 @@ pub async fn ron_error_to_html(req: Request, next: Next) -> Result<Response> {
|
||||||
user: None,
|
user: None,
|
||||||
message,
|
message,
|
||||||
as_code: true,
|
as_code: true,
|
||||||
|
tr,
|
||||||
}
|
}
|
||||||
.into_response());
|
.into_response());
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +51,7 @@ pub async fn ron_error_to_html(req: Request, next: Next) -> Result<Response> {
|
||||||
pub async fn home_page(
|
pub async fn home_page(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
let recipes = Recipes {
|
let recipes = Recipes {
|
||||||
published: connection.get_all_published_recipe_titles().await?,
|
published: connection.get_all_published_recipe_titles().await?,
|
||||||
|
|
@ -59,15 +65,18 @@ pub async fn home_page(
|
||||||
current_id: None,
|
current_id: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(HomeTemplate { user, recipes })
|
Ok(HomeTemplate { user, recipes, tr })
|
||||||
}
|
}
|
||||||
|
|
||||||
///// 404 /////
|
///// 404 /////
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn not_found(Extension(user): Extension<Option<model::User>>) -> impl IntoResponse {
|
pub async fn not_found(
|
||||||
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
MessageTemplate::new_with_user("404: Not found", user),
|
MessageTemplate::new_with_user("404: Not found", tr, user),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use serde::Deserialize;
|
||||||
use crate::{
|
use crate::{
|
||||||
data::{db, model},
|
data::{db, model},
|
||||||
html_templates::*,
|
html_templates::*,
|
||||||
|
translation,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -21,6 +22,7 @@ pub async fn recipes_list_fragments(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
current_recipe: Query<CurrentRecipeId>,
|
current_recipe: Query<CurrentRecipeId>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
let recipes = Recipes {
|
let recipes = Recipes {
|
||||||
published: connection.get_all_published_recipe_titles().await?,
|
published: connection.get_all_published_recipe_titles().await?,
|
||||||
|
|
@ -33,5 +35,5 @@ pub async fn recipes_list_fragments(
|
||||||
},
|
},
|
||||||
current_id: current_recipe.current_recipe_id,
|
current_id: current_recipe.current_recipe_id,
|
||||||
};
|
};
|
||||||
Ok(RecipesListFragmentTemplate { user, recipes })
|
Ok(RecipesListFragmentTemplate { user, tr, recipes })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,20 @@ use crate::{
|
||||||
consts,
|
consts,
|
||||||
data::{db, model},
|
data::{db, model},
|
||||||
html_templates::*,
|
html_templates::*,
|
||||||
|
translation,
|
||||||
};
|
};
|
||||||
|
|
||||||
///// RECIPE /////
|
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
let recipe_id = connection.create_recipe(user.id).await?;
|
let recipe_id = connection.create_recipe(user.id).await?;
|
||||||
Ok(Redirect::to(&format!("/recipe/edit/{}", recipe_id)).into_response())
|
Ok(Redirect::to(&format!("/recipe/edit/{}", recipe_id)).into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new("Not logged in").into_response())
|
Ok(MessageTemplate::new("Not logged in", tr).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -30,10 +30,11 @@ pub async fn create(
|
||||||
pub async fn edit_recipe(
|
pub async fn edit_recipe(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
Path(recipe_id): Path<i64>,
|
Path(recipe_id): Path<i64>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
if let Some(recipe) = connection.get_recipe(recipe_id).await? {
|
if let Some(recipe) = connection.get_recipe(recipe_id, false).await? {
|
||||||
if recipe.user_id == user.id {
|
if recipe.user_id == user.id {
|
||||||
let recipes = Recipes {
|
let recipes = Recipes {
|
||||||
published: connection.get_all_published_recipe_titles().await?,
|
published: connection.get_all_published_recipe_titles().await?,
|
||||||
|
|
@ -45,19 +46,20 @@ pub async fn edit_recipe(
|
||||||
|
|
||||||
Ok(RecipeEditTemplate {
|
Ok(RecipeEditTemplate {
|
||||||
user: Some(user),
|
user: Some(user),
|
||||||
|
tr,
|
||||||
recipes,
|
recipes,
|
||||||
recipe,
|
recipe,
|
||||||
languages: *consts::LANGUAGES,
|
languages: *consts::LANGUAGES,
|
||||||
}
|
}
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new("Not allowed to edit this recipe").into_response())
|
Ok(MessageTemplate::new("Not allowed to edit this recipe", tr).into_response())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new("Recipe not found").into_response())
|
Ok(MessageTemplate::new("Recipe not found", tr).into_response())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new("Not logged in").into_response())
|
Ok(MessageTemplate::new("Not logged in", tr).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,15 +67,17 @@ pub async fn edit_recipe(
|
||||||
pub async fn view(
|
pub async fn view(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
Path(recipe_id): Path<i64>,
|
Path(recipe_id): Path<i64>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
match connection.get_recipe(recipe_id).await? {
|
match connection.get_recipe(recipe_id, true).await? {
|
||||||
Some(recipe) => {
|
Some(recipe) => {
|
||||||
if !recipe.is_published
|
if !recipe.is_published
|
||||||
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
|
&& (user.is_none() || recipe.user_id != user.as_ref().unwrap().id)
|
||||||
{
|
{
|
||||||
return Ok(MessageTemplate::new_with_user(
|
return Ok(MessageTemplate::new_with_user(
|
||||||
&format!("Not allowed the view the recipe {}", recipe_id),
|
&format!("Not allowed the view the recipe {}", recipe_id),
|
||||||
|
tr,
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
.into_response());
|
.into_response());
|
||||||
|
|
@ -93,6 +97,7 @@ pub async fn view(
|
||||||
|
|
||||||
Ok(RecipeViewTemplate {
|
Ok(RecipeViewTemplate {
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
recipes,
|
recipes,
|
||||||
recipe,
|
recipe,
|
||||||
}
|
}
|
||||||
|
|
@ -100,6 +105,7 @@ pub async fn view(
|
||||||
}
|
}
|
||||||
None => Ok(MessageTemplate::new_with_user(
|
None => Ok(MessageTemplate::new_with_user(
|
||||||
&format!("Cannot find the recipe {}", recipe_id),
|
&format!("Cannot find the recipe {}", recipe_id),
|
||||||
|
tr,
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
.into_response()),
|
.into_response()),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ use crate::{
|
||||||
data::{db, model},
|
data::{db, model},
|
||||||
email,
|
email,
|
||||||
html_templates::*,
|
html_templates::*,
|
||||||
|
translation::{self, Sentence},
|
||||||
utils, AppState,
|
utils, AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -27,9 +28,11 @@ use crate::{
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn sign_up_get(
|
pub async fn sign_up_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
Ok(SignUpFormTemplate {
|
Ok(SignUpFormTemplate {
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
email: String::new(),
|
email: String::new(),
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
message_email: String::new(),
|
message_email: String::new(),
|
||||||
|
|
@ -59,34 +62,37 @@ pub async fn sign_up_post(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
State(config): State<Config>,
|
State(config): State<Config>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
Form(form_data): Form<SignUpFormData>,
|
Form(form_data): Form<SignUpFormData>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
fn error_response(
|
fn error_response(
|
||||||
error: SignUpError,
|
error: SignUpError,
|
||||||
form_data: &SignUpFormData,
|
form_data: &SignUpFormData,
|
||||||
user: Option<model::User>,
|
user: Option<model::User>,
|
||||||
|
tr: translation::Tr,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
Ok(SignUpFormTemplate {
|
Ok(SignUpFormTemplate {
|
||||||
user,
|
user,
|
||||||
email: form_data.email.clone(),
|
email: form_data.email.clone(),
|
||||||
message_email: match error {
|
message_email: match error {
|
||||||
SignUpError::InvalidEmail => "Invalid email",
|
SignUpError::InvalidEmail => tr.t(Sentence::InvalidEmail),
|
||||||
_ => "",
|
_ => String::new(),
|
||||||
}
|
},
|
||||||
.to_string(),
|
|
||||||
message_password: match error {
|
message_password: match error {
|
||||||
SignUpError::PasswordsNotEqual => "Passwords don't match",
|
SignUpError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch),
|
||||||
SignUpError::InvalidPassword => "Password must have at least eight characters",
|
SignUpError::InvalidPassword => tr.tp(
|
||||||
_ => "",
|
Sentence::InvalidPassword,
|
||||||
}
|
&[Box::new(common::consts::MIN_PASSWORD_SIZE)],
|
||||||
.to_string(),
|
),
|
||||||
|
_ => String::new(),
|
||||||
|
},
|
||||||
message: match error {
|
message: match error {
|
||||||
SignUpError::UserAlreadyExists => "This email is not available",
|
SignUpError::UserAlreadyExists => tr.t(Sentence::EmailAlreadyTaken),
|
||||||
SignUpError::DatabaseError => "Database error",
|
SignUpError::DatabaseError => "Database error".to_string(),
|
||||||
SignUpError::UnableSendEmail => "Unable to send the validation email",
|
SignUpError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail),
|
||||||
_ => "",
|
_ => String::new(),
|
||||||
}
|
},
|
||||||
.to_string(),
|
tr,
|
||||||
}
|
}
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
@ -95,17 +101,17 @@ pub async fn sign_up_post(
|
||||||
if let common::utils::EmailValidation::NotValid =
|
if let common::utils::EmailValidation::NotValid =
|
||||||
common::utils::validate_email(&form_data.email)
|
common::utils::validate_email(&form_data.email)
|
||||||
{
|
{
|
||||||
return error_response(SignUpError::InvalidEmail, &form_data, user);
|
return error_response(SignUpError::InvalidEmail, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if form_data.password_1 != form_data.password_2 {
|
if form_data.password_1 != form_data.password_2 {
|
||||||
return error_response(SignUpError::PasswordsNotEqual, &form_data, user);
|
return error_response(SignUpError::PasswordsNotEqual, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let common::utils::PasswordValidation::TooShort =
|
if let common::utils::PasswordValidation::TooShort =
|
||||||
common::utils::validate_password(&form_data.password_1)
|
common::utils::validate_password(&form_data.password_1)
|
||||||
{
|
{
|
||||||
return error_response(SignUpError::InvalidPassword, &form_data, user);
|
return error_response(SignUpError::InvalidPassword, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
match connection
|
match connection
|
||||||
|
|
@ -113,7 +119,7 @@ pub async fn sign_up_post(
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(db::user::SignUpResult::UserAlreadyExists) => {
|
Ok(db::user::SignUpResult::UserAlreadyExists) => {
|
||||||
error_response(SignUpError::UserAlreadyExists, &form_data, user)
|
error_response(SignUpError::UserAlreadyExists, &form_data, user, tr)
|
||||||
}
|
}
|
||||||
Ok(db::user::SignUpResult::UserCreatedWaitingForValidation(token)) => {
|
Ok(db::user::SignUpResult::UserCreatedWaitingForValidation(token)) => {
|
||||||
let url = utils::get_url_from_host(&host);
|
let url = utils::get_url_from_host(&host);
|
||||||
|
|
@ -133,16 +139,16 @@ pub async fn sign_up_post(
|
||||||
Ok(()) => Ok(
|
Ok(()) => Ok(
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
"An email has been sent, follow the link to validate your account",
|
"An email has been sent, follow the link to validate your account",
|
||||||
user).into_response()),
|
tr, user).into_response()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// error!("Email validation error: {}", error); // TODO: log
|
// error!("Email validation error: {}", error); // TODO: log
|
||||||
error_response(SignUpError::UnableSendEmail, &form_data, user)
|
error_response(SignUpError::UnableSendEmail, &form_data, user, tr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// error!("Signup database error: {}", error); // TODO: log
|
// error!("Signup database error: {}", error); // TODO: log
|
||||||
error_response(SignUpError::DatabaseError, &form_data, user)
|
error_response(SignUpError::DatabaseError, &form_data, user, tr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +157,7 @@ pub async fn sign_up_post(
|
||||||
pub async fn sign_up_validation(
|
pub async fn sign_up_validation(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
Query(query): Query<HashMap<String, String>>,
|
Query(query): Query<HashMap<String, String>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
|
@ -159,7 +166,7 @@ pub async fn sign_up_validation(
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
return Ok((
|
return Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user("User already exists", user),
|
MessageTemplate::new_with_user("User already exists", tr, user),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
||||||
|
|
@ -183,6 +190,7 @@ pub async fn sign_up_validation(
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
"Email validation successful, your account has been created",
|
"Email validation successful, your account has been created",
|
||||||
|
tr,
|
||||||
user,
|
user,
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
|
@ -191,18 +199,23 @@ pub async fn sign_up_validation(
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
"The validation has expired. Try to sign up again",
|
"The validation has expired. Try to sign up again",
|
||||||
|
tr,
|
||||||
user,
|
user,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
db::user::ValidationResult::UnknownUser => Ok((
|
db::user::ValidationResult::UnknownUser => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user("Validation error. Try to sign up again", user),
|
MessageTemplate::new_with_user(
|
||||||
|
"Validation error. Try to sign up again",
|
||||||
|
tr,
|
||||||
|
user,
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Ok((
|
None => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user("Validation error", user),
|
MessageTemplate::new_with_user("Validation error", tr, user),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -212,9 +225,11 @@ pub async fn sign_up_validation(
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn sign_in_get(
|
pub async fn sign_in_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<impl IntoResponse> {
|
) -> Result<impl IntoResponse> {
|
||||||
Ok(SignInFormTemplate {
|
Ok(SignInFormTemplate {
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
email: String::new(),
|
email: String::new(),
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
})
|
})
|
||||||
|
|
@ -231,6 +246,7 @@ pub async fn sign_in_post(
|
||||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Form(form_data): Form<SignInFormData>,
|
Form(form_data): Form<SignInFormData>,
|
||||||
) -> Result<(CookieJar, Response)> {
|
) -> Result<(CookieJar, Response)> {
|
||||||
|
|
@ -251,7 +267,8 @@ pub async fn sign_in_post(
|
||||||
SignInFormTemplate {
|
SignInFormTemplate {
|
||||||
user,
|
user,
|
||||||
email: form_data.email,
|
email: form_data.email,
|
||||||
message: "This account must be validated first".to_string(),
|
message: tr.t(Sentence::AccountMustBeValidatedFirst),
|
||||||
|
tr,
|
||||||
}
|
}
|
||||||
.into_response(),
|
.into_response(),
|
||||||
)),
|
)),
|
||||||
|
|
@ -260,7 +277,8 @@ pub async fn sign_in_post(
|
||||||
SignInFormTemplate {
|
SignInFormTemplate {
|
||||||
user,
|
user,
|
||||||
email: form_data.email,
|
email: form_data.email,
|
||||||
message: "Wrong email or password".to_string(),
|
message: tr.t(Sentence::WrongEmailOrPassword),
|
||||||
|
tr,
|
||||||
}
|
}
|
||||||
.into_response(),
|
.into_response(),
|
||||||
)),
|
)),
|
||||||
|
|
@ -292,16 +310,19 @@ pub async fn sign_out(
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn ask_reset_password_get(
|
pub async fn ask_reset_password_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
Ok(MessageTemplate::new_with_user(
|
Ok(MessageTemplate::new_with_user(
|
||||||
"Can't ask to reset password when already logged in",
|
"Can't ask to reset password when already logged in",
|
||||||
|
tr,
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(AskResetPasswordTemplate {
|
Ok(AskResetPasswordTemplate {
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
email: String::new(),
|
email: String::new(),
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
message_email: String::new(),
|
message_email: String::new(),
|
||||||
|
|
@ -329,15 +350,18 @@ pub async fn ask_reset_password_post(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
State(config): State<Config>,
|
State(config): State<Config>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
Form(form_data): Form<AskResetPasswordForm>,
|
Form(form_data): Form<AskResetPasswordForm>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
fn error_response(
|
fn error_response(
|
||||||
error: AskResetPasswordError,
|
error: AskResetPasswordError,
|
||||||
email: &str,
|
email: &str,
|
||||||
user: Option<model::User>,
|
user: Option<model::User>,
|
||||||
|
tr: translation::Tr,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
Ok(AskResetPasswordTemplate {
|
Ok(AskResetPasswordTemplate {
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
email: email.to_string(),
|
email: email.to_string(),
|
||||||
message_email: match error {
|
message_email: match error {
|
||||||
AskResetPasswordError::InvalidEmail => "Invalid email",
|
AskResetPasswordError::InvalidEmail => "Invalid email",
|
||||||
|
|
@ -362,7 +386,12 @@ pub async fn ask_reset_password_post(
|
||||||
if let common::utils::EmailValidation::NotValid =
|
if let common::utils::EmailValidation::NotValid =
|
||||||
common::utils::validate_email(&form_data.email)
|
common::utils::validate_email(&form_data.email)
|
||||||
{
|
{
|
||||||
return error_response(AskResetPasswordError::InvalidEmail, &form_data.email, user);
|
return error_response(
|
||||||
|
AskResetPasswordError::InvalidEmail,
|
||||||
|
&form_data.email,
|
||||||
|
user,
|
||||||
|
tr,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
match connection
|
match connection
|
||||||
|
|
@ -376,10 +405,14 @@ pub async fn ask_reset_password_post(
|
||||||
AskResetPasswordError::EmailAlreadyReset,
|
AskResetPasswordError::EmailAlreadyReset,
|
||||||
&form_data.email,
|
&form_data.email,
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
|
),
|
||||||
|
Ok(db::user::GetTokenResetPasswordResult::EmailUnknown) => error_response(
|
||||||
|
AskResetPasswordError::EmailUnknown,
|
||||||
|
&form_data.email,
|
||||||
|
user,
|
||||||
|
tr,
|
||||||
),
|
),
|
||||||
Ok(db::user::GetTokenResetPasswordResult::EmailUnknown) => {
|
|
||||||
error_response(AskResetPasswordError::EmailUnknown, &form_data.email, user)
|
|
||||||
}
|
|
||||||
Ok(db::user::GetTokenResetPasswordResult::Ok(token)) => {
|
Ok(db::user::GetTokenResetPasswordResult::Ok(token)) => {
|
||||||
let url = utils::get_url_from_host(&host);
|
let url = utils::get_url_from_host(&host);
|
||||||
match email::send_email(
|
match email::send_email(
|
||||||
|
|
@ -396,6 +429,7 @@ pub async fn ask_reset_password_post(
|
||||||
{
|
{
|
||||||
Ok(()) => Ok(MessageTemplate::new_with_user(
|
Ok(()) => Ok(MessageTemplate::new_with_user(
|
||||||
"An email has been sent, follow the link to reset your password.",
|
"An email has been sent, follow the link to reset your password.",
|
||||||
|
tr,
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
.into_response()),
|
.into_response()),
|
||||||
|
|
@ -405,13 +439,19 @@ pub async fn ask_reset_password_post(
|
||||||
AskResetPasswordError::UnableSendEmail,
|
AskResetPasswordError::UnableSendEmail,
|
||||||
&form_data.email,
|
&form_data.email,
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
event!(Level::ERROR, "{}", error);
|
event!(Level::ERROR, "{}", error);
|
||||||
error_response(AskResetPasswordError::DatabaseError, &form_data.email, user)
|
error_response(
|
||||||
|
AskResetPasswordError::DatabaseError,
|
||||||
|
&form_data.email,
|
||||||
|
user,
|
||||||
|
tr,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -419,18 +459,20 @@ pub async fn ask_reset_password_post(
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn reset_password_get(
|
pub async fn reset_password_get(
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
Query(query): Query<HashMap<String, String>>,
|
Query(query): Query<HashMap<String, String>>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if let Some(reset_token) = query.get("reset_token") {
|
if let Some(reset_token) = query.get("reset_token") {
|
||||||
Ok(ResetPasswordTemplate {
|
Ok(ResetPasswordTemplate {
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
reset_token: reset_token.to_string(),
|
reset_token: reset_token.to_string(),
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
message_password: String::new(),
|
message_password: String::new(),
|
||||||
}
|
}
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new_with_user("Reset token missing", user).into_response())
|
Ok(MessageTemplate::new_with_user("Reset token missing", tr, user).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -452,15 +494,18 @@ enum ResetPasswordError {
|
||||||
pub async fn reset_password_post(
|
pub async fn reset_password_post(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
Form(form_data): Form<ResetPasswordForm>,
|
Form(form_data): Form<ResetPasswordForm>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
fn error_response(
|
fn error_response(
|
||||||
error: ResetPasswordError,
|
error: ResetPasswordError,
|
||||||
form_data: &ResetPasswordForm,
|
form_data: &ResetPasswordForm,
|
||||||
user: Option<model::User>,
|
user: Option<model::User>,
|
||||||
|
tr: translation::Tr,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
Ok(ResetPasswordTemplate {
|
Ok(ResetPasswordTemplate {
|
||||||
user,
|
user,
|
||||||
|
tr,
|
||||||
reset_token: form_data.reset_token.clone(),
|
reset_token: form_data.reset_token.clone(),
|
||||||
message_password: match error {
|
message_password: match error {
|
||||||
ResetPasswordError::PasswordsNotEqual => "Passwords don't match",
|
ResetPasswordError::PasswordsNotEqual => "Passwords don't match",
|
||||||
|
|
@ -481,13 +526,13 @@ pub async fn reset_password_post(
|
||||||
}
|
}
|
||||||
|
|
||||||
if form_data.password_1 != form_data.password_2 {
|
if form_data.password_1 != form_data.password_2 {
|
||||||
return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, user);
|
return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let common::utils::PasswordValidation::TooShort =
|
if let common::utils::PasswordValidation::TooShort =
|
||||||
common::utils::validate_password(&form_data.password_1)
|
common::utils::validate_password(&form_data.password_1)
|
||||||
{
|
{
|
||||||
return error_response(ResetPasswordError::InvalidPassword, &form_data, user);
|
return error_response(ResetPasswordError::InvalidPassword, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
match connection
|
match connection
|
||||||
|
|
@ -498,34 +543,39 @@ pub async fn reset_password_post(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(db::user::ResetPasswordResult::Ok) => Ok(MessageTemplate::new_with_user(
|
Ok(db::user::ResetPasswordResult::Ok) => {
|
||||||
"Your password has been reset",
|
Ok(
|
||||||
user,
|
MessageTemplate::new_with_user("Your password has been reset", tr, user)
|
||||||
|
.into_response(),
|
||||||
)
|
)
|
||||||
.into_response()),
|
|
||||||
Ok(db::user::ResetPasswordResult::ResetTokenExpired) => {
|
|
||||||
error_response(ResetPasswordError::TokenExpired, &form_data, user)
|
|
||||||
}
|
}
|
||||||
Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user),
|
Ok(db::user::ResetPasswordResult::ResetTokenExpired) => {
|
||||||
|
error_response(ResetPasswordError::TokenExpired, &form_data, user, tr)
|
||||||
|
}
|
||||||
|
Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user, tr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// EDIT PROFILE ///
|
/// EDIT PROFILE ///
|
||||||
|
|
||||||
#[debug_handler]
|
#[debug_handler]
|
||||||
pub async fn edit_user_get(Extension(user): Extension<Option<model::User>>) -> Response {
|
pub async fn edit_user_get(
|
||||||
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
|
) -> Response {
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
ProfileTemplate {
|
ProfileTemplate {
|
||||||
username: user.name.clone(),
|
username: user.name.clone(),
|
||||||
email: user.email.clone(),
|
email: user.email.clone(),
|
||||||
user: Some(user),
|
|
||||||
message: String::new(),
|
message: String::new(),
|
||||||
message_email: String::new(),
|
message_email: String::new(),
|
||||||
message_password: String::new(),
|
message_password: String::new(),
|
||||||
|
user: Some(user),
|
||||||
|
tr,
|
||||||
}
|
}
|
||||||
.into_response()
|
.into_response()
|
||||||
} else {
|
} else {
|
||||||
MessageTemplate::new("Not logged in").into_response()
|
MessageTemplate::new("Not logged in", tr).into_response()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -552,6 +602,7 @@ pub async fn edit_user_post(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
State(config): State<Config>,
|
State(config): State<Config>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
Form(form_data): Form<EditUserForm>,
|
Form(form_data): Form<EditUserForm>,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
|
|
@ -559,6 +610,7 @@ pub async fn edit_user_post(
|
||||||
error: ProfileUpdateError,
|
error: ProfileUpdateError,
|
||||||
form_data: &EditUserForm,
|
form_data: &EditUserForm,
|
||||||
user: model::User,
|
user: model::User,
|
||||||
|
tr: translation::Tr,
|
||||||
) -> Result<Response> {
|
) -> Result<Response> {
|
||||||
Ok(ProfileTemplate {
|
Ok(ProfileTemplate {
|
||||||
user: Some(user),
|
user: Some(user),
|
||||||
|
|
@ -584,6 +636,7 @@ pub async fn edit_user_post(
|
||||||
_ => "",
|
_ => "",
|
||||||
}
|
}
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
tr,
|
||||||
}
|
}
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
@ -591,17 +644,17 @@ pub async fn edit_user_post(
|
||||||
if let common::utils::EmailValidation::NotValid =
|
if let common::utils::EmailValidation::NotValid =
|
||||||
common::utils::validate_email(&form_data.email)
|
common::utils::validate_email(&form_data.email)
|
||||||
{
|
{
|
||||||
return error_response(ProfileUpdateError::InvalidEmail, &form_data, user);
|
return error_response(ProfileUpdateError::InvalidEmail, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_password = if !form_data.password_1.is_empty() || !form_data.password_2.is_empty() {
|
let new_password = if !form_data.password_1.is_empty() || !form_data.password_2.is_empty() {
|
||||||
if form_data.password_1 != form_data.password_2 {
|
if form_data.password_1 != form_data.password_2 {
|
||||||
return error_response(ProfileUpdateError::PasswordsNotEqual, &form_data, user);
|
return error_response(ProfileUpdateError::PasswordsNotEqual, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
if let common::utils::PasswordValidation::TooShort =
|
if let common::utils::PasswordValidation::TooShort =
|
||||||
common::utils::validate_password(&form_data.password_1)
|
common::utils::validate_password(&form_data.password_1)
|
||||||
{
|
{
|
||||||
return error_response(ProfileUpdateError::InvalidPassword, &form_data, user);
|
return error_response(ProfileUpdateError::InvalidPassword, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
Some(form_data.password_1.as_ref())
|
Some(form_data.password_1.as_ref())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -621,7 +674,7 @@ pub async fn edit_user_post(
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(db::user::UpdateUserResult::EmailAlreadyTaken) => {
|
Ok(db::user::UpdateUserResult::EmailAlreadyTaken) => {
|
||||||
return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, user);
|
return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
Ok(db::user::UpdateUserResult::UserUpdatedWaitingForRevalidation(token)) => {
|
Ok(db::user::UpdateUserResult::UserUpdatedWaitingForRevalidation(token)) => {
|
||||||
let url = utils::get_url_from_host(&host);
|
let url = utils::get_url_from_host(&host);
|
||||||
|
|
@ -644,14 +697,17 @@ pub async fn edit_user_post(
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// error!("Email validation error: {}", error); // TODO: log
|
// error!("Email validation error: {}", error); // TODO: log
|
||||||
return error_response(ProfileUpdateError::UnableSendEmail, &form_data, user);
|
return error_response(
|
||||||
|
ProfileUpdateError::UnableSendEmail, &form_data, user, tr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(db::user::UpdateUserResult::Ok) => {
|
Ok(db::user::UpdateUserResult::Ok) => {
|
||||||
message = "Profile saved";
|
message = "Profile saved";
|
||||||
}
|
}
|
||||||
Err(_) => return error_response(ProfileUpdateError::DatabaseError, &form_data, user),
|
Err(_) => {
|
||||||
|
return error_response(ProfileUpdateError::DatabaseError, &form_data, user, tr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload after update.
|
// Reload after update.
|
||||||
|
|
@ -664,10 +720,11 @@ pub async fn edit_user_post(
|
||||||
message: message.to_string(),
|
message: message.to_string(),
|
||||||
message_email: String::new(),
|
message_email: String::new(),
|
||||||
message_password: String::new(),
|
message_password: String::new(),
|
||||||
|
tr,
|
||||||
}
|
}
|
||||||
.into_response())
|
.into_response())
|
||||||
} else {
|
} else {
|
||||||
Ok(MessageTemplate::new("Not logged in").into_response())
|
Ok(MessageTemplate::new("Not logged in", tr).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -675,6 +732,7 @@ pub async fn edit_user_post(
|
||||||
pub async fn email_revalidation(
|
pub async fn email_revalidation(
|
||||||
State(connection): State<db::Connection>,
|
State(connection): State<db::Connection>,
|
||||||
Extension(user): Extension<Option<model::User>>,
|
Extension(user): Extension<Option<model::User>>,
|
||||||
|
Extension(tr): Extension<translation::Tr>,
|
||||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
Query(query): Query<HashMap<String, String>>,
|
Query(query): Query<HashMap<String, String>>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
|
|
@ -683,7 +741,7 @@ pub async fn email_revalidation(
|
||||||
if user.is_some() {
|
if user.is_some() {
|
||||||
return Ok((
|
return Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user("User already exists", user),
|
MessageTemplate::new_with_user("User already exists", tr, user),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr);
|
||||||
|
|
@ -705,13 +763,14 @@ pub async fn email_revalidation(
|
||||||
let user = connection.load_user(user_id).await?;
|
let user = connection.load_user(user_id).await?;
|
||||||
Ok((
|
Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user("Email validation successful", user),
|
MessageTemplate::new_with_user("Email validation successful", tr, user),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
db::user::ValidationResult::ValidationExpired => Ok((
|
db::user::ValidationResult::ValidationExpired => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
"The validation has expired. Try to sign up again with the same email",
|
"The validation has expired. Try to sign up again with the same email",
|
||||||
|
tr,
|
||||||
user,
|
user,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
|
@ -719,6 +778,7 @@ pub async fn email_revalidation(
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user(
|
MessageTemplate::new_with_user(
|
||||||
"Validation error. Try to sign up again with the same email",
|
"Validation error. Try to sign up again with the same email",
|
||||||
|
tr,
|
||||||
user,
|
user,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
|
@ -726,7 +786,7 @@ pub async fn email_revalidation(
|
||||||
}
|
}
|
||||||
None => Ok((
|
None => Ok((
|
||||||
jar,
|
jar,
|
||||||
MessageTemplate::new_with_user("Validation error", user),
|
MessageTemplate::new_with_user("Validation error", tr, user),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
142
backend/src/translation.rs
Normal file
142
backend/src/translation.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
use std::{fs::File, sync::LazyLock};
|
||||||
|
|
||||||
|
use ron::de::from_reader;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::{event, Level};
|
||||||
|
|
||||||
|
use crate::consts;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Clone)]
|
||||||
|
pub enum Sentence {
|
||||||
|
ProfileTitle,
|
||||||
|
MainTitle,
|
||||||
|
CreateNewRecipe,
|
||||||
|
UnpublishedRecipes,
|
||||||
|
UntitledRecipe,
|
||||||
|
|
||||||
|
EmailAddress,
|
||||||
|
Password,
|
||||||
|
|
||||||
|
// Sign in page.
|
||||||
|
SignInMenu,
|
||||||
|
SignInTitle,
|
||||||
|
SignInButton,
|
||||||
|
WrongEmailOrPassword,
|
||||||
|
|
||||||
|
// Sign up page.
|
||||||
|
SignUpMenu,
|
||||||
|
SignUpTitle,
|
||||||
|
SignUpButton,
|
||||||
|
ChooseAPassword,
|
||||||
|
ReEnterPassword,
|
||||||
|
AccountMustBeValidatedFirst,
|
||||||
|
InvalidEmail,
|
||||||
|
PasswordDontMatch,
|
||||||
|
InvalidPassword,
|
||||||
|
EmailAlreadyTaken,
|
||||||
|
UnableToSendEmail,
|
||||||
|
|
||||||
|
// Reset password page.
|
||||||
|
LostPassword,
|
||||||
|
AskResetButton,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Tr {
|
||||||
|
lang: &'static Language,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tr {
|
||||||
|
pub fn new(code: &str) -> Self {
|
||||||
|
for lang in TRANSLATIONS.iter() {
|
||||||
|
if lang.code == code {
|
||||||
|
return Self { lang };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event!(
|
||||||
|
Level::WARN,
|
||||||
|
"Unable to find translation for language {}",
|
||||||
|
code
|
||||||
|
);
|
||||||
|
|
||||||
|
Tr::new("en")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn t(&self, sentence: Sentence) -> String {
|
||||||
|
match self.lang.translation.get(&sentence) {
|
||||||
|
Some(str) => str.clone(),
|
||||||
|
None => format!(
|
||||||
|
"Translation missing, lang: {}/{}, element: {:?}",
|
||||||
|
self.lang.name, self.lang.code, sentence
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tp(&self, sentence: Sentence, params: &[Box<dyn ToString>]) -> String {
|
||||||
|
match self.lang.translation.get(&sentence) {
|
||||||
|
Some(str) => {
|
||||||
|
let mut result = str.clone();
|
||||||
|
for p in params {
|
||||||
|
result = result.replacen("{}", &p.to_string(), 1);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
None => format!(
|
||||||
|
"Translation missing, lang: {}/{}, element: {:?}",
|
||||||
|
self.lang.name, self.lang.code, sentence
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn available_languages() -> Vec<(&'static str, &'static str)> {
|
||||||
|
TRANSLATIONS
|
||||||
|
.iter()
|
||||||
|
.map(|tr| (tr.code.as_ref(), tr.name.as_ref()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn available_codes() -> Vec<&'static str> {
|
||||||
|
TRANSLATIONS.iter().map(|tr| tr.code.as_ref()).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[macro_export]
|
||||||
|
// macro_rules! t {
|
||||||
|
// ($self:expr, $str:expr) => {
|
||||||
|
// $self.t($str)
|
||||||
|
// };
|
||||||
|
// ($self:expr, $str:expr, $( $x:expr ),+ ) => {
|
||||||
|
// {
|
||||||
|
// let mut result = $self.t($str);
|
||||||
|
// $( result = result.replacen("{}", &$x.to_string(), 1); )+
|
||||||
|
// result
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
struct Language {
|
||||||
|
code: String,
|
||||||
|
name: String,
|
||||||
|
translation: FxHashMap<Sentence, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static TRANSLATIONS: LazyLock<Vec<Language>> =
|
||||||
|
LazyLock::new(|| match File::open(consts::TRANSLATION_FILE) {
|
||||||
|
Ok(file) => from_reader(file).unwrap_or_else(|error| {
|
||||||
|
panic!(
|
||||||
|
"Failed to read translation file {}: {}",
|
||||||
|
consts::TRANSLATION_FILE,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Err(error) => {
|
||||||
|
panic!(
|
||||||
|
"Failed to open translation file {}: {}",
|
||||||
|
consts::TRANSLATION_FILE,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
{% block main_container %}
|
{% block main_container %}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
<h1></h1>
|
||||||
<form action="/ask_reset_password" method="post">
|
<form action="/ask_reset_password" method="post">
|
||||||
<label for="email_field">Your email address</label>
|
<label for="email_field">Your email address</label>
|
||||||
<input id="email_field" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
<input id="email_field" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
{% match user %}
|
{% match user %}
|
||||||
{% when Some with (user) %}
|
{% when Some with (user) %}
|
||||||
<a class="create-recipe" href="/recipe/new" >Create a new recipe</a>
|
<a class="create-recipe" href="/recipe/new" >{{ tr.t(Sentence::CreateNewRecipe) }}</a>
|
||||||
<span><a href="/user/edit">
|
<span><a href="/user/edit">
|
||||||
{% if user.name == "" %}
|
{% if user.name == "" %}
|
||||||
{{ user.email }}
|
{{ user.email }}
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
</a> / <a href="/signout" />Sign out</a></span>
|
</a> / <a href="/signout" />Sign out</a></span>
|
||||||
{% when None %}
|
{% when None %}
|
||||||
<span>
|
<span>
|
||||||
<a href="/signin" >Sign in</a>/<a href="/signup">Sign up</a>/<a href="/ask_reset_password">Lost password</a>
|
<a href="/signin" >{{ tr.t(Sentence::SignInMenu) }}</a>/<a href="/signup">{{ tr.t(Sentence::SignUpMenu) }}</a>/<a href="/ask_reset_password">{{ tr.t(Sentence::LostPassword) }}</a>
|
||||||
</span>
|
</span>
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<pre><code>
|
<pre><code>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ message|markdown }}
|
{{ message }}
|
||||||
|
|
||||||
{% if as_code %}
|
{% if as_code %}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
{% when Some with (user) %}
|
{% when Some with (user) %}
|
||||||
|
|
||||||
<div class="content" id="user-edit">
|
<div class="content" id="user-edit">
|
||||||
<h1>Profile</h1>
|
<h1>{{ tr.t(Sentence::ProfileTitle) }}</h1>
|
||||||
|
|
||||||
<form action="/user/edit" method="post">
|
<form action="/user/edit" method="post">
|
||||||
|
|
||||||
<label for="input-name">Name</label>
|
<label for="input-name">Name</label>
|
||||||
|
|
@ -20,7 +21,9 @@
|
||||||
autofocus="autofocus" />
|
autofocus="autofocus" />
|
||||||
|
|
||||||
<label for="input-email">Email (need to be revalidated if changed)</label>
|
<label for="input-email">Email (need to be revalidated if changed)</label>
|
||||||
<input id="input-email" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
<input id="input-email" type="email"
|
||||||
|
name="email" value="{{ email }}"
|
||||||
|
autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
||||||
|
|
||||||
{{ message_email }}
|
{{ message_email }}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
{% if !recipe.description.is_empty() %}
|
{% if !recipe.description.is_empty() %}
|
||||||
<div class="recipe-description" >
|
<div class="recipe-description" >
|
||||||
{{ recipe.description.clone()|markdown }}
|
{{ recipe.description.clone() }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
{% macro recipe_item(id, title, class) %}
|
{% macro recipe_item(id, title, class) %}
|
||||||
<a href="/recipe/view/{{ id }}" class="{{ class }}" id="recipe-{{ id }}">
|
<a href="/recipe/view/{{ id }}" class="{{ class }}" id="recipe-{{ id }}">
|
||||||
{% if title == "" %}
|
{% if title == "" %}
|
||||||
{# TODO: Translation #}
|
{{ tr.t(Sentence::UntitledRecipe) }}
|
||||||
Untitled recipe
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ title }}
|
{{ title }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -13,7 +12,7 @@
|
||||||
<div id="recipes-list">
|
<div id="recipes-list">
|
||||||
|
|
||||||
{% if !recipes.unpublished.is_empty() %}
|
{% if !recipes.unpublished.is_empty() %}
|
||||||
Unpublished recipes
|
{{ tr.t(Sentence::UnpublishedRecipes) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<nav class="recipes-list-unpublished">
|
<nav class="recipes-list-unpublished">
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@
|
||||||
|
|
||||||
<div class="content" id="sign-in">
|
<div class="content" id="sign-in">
|
||||||
|
|
||||||
<h1>Sign in</h1>
|
<h1>{{ tr.t(Sentence::SignInTitle) }}</h1>
|
||||||
|
|
||||||
<form action="/signin" method="post">
|
<form action="/signin" method="post">
|
||||||
<label for="input-email">Email address</label>
|
<label for="input-email">{{ tr.t(Sentence::EmailAddress) }}</label>
|
||||||
<input id="input-email" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
<input id="input-email" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
||||||
|
|
||||||
<label for="input-password">Password</label>
|
<label for="input-password">{{ tr.t(Sentence::Password) }}</label>
|
||||||
<input id="input-password" type="password" name="password" autocomplete="current-password" />
|
<input id="input-password" type="password" name="password" autocomplete="current-password" />
|
||||||
|
|
||||||
<input type="submit" value="Sign in" />
|
<input type="submit" value="{{ tr.t(Sentence::SignInMenu) }}" />
|
||||||
</form>
|
</form>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,27 @@
|
||||||
|
|
||||||
<div class="content" id="sign-up">
|
<div class="content" id="sign-up">
|
||||||
|
|
||||||
<h1>Sign up</h1>
|
<h1>{{ tr.t(Sentence::SignUpTitle) }}</h1>
|
||||||
|
|
||||||
<form action="/signup" method="post">
|
<form action="/signup" method="post">
|
||||||
<label for="input-email">Your email address</label>
|
<label for="input-email">{{ tr.t(Sentence::EmailAddress) }}</label>
|
||||||
<input id="input-email" type="email" name="email" value="{{ email }}" autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
<input id="input-email" type="email"
|
||||||
|
name="email" value="{{ email }}"
|
||||||
|
autocapitalize="none" autocomplete="email" autofocus="autofocus" />
|
||||||
|
|
||||||
{{ message_email }}
|
{{ message_email }}
|
||||||
|
|
||||||
<label for="input-password-1">Choose a password (minimum 8 characters)</label>
|
<label for="input-password-1">
|
||||||
|
{{ 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" />
|
<input id="input-password-1" type="password" name="password_1" autocomplete="new-password" />
|
||||||
|
|
||||||
<label for="input-password-2">Re-enter password</label>
|
<label for="input-password-2">{{ tr.t(Sentence::ReEnterPassword) }}</label>
|
||||||
<input id="input-password-2" type="password" name="password_2" autocomplete="new-password" />
|
<input id="input-password-2" type="password" name="password_2" autocomplete="new-password" />
|
||||||
|
|
||||||
{{ message_password }}
|
{{ message_password }}
|
||||||
|
|
||||||
<input type="submit" name="commit" value="Sign up" />
|
<input type="submit" name="commit" value="{{ tr.t(Sentence::SignUpButton) }}" />
|
||||||
</form>
|
</form>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
<a class="title" href="/">~~ Recettes de cuisine ~~</a>
|
<a class="title" href="/">{{ tr.t(Sentence::MainTitle) }}</a>
|
||||||
70
backend/translation.ron
Normal file
70
backend/translation.ron
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
[
|
||||||
|
(
|
||||||
|
code: "en",
|
||||||
|
name: "English",
|
||||||
|
translation: {
|
||||||
|
ProfileTitle: "Profile",
|
||||||
|
MainTitle: "Cooking Recipes",
|
||||||
|
CreateNewRecipe: "Create a new recipe",
|
||||||
|
UnpublishedRecipes: "Unpublished recipes",
|
||||||
|
UntitledRecipe: "Untitled recipe",
|
||||||
|
|
||||||
|
EmailAddress: "Email address",
|
||||||
|
Password: "Password",
|
||||||
|
|
||||||
|
SignInMenu: "Sign in",
|
||||||
|
SignInTitle: "Sign in",
|
||||||
|
SignInButton: "Sign in",
|
||||||
|
WrongEmailOrPassword: "Wrong email or password",
|
||||||
|
AccountMustBeValidatedFirst: "This account must be validated first",
|
||||||
|
InvalidEmail: "Invalid email",
|
||||||
|
PasswordDontMatch: "Passwords don't match",
|
||||||
|
InvalidPassword: "Password must have at least {} characters",
|
||||||
|
EmailAlreadyTaken: "This email is not available",
|
||||||
|
UnableToSendEmail: "Unable to send the validation email",
|
||||||
|
|
||||||
|
SignUpMenu: "Sign up",
|
||||||
|
SignUpTitle: "Sign up",
|
||||||
|
SignUpButton: "Sign up",
|
||||||
|
ChooseAPassword: "Choose a password (minimum {} characters)",
|
||||||
|
ReEnterPassword: "Re-enter password",
|
||||||
|
|
||||||
|
LostPassword: "Lost password",
|
||||||
|
AskResetButton: "Ask reset",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
(
|
||||||
|
code: "fr",
|
||||||
|
name: "Français",
|
||||||
|
translation: {
|
||||||
|
ProfileTitle: "Profile",
|
||||||
|
MainTitle: "Recette de Cuisine",
|
||||||
|
CreateNewRecipe: "Créer une nouvelle recette",
|
||||||
|
UnpublishedRecipes: "Recettes non-publiés",
|
||||||
|
UntitledRecipe: "Recette sans nom",
|
||||||
|
|
||||||
|
EmailAddress: "Adresse email",
|
||||||
|
Password: "Mot de passe",
|
||||||
|
|
||||||
|
SignInMenu: "Se connecter",
|
||||||
|
SignInTitle: "Se connecter",
|
||||||
|
SignInButton: "Se connecter",
|
||||||
|
WrongEmailOrPassword: "Mot de passe ou email invalide",
|
||||||
|
AccountMustBeValidatedFirst: "Ce compte doit d'abord être validé",
|
||||||
|
InvalidEmail: "Adresse email invalide",
|
||||||
|
PasswordDontMatch: "Les mots de passe ne correspondent pas",
|
||||||
|
InvalidPassword: "Le mot de passe doit avoir au moins {} caractères",
|
||||||
|
EmailAlreadyTaken: "Cette adresse email n'est pas disponible",
|
||||||
|
UnableToSendEmail: "L'email de validation n'a pas pu être envoyé",
|
||||||
|
|
||||||
|
SignUpMenu: "S'inscrire",
|
||||||
|
SignUpTitle: "Inscription",
|
||||||
|
SignUpButton: "Valider",
|
||||||
|
ChooseAPassword: "Choisir un mot de passe (minimum {} caractères)",
|
||||||
|
ReEnterPassword: "Entrez à nouveau le mot de passe",
|
||||||
|
|
||||||
|
LostPassword: "Mot de passe perdu",
|
||||||
|
AskResetButton: "Demander la réinitialisation",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
1
common/src/consts.rs
Normal file
1
common/src/consts.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub const MIN_PASSWORD_SIZE: usize = 8;
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
pub mod consts;
|
||||||
pub mod ron_api;
|
pub mod ron_api;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ use std::sync::LazyLock;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
use crate::consts;
|
||||||
|
|
||||||
pub enum EmailValidation {
|
pub enum EmailValidation {
|
||||||
Ok,
|
Ok,
|
||||||
NotValid,
|
NotValid,
|
||||||
|
|
@ -28,7 +30,7 @@ pub enum PasswordValidation {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_password(password: &str) -> PasswordValidation {
|
pub fn validate_password(password: &str) -> PasswordValidation {
|
||||||
if password.len() < 8 {
|
if password.len() < consts::MIN_PASSWORD_SIZE {
|
||||||
PasswordValidation::TooShort
|
PasswordValidation::TooShort
|
||||||
} else {
|
} else {
|
||||||
PasswordValidation::Ok
|
PasswordValidation::Ok
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ def main [host: string, destination: string, ssh_key: path] {
|
||||||
invoke_ssh [rm -rf recipes/static]
|
invoke_ssh [rm -rf recipes/static]
|
||||||
copy_ssh ./backend/static/ $destination
|
copy_ssh ./backend/static/ $destination
|
||||||
copy_ssh ./backend/sql/ $destination
|
copy_ssh ./backend/sql/ $destination
|
||||||
|
copy_ssh ./backend/translation.ron $destination
|
||||||
invoke_ssh [chmod u+x recipes/recipes]
|
invoke_ssh [chmod u+x recipes/recipes]
|
||||||
invoke_ssh [sudo systemctl start recipes]
|
invoke_ssh [sudo systemctl start recipes]
|
||||||
print "Deployment finished"
|
print "Deployment finished"
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,9 @@ use gloo::{
|
||||||
};
|
};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen_futures::spawn_local;
|
use wasm_bindgen_futures::spawn_local;
|
||||||
use web_sys::{
|
use web_sys::{Element, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement, KeyboardEvent};
|
||||||
Element, Event, HtmlDialogElement, HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement,
|
|
||||||
KeyboardEvent,
|
|
||||||
};
|
|
||||||
|
|
||||||
use common::ron_api::{self, Ingredient};
|
use common::ron_api;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
modal_dialog, request,
|
modal_dialog, request,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue