Merge branch '126-create-global-logged-in-user-resource' into 42-create-profile-page
This commit is contained in:
commit
35abbe19ee
@ -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
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,6 +24,7 @@ playwright/.cache/
|
||||
*.jpeg
|
||||
*.png
|
||||
*.gif
|
||||
*.webp
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
@ -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:
|
||||
|
20
src/app.rs
20
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<User>>;
|
||||
|
||||
#[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 <head>
|
||||
// id=leptos means cargo-leptos will hot-reload this stylesheet
|
||||
@ -43,8 +59,8 @@ pub fn App() -> impl IntoView {
|
||||
<Route path="dashboard" view=Dashboard />
|
||||
<Route path="search" view=Search />
|
||||
</Route>
|
||||
<Route path="/login" view=Login />
|
||||
<Route path="/signup" view=Signup />
|
||||
<Route path="/login" view=move || view!{ <Login user=logged_in_user /> } />
|
||||
<Route path="/signup" view=move || view!{ <Signup user=logged_in_user /> } />
|
||||
</Routes>
|
||||
</main>
|
||||
</Router>
|
||||
|
23
src/auth.rs
23
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<bool, ServerFnError> {
|
||||
pub async fn login(credentials: UserCredentials) -> Result<Option<User>, ServerFnError> {
|
||||
use crate::users::validate_user;
|
||||
|
||||
let mut auth_session = extract::<AuthSession<AuthBackend>>().await
|
||||
@ -66,12 +66,14 @@ pub async fn login(credentials: UserCredentials) -> Result<bool, ServerFnError>
|
||||
let user = validate_user(credentials).await
|
||||
.map_err(|e| ServerFnError::<NoCustomError>::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::<NoCustomError>::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<User, ServerFnError> {
|
||||
auth_session.user.ok_or(ServerFnError::<NoCustomError>::ServerError("User not logged in".to_string()))
|
||||
}
|
||||
|
||||
#[server(endpoint = "get_logged_in_user")]
|
||||
pub async fn get_logged_in_user() -> Result<Option<User>, ServerFnError> {
|
||||
let auth_session = extract::<AuthSession<AuthBackend>>().await
|
||||
.map_err(|e| ServerFnError::<NoCustomError>::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")]
|
||||
|
@ -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<LeptosOptions>, req: Request<Body>) -> 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<Response<Body>, (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<Response<Body>, (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"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
|
@ -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<String>| get_asset_file(song, AssetType::Audio)))
|
||||
.route("/assets/images/:image", get(|Path(image) : Path<String>| get_asset_file(image, AssetType::Image)))
|
||||
.route("/assets/*uri", get(|uri| get_static_file(uri, "")))
|
||||
.layer(auth_layer)
|
||||
.fallback(file_and_error_handler)
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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());
|
||||
|
@ -50,7 +50,6 @@ impl TryInto<Song> 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user