Skip to content

Commit

Permalink
refactor: [torrust#157] extract service: category
Browse files Browse the repository at this point in the history
Decoupling services from actix-web framework.
  • Loading branch information
josecelano committed May 18, 2023
1 parent baa4c7e commit d58f3cc
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 25 deletions.
8 changes: 8 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down
12 changes: 11 additions & 1 deletion src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<UserId, ServiceError> {
let claims = self.get_claims_from_request(req).await?;
Ok(claims.user.user_id)
}
}
12 changes: 12 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,9 +20,13 @@ pub struct AppData {
pub tracker_statistics_importer: Arc<StatisticsImporter>,
pub mailer: Arc<mailer::Service>,
pub image_cache_manager: Arc<ImageCacheService>,
pub category_repository: Arc<DbCategoryRepository>,
pub user_repository: Arc<DbUserRepository>,
pub category_service: Arc<category::Service>,
}

impl AppData {
#[allow(clippy::too_many_arguments)]
pub fn new(
cfg: Arc<Configuration>,
database: Arc<Box<dyn Database>>,
Expand All @@ -29,6 +35,9 @@ impl AppData {
tracker_statistics_importer: Arc<StatisticsImporter>,
mailer: Arc<mailer::Service>,
image_cache_manager: Arc<ImageCacheService>,
category_repository: Arc<DbCategoryRepository>,
user_repository: Arc<DbUserRepository>,
category_service: Arc<category::Service>,
) -> AppData {
AppData {
cfg,
Expand All @@ -38,6 +47,9 @@ impl AppData {
tracker_statistics_importer,
mailer,
image_cache_manager,
category_repository,
user_repository,
category_service,
}
}
}
10 changes: 9 additions & 1 deletion src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/models/category.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#[allow(clippy::module_name_repetitions)]
pub type CategoryId = i64;
1 change: 1 addition & 0 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod category;
pub mod info_hash;
pub mod response;
pub mod torrent;
Expand Down
13 changes: 8 additions & 5 deletions src/models/user.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub date_imported: Option<String>,
pub administrator: bool,
Expand All @@ -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,
Expand All @@ -29,15 +32,15 @@ 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,
}

#[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<String>,
pub date_imported: Option<String>,
pub administrator: bool,
Expand Down
24 changes: 6 additions & 18 deletions src/routes/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<impl Responder> {
let categories = app_data.database.get_categories().await?;
let categories = app_data.category_repository.get_categories().await?;

Ok(HttpResponse::Ok().json(OkResponse { data: categories }))
}
Expand All @@ -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<Category>, app_data: WebAppData) -> ServiceResult<impl Responder> {
// 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(),
Expand All @@ -67,15 +61,9 @@ pub async fn delete(req: HttpRequest, payload: web::Json<Category>, 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(),
Expand Down
113 changes: 113 additions & 0 deletions src/services/category.rs
Original file line number Diff line number Diff line change
@@ -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<DbCategoryRepository>,
user_repository: Arc<DbUserRepository>,
}

impl Service {
#[must_use]
pub fn new(category_repository: Arc<DbCategoryRepository>, user_repository: Arc<DbUserRepository>) -> 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<i64, 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.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<Box<dyn Database>>,
}

impl DbCategoryRepository {
#[must_use]
pub fn new(database: Arc<Box<dyn Database>>) -> 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<Vec<Category>, 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<CategoryId, DatabaseError> {
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
}
}
2 changes: 2 additions & 0 deletions src/services/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod about;
pub mod category;
pub mod user;
31 changes: 31 additions & 0 deletions src/services/user.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn Database>>,
}

impl DbUserRepository {
#[must_use]
pub fn new(database: Arc<Box<dyn Database>>) -> 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<UserCompact, ServiceError> {
// 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)
}
}

0 comments on commit d58f3cc

Please sign in to comment.