From ec76bbe5398f51ffa55bfd315b30c0a07245d4e6 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sun, 22 Jan 2023 13:56:06 +0100 Subject: this is *horrible* --- server/src/routes/ui/account/mod.rs | 148 +++++++++++++++++++++++++------ server/src/routes/ui/account/session.rs | 58 ++++++++++++ server/src/routes/ui/error.rs | 61 ++++++++----- server/src/routes/ui/home.rs | 18 ++-- server/src/routes/ui/layout.rs | 50 ++++++++++- server/src/routes/ui/mod.rs | 40 ++++++--- server/src/routes/ui/node.rs | 30 ++++--- server/src/routes/ui/player.rs | 35 ++++---- server/src/routes/ui/style/transition.js | 2 +- 9 files changed, 334 insertions(+), 108 deletions(-) create mode 100644 server/src/routes/ui/account/session.rs (limited to 'server/src/routes/ui') diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs index 7e329a1..74710d9 100644 --- a/server/src/routes/ui/account/mod.rs +++ b/server/src/routes/ui/account/mod.rs @@ -1,8 +1,20 @@ -use super::HtmlTemplate; +pub mod session; + +use super::error::MyError; +use super::layout::LayoutPage; +use crate::database::Database; use crate::database::User; -use crate::{AppState, CONF}; +use crate::routes::ui::error::MyResult; +use crate::routes::ui::home::rocket_uri_macro_r_home; +use crate::routes::ui::layout::DynLayoutPage; +use crate::CONF; +use anyhow::anyhow; +use argon2::{Argon2, PasswordHasher}; +use rocket::form::Contextual; use rocket::form::Form; -use rocket::{get, post, FromForm, State}; +use rocket::http::{Cookie, CookieJar}; +use rocket::response::Redirect; +use rocket::{get, post, uri, FromForm, State}; #[derive(FromForm)] pub struct RegisterForm { @@ -15,10 +27,10 @@ pub struct RegisterForm { } #[get("/account/register")] -pub fn r_account_register() -> HtmlTemplate> { - HtmlTemplate( - "Register".to_string(), - markup::new! { +pub async fn r_account_register() -> DynLayoutPage<'static> { + LayoutPage { + title: "Register".to_string(), + content: markup::new! { h1 { "Register for " @CONF.brand } form[method="POST", action=""] { label[for="inp-invitation"] { "Invite Code: " } @@ -32,41 +44,121 @@ pub fn r_account_register() -> HtmlTemplate> { input[type="submit", value="Register now!"]; } }, - ) + } +} + +#[derive(FromForm)] +pub struct LoginForm { + #[field(validate = len(4..32))] + pub username: String, + #[field(validate = len(..64))] + pub password: String, } #[get("/account/login")] -pub fn r_account_login() -> HtmlTemplate> { - HtmlTemplate( - "Log in".to_string(), - markup::new! { +pub fn r_account_login() -> DynLayoutPage<'static> { + LayoutPage { + title: "Log in".to_string(), + content: markup::new! { h1 { "Log in to your Account" } + form[method="POST", action=""] { + label[for="inp-username"] { "Username: " } + input[type="text", id="inp-username", name="username"]; br; + label[for="inp-password"] { "Password: " } + input[type="password", id="inp-password", name="password"]; br; + input[type="submit", value="Login"]; + } + p { "While logged in, a cookie will be used to identify you." } }, - ) + } } #[post("/account/register", data = "
")] -pub fn r_account_register_post( - state: &State, - form: Form, -) -> HtmlTemplate> { - state - .database +pub fn r_account_register_post<'a>( + database: &'a State, + form: Form>, +) -> MyResult> { + let form = match &form.value { + Some(v) => v, + None => return Err(format_form_error(form)), + }; + + if database.invites.remove(&form.invitation).unwrap().is_none() { + return Err(MyError(anyhow!("invitation invalid"))); + } + match database .users - .insert( + .compare_and_swap( &form.username, - &User { + None, + Some(&User { display_name: form.username.clone(), name: form.username.clone(), password: form.password.clone().into(), // TODO hash it + admin: false, + }), + ) + .unwrap() + { + Ok(_) => Ok(LayoutPage { + title: "Registration successful".to_string(), + content: markup::new! { + h1 { "Registration successful, you may log in now." } }, + }), + Err(_) => Err(MyError(anyhow!("username is taken"))), + } +} + +#[post("/account/login", data = "")] +pub fn r_account_login_post( + database: &State, + jar: &CookieJar, + form: Form>, +) -> MyResult { + let form = match &form.value { + Some(v) => v, + None => return Err(format_form_error(form)), + }; + + // hashing the password regardless if the accounts exists to prevent timing attacks + let password = hash_password(&form.password); + + let user = database + .users + .get(&form.username)? + .ok_or(anyhow!("invalid password"))?; + + if user.password != password { + Err(anyhow!("invalid password"))? + } + + jar.add_private(Cookie::build("user", user.name).permanent().finish()); + + Ok(Redirect::found(uri!(r_home()))) +} + +fn format_form_error(form: Form>) -> MyError { + let mut k = String::from("form validation failed:"); + for e in form.context.errors() { + k += &format!( + "\n\t{}: {e}", + e.name + .as_ref() + .map(|e| e.to_string()) + .unwrap_or("".to_string()) ) - .unwrap(); - HtmlTemplate( - "Registration successful".to_string(), - markup::new! { - h1 { "Registration successful." } - }, - ) + } + MyError(anyhow!(k)) +} + +pub fn hash_password(s: &str) -> Vec { + Argon2::default() + .hash_password(s.as_bytes(), r"IYMa13osbNeLJKnQ1T8LlA") + .unwrap() + .hash + .unwrap() + .as_bytes() + .to_vec() } diff --git a/server/src/routes/ui/account/session.rs b/server/src/routes/ui/account/session.rs new file mode 100644 index 0000000..9c50099 --- /dev/null +++ b/server/src/routes/ui/account/session.rs @@ -0,0 +1,58 @@ +use crate::{ + database::{Database, User}, + routes::ui::error::MyError, +}; +use anyhow::anyhow; +use rocket::{ + outcome::Outcome, + request::{self, FromRequest}, + Request, State, +}; + +pub struct Session { + pub user: User, +} + +impl Session { + pub async fn from_request_ut(req: &Request<'_>) -> Result { + let cookie = req + .cookies() + .get_private("user") + .ok_or(anyhow!("login required"))?; + let username = cookie.value(); + + let db = req.guard::<&State>().await.unwrap(); + let user = db + .users + .get(&username.to_string())? + .ok_or(anyhow!("user not found"))?; + + Ok(Session { user }) + } +} + +impl<'r> FromRequest<'r> for Session { + type Error = MyError; + + fn from_request<'life0, 'async_trait>( + request: &'r Request<'life0>, + ) -> core::pin::Pin< + Box< + dyn core::future::Future> + + core::marker::Send + + 'async_trait, + >, + > + where + 'r: 'async_trait, + 'life0: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + match Self::from_request_ut(request).await { + Ok(x) => Outcome::Success(x), + Err(_) => Outcome::Forward(()), + } + }) + } +} diff --git a/server/src/routes/ui/error.rs b/server/src/routes/ui/error.rs index 1a50796..011847e 100644 --- a/server/src/routes/ui/error.rs +++ b/server/src/routes/ui/error.rs @@ -1,6 +1,9 @@ +use super::layout::LayoutPage; use super::{layout::Layout, HtmlTemplate}; +use crate::routes::ui::account::rocket_uri_macro_r_account_login; use markup::Render; use rocket::http::Status; +use rocket::uri; use rocket::{ catch, http::ContentType, @@ -10,37 +13,44 @@ use rocket::{ use std::{fmt::Display, io::Cursor}; #[catch(default)] -pub fn r_not_found<'a>(status: Status, _request: &Request) -> HtmlTemplate> { - HtmlTemplate( - "Not found".to_string(), - markup::new! { - h2 { "Error" } - p { @format!("{status:?}") } - }, - ) +pub fn r_catch<'a>(status: Status, _request: &Request) -> () { + // HtmlTemplate(box Layout { + // title: "Not found".to_string(), + // session: None, + // main: markup::new! { + // h2 { "Error" } + // p { @format!("{status}") } + // @if status == Status::NotFound { + // p { "You might need to " a[href=&uri!(r_account_login()).to_string()] { "log in" } ", to see this page" } + // } + // }, + // }) + todo!() } pub type MyResult = Result; #[derive(Debug)] -pub struct MyError(anyhow::Error); +pub struct MyError(pub anyhow::Error); impl<'r> Responder<'r, 'static> for MyError { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { - let mut out = String::new(); - Layout { - title: "Error".to_string(), - main: markup::new! { - h2 { "An error occured. Nobody is sorry"} - pre.error { @format!("{:?}", self.0) } - }, - } - .render(&mut out) - .unwrap(); - Response::build() - .header(ContentType::HTML) - .streamed_body(Cursor::new(out)) - .ok() + // let mut out = String::new(); + // LayoutPage { + // title: "Error".to_string(), + // content: markup::new! { + // h2 { "An error occured. Nobody is sorry"} + // pre.error { @format!("{:?}", self.0) } + // }, + // } + // .render(&mut out) + // .unwrap(); + // Response::build() + // .header(ContentType::HTML) + // .status(Status::BadRequest) + // .streamed_body(Cursor::new(out)) + // .ok() + todo!() } } @@ -64,3 +74,8 @@ impl From for MyError { MyError(anyhow::anyhow!("{err}")) } } +impl From for MyError { + fn from(err: sled::Error) -> Self { + MyError(anyhow::anyhow!("{err}")) + } +} diff --git a/server/src/routes/ui/home.rs b/server/src/routes/ui/home.rs index 04a4c7d..88c6cfb 100644 --- a/server/src/routes/ui/home.rs +++ b/server/src/routes/ui/home.rs @@ -1,15 +1,17 @@ -use crate::routes::ui::node::NodePage; +use super::account::session::Session; +use super::layout::LayoutPage; +use crate::routes::ui::layout::DynLayoutPage; use crate::CONF; -use crate::{routes::ui::HtmlTemplate, AppState}; +use crate::{library::Library, routes::ui::node::NodePage}; use rocket::{get, State}; #[get("/")] -pub async fn r_home(state: &State) -> HtmlTemplate { - HtmlTemplate( - "Home".to_string(), - markup::new! { +pub async fn r_home(_sess: Session, library: &State) -> LayoutPage { + LayoutPage { + title: "Home".to_string(), + content: markup::new! { p { "Welcome to " @CONF.brand } - @NodePage { node: state.library.root.clone() } + @NodePage { node: library.root.clone() } }, - ) + } } diff --git a/server/src/routes/ui/layout.rs b/server/src/routes/ui/layout.rs index cda8b0f..c25e644 100644 --- a/server/src/routes/ui/layout.rs +++ b/server/src/routes/ui/layout.rs @@ -1,9 +1,18 @@ +use super::{account::session::Session, Defer, HtmlTemplate}; +use crate::{uri, CONF}; +use async_std::task::block_on; use markup::Render; - -use crate::CONF; +use rocket::{ + http::ContentType, + request::{FromRequest, Outcome}, + response::{self, Responder}, + Request, Response, +}; +use std::{convert::Infallible, io::Cursor}; +use tokio::runtime::Handle; markup::define! { - Layout(title: String, main: Main) { + Layout(title: String, main: Main, session: Option) { @markup::doctype() html { head { @@ -15,9 +24,44 @@ markup::define! { nav { h1 { a[href="/"] { @CONF.brand } } a[href="/library"] { "My Library" } + + div.account { + @if let Some(session) = session { + + } else { + // a[href=uri!(r_account_register())] { "Register" } + // a[href=uri!(r_account_login())] { "Log in" } + } + } } #main { @main } } } } } + +pub type DynLayoutPage<'a> = LayoutPage>; + +pub struct LayoutPage { + pub title: String, + pub content: T, +} + +impl<'r, Main: Render> Responder<'r, 'static> for LayoutPage
{ + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { + let session = block_on(req.guard::>()).unwrap(); + let mut out = String::new(); + Layout { + main: self.content, + title: self.title, + session, + } + .render(&mut out) + .unwrap(); + + Response::build() + .header(ContentType::HTML) + .streamed_body(Cursor::new(out)) + .ok() + } +} diff --git a/server/src/routes/ui/mod.rs b/server/src/routes/ui/mod.rs index 8a3bc4e..e062a68 100644 --- a/server/src/routes/ui/mod.rs +++ b/server/src/routes/ui/mod.rs @@ -1,34 +1,48 @@ -use self::layout::Layout; use markup::Render; use rocket::{ + futures::FutureExt, http::ContentType, response::{self, Responder}, Request, Response, }; -use std::io::Cursor; +use std::{future::Future, io::Cursor, pin::Pin}; +use tokio::io::AsyncRead; +pub mod account; pub mod error; pub mod home; pub mod layout; pub mod node; -pub mod style; pub mod player; -pub mod account; +pub mod style; -pub struct HtmlTemplate(pub String, pub T); +pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>); -impl<'r, T: Render> Responder<'r, 'static> for HtmlTemplate { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { +impl<'r> Responder<'r, 'static> for HtmlTemplate<'_> { + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { let mut out = String::new(); - Layout { - title: self.0, - main: self.1, - } - .render(&mut out) - .unwrap(); + self.0.render(&mut out).unwrap(); Response::build() .header(ContentType::HTML) .streamed_body(Cursor::new(out)) .ok() } } + +pub struct Defer(Pin + Send>>); + +impl AsyncRead for Defer { + fn poll_read( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + match self.0.poll_unpin(cx) { + std::task::Poll::Ready(r) => { + buf.put_slice(r.as_bytes()); + std::task::Poll::Ready(Ok(())) + } + std::task::Poll::Pending => std::task::Poll::Pending, + } + } +} diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs index 0b7494e..ec9cde8 100644 --- a/server/src/routes/ui/node.rs +++ b/server/src/routes/ui/node.rs @@ -1,9 +1,11 @@ use super::error::MyError; use super::player::player_uri; use crate::{ - library::{Directory, Item, Node}, - routes::ui::HtmlTemplate, - AppState, + library::{Directory, Item, Library, Node}, + routes::ui::{ + account::session::Session, + layout::{DynLayoutPage, LayoutPage}, + }, }; use anyhow::{anyhow, Context}; use log::info; @@ -13,20 +15,20 @@ use tokio::fs::File; #[get("/library/")] pub async fn r_library_node( + _sess: Session, path: PathBuf, - state: &State, -) -> Result, MyError> { - let node = state - .library + library: &State, +) -> Result, MyError> { + let node = library .nested_path(&path) .context("retrieving library node")? .clone(); - Ok(HtmlTemplate( - format!("{}", node.title()), - markup::new! { + Ok(LayoutPage { + title: format!("{}", node.title()), + content: markup::new! { @NodePage { node: node.clone() } }, - )) + }) } markup::define! { @@ -88,11 +90,11 @@ markup::define! { #[get("/item_assets/")] pub async fn r_item_assets( + _sess: Session, path: PathBuf, - state: &State, + library: &State, ) -> Result<(ContentType, File), MyError> { - let node = state - .library + let node = library .nested_path(&path) .context("retrieving library node")? .get_item()? diff --git a/server/src/routes/ui/player.rs b/server/src/routes/ui/player.rs index c93d2c1..764f583 100644 --- a/server/src/routes/ui/player.rs +++ b/server/src/routes/ui/player.rs @@ -1,16 +1,15 @@ -use super::HtmlTemplate; +use super::{account::session::Session, layout::LayoutPage, HtmlTemplate}; use crate::{ - library::Item, + library::{Item, Library}, routes::{ stream::stream_uri, - ui::{error::MyResult, node::rocket_uri_macro_r_item_assets}, + ui::{error::MyResult, layout::DynLayoutPage, node::rocket_uri_macro_r_item_assets}, }, - AppState, }; use jellycommon::SourceTrackKind; use log::warn; use rocket::{get, uri, FromForm, State}; -use std::{path::PathBuf, sync::Arc}; +use std::{alloc::Layout, path::PathBuf, sync::Arc}; pub fn player_uri(path: &PathBuf) -> String { format!("/player/{}", path.to_str().unwrap()) @@ -26,12 +25,12 @@ pub struct PlayerConfig { #[get("/player/?", rank = 4)] pub fn r_player( - state: &State, + _sess: Session, + library: &State, path: PathBuf, conf: PlayerConfig, -) -> MyResult>> { - warn!("{conf:?}"); - let item = state.library.nested_path(&path)?.get_item()?; +) -> MyResult> { + let item = library.nested_path(&path)?.get_item()?; if conf.a.is_none() && conf.v.is_none() && conf.s.is_none() { return player_conf(item.clone()); } @@ -43,15 +42,15 @@ pub fn r_player( .chain(conf.s.into_iter()) .collect::>(); - Ok(HtmlTemplate( - item.info.title.to_owned(), - markup::new! { + Ok(LayoutPage { + title: item.info.title.to_owned(), + content: markup::new! { video[src=stream_uri(&item.lib_path, &tracks), controls]; }, - )) + }) } -pub fn player_conf<'a>(item: Arc) -> MyResult>> { +pub fn player_conf<'a>(item: Arc) -> MyResult> { let mut audio_tracks = vec![]; let mut video_tracks = vec![]; let mut sub_tracks = vec![]; @@ -63,9 +62,9 @@ pub fn player_conf<'a>(item: Arc) -> MyResult(item: Arc) -> MyResult { patch_page() }) -globalThis.addEventListener("popstate", ev => { +globalThis.addEventListener("popstate", () => { transition_to(window.location.href) }) -- cgit v1.2.3-70-g09d2