diff options
-rw-r--r-- | Cargo.lock | 359 | ||||
-rw-r--r-- | remuxer/src/lib.rs | 5 | ||||
-rw-r--r-- | server/Cargo.toml | 4 | ||||
-rw-r--r-- | server/src/config.rs | 3 | ||||
-rw-r--r-- | server/src/database.rs | 6 | ||||
-rw-r--r-- | server/src/library.rs | 2 | ||||
-rw-r--r-- | server/src/main.rs | 32 | ||||
-rw-r--r-- | server/src/routes/mod.rs | 43 | ||||
-rw-r--r-- | server/src/routes/stream.rs | 13 | ||||
-rw-r--r-- | server/src/routes/ui/account/mod.rs | 148 | ||||
-rw-r--r-- | server/src/routes/ui/account/session.rs | 58 | ||||
-rw-r--r-- | server/src/routes/ui/error.rs | 61 | ||||
-rw-r--r-- | server/src/routes/ui/home.rs | 18 | ||||
-rw-r--r-- | server/src/routes/ui/layout.rs | 50 | ||||
-rw-r--r-- | server/src/routes/ui/mod.rs | 40 | ||||
-rw-r--r-- | server/src/routes/ui/node.rs | 30 | ||||
-rw-r--r-- | server/src/routes/ui/player.rs | 35 | ||||
-rw-r--r-- | server/src/routes/ui/style/transition.js | 2 |
18 files changed, 767 insertions, 142 deletions
@@ -53,6 +53,113 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] name = "async-stream" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -74,6 +181,12 @@ dependencies = [ ] [[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] name = "async-trait" version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -94,6 +207,12 @@ dependencies = [ ] [[package]] +name = "atomic-waker" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599" + +[[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -117,6 +236,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" [[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + +[[package]] name = "binascii" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -157,6 +282,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] name = "block-buffer" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -166,6 +300,26 @@ dependencies = [ ] [[package]] +name = "blocking" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", +] + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -247,6 +401,15 @@ dependencies = [ ] [[package]] +name = "concurrent-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] name = "cookie" version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -316,6 +479,16 @@ dependencies = [ ] [[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + +[[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -426,6 +599,12 @@ dependencies = [ ] [[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] name = "fastrand" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -507,6 +686,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] name = "futures-sink" version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -595,6 +789,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "h2" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -823,6 +1029,8 @@ name = "jellything" version = "0.1.0" dependencies = [ "anyhow", + "argon2", + "async-std", "chashmap", "env_logger", "jellycommon", @@ -854,6 +1062,24 @@ dependencies = [ ] [[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -888,6 +1114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", + "value-bag", ] [[package]] @@ -1048,6 +1275,12 @@ dependencies = [ ] [[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] name = "parking_lot" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1118,6 +1351,17 @@ dependencies = [ ] [[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] name = "pear" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1179,6 +1423,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "windows-sys", +] + +[[package]] name = "polyval" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1948,6 +2206,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] +name = "value-bag" +version = "1.0.0-alpha.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1960,6 +2228,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b60dcd6a64dd45abf9bd426970c9843726da7fc08f44cd6fcebf68c21220a63" [[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] name = "want" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1976,6 +2250,91 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/remuxer/src/lib.rs b/remuxer/src/lib.rs index fabde10..12acae3 100644 --- a/remuxer/src/lib.rs +++ b/remuxer/src/lib.rs @@ -13,11 +13,12 @@ use jellymatroska::{ use log::{debug, info, trace, warn}; use std::{collections::VecDeque, fs::File, io::Write, path::PathBuf, sync::Arc}; +#[derive(Debug, Clone)] pub struct RemuxerContext {} impl RemuxerContext { - pub fn new() -> Arc<Self> { - Arc::new(Self {}) + pub fn new() -> Self { + Self {} } pub fn generate_into( diff --git a/server/Cargo.toml b/server/Cargo.toml index 482baa8..a259d98 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -15,8 +15,10 @@ env_logger = "0.10.0" anyhow = "1.0.68" once_cell = "1.17.0" chashmap = "2.2.2" +argon2 = "0.4.1" -rocket = "0.5.0-rc.2" +async-std = "1.12.0" +rocket = { version = "0.5.0-rc.2", features = ["secrets"] } tokio = { version = "1.24.1", features = ["io-util"] } tokio-util = { version = "0.7.4", features = ["io", "io-util"] } markup = "0.13.1" diff --git a/server/src/config.rs b/server/src/config.rs index 10b5b5f..86f7068 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -7,6 +7,9 @@ pub struct GlobalConfig { pub brand: String, pub database_path: PathBuf, pub library_path: PathBuf, + pub admin_username: String, + pub admin_password: String, + pub cookie_key: String, } pub fn load_global_config() -> GlobalConfig { diff --git a/server/src/database.rs b/server/src/database.rs index 39b306c..3b2b4a9 100644 --- a/server/src/database.rs +++ b/server/src/database.rs @@ -8,6 +8,7 @@ use typed_sled::Tree; pub struct Database { pub db: sled::Db, pub users: Tree<String, User>, + pub invites: Tree<String, ()>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -15,14 +16,17 @@ pub struct User { pub name: String, pub display_name: String, pub password: Vec<u8>, + pub admin: bool, } impl Database { pub fn open(path: &PathBuf) -> Result<Self, anyhow::Error> { info!("opening database… (takes O(n) time sadly)"); let db = sled::open(path).context("opening database")?; + info!("ready"); Ok(Self { - users: typed_sled::Tree::open(&db, "users"), + users: Tree::open(&db, "users"), + invites: Tree::open(&db, "invites"), db, }) } diff --git a/server/src/library.rs b/server/src/library.rs index ebafa9d..578edfc 100644 --- a/server/src/library.rs +++ b/server/src/library.rs @@ -156,11 +156,13 @@ impl Node { } } } + impl Item { pub fn path(&self) -> String { self.lib_path.to_str().unwrap().to_string() } } + impl Directory { pub fn path(&self) -> String { self.lib_path.to_str().unwrap().to_string() diff --git a/server/src/main.rs b/server/src/main.rs index 29d51ca..5158b8d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,12 +1,12 @@ #![feature(box_syntax)] use config::{load_global_config, GlobalConfig}; -use database::Database; +use database::{Database, User}; use jellyremuxer::RemuxerContext; use library::Library; use once_cell::sync::Lazy; use rocket::launch; -use routes::build_rocket; +use routes::{build_rocket, ui::account::hash_password}; use std::sync::Arc; pub mod config; @@ -14,12 +14,6 @@ pub mod database; pub mod library; pub mod routes; -pub struct AppState { - pub database: Database, - pub library: Library, - pub remuxer: Arc<RemuxerContext>, -} - pub static CONF: Lazy<GlobalConfig> = Lazy::new(|| load_global_config()); #[launch] @@ -28,10 +22,20 @@ fn rocket() -> _ { .filter_level(log::LevelFilter::Info) .parse_env("LOG") .init(); - let state = AppState { - remuxer: RemuxerContext::new(), - library: Library::open(&CONF.library_path).unwrap(), - database: Database::open(&CONF.database_path).unwrap(), - }; - build_rocket(state) + let remuxer = RemuxerContext::new(); + let library = Library::open(&CONF.library_path).unwrap(); + let database = Database::open(&CONF.database_path).unwrap(); + database + .users + .insert( + &CONF.admin_username, + &User { + admin: true, + display_name: "Admin".to_string(), + name: CONF.admin_username.clone(), + password: hash_password(&CONF.admin_password), + }, + ) + .unwrap(); + build_rocket(remuxer, library, database) } diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 332b4c5..f707a60 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -1,20 +1,40 @@ -use crate::AppState; -use rocket::{catchers, routes, Build, Rocket}; +use crate::{database::Database, library::Library, CONF}; +use jellyremuxer::RemuxerContext; +use rocket::{catchers, config::SecretKey, routes, Build, Config, Rocket}; use stream::r_stream; -use ui::account::{r_account_login, r_account_register, r_account_register_post}; -use ui::error::r_not_found; -use ui::home::r_home; -use ui::node::{r_item_assets, r_library_node}; -use ui::player::r_player; -use ui::style::{r_assets_font, r_assets_js, r_assets_style}; +use ui::{ + account::{r_account_login, r_account_login_post, r_account_register, r_account_register_post}, + error::r_catch, + home::r_home, + node::{r_item_assets, r_library_node}, + player::r_player, + style::{r_assets_font, r_assets_js, r_assets_style}, +}; pub mod stream; pub mod ui; -pub fn build_rocket(state: AppState) -> Rocket<Build> { +#[macro_export] +macro_rules! uri { + ($kk:tt) => { + &rocket::uri!($kk).to_string() + }; +} + +pub fn build_rocket( + remuxer: RemuxerContext, + library: Library, + database: Database, +) -> Rocket<Build> { rocket::build() - .manage(state) - .register("/", catchers![r_not_found]) + .configure(Config { + secret_key: SecretKey::derive_from(CONF.cookie_key.as_bytes()), + ..Default::default() + }) + .manage(remuxer) + .manage(library) + .manage(database) + .register("/", catchers![r_catch]) .mount( "/", routes![ @@ -26,6 +46,7 @@ pub fn build_rocket(state: AppState) -> Rocket<Build> { r_stream, r_player, r_account_login, + r_account_login_post, r_account_register, r_account_register_post, r_item_assets, diff --git a/server/src/routes/stream.rs b/server/src/routes/stream.rs index c49f51d..0e1fccb 100644 --- a/server/src/routes/stream.rs +++ b/server/src/routes/stream.rs @@ -1,9 +1,12 @@ +use crate::library::Library; + use super::ui::error::MyError; -use crate::AppState; use anyhow::{anyhow, Context}; +use jellyremuxer::RemuxerContext; use log::debug; use log::warn; use rocket::{get, http::ContentType, response::stream::ReaderStream, State}; +use std::ops::Deref; use std::path::PathBuf; use tokio::io::{duplex, DuplexStream}; use tokio_util::io::SyncIoBridge; @@ -25,16 +28,16 @@ pub fn r_stream( path: PathBuf, webm: Option<bool>, tracks: String, - state: &State<AppState>, + remuxer: &State<RemuxerContext>, + library: &State<Library>, ) -> Result<(ContentType, ReaderStream![DuplexStream]), MyError> { let (a, b) = duplex(1024); let path = path.to_str().unwrap().to_string(); - let item = state - .library + let item = library .nested(&path) .context("retrieving library node")? .get_item()?; - let remuxer = state.remuxer.clone(); + let remuxer = remuxer.deref().clone(); let tracks = tracks .split(",") .map(|e| e.parse().map_err(|_| anyhow!("invalid number"))) 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<markup::DynRender<'static>> { - 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<markup::DynRender<'static>> { 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<markup::DynRender<'static>> { - 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 = "<form>")] -pub fn r_account_register_post( - state: &State<AppState>, - form: Form<RegisterForm>, -) -> HtmlTemplate<markup::DynRender<'static>> { - state - .database +pub fn r_account_register_post<'a>( + database: &'a State<Database>, + form: Form<Contextual<'a, RegisterForm>>, +) -> MyResult<DynLayoutPage<'a>> { + 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 = "<form>")] +pub fn r_account_login_post( + database: &State<Database>, + jar: &CookieJar, + form: Form<Contextual<LoginForm>>, +) -> MyResult<Redirect> { + 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<T>(form: Form<Contextual<T>>) -> 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("<unknown>".to_string()) ) - .unwrap(); - HtmlTemplate( - "Registration successful".to_string(), - markup::new! { - h1 { "Registration successful." } - }, - ) + } + MyError(anyhow!(k)) +} + +pub fn hash_password(s: &str) -> Vec<u8> { + 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<Self, MyError> { + let cookie = req + .cookies() + .get_private("user") + .ok_or(anyhow!("login required"))?; + let username = cookie.value(); + + let db = req.guard::<&State<Database>>().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<Output = request::Outcome<Self, Self::Error>> + + 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<markup::DynRender<'a>> { - 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<T> = Result<T, MyError>; #[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<std::io::Error> for MyError { MyError(anyhow::anyhow!("{err}")) } } +impl From<sled::Error> 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<AppState>) -> HtmlTemplate<markup::DynRender> { - HtmlTemplate( - "Home".to_string(), - markup::new! { +pub async fn r_home(_sess: Session, library: &State<Library>) -> LayoutPage<markup::DynRender> { + 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<Main: Render>(title: String, main: Main) { + Layout<Main: Render>(title: String, main: Main, session: Option<Session>) { @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<markup::DynRender<'a>>; + +pub struct LayoutPage<T> { + pub title: String, + pub content: T, +} + +impl<'r, Main: Render> Responder<'r, 'static> for LayoutPage<Main> { + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { + let session = block_on(req.guard::<Option<Session>>()).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<T>(pub String, pub T); +pub struct HtmlTemplate<'a>(pub markup::DynRender<'a>); -impl<'r, T: Render> Responder<'r, 'static> for HtmlTemplate<T> { - 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<Box<dyn Future<Output = String> + 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<std::io::Result<()>> { + 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/<path..>")] pub async fn r_library_node( + _sess: Session, path: PathBuf, - state: &State<AppState>, -) -> Result<HtmlTemplate<markup::DynRender>, MyError> { - let node = state - .library + library: &State<Library>, +) -> Result<DynLayoutPage<'_>, 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/<path..>")] pub async fn r_item_assets( + _sess: Session, path: PathBuf, - state: &State<AppState>, + library: &State<Library>, ) -> 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/<path..>?<conf..>", rank = 4)] pub fn r_player( - state: &State<AppState>, + _sess: Session, + library: &State<Library>, path: PathBuf, conf: PlayerConfig, -) -> MyResult<HtmlTemplate<markup::DynRender<'_>>> { - warn!("{conf:?}"); - let item = state.library.nested_path(&path)?.get_item()?; +) -> MyResult<DynLayoutPage<'_>> { + 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::<Vec<_>>(); - 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<Item>) -> MyResult<HtmlTemplate<markup::DynRender<'a>>> { +pub fn player_conf<'a>(item: Arc<Item>) -> MyResult<DynLayoutPage<'a>> { 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<Item>) -> MyResult<HtmlTemplate<markup::DynRend } } - Ok(HtmlTemplate( - "Configure Player".to_string(), - markup::new! { + Ok(LayoutPage { + title: "Configure Player".to_string(), + content: markup::new! { // img.backdrop[src=uri!(r_item_assets(&item.lib_path)).to_string()]; form.playerconf[method = "GET", action = ""] { h2 { "Select tracks for " @item.info.title } @@ -103,5 +102,5 @@ pub fn player_conf<'a>(item: Arc<Item>) -> MyResult<HtmlTemplate<markup::DynRend input[type="submit", value="Start playback"]; } }, - )) + }) } diff --git a/server/src/routes/ui/style/transition.js b/server/src/routes/ui/style/transition.js index dd2d319..8a71f14 100644 --- a/server/src/routes/ui/style/transition.js +++ b/server/src/routes/ui/style/transition.js @@ -5,7 +5,7 @@ globalThis.addEventListener("load", () => { patch_page() }) -globalThis.addEventListener("popstate", ev => { +globalThis.addEventListener("popstate", () => { transition_to(window.location.href) }) |