From d58f3cc275417f18af3e3a66cccbffb29de19be5 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 17 May 2023 16:20:35 +0100 Subject: [PATCH] refactor: [#157] extract service: category Decoupling services from actix-web framework. --- src/app.rs | 8 +++ src/auth.rs | 12 ++++- src/common.rs | 12 +++++ src/errors.rs | 10 +++- src/models/category.rs | 2 + src/models/mod.rs | 1 + src/models/user.rs | 13 +++-- src/routes/category.rs | 24 +++------ src/services/category.rs | 113 +++++++++++++++++++++++++++++++++++++++ src/services/mod.rs | 2 + src/services/user.rs | 31 +++++++++++ 11 files changed, 203 insertions(+), 25 deletions(-) create mode 100644 src/models/category.rs create mode 100644 src/services/category.rs create mode 100644 src/services/user.rs diff --git a/src/app.rs b/src/app.rs index 3dadfb5f..625ec2aa 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,6 +12,8 @@ use crate::cache::image::manager::ImageCacheService; use crate::common::AppData; use crate::config::Configuration; use crate::databases::database; +use crate::services::category::{self, DbCategoryRepository}; +use crate::services::user::DbUserRepository; use crate::tracker::statistics_importer::StatisticsImporter; use crate::{mailer, routes, tracker}; @@ -48,6 +50,9 @@ pub async fn run(configuration: Configuration) -> Running { Arc::new(StatisticsImporter::new(cfg.clone(), tracker_service.clone(), database.clone()).await); let mailer_service = Arc::new(mailer::Service::new(cfg.clone()).await); let image_cache_service = Arc::new(ImageCacheService::new(cfg.clone()).await); + let category_repository = Arc::new(DbCategoryRepository::new(database.clone())); + let user_repository = Arc::new(DbUserRepository::new(database.clone())); + let category_service = Arc::new(category::Service::new(category_repository.clone(), user_repository.clone())); // Build app container @@ -59,6 +64,9 @@ pub async fn run(configuration: Configuration) -> Running { tracker_statistics_importer.clone(), mailer_service, image_cache_service, + category_repository, + user_repository, + category_service, )); // Start repeating task to import tracker torrent data and updating diff --git a/src/auth.rs b/src/auth.rs index a8fbf76b..609496d4 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -6,7 +6,7 @@ use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, use crate::config::Configuration; use crate::databases::database::Database; use crate::errors::ServiceError; -use crate::models::user::{UserClaims, UserCompact}; +use crate::models::user::{UserClaims, UserCompact, UserId}; use crate::utils::clock; pub struct AuthorizationService { @@ -94,4 +94,14 @@ impl AuthorizationService { .await .map_err(|_| ServiceError::UserNotFound) } + + /// Get User id from Request + /// + /// # Errors + /// + /// This function will return an error if it can get claims from the request + pub async fn get_user_id_from_request(&self, req: &HttpRequest) -> Result { + let claims = self.get_claims_from_request(req).await?; + Ok(claims.user.user_id) + } } diff --git a/src/common.rs b/src/common.rs index 51861fae..333c604c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -4,6 +4,8 @@ use crate::auth::AuthorizationService; use crate::cache::image::manager::ImageCacheService; use crate::config::Configuration; use crate::databases::database::Database; +use crate::services::category::{self, DbCategoryRepository}; +use crate::services::user::DbUserRepository; use crate::tracker::statistics_importer::StatisticsImporter; use crate::{mailer, tracker}; pub type Username = String; @@ -18,9 +20,13 @@ pub struct AppData { pub tracker_statistics_importer: Arc, pub mailer: Arc, pub image_cache_manager: Arc, + pub category_repository: Arc, + pub user_repository: Arc, + pub category_service: Arc, } impl AppData { + #[allow(clippy::too_many_arguments)] pub fn new( cfg: Arc, database: Arc>, @@ -29,6 +35,9 @@ impl AppData { tracker_statistics_importer: Arc, mailer: Arc, image_cache_manager: Arc, + category_repository: Arc, + user_repository: Arc, + category_service: Arc, ) -> AppData { AppData { cfg, @@ -38,6 +47,9 @@ impl AppData { tracker_statistics_importer, mailer, image_cache_manager, + category_repository, + user_repository, + category_service, } } } diff --git a/src/errors.rs b/src/errors.rs index 12601e3c..0d3d4067 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -120,8 +120,14 @@ pub enum ServiceError { #[display(fmt = "Failed to send verification email.")] FailedToSendVerificationEmail, - #[display(fmt = "Category already exists..")] + #[display(fmt = "Category already exists.")] CategoryExists, + + #[display(fmt = "Category not found.")] + CategoryNotFound, + + #[display(fmt = "Database error.")] + DatabaseError, } #[derive(Serialize, Deserialize)] @@ -168,6 +174,8 @@ impl ResponseError for ServiceError { ServiceError::EmailMissing => StatusCode::NOT_FOUND, ServiceError::FailedToSendVerificationEmail => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::WhitelistingError => StatusCode::INTERNAL_SERVER_ERROR, + ServiceError::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, + ServiceError::CategoryNotFound => StatusCode::NOT_FOUND, } } diff --git a/src/models/category.rs b/src/models/category.rs new file mode 100644 index 00000000..76b74f20 --- /dev/null +++ b/src/models/category.rs @@ -0,0 +1,2 @@ +#[allow(clippy::module_name_repetitions)] +pub type CategoryId = i64; diff --git a/src/models/mod.rs b/src/models/mod.rs index 6a317c58..5e54368f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,4 @@ +pub mod category; pub mod info_hash; pub mod response; pub mod torrent; diff --git a/src/models/user.rs b/src/models/user.rs index f808c87a..b115e10c 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,8 +1,11 @@ use serde::{Deserialize, Serialize}; +#[allow(clippy::module_name_repetitions)] +pub type UserId = i64; + #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct User { - pub user_id: i64, + pub user_id: UserId, pub date_registered: Option, pub date_imported: Option, pub administrator: bool, @@ -11,14 +14,14 @@ pub struct User { #[allow(clippy::module_name_repetitions)] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct UserAuthentication { - pub user_id: i64, + pub user_id: UserId, pub password_hash: String, } #[allow(clippy::module_name_repetitions)] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct UserProfile { - pub user_id: i64, + pub user_id: UserId, pub username: String, pub email: String, pub email_verified: bool, @@ -29,7 +32,7 @@ pub struct UserProfile { #[allow(clippy::module_name_repetitions)] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct UserCompact { - pub user_id: i64, + pub user_id: UserId, pub username: String, pub administrator: bool, } @@ -37,7 +40,7 @@ pub struct UserCompact { #[allow(clippy::module_name_repetitions)] #[derive(Debug, Serialize, Deserialize, Clone, sqlx::FromRow)] pub struct UserFull { - pub user_id: i64, + pub user_id: UserId, pub date_registered: Option, pub date_imported: Option, pub administrator: bool, diff --git a/src/routes/category.rs b/src/routes/category.rs index c1c09f01..113615f7 100644 --- a/src/routes/category.rs +++ b/src/routes/category.rs @@ -2,7 +2,7 @@ use actix_web::{web, HttpRequest, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; use crate::common::WebAppData; -use crate::errors::{ServiceError, ServiceResult}; +use crate::errors::ServiceResult; use crate::models::response::OkResponse; use crate::routes::API_VERSION; @@ -23,7 +23,7 @@ pub fn init(cfg: &mut web::ServiceConfig) { /// /// This function will return an error if there is a database error. pub async fn get(app_data: WebAppData) -> ServiceResult { - let categories = app_data.database.get_categories().await?; + let categories = app_data.category_repository.get_categories().await?; Ok(HttpResponse::Ok().json(OkResponse { data: categories })) } @@ -41,15 +41,9 @@ pub struct Category { /// This function will return an error if unable to get user. /// This function will return an error if unable to insert into the database the new category. pub async fn add(req: HttpRequest, payload: web::Json, app_data: WebAppData) -> ServiceResult { - // check for user - let user = app_data.auth.get_user_compact_from_request(&req).await?; + let user_id = app_data.auth.get_user_id_from_request(&req).await?; - // check if user is administrator - if !user.administrator { - return Err(ServiceError::Unauthorized); - } - - let _ = app_data.database.insert_category_and_get_id(&payload.name).await?; + let _category_id = app_data.category_service.add_category(&payload.name, &user_id).await?; Ok(HttpResponse::Ok().json(OkResponse { data: payload.name.clone(), @@ -67,15 +61,9 @@ pub async fn delete(req: HttpRequest, payload: web::Json, app_data: We // And we should use the ID instead of the name, because the name could change // or we could add support for multiple languages. - // check for user - let user = app_data.auth.get_user_compact_from_request(&req).await?; - - // check if user is administrator - if !user.administrator { - return Err(ServiceError::Unauthorized); - } + let user_id = app_data.auth.get_user_id_from_request(&req).await?; - app_data.database.delete_category(&payload.name).await?; + app_data.category_service.delete_category(&payload.name, &user_id).await?; Ok(HttpResponse::Ok().json(OkResponse { data: payload.name.clone(), diff --git a/src/services/category.rs b/src/services/category.rs new file mode 100644 index 00000000..53070c5a --- /dev/null +++ b/src/services/category.rs @@ -0,0 +1,113 @@ +//! Category service. +use std::sync::Arc; + +use super::user::DbUserRepository; +use crate::databases::database::{Category, Database, Error as DatabaseError}; +use crate::errors::ServiceError; +use crate::models::category::CategoryId; +use crate::models::user::UserId; + +pub struct Service { + category_repository: Arc, + user_repository: Arc, +} + +impl Service { + #[must_use] + pub fn new(category_repository: Arc, user_repository: Arc) -> Service { + Service { + category_repository, + user_repository, + } + } + + /// Adds a new category. + /// + /// # Errors + /// + /// It returns an error if: + /// + /// * The user does not have the required permissions. + /// * There is a database error. + pub async fn add_category(&self, category_name: &str, user_id: &UserId) -> Result { + let user = self.user_repository.get_compact_user(user_id).await?; + + // Check if user is administrator + // todo: extract authorization service + if !user.administrator { + return Err(ServiceError::Unauthorized); + } + + match self.category_repository.add_category(category_name).await { + Ok(id) => Ok(id), + Err(e) => match e { + DatabaseError::CategoryAlreadyExists => Err(ServiceError::CategoryExists), + _ => Err(ServiceError::DatabaseError), + }, + } + } + + /// Deletes a new category. + /// + /// # Errors + /// + /// It returns an error if: + /// + /// * The user does not have the required permissions. + /// * There is a database error. + pub async fn delete_category(&self, category_name: &str, user_id: &UserId) -> Result<(), ServiceError> { + let user = self.user_repository.get_compact_user(user_id).await?; + + // Check if user is administrator + // todo: extract authorization service + if !user.administrator { + return Err(ServiceError::Unauthorized); + } + + match self.category_repository.delete_category(category_name).await { + Ok(_) => Ok(()), + Err(e) => match e { + DatabaseError::CategoryNotFound => Err(ServiceError::CategoryNotFound), + _ => Err(ServiceError::DatabaseError), + }, + } + } +} + +pub struct DbCategoryRepository { + database: Arc>, +} + +impl DbCategoryRepository { + #[must_use] + pub fn new(database: Arc>) -> Self { + Self { database } + } + + /// It returns the categories. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn get_categories(&self) -> Result, DatabaseError> { + self.database.get_categories().await + } + + /// Adds a new category. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn add_category(&self, category_name: &str) -> Result { + self.database.insert_category_and_get_id(category_name).await + } + + /// Deletes a new category. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn delete_category(&self, category_name: &str) -> Result<(), DatabaseError> { + self.database.delete_category(category_name).await + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index ced75210..901b3b82 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1 +1,3 @@ pub mod about; +pub mod category; +pub mod user; diff --git a/src/services/user.rs b/src/services/user.rs new file mode 100644 index 00000000..a1d19c8c --- /dev/null +++ b/src/services/user.rs @@ -0,0 +1,31 @@ +//! User repository. +use std::sync::Arc; + +use crate::databases::database::Database; +use crate::errors::ServiceError; +use crate::models::user::{UserCompact, UserId}; + +pub struct DbUserRepository { + database: Arc>, +} + +impl DbUserRepository { + #[must_use] + pub fn new(database: Arc>) -> Self { + Self { database } + } + + /// It returns the compact user. + /// + /// # Errors + /// + /// It returns an error if there is a database error. + pub async fn get_compact_user(&self, user_id: &UserId) -> Result { + // todo: persistence layer should have its own errors instead of + // returning a `ServiceError`. + self.database + .get_user_compact_from_id(*user_id) + .await + .map_err(|_| ServiceError::UserNotFound) + } +}