aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/routes')
-rw-r--r--server/src/routes/api/error.rs57
-rw-r--r--server/src/routes/api/mod.rs51
-rw-r--r--server/src/routes/mod.rs9
-rw-r--r--server/src/routes/ui/account/mod.rs32
-rw-r--r--server/src/routes/ui/layout.rs3
-rw-r--r--server/src/routes/ui/node.rs5
6 files changed, 142 insertions, 15 deletions
diff --git a/server/src/routes/api/error.rs b/server/src/routes/api/error.rs
new file mode 100644
index 0000000..ef5374c
--- /dev/null
+++ b/server/src/routes/api/error.rs
@@ -0,0 +1,57 @@
+// TODO: Slightâ„¢ code duplication with `ui/error.rs`
+
+use rocket::{
+ response::{self, Responder},
+ Request,
+};
+use serde_json::{json, Value};
+use std::fmt::Display;
+
+use crate::routes::ui::error::MyError;
+
+pub type ApiResult<T> = Result<T, ApiError>;
+
+#[derive(Debug)]
+pub struct ApiError(pub anyhow::Error);
+
+impl<'r> Responder<'r, 'static> for ApiError {
+ fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
+ json!({ "error": format!("{}", self.0) }).respond_to(req)
+ }
+}
+
+impl Display for ApiError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+impl From<anyhow::Error> for ApiError {
+ fn from(err: anyhow::Error) -> ApiError {
+ ApiError(err)
+ }
+}
+impl From<std::fmt::Error> for ApiError {
+ fn from(err: std::fmt::Error) -> ApiError {
+ ApiError(anyhow::anyhow!("{err}"))
+ }
+}
+impl From<std::io::Error> for ApiError {
+ fn from(err: std::io::Error) -> Self {
+ ApiError(anyhow::anyhow!("{err}"))
+ }
+}
+impl From<sled::Error> for ApiError {
+ fn from(err: sled::Error) -> Self {
+ ApiError(anyhow::anyhow!("{err}"))
+ }
+}
+impl From<serde_json::Error> for ApiError {
+ fn from(err: serde_json::Error) -> Self {
+ ApiError(anyhow::anyhow!("{err}"))
+ }
+}
+impl From<MyError> for ApiError {
+ fn from(value: MyError) -> Self {
+ Self(value.0)
+ }
+}
diff --git a/server/src/routes/api/mod.rs b/server/src/routes/api/mod.rs
new file mode 100644
index 0000000..5f48873
--- /dev/null
+++ b/server/src/routes/api/mod.rs
@@ -0,0 +1,51 @@
+pub mod error;
+
+use std::path::PathBuf;
+
+use super::ui::account::{login_logic, LoginForm};
+use crate::{
+ database::Database,
+ library::{Library, Node},
+ routes::{api::error::ApiResult, ui::account::session::Session},
+};
+use anyhow::Context;
+use rocket::{get, http::CookieJar, post, serde::json::Json, State};
+use serde_json::{json, Value};
+
+#[get("/api/version")]
+pub fn r_api_version() -> &'static str {
+ "1"
+}
+
+#[post("/api/account/login", data = "<data>")]
+pub fn r_api_account_login(
+ database: &State<Database>,
+ jar: &CookieJar,
+ data: Json<LoginForm>,
+) -> ApiResult<Value> {
+ login_logic(jar, database, &data.username, &data.password)?;
+ Ok(json!({ "ok": true }))
+}
+
+#[get("/api/library/<path..>")]
+pub fn r_api_library_node(
+ _sess: Session,
+ path: PathBuf,
+ library: &State<Library>,
+) -> ApiResult<Value> {
+ let node = library
+ .nested_path(&path)
+ .context("retrieving library node")?;
+
+ match node.as_ref() {
+ Node::Directory(d) => Ok(json!({
+ "identifier": d.identifier,
+ "info": d.info,
+ "children": d.children.iter().map(|c| c.identifier().to_string()).collect::<Vec<_>>()
+ })),
+ Node::Item(i) => Ok(json!({
+ "identifier": i.identifier,
+ "info": i.info,
+ })),
+ }
+}
diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs
index 98063da..421907b 100644
--- a/server/src/routes/mod.rs
+++ b/server/src/routes/mod.rs
@@ -4,6 +4,7 @@
Copyright (C) 2023 metamuffin <metamuffin.org>
*/
use crate::{database::Database, library::Library, routes::ui::error::MyResult, CONF};
+use api::{r_api_account_login, r_api_version, r_api_library_node};
use jellyremuxer::RemuxerContext;
use rocket::{
catchers, config::SecretKey, fairing::AdHoc, fs::FileServer, get, http::Header, routes, Build,
@@ -25,6 +26,7 @@ use ui::{
style::{r_assets_font, r_assets_js, r_assets_style},
};
+pub mod api;
pub mod stream;
pub mod ui;
@@ -59,6 +61,8 @@ pub fn build_rocket(
routes![
r_home,
r_home_unpriv,
+ r_favicon,
+ r_item_assets,
r_library_node,
r_assets_style,
r_assets_font,
@@ -76,8 +80,9 @@ pub fn build_rocket(
r_account_admin_remove_user,
r_account_settings,
r_account_settings_post,
- r_favicon,
- r_item_assets,
+ r_api_version,
+ r_api_account_login,
+ r_api_library_node,
],
)
}
diff --git a/server/src/routes/ui/account/mod.rs b/server/src/routes/ui/account/mod.rs
index 9007558..8e6d054 100644
--- a/server/src/routes/ui/account/mod.rs
+++ b/server/src/routes/ui/account/mod.rs
@@ -24,6 +24,7 @@ use rocket::{
response::Redirect,
uri, FromForm, State,
};
+use serde::{Deserialize, Serialize};
#[derive(FromForm)]
pub struct RegisterForm {
@@ -57,7 +58,7 @@ pub async fn r_account_register() -> DynLayoutPage<'static> {
}
}
-#[derive(FromForm)]
+#[derive(FromForm, Serialize, Deserialize)]
pub struct LoginForm {
#[field(validate = len(4..32))]
pub username: String,
@@ -147,12 +148,29 @@ pub fn r_account_login_post(
None => return Err(format_form_error(form)),
};
+ login_logic(jar, database, &form.username, &form.password)?;
+
+ Ok(Redirect::found(uri!(r_home())))
+}
+
+#[post("/account/logout")]
+pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> {
+ jar.remove_private(Cookie::named("user"));
+ Ok(Redirect::found(uri!(r_home())))
+}
+
+pub fn login_logic(
+ jar: &CookieJar,
+ database: &Database,
+ username: &str,
+ password: &str,
+) -> MyResult<()> {
// hashing the password regardless if the accounts exists to prevent timing attacks
- let password = hash_password(&form.username, &form.password);
+ let password = hash_password(username, password);
let user = database
.users
- .get(&form.username)?
+ .get(&username.to_string())?
.ok_or(anyhow!("invalid password"))?;
if user.password != password {
@@ -168,13 +186,7 @@ pub fn r_account_login_post(
.finish(),
);
- Ok(Redirect::found(uri!(r_home())))
-}
-
-#[post("/account/logout")]
-pub fn r_account_logout_post(jar: &CookieJar) -> MyResult<Redirect> {
- jar.remove_private(Cookie::named("user"));
- Ok(Redirect::found(uri!(r_home())))
+ Ok(())
}
pub fn format_form_error<T>(form: Form<Contextual<T>>) -> MyError {
diff --git a/server/src/routes/ui/layout.rs b/server/src/routes/ui/layout.rs
index 0d4e1ef..bc01c2e 100644
--- a/server/src/routes/ui/layout.rs
+++ b/server/src/routes/ui/layout.rs
@@ -38,10 +38,11 @@ markup::define! {
div.account {
@if let Some(session) = session {
- span { "Logged in as " a[href=uri!(r_account_settings())] { @session.user.display_name } }
+ span { "Logged in as " @session.user.display_name }
@if session.user.admin {
a[href=uri!(r_account_admin_dashboard())] { "Administration" }
}
+ a[href=uri!(r_account_settings())] { "Settings" }
a[href=uri!(r_account_logout())] { "Log out" }
} else {
a[href=uri!(r_account_register())] { "Register" }
diff --git a/server/src/routes/ui/node.rs b/server/src/routes/ui/node.rs
index dd98a61..ad44410 100644
--- a/server/src/routes/ui/node.rs
+++ b/server/src/routes/ui/node.rs
@@ -43,11 +43,11 @@ markup::define! {
}
}
DirectoryCard(dir: Arc<Directory>) {
- div.card.dir { a[href=&uri!(r_library_node(&dir.lib_path)).to_string()] { @dir.data.name } }
+ div.card.dir { a[href=&uri!(r_library_node(&dir.lib_path)).to_string()] { @dir.info.name } }
}
DirectoryPage(dir: Arc<Directory>) {
div.page.dir {
- h1 { @dir.data.name }
+ h1 { @dir.info.name }
ul.directorylisting {
@for el in &dir.children {
li { @match el.deref().to_owned() {
@@ -82,6 +82,7 @@ markup::define! {
}
div.title {
h1 { @item.info.title }
+ // TODO release date, duration, ratings
a.play[href=&player_uri(&item.lib_path)] { "Watch now" }
}
div.details {