diff --git a/backend/src/lib/domain/warren/models/warren/requests.rs b/backend/src/lib/domain/warren/models/warren/requests.rs index 0c7276b..63a9780 100644 --- a/backend/src/lib/domain/warren/models/warren/requests.rs +++ b/backend/src/lib/domain/warren/models/warren/requests.rs @@ -8,7 +8,7 @@ use crate::domain::warren::models::file::{ RelativeFilePath, RenameEntryError, RenameEntryRequest, }; -use super::Warren; +use super::{Warren, WarrenName}; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct FetchWarrenRequest { @@ -517,3 +517,87 @@ pub enum ListWarrensError { #[error(transparent)] Unknown(#[from] anyhow::Error), } + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CreateWarrenRequest { + name: WarrenName, + path: AbsoluteFilePath, +} + +impl CreateWarrenRequest { + pub fn new(name: WarrenName, path: AbsoluteFilePath) -> Self { + Self { name, path } + } + + pub fn name(&self) -> &WarrenName { + &self.name + } + + pub fn path(&self) -> &AbsoluteFilePath { + &self.path + } +} + +#[derive(Debug, Error)] +pub enum CreateWarrenError { + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EditWarrenRequest { + id: Uuid, + name: WarrenName, + path: AbsoluteFilePath, +} + +impl EditWarrenRequest { + pub fn new(warren_id: Uuid, name: WarrenName, path: AbsoluteFilePath) -> Self { + Self { + id: warren_id, + name, + path, + } + } + + pub fn id(&self) -> &Uuid { + &self.id + } + + pub fn name(&self) -> &WarrenName { + &self.name + } + + pub fn path(&self) -> &AbsoluteFilePath { + &self.path + } +} + +#[derive(Debug, Error)] +pub enum EditWarrenError { + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeleteWarrenRequest { + id: Uuid, +} + +impl DeleteWarrenRequest { + pub fn new(warren_id: Uuid) -> Self { + Self { id: warren_id } + } + + pub fn id(&self) -> &Uuid { + &self.id + } +} + +#[derive(Debug, Error)] +pub enum DeleteWarrenError { + #[error("This warren does not exist")] + NotFound, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/ports/metrics.rs b/backend/src/lib/domain/warren/ports/metrics.rs index 8dde0ea..9353bba 100644 --- a/backend/src/lib/domain/warren/ports/metrics.rs +++ b/backend/src/lib/domain/warren/ports/metrics.rs @@ -1,4 +1,13 @@ pub trait WarrenMetrics: Clone + Send + Sync + 'static { + fn record_warren_creation_success(&self) -> impl Future + Send; + fn record_warren_creation_failure(&self) -> impl Future + Send; + + fn record_warren_edit_success(&self) -> impl Future + Send; + fn record_warren_edit_failure(&self) -> impl Future + Send; + + fn record_warren_deletion_success(&self) -> impl Future + Send; + fn record_warren_deletion_failure(&self) -> impl Future + Send; + fn record_warren_fetch_success(&self) -> impl Future + Send; fn record_warren_fetch_failure(&self) -> impl Future + Send; @@ -53,6 +62,15 @@ pub trait FileSystemMetrics: Clone + Send + Sync + 'static { } pub trait AuthMetrics: Clone + Send + Sync + 'static { + fn record_auth_warren_creation_success(&self) -> impl Future + Send; + fn record_auth_warren_creation_failure(&self) -> impl Future + Send; + + fn record_auth_warren_edit_success(&self) -> impl Future + Send; + fn record_auth_warren_edit_failure(&self) -> impl Future + Send; + + fn record_auth_warren_deletion_success(&self) -> impl Future + Send; + fn record_auth_warren_deletion_failure(&self) -> impl Future + Send; + fn record_user_registration_success(&self) -> impl Future + Send; fn record_user_registration_failure(&self) -> impl Future + Send; diff --git a/backend/src/lib/domain/warren/ports/mod.rs b/backend/src/lib/domain/warren/ports/mod.rs index 689c263..1dba5f3 100644 --- a/backend/src/lib/domain/warren/ports/mod.rs +++ b/backend/src/lib/domain/warren/ports/mod.rs @@ -35,8 +35,10 @@ use super::models::{ }, warren::{ CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, CreateWarrenDirectoryResponse, - DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, - DeleteWarrenFileError, DeleteWarrenFileRequest, DeleteWarrenFileResponse, FetchWarrenError, + CreateWarrenError, CreateWarrenRequest, DeleteWarrenDirectoryError, + DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, DeleteWarrenError, + DeleteWarrenFileError, DeleteWarrenFileRequest, DeleteWarrenFileResponse, + DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, ListWarrenFilesError, ListWarrenFilesRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest, RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse, @@ -45,6 +47,19 @@ use super::models::{ }; pub trait WarrenService: Clone + Send + Sync + 'static { + fn create_warren( + &self, + request: CreateWarrenRequest, + ) -> impl Future> + Send; + fn edit_warren( + &self, + request: EditWarrenRequest, + ) -> impl Future> + Send; + fn delete_warren( + &self, + request: DeleteWarrenRequest, + ) -> impl Future> + Send; + fn fetch_warrens( &self, request: FetchWarrensRequest, @@ -119,6 +134,25 @@ pub trait FileSystemService: Clone + Send + Sync + 'static { } pub trait AuthService: Clone + Send + Sync + 'static { + /// MUST REQUIRE ADMIN PRIVILEGES + fn create_warren( + &self, + request: AuthRequest, + warren_service: &WS, + ) -> impl Future>> + Send; + /// MUST REQUIRE ADMIN PRIVILEGES + fn edit_warren( + &self, + request: AuthRequest, + warren_service: &WS, + ) -> impl Future>> + Send; + /// MUST REQUIRE ADMIN PRIVILEGES + fn delete_warren( + &self, + request: AuthRequest, + warren_service: &WS, + ) -> impl Future>> + Send; + fn register_user( &self, request: RegisterUserRequest, diff --git a/backend/src/lib/domain/warren/ports/notifier.rs b/backend/src/lib/domain/warren/ports/notifier.rs index 96780a4..8a4050a 100644 --- a/backend/src/lib/domain/warren/ports/notifier.rs +++ b/backend/src/lib/domain/warren/ports/notifier.rs @@ -12,6 +12,10 @@ use crate::domain::warren::models::{ }; pub trait WarrenNotifier: Clone + Send + Sync + 'static { + fn warren_created(&self, warren: &Warren) -> impl Future + Send; + fn warren_edited(&self, warren: &Warren) -> impl Future + Send; + fn warren_deleted(&self, warren: &Warren) -> impl Future + Send; + fn warrens_fetched(&self, warrens: &Vec) -> impl Future + Send; fn warren_fetched(&self, warren: &Warren) -> impl Future + Send; fn warrens_listed(&self, warrens: &Vec) -> impl Future + Send; @@ -72,6 +76,19 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static { } pub trait AuthNotifier: Clone + Send + Sync + 'static { + fn auth_warren_created( + &self, + creator: &User, + warren: &Warren, + ) -> impl Future + Send; + fn auth_warren_edited(&self, editor: &User, warren: &Warren) + -> impl Future + Send; + fn auth_warren_deleted( + &self, + deleter: &User, + warren: &Warren, + ) -> impl Future + Send; + fn user_registered(&self, user: &User) -> impl Future + Send; fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future + Send; fn user_created(&self, creator: &User, created: &User) -> impl Future + Send; diff --git a/backend/src/lib/domain/warren/ports/repository.rs b/backend/src/lib/domain/warren/ports/repository.rs index ab17e29..938cf52 100644 --- a/backend/src/lib/domain/warren/ports/repository.rs +++ b/backend/src/lib/domain/warren/ports/repository.rs @@ -27,14 +27,28 @@ use crate::domain::warren::models::{ }, }, warren::{ - FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, - ListWarrensError, ListWarrensRequest, Warren, + CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest, + EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest, + FetchWarrensError, FetchWarrensRequest, ListWarrensError, ListWarrensRequest, Warren, }, }; use super::WarrenService; pub trait WarrenRepository: Clone + Send + Sync + 'static { + fn create_warren( + &self, + request: CreateWarrenRequest, + ) -> impl Future> + Send; + fn edit_warren( + &self, + request: EditWarrenRequest, + ) -> impl Future> + Send; + fn delete_warren( + &self, + request: DeleteWarrenRequest, + ) -> impl Future> + Send; + fn fetch_warrens( &self, request: FetchWarrensRequest, diff --git a/backend/src/lib/domain/warren/service/auth.rs b/backend/src/lib/domain/warren/service/auth.rs index 2bf2c70..3792219 100644 --- a/backend/src/lib/domain/warren/service/auth.rs +++ b/backend/src/lib/domain/warren/service/auth.rs @@ -26,13 +26,15 @@ use crate::{ }, warren::{ CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, - CreateWarrenDirectoryResponse, DeleteWarrenDirectoryError, - DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, DeleteWarrenFileError, - DeleteWarrenFileRequest, DeleteWarrenFileResponse, FetchWarrenError, - FetchWarrenRequest, FetchWarrensRequest, ListWarrenFilesError, - ListWarrenFilesRequest, ListWarrenFilesResponse, RenameWarrenEntryError, - RenameWarrenEntryRequest, RenameWarrenEntryResponse, UploadWarrenFilesError, - UploadWarrenFilesRequest, UploadWarrenFilesResponse, Warren, + CreateWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest, + DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest, + DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileError, + DeleteWarrenFileRequest, DeleteWarrenFileResponse, DeleteWarrenRequest, + EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest, + FetchWarrensRequest, ListWarrenFilesError, ListWarrenFilesRequest, + ListWarrenFilesResponse, RenameWarrenEntryError, RenameWarrenEntryRequest, + RenameWarrenEntryResponse, UploadWarrenFilesError, UploadWarrenFilesRequest, + UploadWarrenFilesResponse, Warren, }, }, ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService}, @@ -102,6 +104,100 @@ where M: AuthMetrics, N: AuthNotifier, { + async fn create_warren( + &self, + request: AuthRequest, + warren_service: &WS, + ) -> Result> { + let (session, request) = request.unpack(); + + let session_response = self + .fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone())) + .await?; + + if !session_response.user().admin() { + return Err(AuthError::InsufficientPermissions); + } + + let result = warren_service + .create_warren(request) + .await + .map_err(AuthError::Custom); + + if let Ok(warren) = result.as_ref() { + self.metrics.record_auth_warren_creation_success().await; + self.notifier + .auth_warren_created(&session_response.user(), warren) + .await; + } else { + self.metrics.record_auth_warren_creation_failure().await; + } + + result + } + async fn edit_warren( + &self, + request: AuthRequest, + warren_service: &WS, + ) -> Result> { + let (session, request) = request.unpack(); + + let session_response = self + .fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone())) + .await?; + + if !session_response.user().admin() { + return Err(AuthError::InsufficientPermissions); + } + + let result = warren_service + .edit_warren(request) + .await + .map_err(AuthError::Custom); + + if let Ok(warren) = result.as_ref() { + self.metrics.record_auth_warren_edit_success().await; + self.notifier + .auth_warren_edited(&session_response.user(), warren) + .await; + } else { + self.metrics.record_auth_warren_edit_failure().await; + } + + result + } + async fn delete_warren( + &self, + request: AuthRequest, + warren_service: &WS, + ) -> Result> { + let (session, request) = request.unpack(); + + let session_response = self + .fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone())) + .await?; + + if !session_response.user().admin() { + return Err(AuthError::InsufficientPermissions); + } + + let result = warren_service + .delete_warren(request) + .await + .map_err(AuthError::Custom); + + if let Ok(warren) = result.as_ref() { + self.metrics.record_auth_warren_deletion_success().await; + self.notifier + .auth_warren_deleted(&session_response.user(), warren) + .await; + } else { + self.metrics.record_auth_warren_deletion_failure().await; + } + + result + } + async fn register_user(&self, request: RegisterUserRequest) -> Result { let result = self.repository.create_user(request.into()).await; diff --git a/backend/src/lib/domain/warren/service/warren.rs b/backend/src/lib/domain/warren/service/warren.rs index 90093bf..1ef5e8c 100644 --- a/backend/src/lib/domain/warren/service/warren.rs +++ b/backend/src/lib/domain/warren/service/warren.rs @@ -1,9 +1,11 @@ use crate::domain::warren::{ models::warren::{ - CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse, - FetchWarrensError, FetchWarrensRequest, ListWarrenFilesResponse, ListWarrensError, - ListWarrensRequest, RenameWarrenEntryError, RenameWarrenEntryRequest, - RenameWarrenEntryResponse, UploadWarrenFilesResponse, + CreateWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest, + DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileResponse, + DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrensError, + FetchWarrensRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest, + RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse, + UploadWarrenFilesResponse, }, ports::FileSystemService, }; @@ -56,6 +58,49 @@ where N: WarrenNotifier, FSS: FileSystemService, { + async fn create_warren( + &self, + request: CreateWarrenRequest, + ) -> Result { + let result = self.repository.create_warren(request).await; + + if let Ok(warren) = result.as_ref() { + self.metrics.record_warren_creation_success().await; + self.notifier.warren_created(warren).await; + } else { + self.metrics.record_warren_creation_failure().await; + } + + result + } + async fn edit_warren(&self, request: EditWarrenRequest) -> Result { + let result = self.repository.edit_warren(request).await; + + if let Ok(warren) = result.as_ref() { + self.metrics.record_warren_edit_success().await; + self.notifier.warren_edited(warren).await; + } else { + self.metrics.record_warren_edit_failure().await; + } + + result + } + async fn delete_warren( + &self, + request: DeleteWarrenRequest, + ) -> Result { + let result = self.repository.delete_warren(request).await; + + if let Ok(warren) = result.as_ref() { + self.metrics.record_warren_deletion_success().await; + self.notifier.warren_deleted(warren).await; + } else { + self.metrics.record_warren_deletion_failure().await; + } + + result + } + async fn fetch_warrens( &self, request: FetchWarrensRequest, diff --git a/backend/src/lib/inbound/http/handlers/admin/create_warren.rs b/backend/src/lib/inbound/http/handlers/admin/create_warren.rs new file mode 100644 index 0000000..7271b6c --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/admin/create_warren.rs @@ -0,0 +1,83 @@ +use axum::{Json, extract::State, http::StatusCode}; +use serde::Deserialize; +use thiserror::Error; + +use crate::{ + domain::warren::{ + models::{ + auth_session::AuthRequest, + file::{AbsoluteFilePathError, FilePath, FilePathError}, + warren::{CreateWarrenRequest, WarrenName, WarrenNameError}, + }, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::{AdminWarrenData, extractors::SessionIdHeader}, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, Error)] +pub(super) enum ParseCreateWarrenHttpRequestError { + #[error(transparent)] + WarrenName(#[from] WarrenNameError), + #[error(transparent)] + FilePath(#[from] FilePathError), + #[error(transparent)] + AbsoluteFilePath(#[from] AbsoluteFilePathError), +} + +impl From for ApiError { + fn from(value: ParseCreateWarrenHttpRequestError) -> Self { + match value { + ParseCreateWarrenHttpRequestError::WarrenName(err) => match err { + WarrenNameError::Empty => { + Self::BadRequest("The warren name must not be empty".to_string()) + } + }, + ParseCreateWarrenHttpRequestError::FilePath(err) => match err { + FilePathError::InvalidPath => Self::BadRequest("The path is invalid".to_string()), + }, + ParseCreateWarrenHttpRequestError::AbsoluteFilePath(err) => match err { + AbsoluteFilePathError::NotAbsolute => { + Self::BadRequest("The path must be absolute".to_string()) + } + }, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(super) struct CreateWarrenHttpRequestBody { + name: String, + path: String, +} + +impl CreateWarrenHttpRequestBody { + fn try_into_domain(self) -> Result { + let name = WarrenName::new(&self.name)?; + let path = FilePath::new(&self.path)?.try_into()?; + + Ok(CreateWarrenRequest::new(name, path)) + } +} + +pub async fn create_warren( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, + Json(request): Json, +) -> Result, ApiError> { + let domain_request = request.try_into_domain()?; + + state + .auth_service + .create_warren( + AuthRequest::new(session, domain_request), + state.warren_service.as_ref(), + ) + .await + .map(|warren| ApiSuccess::new(StatusCode::CREATED, warren.into())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/admin/delete_warren.rs b/backend/src/lib/inbound/http/handlers/admin/delete_warren.rs new file mode 100644 index 0000000..9e442dc --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/admin/delete_warren.rs @@ -0,0 +1,45 @@ +use axum::{Json, extract::State, http::StatusCode}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{ + domain::warren::{ + models::{auth_session::AuthRequest, warren::DeleteWarrenRequest}, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::{AdminWarrenData, extractors::SessionIdHeader}, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(super) struct DeleteWarrenHttpRequestBody { + id: Uuid, +} + +impl DeleteWarrenHttpRequestBody { + fn into_domain(self) -> DeleteWarrenRequest { + DeleteWarrenRequest::new(self.id) + } +} + +pub async fn delete_warren( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, + Json(request): Json, +) -> Result, ApiError> { + let domain_request = request.into_domain(); + + state + .auth_service + .delete_warren( + AuthRequest::new(session, domain_request), + state.warren_service.as_ref(), + ) + .await + .map(|warren| ApiSuccess::new(StatusCode::CREATED, warren.into())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/admin/edit_warren.rs b/backend/src/lib/inbound/http/handlers/admin/edit_warren.rs new file mode 100644 index 0000000..bc9be85 --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/admin/edit_warren.rs @@ -0,0 +1,85 @@ +use axum::{Json, extract::State, http::StatusCode}; +use serde::Deserialize; +use thiserror::Error; +use uuid::Uuid; + +use crate::{ + domain::warren::{ + models::{ + auth_session::AuthRequest, + file::{AbsoluteFilePathError, FilePath, FilePathError}, + warren::{EditWarrenRequest, WarrenName, WarrenNameError}, + }, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::{AdminWarrenData, extractors::SessionIdHeader}, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, Error)] +pub(super) enum ParseEditWarrenHttpRequestError { + #[error(transparent)] + WarrenName(#[from] WarrenNameError), + #[error(transparent)] + FilePath(#[from] FilePathError), + #[error(transparent)] + AbsoluteFilePath(#[from] AbsoluteFilePathError), +} + +impl From for ApiError { + fn from(value: ParseEditWarrenHttpRequestError) -> Self { + match value { + ParseEditWarrenHttpRequestError::WarrenName(err) => match err { + WarrenNameError::Empty => { + Self::BadRequest("The warren name must not be empty".to_string()) + } + }, + ParseEditWarrenHttpRequestError::FilePath(err) => match err { + FilePathError::InvalidPath => Self::BadRequest("The path is invalid".to_string()), + }, + ParseEditWarrenHttpRequestError::AbsoluteFilePath(err) => match err { + AbsoluteFilePathError::NotAbsolute => { + Self::BadRequest("The path must be absolute".to_string()) + } + }, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(super) struct EditWarrenHttpRequestBody { + id: Uuid, + name: String, + path: String, +} + +impl EditWarrenHttpRequestBody { + fn try_into_domain(self) -> Result { + let name = WarrenName::new(&self.name)?; + let path = FilePath::new(&self.path)?.try_into()?; + + Ok(EditWarrenRequest::new(self.id, name, path)) + } +} + +pub async fn edit_warren( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, + Json(request): Json, +) -> Result, ApiError> { + let domain_request = request.try_into_domain()?; + + state + .auth_service + .edit_warren( + AuthRequest::new(session, domain_request), + state.warren_service.as_ref(), + ) + .await + .map(|warren| ApiSuccess::new(StatusCode::CREATED, warren.into())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/admin/mod.rs b/backend/src/lib/inbound/http/handlers/admin/mod.rs index e2c2e5b..76ade87 100644 --- a/backend/src/lib/inbound/http/handlers/admin/mod.rs +++ b/backend/src/lib/inbound/http/handlers/admin/mod.rs @@ -1,18 +1,24 @@ mod create_user; mod create_user_warren; +mod create_warren; mod delete_user; mod delete_user_warren; +mod delete_warren; mod edit_user; mod edit_user_warren; +mod edit_warren; mod list_all_users_and_warrens; mod list_users; use create_user::create_user; use create_user_warren::create_user_warren; +use create_warren::create_warren; use delete_user::delete_user; use delete_user_warren::delete_user_warren; +use delete_warren::delete_warren; use edit_user::edit_user; use edit_user_warren::edit_user_warren; +use edit_warren::edit_warren; use list_all_users_and_warrens::list_all_users_and_warrens; use list_users::list_users; @@ -33,6 +39,9 @@ pub fn routes() -> Router> .route("/users", post(create_user)) .route("/users", patch(edit_user)) .route("/users", delete(delete_user)) + .route("/warrens", post(create_warren)) + .route("/warrens", patch(edit_warren)) + .route("/warrens", delete(delete_warren)) .route("/user-warrens", post(create_user_warren)) .route("/user-warrens", patch(edit_user_warren)) .route("/user-warrens", delete(delete_user_warren)) diff --git a/backend/src/lib/outbound/metrics_debug_logger.rs b/backend/src/lib/outbound/metrics_debug_logger.rs index b325487..7ed7c17 100644 --- a/backend/src/lib/outbound/metrics_debug_logger.rs +++ b/backend/src/lib/outbound/metrics_debug_logger.rs @@ -10,6 +10,27 @@ impl MetricsDebugLogger { } impl WarrenMetrics for MetricsDebugLogger { + async fn record_warren_creation_success(&self) { + tracing::debug!("[Metrics] Warren creation succeeded"); + } + async fn record_warren_creation_failure(&self) { + tracing::debug!("[Metrics] Warren creation failed"); + } + + async fn record_warren_edit_success(&self) { + tracing::debug!("[Metrics] Warren edit succeeded"); + } + async fn record_warren_edit_failure(&self) { + tracing::debug!("[Metrics] Warren edit failed"); + } + + async fn record_warren_deletion_success(&self) { + tracing::debug!("[Metrics] Warren deletion succeeded"); + } + async fn record_warren_deletion_failure(&self) { + tracing::debug!("[Metrics] Warren deletion failed"); + } + async fn record_warren_list_success(&self) { tracing::debug!("[Metrics] Warren list succeeded"); } @@ -129,6 +150,27 @@ impl FileSystemMetrics for MetricsDebugLogger { } impl AuthMetrics for MetricsDebugLogger { + async fn record_auth_warren_creation_success(&self) { + tracing::debug!("[Metrics] Warren creation by admin succeeded"); + } + async fn record_auth_warren_creation_failure(&self) { + tracing::debug!("[Metrics] Warren creation by admin failed"); + } + + async fn record_auth_warren_edit_success(&self) -> () { + tracing::debug!("[Metrics] Warren edit by admin succeeded"); + } + async fn record_auth_warren_edit_failure(&self) -> () { + tracing::debug!("[Metrics] Warren edit by admin failed"); + } + + async fn record_auth_warren_deletion_success(&self) -> () { + tracing::debug!("[Metrics] Warren deletion by admin succeeded"); + } + async fn record_auth_warren_deletion_failure(&self) -> () { + tracing::debug!("[Metrics] Warren deletion by admin failed"); + } + async fn record_user_registration_success(&self) { tracing::debug!("[Metrics] User registration succeeded"); } diff --git a/backend/src/lib/outbound/notifier_debug_logger.rs b/backend/src/lib/outbound/notifier_debug_logger.rs index 638af13..2e84ed2 100644 --- a/backend/src/lib/outbound/notifier_debug_logger.rs +++ b/backend/src/lib/outbound/notifier_debug_logger.rs @@ -24,6 +24,16 @@ impl NotifierDebugLogger { } impl WarrenNotifier for NotifierDebugLogger { + async fn warren_created(&self, warren: &Warren) { + tracing::debug!("[Notifier] Created warren: {}", warren.name()); + } + async fn warren_edited(&self, warren: &Warren) { + tracing::debug!("[Notifier] Edited warren: {}", warren.name()); + } + async fn warren_deleted(&self, warren: &Warren) { + tracing::debug!("[Notifier] Deleted warren: {}", warren.name()); + } + async fn warrens_fetched(&self, warrens: &Vec) { tracing::debug!("[Notifier] Fetched {} warren(s)", warrens.len()); } @@ -121,6 +131,28 @@ impl FileSystemNotifier for NotifierDebugLogger { } impl AuthNotifier for NotifierDebugLogger { + async fn auth_warren_created(&self, creator: &User, warren: &Warren) { + tracing::debug!( + "[Notifier] Admin {} created warren: {}", + creator.id(), + warren.name() + ); + } + async fn auth_warren_edited(&self, editor: &User, warren: &Warren) { + tracing::debug!( + "[Notifier] Admin {} edited warren: {}", + editor.id(), + warren.name() + ); + } + async fn auth_warren_deleted(&self, deleter: &User, warren: &Warren) { + tracing::debug!( + "[Notifier] Admin {} deleted warren: {}", + deleter.id(), + warren.name() + ); + } + async fn user_registered(&self, user: &User) { tracing::debug!("[Notifier] Registered user {}", user.name()); } diff --git a/backend/src/lib/outbound/postgres/warrens.rs b/backend/src/lib/outbound/postgres/warrens.rs index 26dfb7b..cb9fb48 100644 --- a/backend/src/lib/outbound/postgres/warrens.rs +++ b/backend/src/lib/outbound/postgres/warrens.rs @@ -1,11 +1,16 @@ use anyhow::{Context as _, anyhow}; -use sqlx::PgConnection; +use sqlx::{Acquire as _, PgConnection}; use uuid::Uuid; use crate::domain::warren::{ - models::warren::{ - FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, - ListWarrensError, ListWarrensRequest, Warren, + models::{ + file::AbsoluteFilePath, + warren::{ + CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest, + EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest, + FetchWarrensError, FetchWarrensRequest, ListWarrensError, ListWarrensRequest, Warren, + WarrenName, + }, }, ports::WarrenRepository, }; @@ -13,6 +18,62 @@ use crate::domain::warren::{ use super::{Postgres, is_not_found_error}; impl WarrenRepository for Postgres { + async fn create_warren( + &self, + request: CreateWarrenRequest, + ) -> Result { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let warren = self + .create_warren(&mut connection, request.name(), request.path()) + .await + .context("Failed to create new warren")?; + + Ok(warren) + } + + async fn edit_warren(&self, request: EditWarrenRequest) -> Result { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let warren = self + .edit_warren( + &mut connection, + request.id(), + request.name(), + request.path(), + ) + .await + .context("Failed to edit existing warren")?; + + Ok(warren) + } + + async fn delete_warren( + &self, + request: DeleteWarrenRequest, + ) -> Result { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let warren = self + .delete_warren(&mut connection, request.id()) + .await + .context("Failed to delete existing warren")?; + + Ok(warren) + } + async fn fetch_warrens( &self, request: FetchWarrensRequest, @@ -74,6 +135,96 @@ impl WarrenRepository for Postgres { } impl Postgres { + async fn create_warren( + &self, + connection: &mut PgConnection, + name: &WarrenName, + path: &AbsoluteFilePath, + ) -> Result { + let mut tx = connection.begin().await?; + + let warren: Warren = sqlx::query_as( + " + INSERT INTO warrens ( + name, + path + ) VALUES ( + $1, + $2 + ) + RETURNING + * + ", + ) + .bind(name) + .bind(path) + .fetch_one(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(warren) + } + + async fn edit_warren( + &self, + connection: &mut PgConnection, + id: &Uuid, + name: &WarrenName, + path: &AbsoluteFilePath, + ) -> Result { + let mut tx = connection.begin().await?; + + let warren: Warren = sqlx::query_as( + " + UPDATE + warrens + SET + name = $2, + path = $3 + WHERE + id = $1 + RETURNING + * + ", + ) + .bind(id) + .bind(name) + .bind(path) + .fetch_one(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(warren) + } + + async fn delete_warren( + &self, + connection: &mut PgConnection, + id: &Uuid, + ) -> Result { + let mut tx = connection.begin().await?; + + let warren: Warren = sqlx::query_as( + " + DELETE FROM + warrens + WHERE + id = $1 + RETURNING + * + ", + ) + .bind(id) + .fetch_one(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(warren) + } + async fn get_warren( &self, connection: &mut PgConnection, diff --git a/frontend/components/actions/CreateDirectoryDialog.vue b/frontend/components/actions/CreateDirectoryDialog.vue index e7c6b89..a8d3d4d 100644 --- a/frontend/components/actions/CreateDirectoryDialog.vue +++ b/frontend/components/actions/CreateDirectoryDialog.vue @@ -45,10 +45,16 @@ function onKeyDown(e: KeyboardEvent) { submit(); } } + +function onOpenChange(state: boolean) { + if (!state) { + dialog.reset(); + } +} diff --git a/frontend/lib/api/admin/createWarren.ts b/frontend/lib/api/admin/createWarren.ts new file mode 100644 index 0000000..3af01c2 --- /dev/null +++ b/frontend/lib/api/admin/createWarren.ts @@ -0,0 +1,43 @@ +import type { ApiResponse } from '~/shared/types/api'; +import type { AdminWarrenData } from '~/shared/types/warrens'; +import { getApiHeaders } from '..'; +import { toast } from 'vue-sonner'; + +export async function createWarren( + name: string, + path: string +): Promise<{ success: true; warren: AdminWarrenData } | { success: false }> { + const { data, error } = await useFetch>( + getApiUrl('admin/warrens'), + { + method: 'POST', + headers: getApiHeaders(), + body: JSON.stringify({ + name: name, + path: path, + }), + responseType: 'json', + } + ); + + if (data.value == null) { + toast.error('Create warren', { + description: error.value?.data ?? 'Failed to create warren', + }); + + return { + success: false, + }; + } + + await refreshNuxtData('admin-resources'); + + toast.success('Create warren', { + description: 'Successfully created warren', + }); + + return { + success: true, + warren: data.value.data, + }; +} diff --git a/frontend/lib/api/admin/deleteWarren.ts b/frontend/lib/api/admin/deleteWarren.ts new file mode 100644 index 0000000..e956410 --- /dev/null +++ b/frontend/lib/api/admin/deleteWarren.ts @@ -0,0 +1,41 @@ +import type { ApiResponse } from '~/shared/types/api'; +import type { AdminWarrenData } from '~/shared/types/warrens'; +import { getApiHeaders } from '..'; +import { toast } from 'vue-sonner'; + +export async function deleteWarren( + warrenId: string +): Promise<{ success: true; warren: AdminWarrenData } | { success: false }> { + const { data, error } = await useFetch>( + getApiUrl('admin/warrens'), + { + method: 'DELETE', + headers: getApiHeaders(), + body: JSON.stringify({ + id: warrenId, + }), + responseType: 'json', + } + ); + + if (data.value == null) { + toast.error('Delete warren', { + description: error.value?.data ?? 'Failed to delete warren', + }); + + return { + success: false, + }; + } + + await refreshNuxtData(['warrens', 'admin-resources']); + + toast.success('Delete warren', { + description: 'Successfully deleted warren', + }); + + return { + success: true, + warren: data.value.data, + }; +} diff --git a/frontend/lib/api/admin/editWarren.ts b/frontend/lib/api/admin/editWarren.ts new file mode 100644 index 0000000..4226d6d --- /dev/null +++ b/frontend/lib/api/admin/editWarren.ts @@ -0,0 +1,45 @@ +import type { ApiResponse } from '~/shared/types/api'; +import type { AdminWarrenData } from '~/shared/types/warrens'; +import { getApiHeaders } from '..'; +import { toast } from 'vue-sonner'; + +export async function editWarren( + warrenId: string, + name: string, + path: string +): Promise<{ success: true; warren: AdminWarrenData } | { success: false }> { + const { data, error } = await useFetch>( + getApiUrl('admin/warrens'), + { + method: 'PATCH', + headers: getApiHeaders(), + body: JSON.stringify({ + id: warrenId, + name: name, + path: path, + }), + responseType: 'json', + } + ); + + if (data.value == null) { + toast.error('Edit warren', { + description: error.value?.data ?? 'Failed to edit warren', + }); + + return { + success: false, + }; + } + + await refreshNuxtData('admin-resources'); + + toast.success('Edit warren', { + description: 'Successfully editd warren', + }); + + return { + success: true, + warren: data.value.data, + }; +} diff --git a/frontend/lib/api/auth/logout.ts b/frontend/lib/api/auth/logout.ts index 05f9948..47044b9 100644 --- a/frontend/lib/api/auth/logout.ts +++ b/frontend/lib/api/auth/logout.ts @@ -1,5 +1,6 @@ export async function logout() { useAuthSession().value = null; + useAdminStore().$reset(); await navigateTo({ path: '/login', }); diff --git a/frontend/lib/schemas/admin.ts b/frontend/lib/schemas/admin.ts index ab0d18f..d802493 100644 --- a/frontend/lib/schemas/admin.ts +++ b/frontend/lib/schemas/admin.ts @@ -26,3 +26,13 @@ export const userWarrenSchema = object({ canModifyFiles: boolean().required(), canDeleteFiles: boolean().required(), }); + +export const createWarrenSchema = object({ + name: string().min(1).required('required'), + path: string().min(1).required('required'), +}); + +export const editWarrenSchema = object({ + name: string().min(1).required('required'), + path: string().min(1).required('required'), +}); diff --git a/frontend/pages/admin/index.vue b/frontend/pages/admin/index.vue index a8cdbee..06082fe 100644 --- a/frontend/pages/admin/index.vue +++ b/frontend/pages/admin/index.vue @@ -15,7 +15,7 @@ const adminStore = useAdminStore(); Users Create or manage users - +
Warrens Create or manage warrens - +
@@ -94,7 +106,7 @@ const adminStore = useAdminStore();
-
diff --git a/frontend/stores/admin.ts b/frontend/stores/admin.ts index fe5ff4b..bf5c3ec 100644 --- a/frontend/stores/admin.ts +++ b/frontend/stores/admin.ts @@ -1,5 +1,6 @@ import type { AuthUser } from '#shared/types/auth'; import type { AdminResources, AuthUserWithWarrens } from '~/shared/types/admin'; +import type { AdminWarrenData } from '~/shared/types/warrens'; export const useAdminStore = defineStore('admin', { state: () => ({ @@ -7,11 +8,37 @@ export const useAdminStore = defineStore('admin', { users: [], warrens: {}, } as AdminResources, + createWarrenDialogOpen: false, + editWarrenDialog: null as { warren: AdminWarrenData } | null, + deleteWarrenDialog: null as { warren: AdminWarrenData } | null, + createUserDialogOpen: false, editUserDialog: null as { user: AuthUserWithWarrens } | null, deleteUserDialog: null as { user: AuthUser } | null, }), actions: { + openCreateWarrenDialog() { + this.createWarrenDialogOpen = true; + }, + closeCreateWarrenDialog() { + this.createWarrenDialogOpen = false; + }, + openEditWarrenDialog(warren: AdminWarrenData) { + this.editWarrenDialog = { + warren, + }; + }, + closeEditWarrenDialog() { + this.editWarrenDialog = null; + }, + openDeleteWarrenDialog(warren: AdminWarrenData) { + this.deleteWarrenDialog = { + warren, + }; + }, + closeDeleteWarrenDialog() { + this.deleteWarrenDialog = null; + }, openCreateUserDialog() { this.createUserDialogOpen = true; },