From a43955726abcdd7077374c645416c10718472b4a Mon Sep 17 00:00:00 2001 From: Carter Bertolini Date: Fri, 4 Oct 2024 17:33:51 -0400 Subject: [PATCH 01/10] Setup routing for image and audio assets based on environment variables --- src/fileserv.rs | 30 ++++++++++++++++++++++++++++++ src/main.rs | 6 ++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/fileserv.rs b/src/fileserv.rs index 4fe7a30..dbfccc6 100644 --- a/src/fileserv.rs +++ b/src/fileserv.rs @@ -12,6 +12,7 @@ cfg_if! { if #[cfg(feature = "ssr")] { use tower_http::services::ServeDir; use leptos::*; use crate::app::App; + use std::str::FromStr; pub async fn file_and_error_handler(uri: Uri, State(options): State, req: Request) -> AxumResponse { let root = options.site_root.clone(); @@ -27,6 +28,7 @@ cfg_if! { if #[cfg(feature = "ssr")] { pub async fn get_static_file(uri: Uri, root: &str) -> Result, (StatusCode, String)> { let req = Request::builder().uri(uri.clone()).body(Body::empty()).unwrap(); + // `ServeDir` implements `tower::Service` so we can call it with `tower::ServiceExt::oneshot` // This path is relative to the cargo root match ServeDir::new(root).oneshot(req).await.ok() { @@ -37,4 +39,32 @@ cfg_if! { if #[cfg(feature = "ssr")] { )), } } + + pub enum AssetType { + Audio, + Image, + } + + pub async fn get_asset_file(filename: String, asset_type: AssetType) -> Result, (StatusCode, String)> { + const DEFAULT_AUDIO_PATH: &str = "assets/audio"; + const DEFAULT_IMAGE_PATH: &str = "assets/images"; + + let root = match asset_type { + AssetType::Audio => std::env::var("LIBRETUNES_AUDIO_PATH").unwrap_or(DEFAULT_AUDIO_PATH.to_string()), + AssetType::Image => std::env::var("LIBRETUNES_IMAGE_PATH").unwrap_or(DEFAULT_IMAGE_PATH.to_string()), + }; + + // Create a Uri from the filename + // ServeDir expects a leading `/` + let uri = Uri::from_str(format!("/{}", filename).as_str()); + + match uri { + Ok(uri) => get_static_file(uri, root.as_str()).await, + Err(_) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Attempted to serve an invalid file"), + )), + } + } + }} diff --git a/src/main.rs b/src/main.rs index 6e50edf..a4efbee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,11 +14,11 @@ extern crate diesel_migrations; #[cfg(feature = "ssr")] #[tokio::main] async fn main() { - use axum::{routing::get, Router}; + use axum::{routing::get, Router, extract::Path}; use leptos::*; use leptos_axum::{generate_route_list, LeptosRoutes}; use libretunes::app::*; - use libretunes::fileserv::{file_and_error_handler, get_static_file}; + use libretunes::fileserv::{file_and_error_handler, get_asset_file, get_static_file, AssetType}; use axum_login::tower_sessions::SessionManagerLayer; use tower_sessions_redis_store::{fred::prelude::*, RedisStore}; use axum_login::AuthManagerLayerBuilder; @@ -60,6 +60,8 @@ async fn main() { let app = Router::new() .leptos_routes(&leptos_options, routes, App) + .route("/assets/audio/:song", get(|Path(song) : Path| get_asset_file(song, AssetType::Audio))) + .route("/assets/images/:image", get(|Path(image) : Path| get_asset_file(image, AssetType::Image))) .route("/assets/*uri", get(|uri| get_static_file(uri, ""))) .layer(auth_layer) .fallback(file_and_error_handler) From b222b7911ff68e6019abea92f8e4ac20e37c2fbd Mon Sep 17 00:00:00 2001 From: Carter Bertolini Date: Fri, 11 Oct 2024 16:53:08 -0400 Subject: [PATCH 02/10] Add example environment variables and update docker-compose --- .env.example | 3 +++ docker-compose.yml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.env.example b/.env.example index ef8c8dd..893f587 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,6 @@ DATABASE_URL=postgresql://libretunes:password@localhost:5432/libretunes # POSTGRES_HOST=localhost # POSTGRES_PORT=5432 # POSTGRES_DB=libretunes + +LIBRETUNES_AUDIO_PATH=assets/audio +LIBRETUNES_IMAGE_PATH=assets/images diff --git a/docker-compose.yml b/docker-compose.yml index c2d0865..fffc2b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,8 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} + LIBRETUNES_AUDIO_PATH: /site/assets/audio + LIBRETUNES_IMAGE_PATH: /site/assets/images volumes: - libretunes-audio:/site/audio depends_on: From e90ea56d149b9907618dc7a02bc10d50c03fd004 Mon Sep 17 00:00:00 2001 From: Carter Bertolini Date: Fri, 11 Oct 2024 17:16:51 -0400 Subject: [PATCH 03/10] Remove TODO --- src/songdata.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/songdata.rs b/src/songdata.rs index 61e263f..d8db885 100644 --- a/src/songdata.rs +++ b/src/songdata.rs @@ -47,7 +47,6 @@ impl TryInto for SongData { track: self.track, duration: self.duration, release_date: self.release_date, - // TODO https://gitlab.mregirouard.com/libretunes/libretunes/-/issues/35 storage_path: self.song_path, // Note that if the source of the image_path was the album, the image_path From f39d93bd0c66e551762e2ef58aac1061c4ea6544 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Tue, 15 Oct 2024 23:22:15 -0400 Subject: [PATCH 04/10] Change audio and image path Docker comopse settings Add volume for images --- docker-compose.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index fffc2b5..2a1ea56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,10 +13,11 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} - LIBRETUNES_AUDIO_PATH: /site/assets/audio - LIBRETUNES_IMAGE_PATH: /site/assets/images + LIBRETUNES_AUDIO_PATH: /assets/audio + LIBRETUNES_IMAGE_PATH: /assets/images volumes: - - libretunes-audio:/site/audio + - libretunes-audio:/assets/audio + - libretunes-images:/assets/images depends_on: - redis - postgres From 14aa4d236ea48d0a7e0a71d2cf090efbf6708337 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Fri, 25 Oct 2024 18:28:45 +0000 Subject: [PATCH 05/10] Add libretunes-images to volumes --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 2a1ea56..c3d466d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,5 +53,6 @@ services: volumes: libretunes-audio: + libretunes-images: libretunes-redis: libretunes-postgres: From a70de76c4d3a796ec26fd63a8c4c4178f4587759 Mon Sep 17 00:00:00 2001 From: Daniel Miller Date: Sat, 2 Nov 2024 02:37:50 +0000 Subject: [PATCH 06/10] Added webp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ab27e5e..f472188 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ playwright/.cache/ *.jpeg *.png *.gif +*.webp # Environment variables .env From 251d56786399a7fe50d77219ab41a0f2afd3d605 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 3 Nov 2024 16:52:15 -0500 Subject: [PATCH 07/10] Return logged in user from login endpoint --- src/auth.rs | 10 ++++++---- src/pages/login.rs | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 37f861f..c0751d1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -57,7 +57,7 @@ pub async fn signup(new_user: User) -> Result<(), ServerFnError> { /// 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(credentials: UserCredentials) -> Result { +pub async fn login(credentials: UserCredentials) -> Result, ServerFnError> { use crate::users::validate_user; let mut auth_session = extract::>().await @@ -66,12 +66,14 @@ pub async fn login(credentials: UserCredentials) -> Result let user = validate_user(credentials).await .map_err(|e| ServerFnError::::ServerError(format!("Error validating user: {}", e)))?; - if let Some(user) = user { + if let Some(mut user) = user { auth_session.login(&user).await .map_err(|e| ServerFnError::::ServerError(format!("Error logging in user: {}", e)))?; - Ok(true) + + user.password = None; + Ok(Some(user)) } else { - Ok(false) + Ok(None) } } diff --git a/src/pages/login.rs b/src/pages/login.rs index 62aca49..1fda5ca 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -32,12 +32,12 @@ pub fn Login() -> impl IntoView { if let Err(err) = login_result { // Handle the error here, e.g., log it or display to the user log!("Error logging in: {:?}", err); - } else if let Ok(true) = login_result { + } else if let Ok(Some(_)) = login_result { // Redirect to the login page log!("Logged in Successfully!"); leptos_router::use_navigate()("/", Default::default()); log!("Navigated to home page after login"); - } else if let Ok(false) = login_result { + } else if let Ok(None) = login_result { log!("Invalid username or password"); } }); From 43a5b519fdf8e5fb744db287d907bf59bdc5e23f Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 3 Nov 2024 16:59:23 -0500 Subject: [PATCH 08/10] Add API endpoint to get logged in user --- src/auth.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/auth.rs b/src/auth.rs index c0751d1..558bfe2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -147,6 +147,19 @@ pub async fn get_user() -> Result { auth_session.user.ok_or(ServerFnError::::ServerError("User not logged in".to_string())) } +#[server(endpoint = "get_logged_in_user")] +pub async fn get_logged_in_user() -> Result, ServerFnError> { + let auth_session = extract::>().await + .map_err(|e| ServerFnError::::ServerError(format!("Error getting auth session: {}", e)))?; + + let user = auth_session.user.map(|mut user| { + user.password = None; + user + }); + + Ok(user) +} + /// Check if a user is an admin /// Returns a Result with a boolean indicating if the user is logged in and an admin #[server(endpoint = "check_admin")] From 8ac3a87c58a2e1512e4e7ec017341713e5d0dac1 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 3 Nov 2024 16:59:46 -0500 Subject: [PATCH 09/10] Add global logged in user Resource --- src/app.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/app.rs b/src/app.rs index ed9fd1b..9e91878 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,12 +3,16 @@ use crate::playbar::CustomTitle; use crate::playstatus::PlayStatus; use crate::queue::Queue; use leptos::*; +use leptos::logging::*; use leptos_meta::*; use leptos_router::*; use crate::pages::login::*; use crate::pages::signup::*; use crate::error_template::{AppError, ErrorTemplate}; +use crate::auth::get_logged_in_user; +use crate::models::User; +pub type LoggedInUserResource = Resource<(), Option>; #[component] pub fn App() -> impl IntoView { @@ -19,6 +23,18 @@ pub fn App() -> impl IntoView { let play_status = create_rw_signal(play_status); let upload_open = create_rw_signal(false); + // A resource that fetches the logged in user + // This will not automatically refetch, so any login/logout related code + // should call `refetch` on this resource + let logged_in_user: LoggedInUserResource = create_resource(|| (), |_| async { + get_logged_in_user().await + .inspect_err(|e| { + error!("Error getting logged in user: {:?}", e); + }) + .ok() + .flatten() + }); + view! { // injects a stylesheet into the document // id=leptos means cargo-leptos will hot-reload this stylesheet From 2b380d77876cd7aef245a4ab1e2e5cf26b70ae9c Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Sun, 3 Nov 2024 17:13:40 -0500 Subject: [PATCH 10/10] Refetch logged in user on login/signup --- src/app.rs | 4 ++-- src/pages/login.rs | 14 ++++++++++++-- src/pages/signup.rs | 14 +++++++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9e91878..50cad40 100644 --- a/src/app.rs +++ b/src/app.rs @@ -59,8 +59,8 @@ pub fn App() -> impl IntoView { - - + } /> + } /> diff --git a/src/pages/login.rs b/src/pages/login.rs index 1fda5ca..585f2f0 100644 --- a/src/pages/login.rs +++ b/src/pages/login.rs @@ -3,9 +3,10 @@ use leptos::leptos_dom::*; use leptos::*; use leptos_icons::*; use crate::users::UserCredentials; +use crate::app::LoggedInUserResource; #[component] -pub fn Login() -> impl IntoView { +pub fn Login(user: LoggedInUserResource) -> impl IntoView { let (username_or_email, set_username_or_email) = create_signal("".to_string()); let (password, set_password) = create_signal("".to_string()); @@ -32,13 +33,22 @@ pub fn Login() -> impl IntoView { if let Err(err) = login_result { // Handle the error here, e.g., log it or display to the user log!("Error logging in: {:?}", err); - } else if let Ok(Some(_)) = login_result { + + // Since we're not sure what the state is, manually refetch the user + user.refetch(); + } else if let Ok(Some(login_user)) = login_result { + // Manually set the user to the new user, avoiding a refetch + user.set(Some(login_user)); + // Redirect to the login page log!("Logged in Successfully!"); leptos_router::use_navigate()("/", Default::default()); log!("Navigated to home page after login"); } else if let Ok(None) = login_result { log!("Invalid username or password"); + + // User could be already logged in or not, so refetch the user + user.refetch(); } }); }; diff --git a/src/pages/signup.rs b/src/pages/signup.rs index f02dfab..8e9a0ac 100644 --- a/src/pages/signup.rs +++ b/src/pages/signup.rs @@ -3,9 +3,10 @@ use crate::models::User; use leptos::leptos_dom::*; use leptos::*; use leptos_icons::*; +use crate::app::LoggedInUserResource; #[component] -pub fn Signup() -> impl IntoView { +pub fn Signup(user: LoggedInUserResource) -> 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()); @@ -19,7 +20,7 @@ pub fn Signup() -> impl IntoView { let on_submit = move |ev: leptos::ev::SubmitEvent| { ev.prevent_default(); - let new_user = User { + let mut new_user = User { id: None, username: username.get(), email: email.get(), @@ -30,10 +31,17 @@ pub fn Signup() -> impl IntoView { log!("new user: {:?}", new_user); spawn_local(async move { - if let Err(err) = signup(new_user).await { + if let Err(err) = signup(new_user.clone()).await { // Handle the error here, e.g., log it or display to the user log!("Error signing up: {:?}", err); + + // Since we're not sure what the state is, manually refetch the user + user.refetch(); } else { + // Manually set the user to the new user, avoiding a refetch + new_user.password = None; + user.set(Some(new_user)); + // Redirect to the login page log!("Signed up successfully!"); leptos_router::use_navigate()("/", Default::default());