From f985d036d12895de54cf96f61deb8e91b766da69 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 25 Jan 2024 01:04:12 -0500 Subject: [PATCH 01/49] Use CI environment variables --- .gitlab-ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b2551cc..2361f47 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ # Build the project build: needs: [] - image: registry.mregirouard.com/libretunes/ops/docker-leptos:latest + image: $CI_REGISTRY/libretunes/ops/docker-leptos:latest script: - cargo-leptos build @@ -12,18 +12,18 @@ docker-build: script: - /usr/local/bin/dockerd-entrypoint.sh & - while ! docker info; do echo "Waiting for Docker to become available..."; sleep 1; done - - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.mregirouard.com - - docker build -t registry.mregirouard.com/libretunes/libretunes:$CI_COMMIT_SHORT_SHA . + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA . # If running on the default branch, tag as latest - if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then docker tag - registry.mregirouard.com/libretunes/libretunes:$CI_COMMIT_SHORT_SHA - registry.mregirouard.com/libretunes/libretunes:latest; fi - - docker push registry.mregirouard.com/libretunes/libretunes --all-tags + $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA + $CI_REGISTRY_IMAGE:latest; fi + - docker push $CI_REGISTRY_IMAGE --all-tags # Run unit tests test: needs: ["build"] - image: registry.mregirouard.com/libretunes/ops/docker-leptos:latest + image: $CI_REGISTRY/libretunes/ops/docker-leptos:latest script: - cargo-leptos test From ec9b5154842abb59d3d730296634f2d31b58a63a Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 28 Jan 2024 15:21:50 -0500 Subject: [PATCH 02/49] Make serde dependency mandatory --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ea9ed07..5952557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ leptos_icons = { version = "0.1.0", default_features = false, features = [ dotenv = { version = "0.15.0", optional = true } diesel = { version = "2.1.4", features = ["postgres", "r2d2"], optional = true } lazy_static = { version = "1.4.0", optional = true } -serde = { versions = "1.0.195", features = ["derive"], optional = true } +serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } [features] @@ -43,7 +43,6 @@ ssr = [ "dotenv", "diesel", "lazy_static", - "serde", "openssl", ] From 770dd4d2cdd8380db909045f6d2da972838888e8 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 28 Jan 2024 15:29:23 -0500 Subject: [PATCH 03/49] Add types for users --- src/lib.rs | 7 +++++++ src/models.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/models.rs diff --git a/src/lib.rs b/src/lib.rs index 4be1145..28db59f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,15 @@ pub mod songdata; pub mod playstatus; pub mod playbar; pub mod database; +pub mod models; use cfg_if::cfg_if; +cfg_if! { + if #[cfg(feature = "ssr")] { + pub mod schema; + } +} + cfg_if! { if #[cfg(feature = "hydrate")] { diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..2daf3ae --- /dev/null +++ b/src/models.rs @@ -0,0 +1,45 @@ +use std::time::SystemTime; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "ssr")] +use diesel::prelude::*; + +// These "models" are used to represent the data in the database +// Diesel uses these models to generate the SQL queries that are used to interact with the database. +// These types are also used for API endpoints, for consistency. Because the file must be compiled +// for both the server and the client, we use the `cfg_attr` attribute to conditionally add +// diesel-specific attributes to the models when compiling for the server + +/// Model for a "User", used for querying the database +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] +pub struct User { + /// A unique id for the user + pub id: i32, + /// The user's username + pub username: String, + /// The user's email + pub email: String, + /// The user's password, stored as a hash + pub password: String, + /// The time the user was created + pub created_at: SystemTime, +} + +/// Model for a "New User", used for inserting into the database +/// Note that this model does not have an id or created_at field, as those are automatically +/// generated by the database and we don't want to deal with them ourselves +#[cfg_attr(feature = "ssr", derive(Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct NewUser { + /// The user's username + pub username: String, + /// The user's email + pub email: String, + /// The user's password, stored as a hash + pub password: String, +} From e807cc7468f9cc4a94c72531c60095b57e012706 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 28 Jan 2024 17:10:28 -0500 Subject: [PATCH 04/49] Add CI jobs to manage ArgoCD review environments --- .gitlab-ci.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2361f47..ef998d5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,3 +36,36 @@ cargo-doc: artifacts: paths: - target/doc + +.argocd: + image: argoproj/argocd:v2.6.15 + before_script: + - argocd login ${ARGOCD_SERVER} --username ${ARGOCD_USERNAME} --password ${ARGOCD_PASSWORD} --grpc-web + +# Start the review environment +start-review: + extends: .argocd + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: manual + script: + - argocd app sync argocd/libretunes-review-${CI_COMMIT_SHORT_SHA} + - argocd app wait argocd/libretunes-review-${CI_COMMIT_SHORT_SHA} + environment: + name: review/$CI_COMMIT_SHORT_SHA + url: https://review-$CI_COMMIT_SHORT_SHA.libretunes.mregirouard.com + on_stop: stop-review + +# Stop the review environment +stop-review: + needs: ["start-review"] + extends: .argocd + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + when: manual + allow_failure: true + script: + - argocd app delete argocd/libretunes-review-${CI_COMMIT_SHORT_SHA} --cascade + environment: + name: review/$CI_COMMIT_SHORT_SHA + action: stop From ce7866142d3aa66ade673db9a986f8a19aaf431b Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 30 Jan 2024 17:22:08 -0500 Subject: [PATCH 05/49] Add .sass-cache to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8a41ec5..ab27e5e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ playwright/.cache/ # Environment variables .env + +# Sass cache +.sass-cache From f5ffba390753e764aac8f5883c6780ee222f6826 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 16:09:03 -0500 Subject: [PATCH 06/49] Don't update status on every time_update --- src/playbar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playbar.rs b/src/playbar.rs index 60b3b95..5aa8a90 100644 --- a/src/playbar.rs +++ b/src/playbar.rs @@ -396,7 +396,7 @@ pub fn PlayBar(status: RwSignal) -> impl IntoView { }; let on_time_update = move |_| { - status.update(|status| { + status.with_untracked(|status| { if let Some(audio) = status.get_audio() { set_elapsed_secs(audio.current_time() as i64); set_total_secs(audio.duration() as i64); From 00ca15ef2c89f19177f4b4e7235f3e0c322ab9c6 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 21:40:00 -0500 Subject: [PATCH 07/49] Add CI job for all tests Rename existing test job to leptos-tests --- .gitlab-ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ef998d5..c74cbba 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,13 +20,20 @@ docker-build: $CI_REGISTRY_IMAGE:latest; fi - docker push $CI_REGISTRY_IMAGE --all-tags -# Run unit tests -test: +# Run leptos tests +leptos-tests: needs: ["build"] image: $CI_REGISTRY/libretunes/ops/docker-leptos:latest script: - cargo-leptos test +# Run all tests +tests: + needs: ["build"] + image: $CI_REGISTRY/libretunes/ops/docker-leptos:latest + script: + - cargo test --all-targets --all-features + # Generate docs cargo-doc: needs: [] From 3ef53b7d480f3cfaa4b55e835d3cc9db116fe0b2 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Mon, 29 Jan 2024 19:52:02 -0500 Subject: [PATCH 08/49] Add actix_identity package --- Cargo.lock | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + 2 files changed, 189 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f2a07e..d297a86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,7 +53,7 @@ dependencies = [ "actix-service", "actix-utils", "ahash 0.8.6", - "base64", + "base64 0.21.5", "bitflags 2.4.1", "brotli", "bytes", @@ -81,6 +81,22 @@ dependencies = [ "zstd", ] +[[package]] +name = "actix-identity" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1478456bca71c7b04411da1afb0c506e16dec6863815207693b791247511027f" +dependencies = [ + "actix-service", + "actix-session", + "actix-utils", + "actix-web", + "derive_more", + "futures-core", + "serde", + "tracing", +] + [[package]] name = "actix-macros" version = "0.2.4" @@ -142,6 +158,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-session" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b671404ec72194d8af58c2bdaf51e3c477a0595056bd5010148405870dda8df2" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "derive_more", + "serde", + "serde_json", + "tracing", +] + [[package]] name = "actix-utils" version = "3.0.1" @@ -219,6 +251,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.7" @@ -356,6 +423,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64" version = "0.21.5" @@ -547,6 +620,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "collection_literals" version = "1.0.1" @@ -618,7 +701,14 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", "percent-encoding", + "rand", + "sha2", + "subtle", "time", "version_check", ] @@ -664,9 +754,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.14.4" @@ -778,6 +878,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -971,6 +1072,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1060,6 +1171,24 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html-escape" version = "0.2.13" @@ -1184,6 +1313,15 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1434,7 +1572,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22207568e096ac153ba8da68635e3136c1ec614ea9012736fa861c05bfb2eeff" dependencies = [ - "base64", + "base64 0.21.5", "cfg-if", "futures", "indexmap", @@ -1514,6 +1652,7 @@ name = "libretunes" version = "0.1.0" dependencies = [ "actix-files", + "actix-identity", "actix-web", "cfg-if", "console_error_panic_hook", @@ -1680,6 +1819,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.63" @@ -1803,6 +1948,18 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2042,7 +2199,7 @@ version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -2318,6 +2475,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2374,6 +2542,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -2651,6 +2825,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "url" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 5952557..fb58d19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ diesel = { version = "2.1.4", features = ["postgres", "r2d2"], optional = true } lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } +actix-identity = { version = "0.7.0", optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -44,6 +45,7 @@ ssr = [ "diesel", "lazy_static", "openssl", + "actix-identity", ] # Defines a size-optimized profile for the WASM bundle in release mode From 67f2a470f7e2ee4fc493508106ce6386d83cc912 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 17:26:42 -0500 Subject: [PATCH 09/49] Add actix_session package --- Cargo.lock | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 58 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d297a86..4b63124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,8 @@ dependencies = [ "actix-web", "anyhow", "derive_more", + "rand", + "redis", "serde", "serde_json", "tracing", @@ -346,6 +348,12 @@ version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9d19de80eff169429ac1e9f48fffb163916b448a44e8e046186232046d9e1f9" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "askama_escape" version = "0.10.3" @@ -636,6 +644,20 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "config" version = "0.13.4" @@ -1653,6 +1675,7 @@ version = "0.1.0" dependencies = [ "actix-files", "actix-identity", + "actix-session", "actix-web", "cfg-if", "console_error_panic_hook", @@ -2146,6 +2169,28 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redis" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c580d9cbbe1d1b479e8d67cf9daf6a62c957e6846048408b80b43ac3f6af84cd" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "tokio", + "tokio-retry", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -2690,6 +2735,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.10" diff --git a/Cargo.toml b/Cargo.toml index fb58d19..259386f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } actix-identity = { version = "0.7.0", optional = true } +actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -46,6 +47,7 @@ ssr = [ "lazy_static", "openssl", "actix-identity", + "actix-session", ] # Defines a size-optimized profile for the WASM bundle in release mode From 35eee117d796bc60bb9512a6ddaa2567272d38c3 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 17:34:45 -0500 Subject: [PATCH 10/49] Add PublicUser model --- src/models.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/models.rs b/src/models.rs index 2daf3ae..d93bc97 100644 --- a/src/models.rs +++ b/src/models.rs @@ -43,3 +43,20 @@ pub struct NewUser { /// The user's password, stored as a hash pub password: String, } + +/// Model for a "Public User", used for returning user data to the client +/// This model omits the password field, so that the hashed password is not sent to the client +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] +pub struct PublicUser { + /// A unique id for the user + pub id: i32, + /// The user's username + pub username: String, + /// The user's email + pub email: String, + /// The time the user was created + pub created_at: SystemTime, +} From ee5e8694425423b41c5afc975e27c867ac42fbce Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 17:43:17 -0500 Subject: [PATCH 11/49] Implement User conversions --- src/models.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/models.rs b/src/models.rs index d93bc97..fec0ab6 100644 --- a/src/models.rs +++ b/src/models.rs @@ -44,6 +44,17 @@ pub struct NewUser { pub password: String, } +/// Convert a User into a NewUser, omitting the id and created_at fields +impl From for NewUser { + fn from(user: User) -> NewUser { + NewUser { + username: user.username, + email: user.email, + password: user.password, + } + } +} + /// Model for a "Public User", used for returning user data to the client /// This model omits the password field, so that the hashed password is not sent to the client #[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] @@ -60,3 +71,15 @@ pub struct PublicUser { /// The time the user was created pub created_at: SystemTime, } + +/// Convert a User into a PublicUser, omitting the password field +impl From for PublicUser { + fn from(user: User) -> PublicUser { + PublicUser { + id: user.id, + username: user.username, + email: user.email, + created_at: user.created_at, + } + } +} From 6d35aa4d786eba8e14d388365b14bc3cb408a767 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 17:46:48 -0500 Subject: [PATCH 12/49] Add pbkdf2 package --- Cargo.lock | 30 ++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 32 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4b63124..2da0866 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,6 +443,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -1689,6 +1695,7 @@ dependencies = [ "leptos_meta", "leptos_router", "openssl", + "pbkdf2", "serde", "wasm-bindgen", ] @@ -1915,6 +1922,17 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -1927,6 +1945,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "percent-encoding" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index 259386f..59e9c73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } actix-identity = { version = "0.7.0", optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } +pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -48,6 +49,7 @@ ssr = [ "openssl", "actix-identity", "actix-session", + "pbkdf2", ] # Defines a size-optimized profile for the WASM bundle in release mode From 7013b2e22e0a9c4721ab4460300e5ec1447507eb Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 21:22:05 -0500 Subject: [PATCH 13/49] Add basic database functions for users --- src/lib.rs | 1 + src/users.rs | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/users.rs diff --git a/src/lib.rs b/src/lib.rs index 28db59f..ad311de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod playstatus; pub mod playbar; pub mod database; pub mod models; +pub mod users; use cfg_if::cfg_if; cfg_if! { diff --git a/src/users.rs b/src/users.rs new file mode 100644 index 0000000..d2a42c9 --- /dev/null +++ b/src/users.rs @@ -0,0 +1,93 @@ +cfg_if::cfg_if! { + if #[cfg(feature = "ssr")] { + use diesel::prelude::*; + use crate::database::get_db_conn; + + use pbkdf2::{ + password_hash::{ + rand_core::OsRng, + PasswordHasher, PasswordHash, SaltString, PasswordVerifier, Error + }, + Pbkdf2 + }; + } +} + +use leptos::*; +use crate::models::{NewUser, PublicUser, User}; + +/// Get a user from the database by username or email +/// Returns a Result with the user if found, None if not found, or an error if there was a problem +#[cfg(feature = "ssr")] +pub async fn find_user(username_or_email: String) -> Result, ServerFnError> { + use crate::schema::users::dsl::*; + + // Look for either a username or email that matches the input, and return an option with None if no user is found + let db_con = &mut get_db_conn(); + let user = users.filter(username.eq(username_or_email.clone())).or_filter(email.eq(username_or_email)) + .first::(db_con).optional() + .map_err(|e| ServerFnError::ServerError(format!("Error getting user from database: {}", e)))?; + + Ok(user) +} + +/// Create a new user in the database +/// Returns an empty Result if successful, or an error if there was a problem +#[cfg(feature = "ssr")] +pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> { + use crate::schema::users::dsl::*; + + let salt = SaltString::generate(&mut OsRng); + let password_hash = Pbkdf2.hash_password(new_user.password.as_bytes(), &salt) + .map_err(|_| ServerFnError::ServerError("Error hashing password".to_string()))?.to_string(); + + let new_user = NewUser { + username: new_user.username.clone(), + email: new_user.email.clone(), + password: password_hash, + }; + + let db_con = &mut get_db_conn(); + + diesel::insert_into(users).values(&new_user).execute(db_con) + .map_err(|e| ServerFnError::ServerError(format!("Error creating user: {}", e)))?; + + Ok(()) +} + +/// Validate a user's credentials +/// Returns a Result with the user if the credentials are valid, None if not valid, or an error if there was a problem +#[cfg(feature = "ssr")] +pub async fn validate_user(username_or_email: String, password: String) -> Result, ServerFnError> { + let db_user = find_user(username_or_email.clone()).await + .map_err(|e| ServerFnError::ServerError(format!("Error getting user from database: {}", e)))?; + + // If the user is not found, return None + let db_user = match db_user { + Some(user) => user, + None => return Ok(None) + }; + + let password_hash = PasswordHash::new(&db_user.password) + .map_err(|e| ServerFnError::ServerError(format!("Error hashing supplied password: {}", e)))?; + + match Pbkdf2.verify_password(password.as_bytes(), &password_hash) { + Ok(()) => {}, + Err(Error::Password) => { + return Ok(None); + }, + Err(e) => { + return Err(ServerFnError::ServerError(format!("Error verifying password: {}", e))); + } + } + + Ok(Some(db_user)) +} + +/// Get a user from the database by username or email +/// Returns a Result with the user if found, None if not found, or an error if there was a problem +#[server(endpoint = "get_user")] +pub async fn get_user(username_or_email: String) -> Result, ServerFnError> { + let user = find_user(username_or_email).await?; + Ok(user.map(|u| u.into())) +} From 8f9d7b5bc5f18d06439dc0c86b8dfc548a42ea32 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 21:22:49 -0500 Subject: [PATCH 14/49] Implement authentication on backend --- src/auth.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 66 insertions(+) create mode 100644 src/auth.rs diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..1d702a3 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,65 @@ +use leptos::*; +use crate::models::NewUser; + +/// Create a new user and log them in +/// Takes in a NewUser struct, with the password in plaintext +/// Returns a Result with the error message if the user could not be created +#[server(endpoint = "signup")] +pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> { + use crate::users::create_user; + + use leptos_actix::extract; + use actix_web::{HttpMessage, HttpRequest}; + use actix_identity::Identity; + + create_user(&new_user).await + .map_err(|e| ServerFnError::ServerError(format!("Error creating user: {}", e)))?; + + extract(|request: HttpRequest| async move { + Identity::login(&request.extensions(), new_user.username.clone()) + }).await??; + + Ok(()) +} + +/// Log a user in +/// Takes in a username or email and a password in plaintext +/// Returns a Result with a boolean indicating if the login was successful +#[server(endpoint = "login")] +pub async fn login(username_or_email: String, password: String) -> Result { + use crate::users::validate_user; + use actix_web::{HttpMessage, HttpRequest}; + use actix_identity::Identity; + use leptos_actix::extract; + + let possible_user = validate_user(username_or_email, password).await + .map_err(|e| ServerFnError::ServerError(format!("Error validating user: {}", e)))?; + + let user = match possible_user { + Some(user) => user, + None => return Ok(false) + }; + + extract(|request: HttpRequest| async move { + Identity::login(&request.extensions(), user.username.clone()) + }).await??; + + Ok(true) +} + +/// Log a user out +/// Returns a Result with the error message if the user could not be logged out +#[server(endpoint = "logout")] +pub async fn logout() -> Result<(), ServerFnError> { + use leptos_actix::extract; + use actix_identity::Identity; + + extract(|user: Option| async move { + if let Some(user) = user { + user.logout(); + } + }).await?; + + Ok(()) +} + diff --git a/src/lib.rs b/src/lib.rs index ad311de..5eaf272 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +pub mod auth; pub mod songdata; pub mod playstatus; pub mod playbar; From 960d0d4662d022682d0192917ce7c7d723a0e7e1 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 22:04:37 -0500 Subject: [PATCH 15/49] Add identity and session middlewares --- src/main.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main.rs b/src/main.rs index 91dde65..b16a7c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,23 @@ extern crate diesel; #[cfg(feature = "ssr")] #[actix_web::main] async fn main() -> std::io::Result<()> { + use actix_identity::IdentityMiddleware; + use actix_session::storage::RedisSessionStore; + use actix_session::SessionMiddleware; + use actix_web::cookie::Key; + use dotenv::dotenv; dotenv().ok(); + let session_secret_key = if let Ok(key) = std::env::var("SESSION_SECRET_KEY") { + Key::from(key.as_bytes()) + } else { + Key::generate() + }; + + let redis_url = std::env::var("REDIS_URL").expect("REDIS_URL must be set"); + let redis_store = RedisSessionStore::new(redis_url).await.unwrap(); + use actix_files::Files; use actix_web::*; use leptos::*; @@ -40,6 +54,8 @@ async fn main() -> std::io::Result<()> { .service(favicon) .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) .app_data(web::Data::new(leptos_options.to_owned())) + .wrap(IdentityMiddleware::default()) + .wrap(SessionMiddleware::new(redis_store.clone(), session_secret_key.clone())) //.wrap(middleware::Compress::default()) }) .bind(&addr)? From cec73a18e9abce3a15a3ee93de516b886b62df04 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 23:31:37 -0500 Subject: [PATCH 16/49] Add diesel_migrations package --- Cargo.lock | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 ++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5f2a07e..15d3947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -564,7 +564,7 @@ dependencies = [ "nom", "pathdiff", "serde", - "toml", + "toml 0.5.11", ] [[package]] @@ -761,6 +761,17 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + [[package]] name = "diesel_table_macro_syntax" version = "0.1.0" @@ -1518,6 +1529,7 @@ dependencies = [ "cfg-if", "console_error_panic_hook", "diesel", + "diesel_migrations", "dotenv", "http", "lazy_static", @@ -1612,6 +1624,27 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + [[package]] name = "mime" version = "0.3.17" @@ -2236,6 +2269,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_test" version = "1.0.176" @@ -2539,6 +2581,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -2887,6 +2963,15 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "winnow" +version = "0.5.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 5952557..3ae4a72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ diesel = { version = "2.1.4", features = ["postgres", "r2d2"], optional = true } lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } +diesel_migrations = { version = "2.1.0", optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -44,6 +45,7 @@ ssr = [ "diesel", "lazy_static", "openssl", + "diesel_migrations", ] # Defines a size-optimized profile for the WASM bundle in release mode From bd8b27a9ad7316b144f42c6096a6abe089299983 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 23:33:46 -0500 Subject: [PATCH 17/49] Add database migration function --- src/database.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/database.rs b/src/database.rs index a90de44..22b9652 100644 --- a/src/database.rs +++ b/src/database.rs @@ -12,6 +12,12 @@ use diesel::{ r2d2::Pool, }; +use diesel_migrations::{ + embed_migrations, + EmbeddedMigrations, + MigrationHarness, +}; + // See https://leward.eu/notes-on-diesel-a-rust-orm/ // Define some types to make it easier to work with Diesel @@ -47,5 +53,15 @@ pub fn get_db_conn() -> PgPooledConn { DB_POOL.get().expect("Failed to get a database connection from the pool.") } +/// Embedded database migrations into the binary +const DB_MIGRATIONS: EmbeddedMigrations = embed_migrations!(); + +/// Run any pending migrations in the database +/// Always safe to call, as it will only run migrations that have not already been run +pub fn migrate() { + let db_con = &mut get_db_conn(); + db_con.run_pending_migrations(DB_MIGRATIONS).expect("Could not run database migrations"); +} + } } From 540f9d2f72129153ee4d12b68a5811d1d9a67c34 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 23:33:59 -0500 Subject: [PATCH 18/49] Run database migrations on start --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index 91dde65..7c066dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,12 +8,18 @@ extern crate openssl; #[macro_use] extern crate diesel; +#[cfg(feature = "ssr")] +extern crate diesel_migrations; + #[cfg(feature = "ssr")] #[actix_web::main] async fn main() -> std::io::Result<()> { use dotenv::dotenv; dotenv().ok(); + // Bring the database up to date + libretunes::database::migrate(); + use actix_files::Files; use actix_web::*; use leptos::*; From 8d9b84442e4eb396ad493a9c2c554dd95c04f33e Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 4 Feb 2024 23:34:29 -0500 Subject: [PATCH 19/49] Copy migrations into Docker container --- .dockerignore | 1 + Dockerfile | 1 + 2 files changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 0c102f2..59dfed4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ # Except: !/assets +!/migrations !/src !/style !/Cargo.lock diff --git a/Dockerfile b/Dockerfile index 9292b01..c02cd2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,7 @@ RUN npx tailwindcss -i /app/style/main.scss -o /app/style/main.scss --minify COPY assets /app/assets COPY src /app/src +COPY migrations /app/migrations # Touch files to force rebuild RUN touch /app/src/main.rs && touch /app/src/lib.rs && touch /app/src/build.rs From 645e38fc6c425900aec73c7d1ac48c2799b5d45f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Mon, 5 Feb 2024 23:06:42 -0500 Subject: [PATCH 20/49] Build database URL from POSTGRES_ environment variables --- src/database.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/database.rs b/src/database.rs index a90de44..ca05c46 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,4 +1,5 @@ use cfg_if::cfg_if; +use leptos::logging::log; cfg_if! { if #[cfg(feature = "ssr")] { @@ -25,12 +26,59 @@ lazy_static! { /// Initialize the database pool /// -/// Will panic if the DATABASE_URL environment variable is not set, or if there is an error creating the pool. +/// Uses DATABASE_URL environment variable to connect to the database if set, +/// otherwise builds a connection string from other environment variables. +/// +/// Will panic if either the DATABASE_URL or POSTGRES_HOST environment variables +/// are not set, or if there is an error creating the pool. /// /// # Returns /// A database pool object, which can be used to get pooled connections fn init_db_pool() -> PgPool { - let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| { + // Build the database URL from environment variables + // Construct a separate log_url to avoid logging the password + let mut log_url = "postgres://".to_string(); + let mut url = "postgres://".to_string(); + + if let Ok(user) = env::var("POSTGRES_USER") { + url.push_str(&user); + log_url.push_str(&user); + + if let Ok(password) = env::var("POSTGRES_PASSWORD") { + url.push_str(":"); + log_url.push_str(":"); + url.push_str(&password); + log_url.push_str("********"); + } + + url.push_str("@"); + log_url.push_str("@"); + } + + let host = env::var("POSTGRES_HOST").expect("DATABASE_URL or POSTGRES_HOST must be set"); + + url.push_str(&host); + log_url.push_str(&host); + + if let Ok(port) = env::var("POSTGRES_PORT") { + url.push_str(":"); + url.push_str(&port); + log_url.push_str(":"); + log_url.push_str(&port); + } + + if let Ok(dbname) = env::var("POSTGRES_DB") { + url.push_str("/"); + url.push_str(&dbname); + log_url.push_str("/"); + log_url.push_str(&dbname); + } + + log!("Connecting to database: {}", log_url); + url + }); + let manager = ConnectionManager::::new(database_url); PgPool::builder() .build(manager) From a04fd80d6d57ef4bc7386e356fd38f8baebf71e0 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 6 Feb 2024 09:05:02 -0500 Subject: [PATCH 21/49] Add Docker compose --- docker-compose.yml | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c2d0865 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +name: libretunes + +services: + libretunes: + container_name: libretunes + # image: registry.mregirouard.com/libretunes/libretunes:latest + build: . + ports: + - "3000:3000" + environment: + REDIS_URL: redis://redis:6379 + POSTGRES_HOST: postgres + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - libretunes-audio:/site/audio + depends_on: + - redis + - postgres + restart: always + + redis: + container_name: redis + image: redis:latest + volumes: + - libretunes-redis:/data + restart: always + healthcheck: + test: ["CMD-SHELL", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + postgres: + container_name: postgres + image: postgres:latest + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - libretunes-postgres:/var/lib/postgresql/data + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + libretunes-audio: + libretunes-redis: + libretunes-postgres: From a6f141e8414f021de2b3b5dd2a413c10832568c9 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 17:31:42 -0500 Subject: [PATCH 22/49] Add time feature to Diesel --- Cargo.lock | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5f2a07e..a1c721f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -747,6 +747,7 @@ dependencies = [ "itoa", "pq-sys", "r2d2", + "time", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5952557..c15944b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ leptos_icons = { version = "0.1.0", default_features = false, features = [ "BsSkipEndFill" ] } dotenv = { version = "0.15.0", optional = true } -diesel = { version = "2.1.4", features = ["postgres", "r2d2"], optional = true } +diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional = true } lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } From 256b999391640736dfde1c52196de9b87b8d1fd0 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:34:51 -0500 Subject: [PATCH 23/49] Merge user models into a single struct --- src/auth.rs | 10 +++++-- src/models.rs | 75 +++++++++------------------------------------------ src/users.rs | 33 +++++++++++++++-------- 3 files changed, 43 insertions(+), 75 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 1d702a3..1995deb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,17 +1,23 @@ use leptos::*; -use crate::models::NewUser; +use crate::models::User; /// Create a new user and log them in /// Takes in a NewUser struct, with the password in plaintext /// Returns a Result with the error message if the user could not be created #[server(endpoint = "signup")] -pub async fn signup(new_user: NewUser) -> Result<(), ServerFnError> { +pub async fn signup(new_user: User) -> Result<(), ServerFnError> { use crate::users::create_user; use leptos_actix::extract; use actix_web::{HttpMessage, HttpRequest}; use actix_identity::Identity; + // Ensure the user has no id + let new_user = User { + id: None, + ..new_user + }; + create_user(&new_user).await .map_err(|e| ServerFnError::ServerError(format!("Error creating user: {}", e)))?; diff --git a/src/models.rs b/src/models.rs index fec0ab6..e6ee017 100644 --- a/src/models.rs +++ b/src/models.rs @@ -11,75 +11,26 @@ use diesel::prelude::*; // diesel-specific attributes to the models when compiling for the server /// Model for a "User", used for querying the database -#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] -#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] -#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] -#[derive(Serialize, Deserialize)] -pub struct User { - /// A unique id for the user - pub id: i32, - /// The user's username - pub username: String, - /// The user's email - pub email: String, - /// The user's password, stored as a hash - pub password: String, - /// The time the user was created - pub created_at: SystemTime, -} - -/// Model for a "New User", used for inserting into the database -/// Note that this model does not have an id or created_at field, as those are automatically -/// generated by the database and we don't want to deal with them ourselves -#[cfg_attr(feature = "ssr", derive(Insertable))] +/// Various fields are wrapped in Options, because they are not always wanted for inserts/retrieval +/// Using deserialize_as makes Diesel use the specified type when deserializing from the database, +/// and then call .into() to convert it into the Option +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] #[derive(Serialize, Deserialize, Clone, Debug)] -pub struct NewUser { +pub struct User { + /// A unique id for the user + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + // #[cfg_attr(feature = "ssr", diesel(skip_insertion))] // This feature is not yet released + pub id: Option, /// The user's username pub username: String, /// The user's email pub email: String, /// The user's password, stored as a hash - pub password: String, -} - -/// Convert a User into a NewUser, omitting the id and created_at fields -impl From for NewUser { - fn from(user: User) -> NewUser { - NewUser { - username: user.username, - email: user.email, - password: user.password, - } - } -} - -/// Model for a "Public User", used for returning user data to the client -/// This model omits the password field, so that the hashed password is not sent to the client -#[cfg_attr(feature = "ssr", derive(Queryable, Selectable))] -#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::users))] -#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] -#[derive(Serialize, Deserialize)] -pub struct PublicUser { - /// A unique id for the user - pub id: i32, - /// The user's username - pub username: String, - /// The user's email - pub email: String, + #[cfg_attr(feature = "ssr", diesel(deserialize_as = String))] + pub password: Option, /// The time the user was created - pub created_at: SystemTime, -} - -/// Convert a User into a PublicUser, omitting the password field -impl From for PublicUser { - fn from(user: User) -> PublicUser { - PublicUser { - id: user.id, - username: user.username, - email: user.email, - created_at: user.created_at, - } - } + #[cfg_attr(feature = "ssr", diesel(deserialize_as = SystemTime))] + pub created_at: Option, } diff --git a/src/users.rs b/src/users.rs index d2a42c9..7d81b31 100644 --- a/src/users.rs +++ b/src/users.rs @@ -14,7 +14,7 @@ cfg_if::cfg_if! { } use leptos::*; -use crate::models::{NewUser, PublicUser, User}; +use crate::models::User; /// Get a user from the database by username or email /// Returns a Result with the user if found, None if not found, or an error if there was a problem @@ -34,17 +34,19 @@ pub async fn find_user(username_or_email: String) -> Result, Server /// Create a new user in the database /// Returns an empty Result if successful, or an error if there was a problem #[cfg(feature = "ssr")] -pub async fn create_user(new_user: &NewUser) -> Result<(), ServerFnError> { +pub async fn create_user(new_user: &User) -> Result<(), ServerFnError> { use crate::schema::users::dsl::*; + let new_password = new_user.password.clone() + .ok_or(ServerFnError::ServerError(format!("No password provided for user {}", new_user.username)))?; + let salt = SaltString::generate(&mut OsRng); - let password_hash = Pbkdf2.hash_password(new_user.password.as_bytes(), &salt) + let password_hash = Pbkdf2.hash_password(new_password.as_bytes(), &salt) .map_err(|_| ServerFnError::ServerError("Error hashing password".to_string()))?.to_string(); - let new_user = NewUser { - username: new_user.username.clone(), - email: new_user.email.clone(), - password: password_hash, + let new_user = User { + password: Some(password_hash), + ..new_user.clone() }; let db_con = &mut get_db_conn(); @@ -68,7 +70,10 @@ pub async fn validate_user(username_or_email: String, password: String) -> Resul None => return Ok(None) }; - let password_hash = PasswordHash::new(&db_user.password) + let db_password = db_user.password.clone() + .ok_or(ServerFnError::ServerError(format!("No password found for user {}", db_user.username)))?; + + let password_hash = PasswordHash::new(&db_password) .map_err(|e| ServerFnError::ServerError(format!("Error hashing supplied password: {}", e)))?; match Pbkdf2.verify_password(password.as_bytes(), &password_hash) { @@ -87,7 +92,13 @@ pub async fn validate_user(username_or_email: String, password: String) -> Resul /// Get a user from the database by username or email /// Returns a Result with the user if found, None if not found, or an error if there was a problem #[server(endpoint = "get_user")] -pub async fn get_user(username_or_email: String) -> Result, ServerFnError> { - let user = find_user(username_or_email).await?; - Ok(user.map(|u| u.into())) +pub async fn get_user(username_or_email: String) -> Result, ServerFnError> { + let mut user = find_user(username_or_email).await?; + + // Remove the password hash before returning the user + if let Some(user) = user.as_mut() { + user.password = None; + } + + Ok(user) } From 0924c954b800cdf27f9a773f1d07748e555afaf8 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:37:55 -0500 Subject: [PATCH 24/49] Create artists table --- .../2024-02-06-145714_create_artists_table/down.sql | 1 + .../2024-02-06-145714_create_artists_table/up.sql | 4 ++++ src/schema.rs | 12 ++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 migrations/2024-02-06-145714_create_artists_table/down.sql create mode 100644 migrations/2024-02-06-145714_create_artists_table/up.sql diff --git a/migrations/2024-02-06-145714_create_artists_table/down.sql b/migrations/2024-02-06-145714_create_artists_table/down.sql new file mode 100644 index 0000000..943c085 --- /dev/null +++ b/migrations/2024-02-06-145714_create_artists_table/down.sql @@ -0,0 +1 @@ +DROP TABLE artists; diff --git a/migrations/2024-02-06-145714_create_artists_table/up.sql b/migrations/2024-02-06-145714_create_artists_table/up.sql new file mode 100644 index 0000000..73802e6 --- /dev/null +++ b/migrations/2024-02-06-145714_create_artists_table/up.sql @@ -0,0 +1,4 @@ +CREATE TABLE artists ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + name VARCHAR NOT NULL +); diff --git a/src/schema.rs b/src/schema.rs index 2e9b462..58b691a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,12 @@ // @generated automatically by Diesel CLI. +diesel::table! { + artists (id) { + id -> Int4, + name -> Varchar, + } +} + diesel::table! { users (id) { id -> Int4, @@ -9,3 +16,8 @@ diesel::table! { created_at -> Timestamp, } } + +diesel::allow_tables_to_appear_in_same_query!( + artists, + users, +); From bf99dac25cb441b06162dcd20bb175f86615d2a5 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:38:18 -0500 Subject: [PATCH 25/49] Create albums table --- .../down.sql | 2 ++ .../up.sql | 13 ++++++++++++ src/schema.rs | 20 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 migrations/2024-02-06-150214_create_albums_table/down.sql create mode 100644 migrations/2024-02-06-150214_create_albums_table/up.sql diff --git a/migrations/2024-02-06-150214_create_albums_table/down.sql b/migrations/2024-02-06-150214_create_albums_table/down.sql new file mode 100644 index 0000000..31baf23 --- /dev/null +++ b/migrations/2024-02-06-150214_create_albums_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE album_artists; +DROP TABLE albums; diff --git a/migrations/2024-02-06-150214_create_albums_table/up.sql b/migrations/2024-02-06-150214_create_albums_table/up.sql new file mode 100644 index 0000000..154bab2 --- /dev/null +++ b/migrations/2024-02-06-150214_create_albums_table/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE albums ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + title VARCHAR NOT NULL, + release_date DATE +); + +-- A table to store artists for each album +-- Needed because an album can have multiple artists, but in Postgres we can't store an array of foreign keys +CREATE TABLE album_artists ( + album_id INTEGER REFERENCES albums(id) ON DELETE CASCADE NOT NULL, + artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE NULL, + PRIMARY KEY (album_id, artist_id) +); diff --git a/src/schema.rs b/src/schema.rs index 58b691a..1029c02 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,20 @@ // @generated automatically by Diesel CLI. +diesel::table! { + album_artists (album_id, artist_id) { + album_id -> Int4, + artist_id -> Int4, + } +} + +diesel::table! { + albums (id) { + id -> Int4, + title -> Varchar, + release_date -> Nullable, + } +} + diesel::table! { artists (id) { id -> Int4, @@ -17,7 +32,12 @@ diesel::table! { } } +diesel::joinable!(album_artists -> albums (album_id)); +diesel::joinable!(album_artists -> artists (artist_id)); + diesel::allow_tables_to_appear_in_same_query!( + album_artists, + albums, artists, users, ); From 319958f26470720fb9f49e9aedceceb343f810cd Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:38:36 -0500 Subject: [PATCH 26/49] Create songs table --- .../down.sql | 2 ++ .../up.sql | 16 ++++++++++++ src/schema.rs | 25 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 migrations/2024-02-06-150334_create_songs_table/down.sql create mode 100644 migrations/2024-02-06-150334_create_songs_table/up.sql diff --git a/migrations/2024-02-06-150334_create_songs_table/down.sql b/migrations/2024-02-06-150334_create_songs_table/down.sql new file mode 100644 index 0000000..b5ef474 --- /dev/null +++ b/migrations/2024-02-06-150334_create_songs_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE song_artists; +DROP TABLE songs; diff --git a/migrations/2024-02-06-150334_create_songs_table/up.sql b/migrations/2024-02-06-150334_create_songs_table/up.sql new file mode 100644 index 0000000..91249a1 --- /dev/null +++ b/migrations/2024-02-06-150334_create_songs_table/up.sql @@ -0,0 +1,16 @@ +CREATE TABLE songs ( + id SERIAL PRIMARY KEY UNIQUE NOT NULL, + title VARCHAR NOT NULL, + album_id INTEGER REFERENCES albums(id), + track INTEGER, + duration INTEGER NOT NULL, + release_date DATE, + storage_path VARCHAR NOT NULL, + image_path VARCHAR +); + +CREATE TABLE song_artists ( + song_id INTEGER REFERENCES songs(id) ON DELETE CASCADE NOT NULL, + artist_id INTEGER REFERENCES artists(id) ON DELETE CASCADE NOT NULL, + PRIMARY KEY (song_id, artist_id) +); diff --git a/src/schema.rs b/src/schema.rs index 1029c02..b98d736 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -22,6 +22,26 @@ diesel::table! { } } +diesel::table! { + song_artists (song_id, artist_id) { + song_id -> Int4, + artist_id -> Int4, + } +} + +diesel::table! { + songs (id) { + id -> Int4, + title -> Varchar, + album_id -> Nullable, + track -> Nullable, + duration -> Int4, + release_date -> Nullable, + storage_path -> Varchar, + image_path -> Nullable, + } +} + diesel::table! { users (id) { id -> Int4, @@ -34,10 +54,15 @@ diesel::table! { diesel::joinable!(album_artists -> albums (album_id)); diesel::joinable!(album_artists -> artists (artist_id)); +diesel::joinable!(song_artists -> artists (artist_id)); +diesel::joinable!(song_artists -> songs (song_id)); +diesel::joinable!(songs -> albums (album_id)); diesel::allow_tables_to_appear_in_same_query!( album_artists, albums, artists, + song_artists, + songs, users, ); From 79add82c2da3171e8167f2c58ae9ff47e62126d6 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:40:02 -0500 Subject: [PATCH 27/49] Create artist model --- src/models.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/models.rs b/src/models.rs index 2daf3ae..f336c79 100644 --- a/src/models.rs +++ b/src/models.rs @@ -43,3 +43,16 @@ pub struct NewUser { /// The user's password, stored as a hash pub password: String, } + +/// Model for an artist +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Artist { + /// A unique id for the artist + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The artist's name + pub name: String, +} + From 577090aa1ae292914c3e99affa0e8ea9da80a6bf Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:40:38 -0500 Subject: [PATCH 28/49] Add time package --- Cargo.lock | 17 +++++++++++++---- Cargo.toml | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1c721f..7122c48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1529,6 +1529,7 @@ dependencies = [ "leptos_router", "openssl", "serde", + "time", "wasm-bindgen", ] @@ -1666,6 +1667,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "object" version = "0.32.2" @@ -2458,12 +2465,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2478,10 +2486,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] diff --git a/Cargo.toml b/Cargo.toml index c15944b..25e3b1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } +time = "0.3.34" [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] From 8638265fa3cecadf03684ca6647c9e0c5ad52b8f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:41:28 -0500 Subject: [PATCH 29/49] Create album model --- src/models.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/models.rs b/src/models.rs index f336c79..74c4ae7 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,5 @@ use std::time::SystemTime; +use time::Date; use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] @@ -56,3 +57,17 @@ pub struct Artist { pub name: String, } +/// Model for an album +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Album { + /// A unique id for the album + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The album's title + pub title: String, + /// The album's release date + pub release_date: Option, +} + From a6556b7a98b0a812cdddc8a23a81d54990e237e5 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 18:41:37 -0500 Subject: [PATCH 30/49] Create song model --- src/models.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/models.rs b/src/models.rs index 74c4ae7..d71fc6c 100644 --- a/src/models.rs +++ b/src/models.rs @@ -71,3 +71,26 @@ pub struct Album { pub release_date: Option, } +/// Model for a song +#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] +#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] +#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +pub struct Song { + /// A unique id for the song + #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] + pub id: Option, + /// The song's title + pub title: String, + /// The album the song is from + pub album_id: Option, + /// The track number of the song on the album + pub track: Option, + /// The duration of the song in seconds + pub duration: i32, + /// The song's release date + pub release_date: Option, + /// The path to the song's audio file + pub storage_path: String, + /// The path to the song's image file + pub image_path: Option, +} From 7476aa6d399d2c04926fc159427d94bbfef366da Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 23:07:06 -0500 Subject: [PATCH 31/49] Rebuild project when DB migrations changed --- src/build.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/build.rs b/src/build.rs index 32eb98c..64a96cb 100644 --- a/src/build.rs +++ b/src/build.rs @@ -6,4 +6,6 @@ fn main() { "cargo:rustc-cfg=target=\"{}\"", std::env::var("TARGET").unwrap() ); + + println!("cargo:rerun-if-changed=migrations"); } From 598f3b2a1806793324a205ea11be43ea42f9ae1f Mon Sep 17 00:00:00 2001 From: dannyzou18 Date: Fri, 9 Feb 2024 17:15:00 -0500 Subject: [PATCH 32/49] Finished Initial Design of Login Page modified: src/app.rs modified: src/lib.rs new file: src/pages.rs new file: src/pages/login.rs new file: src/pages/signup.rs new file: style/login.scss modified: style/main.scss new file: style/signup.scss modified: style/theme.scss --- src/app.rs | 4 ++ src/lib.rs | 1 + src/pages.rs | 2 + src/pages/login.rs | 48 ++++++++++++++++ src/pages/signup.rs | 14 +++++ style/login.scss | 136 ++++++++++++++++++++++++++++++++++++++++++++ style/main.scss | 2 + style/signup.scss | 1 + style/theme.scss | 1 + 9 files changed, 209 insertions(+) create mode 100644 src/pages.rs create mode 100644 src/pages/login.rs create mode 100644 src/pages/signup.rs create mode 100644 style/login.scss create mode 100644 style/signup.scss diff --git a/src/app.rs b/src/app.rs index ae1aa39..5955bb5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,8 @@ use leptos::*; use leptos_meta::*; use leptos_router::*; +use crate::pages::login::*; +use crate::pages::signup::*; #[component] pub fn App() -> impl IntoView { @@ -21,6 +23,8 @@ pub fn App() -> impl IntoView { + + diff --git a/src/lib.rs b/src/lib.rs index 28db59f..552bdcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod playstatus; pub mod playbar; pub mod database; pub mod models; +pub mod pages; use cfg_if::cfg_if; cfg_if! { diff --git a/src/pages.rs b/src/pages.rs new file mode 100644 index 0000000..40f63fd --- /dev/null +++ b/src/pages.rs @@ -0,0 +1,2 @@ +pub mod login; +pub mod signup; \ No newline at end of file diff --git a/src/pages/login.rs b/src/pages/login.rs new file mode 100644 index 0000000..1f5662e --- /dev/null +++ b/src/pages/login.rs @@ -0,0 +1,48 @@ +use leptos::ev; +use leptos::leptos_dom::*; +use leptos::*; +use leptos_router::*; + +#[component] +pub fn Login() -> impl IntoView { + let (username, set_username) = create_signal("".to_string()); + let (password, set_password) = create_signal("".to_string()); + + view! { +
+ +
+ } +} diff --git a/src/pages/signup.rs b/src/pages/signup.rs new file mode 100644 index 0000000..3f04544 --- /dev/null +++ b/src/pages/signup.rs @@ -0,0 +1,14 @@ +use leptos::leptos_dom::*; +use leptos::*; + +#[component] +pub fn Signup() -> impl IntoView { + view!{ +
+ +
+ } +} \ No newline at end of file diff --git a/style/login.scss b/style/login.scss new file mode 100644 index 0000000..e01be7c --- /dev/null +++ b/style/login.scss @@ -0,0 +1,136 @@ +@import 'theme.scss'; + +.login-container { + display: flex; + flex-direction: column; + align-items: center; + position: fixed; + top: 50%; + left: 50%; + width: 27rem; + height: 30rem; + transform: translate(-50%, -50%); + background: purple; + z-index: 1; + border-radius: 8px; + overflow: hidden; + } + +.login-container .header h1 { + margin-top: 3rem; + font-size: 2.5rem; + color: #2c1308; +} +.login-container .login-form { + width: 75%; +} +.login-form .input-box:first-child { + margin-top: .5rem; +} +.login-form .input-box { + position: relative; + margin-top: 3rem; +} +.login-form .input-box input { + position: relative; + width: 100%; + max-width: 34vw; + padding: 17px 0px 10px; + background: transparent; + outline: none; + border: none; + box-shadow: none; + color: #23242a; + font-size: 1.1em; + font-family: "Roboto", sans-serif; + font-weight: 400; + letter-spacing: 0px; + text-indent: 10px; + vertical-align: middle; + z-index: 10; + color: #fff; +} +.login-form .input-box span { + position: absolute; + left: 0; + padding: 15px 0px 10px; + pointer-events: none; + color: #907163; + font-size: 1.2em; + letter-spacing: 0.5px; + transition: 0.5s; +} +.login-form .input-box input:valid ~ span, +.login-form .input-box input:focus ~ span { + color: #2c1308; + font-size: 1rem; + transform: translateY(-30px); + font-weight: 400; +} +.login-form .input-box i { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 2px; + background: #907163; + border-radius: 4px; + overflow: hidden; + transition: 0.5s; + pointer-events: none; +} + +.login-form .input-box input:valid ~ i, +.login-form .input-box input:focus ~ i { + height: 2.6rem; +} +.login-form .forgot-pw { + display: inline-flex; + margin-top: 3px; + cursor: pointer; + color: #8f8f8f; + text-decoration: underline; +} +.login-form .forgot-pw:hover { + color: #fff; + transition: all 0.2s; +} +.login-form input[type="submit"] { + margin-top: 2.8rem; + width: 100%; + height: 3rem; + border: none; + border-radius: 8px; + color: rgb(210, 207, 207); + cursor: pointer; + font-size: 1.1rem; + font-weight: 600; + background-color: #582b17; +} +.login-form .go-to-signup { + margin-top: 11px; + color: #8f8f8f; +} +.login-form .go-to-signup span { + cursor: pointer; + color: #8f8f8f; + text-decoration: underline; +} +.login-form .go-to-signup span:hover { + color: #fff; + transition: all 0.2s; +} +.login-container .return { + position: absolute; + left: 15px; + top: 15px; + font-size: 1.8rem; + color: white; + cursor: pointer; + width: 50px; + transition: all 0.3s; + border-radius: 8px; +} +.login-container .return:hover { + background-color: rgba(0, 0, 0, 0.4); +} diff --git a/style/main.scss b/style/main.scss index e49fe50..60b68fd 100644 --- a/style/main.scss +++ b/style/main.scss @@ -1,5 +1,7 @@ @import 'playbar.scss'; @import 'theme.scss'; +@import 'login.scss'; +@import 'signup.scss'; body { font-family: sans-serif; diff --git a/style/signup.scss b/style/signup.scss new file mode 100644 index 0000000..6221d7b --- /dev/null +++ b/style/signup.scss @@ -0,0 +1 @@ +@import 'theme.scss'; \ No newline at end of file diff --git a/style/theme.scss b/style/theme.scss index 9ac5872..f795d06 100644 --- a/style/theme.scss +++ b/style/theme.scss @@ -6,3 +6,4 @@ $controls-click-color: #909090; $play-bar-background-color: #212121; $play-grad-start: #0a0533; $play-grad-end: $accent-color; + From 5cc7d6ce94fc5bb37a245a2787c80d503981a83f Mon Sep 17 00:00:00 2001 From: dannyzou18 Date: Sat, 10 Feb 2024 00:54:18 -0500 Subject: [PATCH 33/49] Completely Finished Design of Login Page modified: src/pages/login.rs modified: src/pages/signup.rs modified: style/login.scss --- src/pages/login.rs | 19 ++++++++++--------- src/pages/signup.rs | 5 ++++- style/login.scss | 20 ++++++++++++-------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/pages/login.rs b/src/pages/login.rs index 1f5662e..bbb4823 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -1,7 +1,7 @@ -use leptos::ev; use leptos::leptos_dom::*; use leptos::*; -use leptos_router::*; +use leptos_icons::IoIcon::*; +use leptos_icons::*; #[component] pub fn Login() -> impl IntoView { @@ -11,9 +11,10 @@ pub fn Login() -> impl IntoView { view! {
diff --git a/src/pages/signup.rs b/src/pages/signup.rs index 3f04544..0ac67ae 100644 --- a/src/pages/signup.rs +++ b/src/pages/signup.rs @@ -3,10 +3,13 @@ use leptos::*; #[component] pub fn Signup() -> impl IntoView { + let (username, set_username) = create_signal("".to_string()); + let (email, set_email) = create_signal("".to_string()); + let (password, set_password) = create_signal("".to_string()); + view!{
diff --git a/style/login.scss b/style/login.scss index e01be7c..475a1ad 100644 --- a/style/login.scss +++ b/style/login.scss @@ -56,7 +56,7 @@ padding: 15px 0px 10px; pointer-events: none; color: #907163; - font-size: 1.2em; + font-size: 1.19em; letter-spacing: 0.5px; transition: 0.5s; } @@ -87,6 +87,7 @@ .login-form .forgot-pw { display: inline-flex; margin-top: 3px; + font-size: .9rem; cursor: pointer; color: #8f8f8f; text-decoration: underline; @@ -96,7 +97,7 @@ transition: all 0.2s; } .login-form input[type="submit"] { - margin-top: 2.8rem; + margin-top: 3rem; width: 100%; height: 3rem; border: none; @@ -108,28 +109,31 @@ background-color: #582b17; } .login-form .go-to-signup { - margin-top: 11px; color: #8f8f8f; + font-size: .9rem; } -.login-form .go-to-signup span { +.login-form .go-to-signup a { cursor: pointer; color: #8f8f8f; text-decoration: underline; } -.login-form .go-to-signup span:hover { +.login-form .go-to-signup a:hover { color: #fff; transition: all 0.2s; } .login-container .return { position: absolute; - left: 15px; - top: 15px; + left: 10px; + top: 10px; font-size: 1.8rem; color: white; cursor: pointer; - width: 50px; transition: all 0.3s; border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + padding: .3rem; } .login-container .return:hover { background-color: rgba(0, 0, 0, 0.4); From 5a630daebc0591faa26f1b0d3a1f3ce5dcee1cab Mon Sep 17 00:00:00 2001 From: dannyzou18 Date: Sat, 10 Feb 2024 00:55:05 -0500 Subject: [PATCH 34/49] Why is it not commiting --- Cargo.lock | 10 ++++++++++ Cargo.toml | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 5f2a07e..6a343a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1141,6 +1141,7 @@ checksum = "f41f2deec9249d16ef6b1a8442fbe16013f67053797052aa0b7d2f5ebd0f0098" dependencies = [ "icondata_bs", "icondata_core", + "icondata_io", ] [[package]] @@ -1158,6 +1159,15 @@ version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1640a4c1d5ddd08ab1d9854ffa7a2fa3dc52339492676b6d3031e77ca579f434" +[[package]] +name = "icondata_io" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134d9fb91cdd0e7ac971199e2c8c8eb917a975faeeee54b227a0068c4f70c886" +dependencies = [ + "icondata_core", +] + [[package]] name = "ident_case" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 5952557..57e7e33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ leptos_icons = { version = "0.1.0", default_features = false, features = [ "BsPlayFill", "BsPauseFill", "BsSkipStartFill", - "BsSkipEndFill" + "BsSkipEndFill", + "IoReturnUpBackSharp" ] } dotenv = { version = "0.15.0", optional = true } diesel = { version = "2.1.4", features = ["postgres", "r2d2"], optional = true } From 2a183eec2cb96cd908bdec1100664a0548e542c1 Mon Sep 17 00:00:00 2001 From: dannyzou18 Date: Sat, 10 Feb 2024 01:12:19 -0500 Subject: [PATCH 35/49] Finished Initial Signup Page Design/Outline --- src/pages/signup.rs | 45 ++++++++++++++- style/signup.scss | 130 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 173 insertions(+), 2 deletions(-) diff --git a/src/pages/signup.rs b/src/pages/signup.rs index 0ac67ae..6bc06da 100644 --- a/src/pages/signup.rs +++ b/src/pages/signup.rs @@ -1,5 +1,7 @@ use leptos::leptos_dom::*; use leptos::*; +use leptos_icons::IoIcon::*; +use leptos_icons::*; #[component] pub fn Signup() -> impl IntoView { @@ -7,10 +9,51 @@ pub fn Signup() -> impl IntoView { let (email, set_email) = create_signal("".to_string()); let (password, set_password) = create_signal("".to_string()); + view!{
} diff --git a/style/signup.scss b/style/signup.scss index 6221d7b..cd45ab6 100644 --- a/style/signup.scss +++ b/style/signup.scss @@ -1 +1,129 @@ -@import 'theme.scss'; \ No newline at end of file +@import 'theme.scss'; + +.signup-container { + display: flex; + flex-direction: column; + align-items: center; + position: fixed; + top: 50%; + left: 50%; + width: 27rem; + height: 35rem; + transform: translate(-50%, -50%); + background: purple; + z-index: 1; + border-radius: 8px; + overflow: hidden; + } + .signup-container .header h1 { + margin-top: 3rem; + font-size: 2.5rem; + color: #2c1308; + } + .signup-container .signup-form { + width: 80%; + } + .signup-form .input-box { + position: relative; + margin-top: 3rem; + } + .signup-form .input-box:first-child { + margin-top: 1rem; + } + .signup-form .input-box input { + position: relative; + width: 100%; + max-width: 34vw; + padding: 17px 0px 10px; + background: transparent; + outline: none; + border: none; + box-shadow: none; + color: #23242a; + font-size: 1.1em; + font-family: "Roboto", sans-serif; + font-weight: 400; + letter-spacing: 0px; + text-indent: 10px; + vertical-align: middle; + z-index: 10; + color: #fff; + } + .signup-form .input-box span { + position: absolute; + left: 0; + padding: 15px 0px 10px; + pointer-events: none; + color: #907163; + font-size: 1.19em; + letter-spacing: 0.5px; + transition: 0.5s; + } + .signup-form .input-box input:valid ~ span, + .signup-form .input-box input:focus ~ span { + color: #2c1308; + font-size: 1rem; + transform: translateY(-30px); + font-weight: 400; + } + .signup-form .input-box i { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 2px; + background: #907163; + border-radius: 4px; + overflow: hidden; + transition: 0.5s; + pointer-events: none; + } + + .signup-form .input-box input:valid ~ i, + .signup-form .input-box input:focus ~ i { + height: 2.6rem; + } + + .signup-form input[type="submit"] { + margin-top: 3.5rem; + width: 100%; + height: 45px; + border: none; + border-radius: 8px; + color: rgb(210, 207, 207); + cursor: pointer; + font-size: 1.1rem; + font-weight: 600; + background-color: #582b17; + } + .signup-form .go-to-login { + color: #8f8f8f; + font-size: 0.9rem; + } + .signup-form .go-to-login span { + cursor: pointer; + color: #8f8f8f; + text-decoration: underline; + } + .signup-form .go-to-login span:hover { + color: #fff; + transition: all 0.2s; + } + .signup-container .return { + position: absolute; + left: 10px; + top: 10px; + font-size: 1.8rem; + color: white; + cursor: pointer; + transition: all 0.3s; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + padding: .3rem; + } + .signup-container .return:hover { + background-color: rgba(0, 0, 0, 0.4); + } + \ No newline at end of file From 7e24038b228826a3d14dc0cd73d441009fc27257 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 8 Feb 2024 22:47:14 -0500 Subject: [PATCH 36/49] Implement data manipulation methods for artists, albums, songs --- src/models.rs | 206 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 2 deletions(-) diff --git a/src/models.rs b/src/models.rs index d71fc6c..671ec15 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,9 +1,16 @@ use std::time::SystemTime; +use std::error::Error; use time::Date; use serde::{Deserialize, Serialize}; -#[cfg(feature = "ssr")] -use diesel::prelude::*; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(feature = "ssr")] { + use diesel::prelude::*; + use crate::database::PgPooledConn; + } +} // These "models" are used to represent the data in the database // Diesel uses these models to generate the SQL queries that are used to interact with the database. @@ -57,6 +64,114 @@ pub struct Artist { pub name: String, } +impl Artist { + /// Add an album to this artist in the database + /// + /// # Arguments + /// + /// * `new_album_id` - The id of the album to add to this artist + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_album(self: &Self, new_album_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to add an album")?; + + diesel::insert_into(album_artists) + .values((album_id.eq(new_album_id), artist_id.eq(my_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get albums by artist from the database + /// + /// The `id` field of this artist must be present (Some) to get albums + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of albums, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_albums(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::albums::dsl::*; + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to get albums")?; + + let my_albums = albums + .inner_join(album_artists) + .filter(artist_id.eq(my_id)) + .select(albums::all_columns()) + .load(conn)?; + + Ok(my_albums) + } + + /// Add a song to this artist in the database + /// + /// The `id` field of this artist must be present (Some) to add a song + /// + /// # Arguments + /// + /// * `new_song_id` - The id of the song to add to this artist + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_song(self: &Self, new_song_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to add an album")?; + + diesel::insert_into(song_artists) + .values((song_id.eq(new_song_id), artist_id.eq(my_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get songs by this artist from the database + /// + /// The `id` field of this artist must be present (Some) to get songs + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of songs, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::songs::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Artist id must be present (Some) to get songs")?; + + let my_songs = songs + .inner_join(song_artists) + .filter(artist_id.eq(my_id)) + .select(songs::all_columns()) + .load(conn)?; + + Ok(my_songs) + } +} + /// Model for an album #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] @@ -71,6 +186,62 @@ pub struct Album { pub release_date: Option, } +impl Album { + /// Add an artist to this album in the database + /// + /// The `id` field of this album must be present (Some) to add an artist + /// + /// # Arguments + /// + /// * `new_artist_id` - The id of the artist to add to this album + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result<(), Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn add_artist(self: &Self, new_artist_id: i32, conn: &mut PgPooledConn) -> Result<(), Box> { + use crate::schema::album_artists::dsl::*; + + let my_id = self.id.ok_or("Album id must be present (Some) to add an artist")?; + + diesel::insert_into(album_artists) + .values((album_id.eq(my_id), artist_id.eq(new_artist_id))) + .execute(conn)?; + + Ok(()) + } + + /// Get songs by this artist from the database + /// + /// The `id` field of this album must be present (Some) to get songs + /// + /// # Arguments + /// + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with a vector of songs, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_songs(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::songs::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Album id must be present (Some) to get songs")?; + + let my_songs = songs + .inner_join(song_artists) + .filter(album_id.eq(my_id)) + .select(songs::all_columns()) + .load(conn)?; + + Ok(my_songs) + } +} + /// Model for a song #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] @@ -94,3 +265,34 @@ pub struct Song { /// The path to the song's image file pub image_path: Option, } + +impl Song { + /// Add an artist to this song in the database + /// + /// The `id` field of this song must be present (Some) to add an artist + /// + /// # Arguments + /// + /// * `new_artist_id` - The id of the artist to add to this song + /// * `conn` - A mutable reference to a database connection + /// + /// # Returns + /// + /// * `Result, Box>` - A result indicating success with an empty value, or an error + /// + #[cfg(feature = "ssr")] + pub fn get_artists(self: &Self, conn: &mut PgPooledConn) -> Result, Box> { + use crate::schema::artists::dsl::*; + use crate::schema::song_artists::dsl::*; + + let my_id = self.id.ok_or("Song id must be present (Some) to get artists")?; + + let my_artists = artists + .inner_join(song_artists) + .filter(song_id.eq(my_id)) + .select(artists::all_columns()) + .load(conn)?; + + Ok(my_artists) + } +} From 11bc55bf619ecdfe0e2fea33a1004be168cc6684 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 01:51:27 -0500 Subject: [PATCH 37/49] Add migration to install pg_trgm extension --- migrations/2024-02-16-064035_create_pg_trgm/down.sql | 1 + migrations/2024-02-16-064035_create_pg_trgm/up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 migrations/2024-02-16-064035_create_pg_trgm/down.sql create mode 100644 migrations/2024-02-16-064035_create_pg_trgm/up.sql diff --git a/migrations/2024-02-16-064035_create_pg_trgm/down.sql b/migrations/2024-02-16-064035_create_pg_trgm/down.sql new file mode 100644 index 0000000..47fd365 --- /dev/null +++ b/migrations/2024-02-16-064035_create_pg_trgm/down.sql @@ -0,0 +1 @@ +DROP EXTENSION pg_trgm; diff --git a/migrations/2024-02-16-064035_create_pg_trgm/up.sql b/migrations/2024-02-16-064035_create_pg_trgm/up.sql new file mode 100644 index 0000000..d497121 --- /dev/null +++ b/migrations/2024-02-16-064035_create_pg_trgm/up.sql @@ -0,0 +1 @@ +CREATE EXTENSION pg_trgm; From bd7b1ebd1a1deffc5c2c4fcb81b7556409ad114a Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 14:18:06 -0500 Subject: [PATCH 38/49] Add migration to create artist, album, and song title indicies --- migrations/2024-02-16-191139_create_title_indicies/down.sql | 3 +++ migrations/2024-02-16-191139_create_title_indicies/up.sql | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 migrations/2024-02-16-191139_create_title_indicies/down.sql create mode 100644 migrations/2024-02-16-191139_create_title_indicies/up.sql diff --git a/migrations/2024-02-16-191139_create_title_indicies/down.sql b/migrations/2024-02-16-191139_create_title_indicies/down.sql new file mode 100644 index 0000000..1306204 --- /dev/null +++ b/migrations/2024-02-16-191139_create_title_indicies/down.sql @@ -0,0 +1,3 @@ +DROP INDEX artists_name_idx; +DROP INDEX albums_title_idx; +DROP INDEX songs_title_idx; diff --git a/migrations/2024-02-16-191139_create_title_indicies/up.sql b/migrations/2024-02-16-191139_create_title_indicies/up.sql new file mode 100644 index 0000000..61e63ab --- /dev/null +++ b/migrations/2024-02-16-191139_create_title_indicies/up.sql @@ -0,0 +1,3 @@ +CREATE INDEX artists_name_idx ON artists USING GIST (name gist_trgm_ops); +CREATE INDEX albums_title_idx ON albums USING GIST (title gist_trgm_ops); +CREATE INDEX songs_title_idx ON songs USING GIST (title gist_trgm_ops); From 69eb1e866a05eb0fb56104874fe80e9d7f8458c5 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 14:18:29 -0500 Subject: [PATCH 39/49] Add serde feature to time crate --- Cargo.lock | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 14117ad..3b442bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -837,6 +837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index aecc4be..93a1db9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ diesel = { version = "2.1.4", features = ["postgres", "r2d2", "time"], optional lazy_static = { version = "1.4.0", optional = true } serde = { versions = "1.0.195", features = ["derive"] } openssl = { version = "0.10.63", optional = true } -time = "0.3.34" +time = { version = "0.3.34", features = ["serde"] } diesel_migrations = { version = "2.1.0", optional = true } actix-identity = { version = "0.7.0", optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } From 201afec219643ec40174a34ffe24da96448233a2 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 17:22:58 -0500 Subject: [PATCH 40/49] Add futures crate --- Cargo.lock | 1 + Cargo.toml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3b442bd..78e41a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1701,6 +1701,7 @@ dependencies = [ "diesel", "diesel_migrations", "dotenv", + "futures", "http", "lazy_static", "leptos", diff --git a/Cargo.toml b/Cargo.toml index 93a1db9..6b8531e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ diesel_migrations = { version = "2.1.0", optional = true } actix-identity = { version = "0.7.0", optional = true } actix-session = { version = "0.9.0", features = ["redis-rs-session"], optional = true } pbkdf2 = { version = "0.12.2", features = ["simple"], optional = true } +futures = { version = "0.3.30", default-features = false, optional = true } [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -53,6 +54,7 @@ ssr = [ "actix-identity", "actix-session", "pbkdf2", + "futures", ] # Defines a size-optimized profile for the WASM bundle in release mode From 8066c80459c309964e1873c6af4a6dd8e2374688 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 16 Feb 2024 23:45:38 -0500 Subject: [PATCH 41/49] Add search functions --- src/lib.rs | 1 + src/models.rs | 3 ++ src/search.rs | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/search.rs diff --git a/src/lib.rs b/src/lib.rs index 5eaf272..1897b87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod playbar; pub mod database; pub mod models; pub mod users; +pub mod search; use cfg_if::cfg_if; cfg_if! { diff --git a/src/models.rs b/src/models.rs index 0c94672..7ad0cda 100644 --- a/src/models.rs +++ b/src/models.rs @@ -47,6 +47,7 @@ pub struct User { #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::artists))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] pub struct Artist { /// A unique id for the artist #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] @@ -167,6 +168,7 @@ impl Artist { #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::albums))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] pub struct Album { /// A unique id for the album #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] @@ -237,6 +239,7 @@ impl Album { #[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))] #[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::songs))] #[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))] +#[derive(Serialize, Deserialize)] pub struct Song { /// A unique id for the song #[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))] diff --git a/src/search.rs b/src/search.rs new file mode 100644 index 0000000..3b24f08 --- /dev/null +++ b/src/search.rs @@ -0,0 +1,109 @@ +use leptos::*; +use crate::models::{Artist, Album, Song}; + +use cfg_if::cfg_if; + +cfg_if! { +if #[cfg(feature = "ssr")] { + use diesel::sql_types::*; + use diesel::*; + use diesel::pg::Pg; + use diesel::expression::AsExpression; + + use crate::database::get_db_conn; + + // Define pg_trgm operators + // Functions do not use indices for queries, so we need to use operators + diesel::infix_operator!(Similarity, " % ", backend: Pg); + diesel::infix_operator!(Distance, " <-> ", Float, backend: Pg); + + // Create functions to make use of the operators in queries + fn trgm_similar, U: AsExpression>(left: T, right: U) + -> Similarity { + Similarity::new(left.as_expression(), right.as_expression()) + } + + fn trgm_distance, U: AsExpression>(left: T, right: U) + -> Distance { + Distance::new(left.as_expression(), right.as_expression()) + } +} +} + +/// Search for albums by title +/// +/// # Arguments +/// `query` - The search query. This will be used to perform a fuzzy search on the album titles +/// `limit` - The maximum number of results to return +/// +/// # Returns +/// A Result containing a vector of albums if the search was successful, or an error if the search failed +#[server(endpoint = "search_albums")] +pub async fn search_albums(query: String, limit: i64) -> Result, ServerFnError> { + use crate::schema::albums::dsl::*; + + Ok(albums + .filter(trgm_similar(title, query.clone())) + .order_by(trgm_distance(title, query)) + .limit(limit) + .load(&mut get_db_conn())?) +} + +/// Search for artists by name +/// +/// # Arguments +/// `query` - The search query. This will be used to perform a fuzzy search on the artist names +/// `limit` - The maximum number of results to return +/// +/// # Returns +/// A Result containing a vector of artists if the search was successful, or an error if the search failed +#[server(endpoint = "search_artists")] +pub async fn search_artists(query: String, limit: i64) -> Result, ServerFnError> { + use crate::schema::artists::dsl::*; + + Ok(artists + .filter(trgm_similar(name, query.clone())) + .order_by(trgm_distance(name, query)) + .limit(limit) + .load(&mut get_db_conn())?) +} + +/// Search for songs by title +/// +/// # Arguments +/// `query` - The search query. This will be used to perform a fuzzy search on the song titles +/// `limit` - The maximum number of results to return +/// +/// # Returns +/// A Result containing a vector of songs if the search was successful, or an error if the search failed +#[server(endpoint = "search_songs")] +pub async fn search_songs(query: String, limit: i64) -> Result, ServerFnError> { + use crate::schema::songs::dsl::*; + + Ok(songs + .filter(trgm_similar(title, query.clone())) + .order_by(trgm_distance(title, query)) + .limit(limit) + .load(&mut get_db_conn())?) +} + +/// Search for songs, albums, and artists by title or name +/// +/// # Arguments +/// `query` - The search query. This will be used to perform a fuzzy search on the +/// song titles, album titles, and artist names +/// `limit` - The maximum number of results to return for each type +/// +/// # Returns +/// A Result containing a tuple of vectors of albums, artists, and songs if the search was successful, +#[server(endpoint = "search")] +pub async fn search(query: String, limit: i64) -> Result<(Vec, Vec, Vec), ServerFnError> { + let albums = search_albums(query.clone(), limit); + let artists = search_artists(query.clone(), limit); + let songs = search_songs(query.clone(), limit); + + use futures::join; + + let (albums, artists, songs) = join!(albums, artists, songs); + Ok((albums?, artists?, songs?)) +} From c30ba689b1e4cda0340fead3c3d8d454824b9457 Mon Sep 17 00:00:00 2001 From: dannyzou18 Date: Sun, 18 Feb 2024 01:53:57 -0500 Subject: [PATCH 42/49] Added functionalty to the signup page. modified: src/pages/signup.rs --- src/pages/signup.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/pages/signup.rs b/src/pages/signup.rs index 6bc06da..7ea107e 100644 --- a/src/pages/signup.rs +++ b/src/pages/signup.rs @@ -2,6 +2,9 @@ use leptos::leptos_dom::*; use leptos::*; use leptos_icons::IoIcon::*; use leptos_icons::*; +use crate::auth::signup; +use crate::models::User; + #[component] pub fn Signup() -> impl IntoView { @@ -9,6 +12,26 @@ pub fn Signup() -> impl IntoView { let (email, set_email) = create_signal("".to_string()); let (password, set_password) = create_signal("".to_string()); + let on_submit = move |ev: leptos::ev::SubmitEvent| { + ev.prevent_default(); + let new_user = User { + id: None, + username: username.get(), + email: email.get(), + password: Some(password.get()), + created_at: None, + }; + + spawn_local(async move { + if let Err(err) = signup(new_user).await { + // Handle the error here, e.g., log it or display to the user + log!("Error signing up: {:?}", err); + } else { + // Redirect to the login page + log!("Signed up successfully!"); + } + }); + }; view!{
@@ -17,7 +40,7 @@ pub fn Signup() -> impl IntoView {

LibreTunes

-
-