diff --git a/src/database/category.rs b/src/database/category.rs index e48beb8..04548a7 100644 --- a/src/database/category.rs +++ b/src/database/category.rs @@ -128,7 +128,7 @@ impl CategoryOperator { } /// Delete a category - pub async fn delete(&self, id: i32, user: Option) -> Result { + pub async fn delete(&self, id: i32) -> Result { let db = &self.state.database; let category: Option = Entity::find_by_id(id).one(db).await.context(DBSnafu)?; @@ -138,7 +138,7 @@ impl CategoryOperator { category.delete(db).await.context(DBSnafu)?; let operation_log = OperationLog { - user, + user: self.user.clone(), date: Utc::now(), table: Table::Category, operation: OperationType::Delete, @@ -167,11 +167,7 @@ impl CategoryOperator { /// /// - name or path is already taken (they should be unique) /// - path parent directory does not exist (to avoid completely wrong paths) - pub async fn create( - &self, - f: &CategoryForm, - user: Option, - ) -> Result { + pub async fn create(&self, f: &CategoryForm) -> Result { let dir = Utf8PathBuf::from(&f.path); let parent = dir.parent().unwrap(); @@ -208,7 +204,7 @@ impl CategoryOperator { let model = model.try_into_model().unwrap(); let operation_log = OperationLog { - user, + user: self.user.clone(), date: Utc::now(), table: Table::Category, operation: OperationType::Create, diff --git a/src/database/content_folder.rs b/src/database/content_folder.rs index de567e9..0ed1c36 100644 --- a/src/database/content_folder.rs +++ b/src/database/content_folder.rs @@ -115,11 +115,7 @@ impl ContentFolderOperator { /// /// - name or path is already taken (they should be unique in one folder) /// - path parent directory does not exist (to avoid completely wrong paths) - pub async fn create( - &self, - f: &ContentFolderForm, - user: Option, - ) -> Result { + pub async fn create(&self, f: &ContentFolderForm) -> Result { // Check duplicates in same folder let list = if let Some(parent_id) = f.parent_id { self.list_child_folders(parent_id).await? @@ -158,7 +154,7 @@ impl ContentFolderOperator { let model = model.try_into_model().unwrap(); let operation_log = OperationLog { - user, + user: self.user.clone(), date: Utc::now(), table: Table::ContentFolder, operation: OperationType::Create, diff --git a/src/database/mod.rs b/src/database/mod.rs index d795369..bf0b773 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -2,3 +2,4 @@ pub mod category; pub mod content_folder; pub mod operation; +pub mod operator; diff --git a/src/database/operator.rs b/src/database/operator.rs new file mode 100644 index 0000000..566aaf8 --- /dev/null +++ b/src/database/operator.rs @@ -0,0 +1,29 @@ +use crate::database::{category::CategoryOperator, content_folder::ContentFolderOperator}; +use crate::extractors::user::User; +use crate::state::AppState; + +#[derive(Clone, Debug)] +pub struct DatabaseOperator { + pub state: AppState, + pub user: Option, +} + +impl DatabaseOperator { + pub fn new(state: AppState, user: Option) -> Self { + Self { state, user } + } + + pub fn category(&self) -> CategoryOperator { + CategoryOperator { + state: self.state.clone(), + user: self.user.clone(), + } + } + + pub fn content_folder(&self) -> ContentFolderOperator { + ContentFolderOperator { + state: self.state.clone(), + user: self.user.clone(), + } + } +} diff --git a/src/extractors/user.rs b/src/extractors/user.rs index c062712..70213ab 100644 --- a/src/extractors/user.rs +++ b/src/extractors/user.rs @@ -1,10 +1,9 @@ -use axum::{ - extract::OptionalFromRequestParts, - http::{StatusCode, request::Parts}, -}; +use axum::{extract::OptionalFromRequestParts, http::request::Parts}; use derive_more::Display; use serde::{Deserialize, Serialize}; +use crate::state::error::AppStateError; + /// A logged-in user, as expressed by the Remote-User header. /// /// Cannot be produced outside of header extraction. @@ -16,7 +15,7 @@ impl OptionalFromRequestParts for User where S: Send + Sync, { - type Rejection = (StatusCode, &'static str); + type Rejection = AppStateError; async fn from_request_parts( parts: &mut Parts, @@ -25,10 +24,9 @@ where if let Some(username) = parts.headers.get("remote-user") { match username.to_str() { Ok(username) => Ok(Some(User(String::from(username)))), - Err(_e) => Err(( - StatusCode::INTERNAL_SERVER_ERROR, - "The remote-user header returned by the reverse proxy is invalid.", - )), + Err(_e) => Err(AppStateError::Static { + reason: "The remote-user header returned by the reverse proxy is invalid.", + }), } } else { Ok(None) diff --git a/src/routes/category.rs b/src/routes/category.rs index 92f8c12..d147374 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -1,19 +1,18 @@ use askama::Template; use askama_web::WebTemplate; use axum::Form; -use axum::extract::{Path, State}; +use axum::extract::Path; use axum::response::{IntoResponse, Redirect}; use axum_extra::extract::CookieJar; use serde::{Deserialize, Serialize}; use snafu::prelude::*; +use crate::database::category; use crate::database::category::CategoryError; use crate::database::content_folder; -use crate::database::{category, category::CategoryOperator}; use crate::extractors::normalized_path::*; -use crate::extractors::user::User; use crate::state::flash_message::{OperationStatus, get_cookie}; -use crate::state::{AppState, AppStateContext, error::*}; +use crate::state::{AppStateContext, error::*}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CategoryForm { @@ -26,8 +25,6 @@ pub struct CategoryForm { pub struct NewCategoryTemplate { /// Global application state pub state: AppStateContext, - /// Logged-in user. - pub user: Option, /// Error pub error: Option, /// Default form with value @@ -35,31 +32,21 @@ pub struct NewCategoryTemplate { } pub async fn new( - State(app_state): State, - user: Option, + app_state_context: AppStateContext, ) -> Result { - let app_state_context = app_state.context().await?; - Ok(NewCategoryTemplate { state: app_state_context, - user, category_form: None, error: None, }) } pub async fn delete( - State(app_state): State, - user: Option, + context: AppStateContext, Path(id): Path, jar: CookieJar, ) -> Result { - // let app_state_context = app_state.context().await?; - let categories = CategoryOperator::new(app_state.clone(), user.clone()); - - let deleted = categories.delete(id, user.clone()).await; - - let operation_status = match deleted { + let operation_status = match context.db.category().delete(id).await { Ok(name) => OperationStatus { success: true, message: format!("The category {} has been successfully deleted", name), @@ -76,16 +63,11 @@ pub async fn delete( } pub async fn create( - State(app_state): State, - user: Option, + context: AppStateContext, jar: CookieJar, Form(form): Form, ) -> Result { - let categories = CategoryOperator::new(app_state.clone(), user.clone()); - - let created = categories.create(&form, user.clone()).await; - - match created { + match context.db.category().create(&form).await { Ok(created) => { let operation_status = OperationStatus { success: true, @@ -119,8 +101,6 @@ pub struct CategoryShowTemplate { pub state: AppStateContext, /// Categories found in database pub content_folders: Vec, - /// Logged-in user. - pub user: Option, /// Category category: category::Model, /// Operation status for UI confirmation (Cookie) @@ -128,24 +108,22 @@ pub struct CategoryShowTemplate { } pub async fn show( - State(app_state): State, - user: Option, + context: AppStateContext, Path(category_name): Path, jar: CookieJar, ) -> Result { - let app_state_context = app_state.context().await?; + let categories = context.db.category(); - let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) + let category = categories .find_by_name(category_name.to_string()) .await .context(CategorySnafu)?; // get all content folders in this category - let content_folders: Vec = - CategoryOperator::new(app_state.clone(), user.clone()) - .list_folders(category.id) - .await - .context(CategorySnafu)?; + let content_folders = categories + .list_folders(category.id) + .await + .context(CategorySnafu)?; let (jar, operation_status) = get_cookie(jar); @@ -154,8 +132,7 @@ pub async fn show( CategoryShowTemplate { content_folders, category, - state: app_state_context, - user, + state: context, flash: operation_status, }, )) diff --git a/src/routes/content_folder.rs b/src/routes/content_folder.rs index ea5984d..69df004 100644 --- a/src/routes/content_folder.rs +++ b/src/routes/content_folder.rs @@ -1,20 +1,17 @@ use askama::Template; use askama_web::WebTemplate; use axum::Form; -use axum::extract::State; use axum::response::{IntoResponse, Redirect}; use axum_extra::extract::CookieJar; use camino::Utf8PathBuf; use serde::{Deserialize, Serialize}; use snafu::prelude::*; -use crate::database::category::CategoryOperator; -use crate::database::content_folder::{ContentFolderOperator, PathBreadcrumb}; +use crate::database::content_folder::PathBreadcrumb; use crate::database::{category, content_folder}; use crate::extractors::folder_request::FolderRequest; -use crate::extractors::user::User; use crate::state::flash_message::{OperationStatus, get_cookie}; -use crate::state::{AppState, AppStateContext, error::*}; +use crate::state::{AppStateContext, error::*}; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ContentFolderForm { @@ -33,8 +30,6 @@ pub struct ContentFolderShowTemplate { pub current_content_folder: content_folder::Model, /// Folders with parent_id set to current folder pub sub_content_folders: Vec, - /// Logged-in user. - pub user: Option, /// Category pub category: category::Model, /// BreadCrumb extract from path @@ -46,13 +41,10 @@ pub struct ContentFolderShowTemplate { } pub async fn show( - State(app_state): State, + context: AppStateContext, folder: FolderRequest, - user: Option, jar: CookieJar, ) -> Result<(CookieJar, ContentFolderShowTemplate), AppStateError> { - let app_state_context = app_state.context().await?; - let (jar, operation_status) = get_cookie(jar); Ok(( @@ -63,25 +55,23 @@ pub async fn show( sub_content_folders: folder.sub_folders, current_content_folder: folder.folder, category: folder.category, - state: app_state_context, - user, + state: context, flash: operation_status, }, )) } pub async fn create( - State(app_state): State, - user: Option, + context: AppStateContext, jar: CookieJar, Form(mut form): Form, ) -> Result { - // let app_state_context = app_state.context().await?; - let content_folder = ContentFolderOperator::new(app_state.clone(), user.clone()); + let categories = context.db.category(); + let content_folders = context.db.content_folder(); // build path with Parent folder path (or category path if parent is None) + folder.name let parent_path = if let Some(parent_id) = form.parent_id { - let parent_folder = content_folder + let parent_folder = content_folders .find_by_id(parent_id) .await .context(ContentFolderSnafu)?; @@ -91,7 +81,7 @@ pub async fn create( }; // Get folder category - let category: category::Model = CategoryOperator::new(app_state.clone(), user.clone()) + let category: category::Model = categories .find_by_id(form.category_id) .await .context(CategorySnafu)?; @@ -115,7 +105,7 @@ pub async fn create( // build final path with parent_path and path of form form.path = format!("{}/{}", parent_path, form.name); - let created = content_folder.create(&form, user.clone()).await; + let created = content_folders.create(&form).await; match created { Ok(created) => { diff --git a/src/routes/index.rs b/src/routes/index.rs index d20d7e3..7861ab9 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -1,22 +1,18 @@ use askama::Template; use askama_web::WebTemplate; -use axum::extract::State; use axum_extra::extract::CookieJar; use snafu::prelude::*; // TUTORIAL: https://github.com/SeaQL/sea-orm/blob/master/examples/axum_example/ -use crate::database::category::{self, CategoryOperator}; -use crate::extractors::user::User; +use crate::database::category; use crate::state::flash_message::{OperationStatus, get_cookie}; -use crate::state::{AppState, AppStateContext, error::*}; +use crate::state::{AppStateContext, error::*}; #[derive(Template, WebTemplate)] #[template(path = "index.html")] pub struct IndexTemplate { /// Global application state (errors/warnings) pub state: AppStateContext, - /// Logged-in user. - pub user: Option, /// Categories pub categories: Vec, /// Operation status for UI confirmation @@ -28,32 +24,23 @@ pub struct IndexTemplate { pub struct UploadTemplate { /// Global application state (errors/warnings) pub state: AppStateContext, - /// Logged-in user. - pub user: Option, /// Categories pub categories: Vec, } impl IndexTemplate { pub async fn new( - app_state: AppState, - user: Option, + context: AppStateContext, jar: CookieJar, ) -> Result<(CookieJar, Self), AppStateError> { - let app_state_context = app_state.context().await?; - - let categories = CategoryOperator::new(app_state.clone(), user.clone()) - .list() - .await - .context(CategorySnafu)?; + let categories = context.db.category().list().await.context(CategorySnafu)?; let (jar, operation_status) = get_cookie(jar); Ok(( jar, IndexTemplate { - state: app_state_context, - user, + state: context, categories, flash: operation_status, }, @@ -62,8 +49,10 @@ impl IndexTemplate { } impl UploadTemplate { - pub async fn new(app_state: AppState, user: Option) -> Result { - let categories: Vec = CategoryOperator::new(app_state.clone(), user.clone()) + pub async fn new(context: AppStateContext) -> Result { + let categories: Vec = context + .db + .category() .list() .await .context(CategorySnafu)? @@ -72,24 +61,19 @@ impl UploadTemplate { .collect(); Ok(UploadTemplate { - state: app_state.context().await?, - user, + state: context, categories, }) } } pub async fn index( - State(app_state): State, - user: Option, + context: AppStateContext, jar: CookieJar, ) -> Result<(CookieJar, IndexTemplate), AppStateError> { - IndexTemplate::new(app_state, user, jar).await + IndexTemplate::new(context, jar).await } -pub async fn upload( - State(app_state): State, - user: Option, -) -> Result { - UploadTemplate::new(app_state, user).await +pub async fn upload(context: AppStateContext) -> Result { + UploadTemplate::new(context).await } diff --git a/src/routes/logs.rs b/src/routes/logs.rs index 20a8445..5533f96 100644 --- a/src/routes/logs.rs +++ b/src/routes/logs.rs @@ -1,31 +1,23 @@ use askama::Template; use askama_web::WebTemplate; -use axum::extract::State; use snafu::prelude::*; use crate::database::operation::OperationLog; use crate::database::operation::OperationType; -use crate::extractors::user::User; -use crate::state::{AppState, AppStateContext, error::*}; +use crate::state::{AppStateContext, error::*}; #[derive(Template, WebTemplate)] #[template(path = "logs/index.html")] pub struct LogTemplate { pub state: AppStateContext, pub logs: Vec, - pub user: Option, } -pub async fn index( - State(app_state): State, - user: Option, -) -> Result { - let app_state_context = app_state.context().await?; - let logs = app_state.logger.read().await.context(LoggerSnafu)?; +pub async fn index(context: AppStateContext) -> Result { + let logs = context.state.logger.read().await.context(LoggerSnafu)?; Ok(LogTemplate { - state: app_state_context, + state: context, logs, - user, }) } diff --git a/src/routes/progress.rs b/src/routes/progress.rs index ac4b807..6095297 100644 --- a/src/routes/progress.rs +++ b/src/routes/progress.rs @@ -1,12 +1,12 @@ use askama::Template; use askama_web::WebTemplate; -use axum::extract::{Path, State}; +use axum::extract::Path; use hightorrent_api::hightorrent::{SingleTarget, Torrent, TorrentContent}; use crate::extractors::torrent_list::{ TorrentListCounter, TorrentListFilter, TorrentListView, TorrentListViewRequest, }; -use crate::state::{AppState, AppStateContext, error::AppStateError}; +use crate::state::{AppStateContext, error::AppStateError}; #[derive(Template, WebTemplate)] #[template(path = "progress.html")] @@ -17,8 +17,6 @@ pub struct TorrentListTemplate { torrent_list: TorrentListContext, // Filter object filter: TorrentListViewRequest, - /// Logged-in user. - user: Option, } #[derive(Debug)] @@ -36,22 +34,21 @@ pub struct TorrentListContext { } pub async fn progress( - State(app_state): State, + context: AppStateContext, Path(view_request): Path, ) -> Result { - let app_state_context = app_state.context().await?; - // Failing to load the TorrentListView is a fatal error let TorrentListView { counter, filtered_list, - } = TorrentListView::apply_request(view_request.clone(), &app_state).await?; + } = TorrentListView::apply_request(view_request.clone(), &context.state).await?; // If only one torrent is inspected, display the content files let files = if filtered_list.len() == 1 { let torrent_id = &filtered_list.first().unwrap().id; Some( - app_state + context + .state .torrent_get_files(&SingleTarget::from(torrent_id)) .await?, ) @@ -60,13 +57,12 @@ pub async fn progress( }; Ok(TorrentListTemplate { - state: app_state_context, + state: context, filter: view_request, torrent_list: TorrentListContext { counter, files, torrents: filtered_list, }, - user: None, }) } diff --git a/src/state/context.rs b/src/state/context.rs new file mode 100644 index 0000000..e075e9a --- /dev/null +++ b/src/state/context.rs @@ -0,0 +1,38 @@ +use axum::extract::{FromRequestParts, OptionalFromRequestParts}; +use axum::http::request::Parts; + +use crate::database::operator::DatabaseOperator; + +use super::*; + +/// Basic templating context used across pages. +/// +/// Loading it may fail for some reasons, but it's so rare +/// and unrecoverable that it will trigger a global error +/// by rendering the AppStateError into an axum Response. +pub struct AppStateContext { + pub db: DatabaseOperator, + pub errors: Vec, + pub free_space: FreeSpace, + pub state: AppState, + pub user: Option, +} + +impl FromRequestParts for AppStateContext { + type Rejection = AppStateError; + + async fn from_request_parts( + parts: &mut Parts, + state: &AppState, + ) -> Result { + let user = User::from_request_parts(parts, state).await?; + + Ok(Self { + db: DatabaseOperator::new(state.clone(), user.clone()), + errors: vec![], + free_space: state.free_space()?, + state: state.clone(), + user, + }) + } +} diff --git a/src/state/error.rs b/src/state/error.rs index 21d58a3..a2a3876 100644 --- a/src/state/error.rs +++ b/src/state/error.rs @@ -36,6 +36,8 @@ pub enum AppStateError { ContentFolder { source: ContentFolderError }, #[snafu(display("IO error"))] IO { source: std::io::Error }, + #[snafu(display("{reason}"))] + Static { reason: &'static str }, } impl AppStateError { diff --git a/src/state/mod.rs b/src/state/mod.rs index 1550674..7fddd15 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -4,8 +4,11 @@ use sea_orm::*; use snafu::prelude::*; use crate::config::AppConfig; +use crate::extractors::user::User; use crate::migration::{Migrator, MigratorTrait}; +mod context; +pub use context::AppStateContext; pub mod error; pub mod flash_message; pub mod free_space; @@ -34,31 +37,7 @@ pub struct AppState { pub torrent_client: QBittorrentClient, } -/// Basic templating context used across pages. -/// -/// Loading it may fail for some reasons, but it's so rare -/// and unrecoverable that it will trigger a global error -/// by rendering the AppStateError into an axum Response. -pub struct AppStateContext { - pub errors: Vec, - // pub errors: Vec, - pub free_space: FreeSpace, -} - -impl AppStateContext { - fn from_app_state(state: &AppState) -> Result { - Ok(Self { - errors: vec![], - free_space: state.free_space()?, - }) - } -} - impl AppState { - pub async fn context(&self) -> Result { - AppStateContext::from_app_state(self) - } - pub async fn new(config: AppConfig) -> Result { // TODO: config for torrent backend diff --git a/templates/menus/footer.html b/templates/menus/footer.html index 68ea83c..36707a9 100755 --- a/templates/menus/footer.html +++ b/templates/menus/footer.html @@ -1,6 +1,6 @@
- {% if let Some(user) = user %} + {% if let Some(user) = state.user %}

Logged in as {{ user|safe }} {% else %}

Not logged in.