aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock359
-rw-r--r--remuxer/src/lib.rs5
-rw-r--r--server/Cargo.toml4
-rw-r--r--server/src/config.rs3
-rw-r--r--server/src/database.rs6
-rw-r--r--server/src/library.rs2
-rw-r--r--server/src/main.rs32
-rw-r--r--server/src/routes/mod.rs43
-rw-r--r--server/src/routes/stream.rs13
-rw-r--r--server/src/routes/ui/account/mod.rs148
-rw-r--r--server/src/routes/ui/account/session.rs58
-rw-r--r--server/src/routes/ui/error.rs61
-rw-r--r--server/src/routes/ui/home.rs18
-rw-r--r--server/src/routes/ui/layout.rs50
-rw-r--r--server/src/routes/ui/mod.rs40
-rw-r--r--server/src/routes/ui/node.rs30
-rw-r--r--server/src/routes/ui/player.rs35
-rw-r--r--server/src/routes/ui/style/transition.js2
18 files changed, 767 insertions, 142 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b9ab2f9..89b4295 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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)
})