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/.gitignore b/.gitignore index ab27e5e..f472188 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ playwright/.cache/ *.jpeg *.png *.gif +*.webp # Environment variables .env diff --git a/docker-compose.yml b/docker-compose.yml index c2d0865..c3d466d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,8 +13,11 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} + 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 @@ -50,5 +53,6 @@ services: volumes: libretunes-audio: + libretunes-images: libretunes-redis: libretunes-postgres: diff --git a/src/app.rs b/src/app.rs index ed9fd1b..50cad40 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 @@ -43,8 +59,8 @@ pub fn App() -> impl IntoView { - - + } /> + } /> diff --git a/src/auth.rs b/src/auth.rs index 37f861f..558bfe2 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) } } @@ -145,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")] 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) diff --git a/src/pages/login.rs b/src/pages/login.rs index 62aca49..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(true) = 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(false) = login_result { + } 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()); diff --git a/src/songdata.rs b/src/songdata.rs index 1266bf4..bbbb64b 100644 --- a/src/songdata.rs +++ b/src/songdata.rs @@ -50,7 +50,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