From 2c26002507ff73046f74a9117b8aabb266c26761 Mon Sep 17 00:00:00 2001 From: 409 <409dev@protonmail.com> Date: Mon, 21 Jul 2025 19:27:41 +0200 Subject: [PATCH] add / edit / delete user warrens --- ...502_user_warrens_drop_some_permissions.sql | 1 + .../domain/warren/models/user_warren/mod.rs | 14 - .../requests/create_user_warren.rs | 25 ++ .../requests/delete_user_warren.rs | 31 +++ .../user_warren/requests/edit_user_warren.rs | 27 ++ .../requests/fetch_user_warrens.rs | 19 +- .../warren/models/user_warren/requests/mod.rs | 7 + .../domain/warren/models/warren/requests.rs | 15 ++ .../src/lib/domain/warren/ports/metrics.rs | 18 +- backend/src/lib/domain/warren/ports/mod.rs | 37 ++- .../src/lib/domain/warren/ports/notifier.rs | 18 +- .../src/lib/domain/warren/ports/repository.rs | 25 +- backend/src/lib/domain/warren/service/auth.rs | 104 +++++++- .../src/lib/domain/warren/service/warren.rs | 23 +- backend/src/lib/inbound/http/errors.rs | 4 +- .../http/handlers/admin/create_user_warren.rs | 57 ++++ .../http/handlers/admin/delete_user_warren.rs | 43 +++ .../http/handlers/admin/edit_user_warren.rs | 57 ++++ .../admin/list_all_users_and_warrens.rs | 4 +- .../lib/inbound/http/handlers/admin/mod.rs | 9 + .../http/handlers/auth/fetch_session.rs | 3 +- backend/src/lib/inbound/http/handlers/mod.rs | 23 +- .../http/handlers/warrens/list_warrens.rs | 6 +- .../src/lib/outbound/metrics_debug_logger.rs | 30 ++- .../src/lib/outbound/notifier_debug_logger.rs | 31 +++ backend/src/lib/outbound/postgres.rs | 218 ++++++++++++++- frontend/assets/css/tailwind.css | 18 ++ frontend/components/SidebarAdminMenu.vue | 28 -- .../components/admin/AddUserWarrenDialog.vue | 248 ++++++++++++++++++ .../components/admin/CreateUserDialog.vue | 7 +- .../components/admin/DeleteUserDialog.vue | 2 +- frontend/components/admin/EditUserDialog.vue | 232 +++++++++------- frontend/components/admin/UserListing.vue | 12 +- .../components/admin/UserWarrenListing.vue | 113 ++++++++ frontend/components/admin/WarrenListing.vue | 26 ++ .../components/ui/accordion/Accordion.vue | 19 ++ .../ui/accordion/AccordionContent.vue | 22 ++ .../components/ui/accordion/AccordionItem.vue | 22 ++ .../ui/accordion/AccordionTrigger.vue | 37 +++ frontend/components/ui/accordion/index.ts | 4 + frontend/components/ui/combobox/Combobox.vue | 17 ++ .../components/ui/combobox/ComboboxAnchor.vue | 23 ++ .../components/ui/combobox/ComboboxEmpty.vue | 21 ++ .../components/ui/combobox/ComboboxGroup.vue | 27 ++ .../components/ui/combobox/ComboboxInput.vue | 41 +++ .../components/ui/combobox/ComboboxItem.vue | 24 ++ .../ui/combobox/ComboboxItemIndicator.vue | 23 ++ .../components/ui/combobox/ComboboxList.vue | 29 ++ .../ui/combobox/ComboboxSeparator.vue | 21 ++ .../ui/combobox/ComboboxTrigger.vue | 24 ++ .../ui/combobox/ComboboxViewport.vue | 23 ++ frontend/components/ui/combobox/index.ts | 12 + frontend/components/ui/popover/Popover.vue | 18 ++ .../components/ui/popover/PopoverAnchor.vue | 15 ++ .../components/ui/popover/PopoverContent.vue | 46 ++++ .../components/ui/popover/PopoverTrigger.vue | 14 + frontend/components/ui/popover/index.ts | 4 + frontend/components/ui/tabs/Tabs.vue | 23 ++ frontend/components/ui/tabs/TabsContent.vue | 20 ++ frontend/components/ui/tabs/TabsList.vue | 23 ++ frontend/components/ui/tabs/TabsTrigger.vue | 25 ++ frontend/components/ui/tabs/index.ts | 4 + frontend/layouts/default.vue | 13 +- frontend/lib/api/admin/createUserWarren.ts | 48 ++++ frontend/lib/api/admin/deleteUser.ts | 2 +- frontend/lib/api/admin/deleteUserWarren.ts | 52 ++++ frontend/lib/api/admin/editUser.ts | 9 +- frontend/lib/api/admin/editUserWarren.ts | 30 +++ frontend/lib/api/admin/fetchAll.ts | 6 +- frontend/lib/api/auth/getSession.ts | 6 +- frontend/lib/api/warrens.ts | 19 +- frontend/lib/schemas/admin.ts | 9 + frontend/middleware/authenticated.ts | 29 +- frontend/middleware/is-admin.ts | 2 +- frontend/pages/admin/index.vue | 53 +++- frontend/pages/admin/stats.vue | 10 - frontend/pages/admin/users.vue | 22 -- frontend/pages/admin/warrens.vue | 10 - frontend/pages/login.vue | 2 +- frontend/pages/register.vue | 2 +- frontend/pages/warrens/index.vue | 4 +- frontend/shared/types/admin.ts | 4 +- frontend/shared/types/warrens.ts | 8 +- frontend/stores/admin.ts | 6 +- frontend/stores/index.ts | 4 +- frontend/utils/warrens.ts | 31 +++ 86 files changed, 2197 insertions(+), 300 deletions(-) create mode 100644 backend/migrations/20250721171502_user_warrens_drop_some_permissions.sql create mode 100644 backend/src/lib/domain/warren/models/user_warren/requests/create_user_warren.rs create mode 100644 backend/src/lib/domain/warren/models/user_warren/requests/delete_user_warren.rs create mode 100644 backend/src/lib/domain/warren/models/user_warren/requests/edit_user_warren.rs create mode 100644 backend/src/lib/inbound/http/handlers/admin/create_user_warren.rs create mode 100644 backend/src/lib/inbound/http/handlers/admin/delete_user_warren.rs create mode 100644 backend/src/lib/inbound/http/handlers/admin/edit_user_warren.rs create mode 100644 frontend/components/admin/AddUserWarrenDialog.vue create mode 100644 frontend/components/admin/UserWarrenListing.vue create mode 100644 frontend/components/admin/WarrenListing.vue create mode 100644 frontend/components/ui/accordion/Accordion.vue create mode 100644 frontend/components/ui/accordion/AccordionContent.vue create mode 100644 frontend/components/ui/accordion/AccordionItem.vue create mode 100644 frontend/components/ui/accordion/AccordionTrigger.vue create mode 100644 frontend/components/ui/accordion/index.ts create mode 100644 frontend/components/ui/combobox/Combobox.vue create mode 100644 frontend/components/ui/combobox/ComboboxAnchor.vue create mode 100644 frontend/components/ui/combobox/ComboboxEmpty.vue create mode 100644 frontend/components/ui/combobox/ComboboxGroup.vue create mode 100644 frontend/components/ui/combobox/ComboboxInput.vue create mode 100644 frontend/components/ui/combobox/ComboboxItem.vue create mode 100644 frontend/components/ui/combobox/ComboboxItemIndicator.vue create mode 100644 frontend/components/ui/combobox/ComboboxList.vue create mode 100644 frontend/components/ui/combobox/ComboboxSeparator.vue create mode 100644 frontend/components/ui/combobox/ComboboxTrigger.vue create mode 100644 frontend/components/ui/combobox/ComboboxViewport.vue create mode 100644 frontend/components/ui/combobox/index.ts create mode 100644 frontend/components/ui/popover/Popover.vue create mode 100644 frontend/components/ui/popover/PopoverAnchor.vue create mode 100644 frontend/components/ui/popover/PopoverContent.vue create mode 100644 frontend/components/ui/popover/PopoverTrigger.vue create mode 100644 frontend/components/ui/popover/index.ts create mode 100644 frontend/components/ui/tabs/Tabs.vue create mode 100644 frontend/components/ui/tabs/TabsContent.vue create mode 100644 frontend/components/ui/tabs/TabsList.vue create mode 100644 frontend/components/ui/tabs/TabsTrigger.vue create mode 100644 frontend/components/ui/tabs/index.ts create mode 100644 frontend/lib/api/admin/createUserWarren.ts create mode 100644 frontend/lib/api/admin/deleteUserWarren.ts create mode 100644 frontend/lib/api/admin/editUserWarren.ts delete mode 100644 frontend/pages/admin/stats.vue delete mode 100644 frontend/pages/admin/users.vue delete mode 100644 frontend/pages/admin/warrens.vue create mode 100644 frontend/utils/warrens.ts diff --git a/backend/migrations/20250721171502_user_warrens_drop_some_permissions.sql b/backend/migrations/20250721171502_user_warrens_drop_some_permissions.sql new file mode 100644 index 0000000..5299300 --- /dev/null +++ b/backend/migrations/20250721171502_user_warrens_drop_some_permissions.sql @@ -0,0 +1 @@ +ALTER TABLE user_warrens DROP COLUMN can_create_children, DROP COLUMN can_delete_warren; diff --git a/backend/src/lib/domain/warren/models/user_warren/mod.rs b/backend/src/lib/domain/warren/models/user_warren/mod.rs index c67bad0..b255cce 100644 --- a/backend/src/lib/domain/warren/models/user_warren/mod.rs +++ b/backend/src/lib/domain/warren/models/user_warren/mod.rs @@ -6,34 +6,28 @@ use uuid::Uuid; pub struct UserWarren { user_id: Uuid, warren_id: Uuid, - can_create_children: bool, can_list_files: bool, can_read_files: bool, can_modify_files: bool, can_delete_files: bool, - can_delete_warren: bool, } impl UserWarren { pub fn new( user_id: Uuid, warren_id: Uuid, - can_create_children: bool, can_list_files: bool, can_read_files: bool, can_modify_files: bool, can_delete_files: bool, - can_delete_warren: bool, ) -> Self { Self { user_id, warren_id, - can_create_children, can_list_files, can_read_files, can_modify_files, can_delete_files, - can_delete_warren, } } @@ -48,10 +42,6 @@ impl UserWarren { self.warren_id } - pub fn can_create_children(&self) -> bool { - self.can_create_children - } - pub fn can_list_files(&self) -> bool { self.can_list_files } @@ -67,8 +57,4 @@ impl UserWarren { pub fn can_delete_files(&self) -> bool { self.can_delete_files } - - pub fn can_delete_warren(&self) -> bool { - self.can_delete_warren - } } diff --git a/backend/src/lib/domain/warren/models/user_warren/requests/create_user_warren.rs b/backend/src/lib/domain/warren/models/user_warren/requests/create_user_warren.rs new file mode 100644 index 0000000..72c5dde --- /dev/null +++ b/backend/src/lib/domain/warren/models/user_warren/requests/create_user_warren.rs @@ -0,0 +1,25 @@ +use thiserror::Error; + +use crate::domain::warren::models::user_warren::UserWarren; + +/// A request to create a new user warren (admin only) +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CreateUserWarrenRequest { + user_warren: UserWarren, +} + +impl CreateUserWarrenRequest { + pub fn new(user_warren: UserWarren) -> Self { + Self { user_warren } + } + + pub fn user_warren(&self) -> &UserWarren { + &self.user_warren + } +} + +#[derive(Debug, Error)] +pub enum CreateUserWarrenError { + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/models/user_warren/requests/delete_user_warren.rs b/backend/src/lib/domain/warren/models/user_warren/requests/delete_user_warren.rs new file mode 100644 index 0000000..348aa84 --- /dev/null +++ b/backend/src/lib/domain/warren/models/user_warren/requests/delete_user_warren.rs @@ -0,0 +1,31 @@ +use thiserror::Error; +use uuid::Uuid; + +/// A request to delete an existing user warren (admin only) +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeleteUserWarrenRequest { + user_id: Uuid, + warren_id: Uuid, +} + +impl DeleteUserWarrenRequest { + pub fn new(user_id: Uuid, warren_id: Uuid) -> Self { + Self { user_id, warren_id } + } + + pub fn user_id(&self) -> &Uuid { + &self.user_id + } + + pub fn warren_id(&self) -> &Uuid { + &self.warren_id + } +} + +#[derive(Debug, Error)] +pub enum DeleteUserWarrenError { + #[error("This user warren does not exist")] + NotFound, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/models/user_warren/requests/edit_user_warren.rs b/backend/src/lib/domain/warren/models/user_warren/requests/edit_user_warren.rs new file mode 100644 index 0000000..94a5165 --- /dev/null +++ b/backend/src/lib/domain/warren/models/user_warren/requests/edit_user_warren.rs @@ -0,0 +1,27 @@ +use thiserror::Error; + +use crate::domain::warren::models::user_warren::UserWarren; + +/// A request to edit a new user warren (admin only) +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EditUserWarrenRequest { + user_warren: UserWarren, +} + +impl EditUserWarrenRequest { + pub fn new(user_warren: UserWarren) -> Self { + Self { user_warren } + } + + pub fn user_warren(&self) -> &UserWarren { + &self.user_warren + } +} + +#[derive(Debug, Error)] +pub enum EditUserWarrenError { + #[error("This user warren does not exist")] + NotFound, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/models/user_warren/requests/fetch_user_warrens.rs b/backend/src/lib/domain/warren/models/user_warren/requests/fetch_user_warrens.rs index f7bedb1..fed0117 100644 --- a/backend/src/lib/domain/warren/models/user_warren/requests/fetch_user_warrens.rs +++ b/backend/src/lib/domain/warren/models/user_warren/requests/fetch_user_warrens.rs @@ -21,24 +21,7 @@ impl FetchUserWarrensRequest { #[derive(Debug, Error)] pub enum FetchUserWarrensError { #[error(transparent)] - Unknown(#[from] anyhow::Error), -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ListWarrensRequest {} - -impl ListWarrensRequest { - pub fn new() -> Self { - Self {} - } -} - -#[derive(Debug, Error)] -pub enum ListWarrensError { - #[error(transparent)] - FetchUserWarrenIds(#[from] FetchUserWarrensError), - #[error(transparent)] - ListWarrens(#[from] FetchWarrensError), + FetchWarrens(#[from] FetchWarrensError), #[error(transparent)] Unknown(#[from] anyhow::Error), } diff --git a/backend/src/lib/domain/warren/models/user_warren/requests/mod.rs b/backend/src/lib/domain/warren/models/user_warren/requests/mod.rs index 6bb3363..82d0e9f 100644 --- a/backend/src/lib/domain/warren/models/user_warren/requests/mod.rs +++ b/backend/src/lib/domain/warren/models/user_warren/requests/mod.rs @@ -1,6 +1,13 @@ +mod create_user_warren; +mod delete_user_warren; +mod edit_user_warren; mod fetch_user_warren; mod fetch_user_warrens; mod list_user_warrens; + +pub use create_user_warren::*; +pub use delete_user_warren::*; +pub use edit_user_warren::*; pub use fetch_user_warren::*; pub use fetch_user_warrens::*; pub use list_user_warrens::*; diff --git a/backend/src/lib/domain/warren/models/warren/requests.rs b/backend/src/lib/domain/warren/models/warren/requests.rs index 2da449e..0c7276b 100644 --- a/backend/src/lib/domain/warren/models/warren/requests.rs +++ b/backend/src/lib/domain/warren/models/warren/requests.rs @@ -502,3 +502,18 @@ pub enum RenameWarrenEntryError { #[error(transparent)] Unknown(#[from] anyhow::Error), } + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ListWarrensRequest {} + +impl ListWarrensRequest { + pub fn new() -> Self { + Self {} + } +} + +#[derive(Debug, Error)] +pub enum ListWarrensError { + #[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 5a5dca2..8dde0ea 100644 --- a/backend/src/lib/domain/warren/ports/metrics.rs +++ b/backend/src/lib/domain/warren/ports/metrics.rs @@ -1,10 +1,13 @@ pub trait WarrenMetrics: Clone + Send + Sync + 'static { - fn record_warren_list_success(&self) -> impl Future + Send; - fn record_warren_list_failure(&self) -> impl Future + Send; - fn record_warren_fetch_success(&self) -> impl Future + Send; fn record_warren_fetch_failure(&self) -> impl Future + Send; + fn record_warrens_fetch_success(&self) -> impl Future + Send; + fn record_warrens_fetch_failure(&self) -> impl Future + Send; + + fn record_warren_list_success(&self) -> impl Future + Send; + fn record_warren_list_failure(&self) -> impl Future + Send; + fn record_list_warren_files_success(&self) -> impl Future + Send; fn record_list_warren_files_failure(&self) -> impl Future + Send; @@ -77,6 +80,15 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static { fn record_auth_session_fetch_success(&self) -> impl Future + Send; fn record_auth_session_fetch_failure(&self) -> impl Future + Send; + fn record_auth_user_warren_creation_success(&self) -> impl Future + Send; + fn record_auth_user_warren_creation_failure(&self) -> impl Future + Send; + + fn record_auth_user_warren_edit_success(&self) -> impl Future + Send; + fn record_auth_user_warren_edit_failure(&self) -> impl Future + Send; + + fn record_auth_user_warren_deletion_success(&self) -> impl Future + Send; + fn record_auth_user_warren_deletion_failure(&self) -> impl Future + Send; + fn record_auth_fetch_user_warrens_success(&self) -> impl Future + Send; fn record_auth_fetch_user_warrens_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 71d4a16..689c263 100644 --- a/backend/src/lib/domain/warren/ports/mod.rs +++ b/backend/src/lib/domain/warren/ports/mod.rs @@ -28,7 +28,9 @@ use super::models::{ user_warren::{ UserWarren, requests::{ - FetchUserWarrensError, FetchUserWarrensRequest, ListWarrensError, ListWarrensRequest, + CreateUserWarrenError, CreateUserWarrenRequest, DeleteUserWarrenError, + DeleteUserWarrenRequest, EditUserWarrenError, EditUserWarrenRequest, + FetchUserWarrensError, FetchUserWarrensRequest, }, }, warren::{ @@ -36,18 +38,21 @@ use super::models::{ DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, DeleteWarrenFileError, DeleteWarrenFileRequest, DeleteWarrenFileResponse, FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, ListWarrenFilesError, - ListWarrenFilesRequest, ListWarrenFilesResponse, RenameWarrenEntryError, - RenameWarrenEntryRequest, RenameWarrenEntryResponse, UploadWarrenFilesError, - UploadWarrenFilesRequest, UploadWarrenFilesResponse, Warren, + ListWarrenFilesRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest, + RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse, + UploadWarrenFilesError, UploadWarrenFilesRequest, UploadWarrenFilesResponse, Warren, }, }; pub trait WarrenService: Clone + Send + Sync + 'static { - fn list_warrens( + fn fetch_warrens( &self, request: FetchWarrensRequest, ) -> impl Future, FetchWarrensError>> + Send; - + fn list_warrens( + &self, + request: ListWarrensRequest, + ) -> impl Future, ListWarrensError>> + Send; fn fetch_warren( &self, request: FetchWarrenRequest, @@ -164,9 +169,25 @@ pub trait AuthService: Clone + Send + Sync + 'static { fn list_warrens( &self, - request: AuthRequest, + request: AuthRequest<()>, warren_service: &WS, - ) -> impl Future, AuthError>> + Send; + ) -> impl Future, AuthError>> + Send; + + /// An action that creates an association between a user and a warren (MUST REQUIRE ADMIN PRIVILEGES) + fn create_user_warren( + &self, + request: AuthRequest, + ) -> impl Future>> + Send; + /// An action that edits an association between a user and a warren (MUST REQUIRE ADMIN PRIVILEGES) + fn edit_user_warren( + &self, + request: AuthRequest, + ) -> impl Future>> + Send; + /// An action that deletes an association between a user and a warren (MUST REQUIRE ADMIN PRIVILEGES) + fn delete_user_warren( + &self, + request: AuthRequest, + ) -> impl Future>> + Send; fn fetch_user_warrens( &self, diff --git a/backend/src/lib/domain/warren/ports/notifier.rs b/backend/src/lib/domain/warren/ports/notifier.rs index 696ace8..96780a4 100644 --- a/backend/src/lib/domain/warren/ports/notifier.rs +++ b/backend/src/lib/domain/warren/ports/notifier.rs @@ -12,8 +12,9 @@ use crate::domain::warren::models::{ }; pub trait WarrenNotifier: Clone + Send + Sync + 'static { - fn warrens_listed(&self, warrens: &Vec) -> 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; fn warren_files_listed( &self, response: &ListWarrenFilesResponse, @@ -93,6 +94,21 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static { response: &FetchAuthSessionResponse, ) -> impl Future + Send; + fn auth_user_warren_created( + &self, + creator: &User, + user_warren: &UserWarren, + ) -> impl Future + Send; + fn auth_user_warren_edited( + &self, + editor: &User, + user_warren: &UserWarren, + ) -> impl Future + Send; + fn auth_user_warren_deleted( + &self, + deleter: &User, + user_warren: &UserWarren, + ) -> impl Future + Send; fn auth_user_warrens_listed( &self, user: &User, diff --git a/backend/src/lib/domain/warren/ports/repository.rs b/backend/src/lib/domain/warren/ports/repository.rs index 0777f4f..ab17e29 100644 --- a/backend/src/lib/domain/warren/ports/repository.rs +++ b/backend/src/lib/domain/warren/ports/repository.rs @@ -20,23 +20,31 @@ use crate::domain::warren::models::{ user_warren::{ UserWarren, requests::{ + CreateUserWarrenError, CreateUserWarrenRequest, DeleteUserWarrenError, + DeleteUserWarrenRequest, EditUserWarrenError, EditUserWarrenRequest, FetchUserWarrenError, FetchUserWarrenRequest, FetchUserWarrensError, FetchUserWarrensRequest, ListUserWarrensError, ListUserWarrensRequest, }, }, warren::{ - FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, Warren, + FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, + ListWarrensError, ListWarrensRequest, Warren, }, }; use super::WarrenService; pub trait WarrenRepository: Clone + Send + Sync + 'static { - fn list_warrens( + fn fetch_warrens( &self, request: FetchWarrensRequest, ) -> impl Future, FetchWarrensError>> + Send; + fn list_warrens( + &self, + request: ListWarrensRequest, + ) -> impl Future, ListWarrensError>> + Send; + fn fetch_warren( &self, request: FetchWarrenRequest, @@ -104,6 +112,19 @@ pub trait AuthRepository: Clone + Send + Sync + 'static { request: VerifyUserPasswordRequest, ) -> impl Future> + Send; + fn create_user_warren( + &self, + request: CreateUserWarrenRequest, + ) -> impl Future> + Send; + fn edit_user_warren( + &self, + request: EditUserWarrenRequest, + ) -> impl Future> + Send; + fn delete_user_warren( + &self, + request: DeleteUserWarrenRequest, + ) -> impl Future> + Send; + fn list_user_warrens( &self, request: ListUserWarrensRequest, diff --git a/backend/src/lib/domain/warren/service/auth.rs b/backend/src/lib/domain/warren/service/auth.rs index b172b0a..2bf2c70 100644 --- a/backend/src/lib/domain/warren/service/auth.rs +++ b/backend/src/lib/domain/warren/service/auth.rs @@ -19,8 +19,9 @@ use crate::{ user_warren::{ UserWarren, requests::{ + CreateUserWarrenError, CreateUserWarrenRequest, DeleteUserWarrenError, + DeleteUserWarrenRequest, EditUserWarrenError, EditUserWarrenRequest, FetchUserWarrenRequest, FetchUserWarrensError, FetchUserWarrensRequest, - ListWarrensError, ListWarrensRequest, }, }, warren::{ @@ -343,6 +344,101 @@ where result.map_err(AuthError::Custom) } + async fn create_user_warren( + &self, + request: AuthRequest, + ) -> Result> { + let (session, request) = request.unpack(); + + let session_response = self + .repository + .fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone())) + .await?; + + if !session_response.user().admin() { + return Err(AuthError::InsufficientPermissions); + } + + let result = self.repository.create_user_warren(request).await; + + if let Ok(user_warren) = result.as_ref() { + self.metrics + .record_auth_user_warren_creation_success() + .await; + self.notifier + .auth_user_warren_created(session_response.user(), user_warren) + .await; + } else { + self.metrics + .record_auth_user_warren_creation_failure() + .await; + } + + result.map_err(AuthError::Custom) + } + + async fn edit_user_warren( + &self, + request: AuthRequest, + ) -> Result> { + let (session, request) = request.unpack(); + + let session_response = self + .repository + .fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone())) + .await?; + + if !session_response.user().admin() { + return Err(AuthError::InsufficientPermissions); + } + + let result = self.repository.edit_user_warren(request).await; + + if let Ok(user_warren) = result.as_ref() { + self.metrics.record_auth_user_warren_edit_success().await; + self.notifier + .auth_user_warren_edited(session_response.user(), user_warren) + .await; + } else { + self.metrics.record_auth_user_warren_edit_failure().await; + } + + result.map_err(AuthError::Custom) + } + + async fn delete_user_warren( + &self, + request: AuthRequest, + ) -> Result> { + let (session, request) = request.unpack(); + + let session_response = self + .repository + .fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone())) + .await?; + + if !session_response.user().admin() { + return Err(AuthError::InsufficientPermissions); + } + + let result = self.repository.delete_user_warren(request).await; + + if let Ok(user_warren) = result.as_ref() { + self.metrics + .record_auth_user_warren_deletion_success() + .await; + self.notifier + .auth_user_warren_deleted(session_response.user(), user_warren) + .await; + } else { + self.metrics + .record_auth_user_warren_deletion_failure() + .await; + } + + result.map_err(AuthError::Custom) + } + async fn fetch_user_warrens( &self, request: FetchUserWarrensRequest, @@ -364,9 +460,9 @@ where async fn list_warrens( &self, - request: AuthRequest, + request: AuthRequest<()>, warren_service: &WS, - ) -> Result, AuthError> { + ) -> Result, AuthError> { let (session, _) = request.unpack(); let session_response = self @@ -380,7 +476,7 @@ where .map_err(|e| AuthError::Custom(e.into()))?; let result = warren_service - .list_warrens(FetchWarrensRequest::new( + .fetch_warrens(FetchWarrensRequest::new( ids.into_iter().map(|uw| uw.into_warren_id()).collect(), )) .await; diff --git a/backend/src/lib/domain/warren/service/warren.rs b/backend/src/lib/domain/warren/service/warren.rs index 54f8e0c..90093bf 100644 --- a/backend/src/lib/domain/warren/service/warren.rs +++ b/backend/src/lib/domain/warren/service/warren.rs @@ -1,8 +1,9 @@ use crate::domain::warren::{ models::warren::{ CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse, - FetchWarrensError, FetchWarrensRequest, ListWarrenFilesResponse, RenameWarrenEntryError, - RenameWarrenEntryRequest, RenameWarrenEntryResponse, UploadWarrenFilesResponse, + FetchWarrensError, FetchWarrensRequest, ListWarrenFilesResponse, ListWarrensError, + ListWarrensRequest, RenameWarrenEntryError, RenameWarrenEntryRequest, + RenameWarrenEntryResponse, UploadWarrenFilesResponse, }, ports::FileSystemService, }; @@ -55,10 +56,26 @@ where N: WarrenNotifier, FSS: FileSystemService, { - async fn list_warrens( + async fn fetch_warrens( &self, request: FetchWarrensRequest, ) -> Result, FetchWarrensError> { + let result = self.repository.fetch_warrens(request).await; + + if let Ok(warren) = result.as_ref() { + self.metrics.record_warrens_fetch_success().await; + self.notifier.warrens_fetched(warren).await; + } else { + self.metrics.record_warrens_fetch_failure().await; + } + + result + } + + async fn list_warrens( + &self, + request: ListWarrensRequest, + ) -> Result, ListWarrensError> { let result = self.repository.list_warrens(request).await; if let Ok(warren) = result.as_ref() { diff --git a/backend/src/lib/inbound/http/errors.rs b/backend/src/lib/inbound/http/errors.rs index 29e2d5c..4626ad9 100644 --- a/backend/src/lib/inbound/http/errors.rs +++ b/backend/src/lib/inbound/http/errors.rs @@ -164,10 +164,10 @@ impl From for ApiError { fn from(value: FetchAuthSessionError) -> Self { match value { FetchAuthSessionError::NotFound => { - Self::BadRequest("This session does not exist".to_string()) + Self::Unauthorized("This session does not exist".to_string()) } FetchAuthSessionError::Expired => { - Self::BadRequest("This session has expired".to_string()) + Self::Unauthorized("This session has expired".to_string()) } FetchAuthSessionError::Unknown(e) => Self::InternalServerError(e.to_string()), } diff --git a/backend/src/lib/inbound/http/handlers/admin/create_user_warren.rs b/backend/src/lib/inbound/http/handlers/admin/create_user_warren.rs new file mode 100644 index 0000000..b0c47b9 --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/admin/create_user_warren.rs @@ -0,0 +1,57 @@ +use axum::{Json, extract::State, http::StatusCode}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{ + domain::warren::{ + models::{ + auth_session::AuthRequest, + user_warren::{UserWarren, requests::CreateUserWarrenRequest}, + }, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::{UserWarrenData, extractors::SessionIdHeader}, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(super) struct CreateUserWarrenHttpRequestBody { + user_id: Uuid, + warren_id: Uuid, + can_list_files: bool, + can_read_files: bool, + can_modify_files: bool, + can_delete_files: bool, +} + +impl CreateUserWarrenHttpRequestBody { + fn into_domain(self) -> CreateUserWarrenRequest { + CreateUserWarrenRequest::new(UserWarren::new( + self.user_id, + self.warren_id, + self.can_list_files, + self.can_read_files, + self.can_modify_files, + self.can_delete_files, + )) + } +} + +pub async fn create_user_warren( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, + Json(request): Json, +) -> Result, ApiError> { + let domain_request = request.into_domain(); + + state + .auth_service + .create_user_warren(AuthRequest::new(session, domain_request)) + .await + .map(|user_warren| ApiSuccess::new(StatusCode::OK, user_warren.into())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/admin/delete_user_warren.rs b/backend/src/lib/inbound/http/handlers/admin/delete_user_warren.rs new file mode 100644 index 0000000..7cec3b3 --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/admin/delete_user_warren.rs @@ -0,0 +1,43 @@ +use axum::{Json, extract::State, http::StatusCode}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{ + domain::warren::{ + models::{auth_session::AuthRequest, user_warren::requests::DeleteUserWarrenRequest}, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::{UserWarrenData, extractors::SessionIdHeader}, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(super) struct DeleteUserWarrenHttpRequestBody { + user_id: Uuid, + warren_id: Uuid, +} + +impl DeleteUserWarrenHttpRequestBody { + fn into_domain(self) -> DeleteUserWarrenRequest { + DeleteUserWarrenRequest::new(self.user_id, self.warren_id) + } +} + +pub async fn delete_user_warren( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, + Json(request): Json, +) -> Result, ApiError> { + let domain_request = request.into_domain(); + + state + .auth_service + .delete_user_warren(AuthRequest::new(session, domain_request)) + .await + .map(|user_warren| ApiSuccess::new(StatusCode::OK, user_warren.into())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/admin/edit_user_warren.rs b/backend/src/lib/inbound/http/handlers/admin/edit_user_warren.rs new file mode 100644 index 0000000..bd5e8bb --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/admin/edit_user_warren.rs @@ -0,0 +1,57 @@ +use axum::{Json, extract::State, http::StatusCode}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{ + domain::warren::{ + models::{ + auth_session::AuthRequest, + user_warren::{UserWarren, requests::EditUserWarrenRequest}, + }, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::{UserWarrenData, extractors::SessionIdHeader}, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(super) struct EditUserWarrenHttpRequestBody { + user_id: Uuid, + warren_id: Uuid, + can_list_files: bool, + can_read_files: bool, + can_modify_files: bool, + can_delete_files: bool, +} + +impl EditUserWarrenHttpRequestBody { + fn into_domain(self) -> EditUserWarrenRequest { + EditUserWarrenRequest::new(UserWarren::new( + self.user_id, + self.warren_id, + self.can_list_files, + self.can_read_files, + self.can_modify_files, + self.can_delete_files, + )) + } +} + +pub async fn edit_user_warren( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, + Json(request): Json, +) -> Result, ApiError> { + let domain_request = request.into_domain(); + + state + .auth_service + .edit_user_warren(AuthRequest::new(session, domain_request)) + .await + .map(|user_warren| ApiSuccess::new(StatusCode::OK, user_warren.into())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/admin/list_all_users_and_warrens.rs b/backend/src/lib/inbound/http/handlers/admin/list_all_users_and_warrens.rs index 97dea8d..c6ee476 100644 --- a/backend/src/lib/inbound/http/handlers/admin/list_all_users_and_warrens.rs +++ b/backend/src/lib/inbound/http/handlers/admin/list_all_users_and_warrens.rs @@ -11,7 +11,7 @@ use crate::{ }, inbound::http::{ AppState, - handlers::{UserData, UserWarrenData, WarrenData, extractors::SessionIdHeader}, + handlers::{AdminWarrenData, UserData, UserWarrenData, extractors::SessionIdHeader}, responses::{ApiError, ApiSuccess}, }, }; @@ -21,7 +21,7 @@ use crate::{ pub(super) struct ListAllUsersAndWarrensHttpResponseBody { users: Vec, user_warrens: Vec, - warrens: Vec, + warrens: Vec, } impl From for ListAllUsersAndWarrensHttpResponseBody { diff --git a/backend/src/lib/inbound/http/handlers/admin/mod.rs b/backend/src/lib/inbound/http/handlers/admin/mod.rs index 6a8ff26..e2c2e5b 100644 --- a/backend/src/lib/inbound/http/handlers/admin/mod.rs +++ b/backend/src/lib/inbound/http/handlers/admin/mod.rs @@ -1,12 +1,18 @@ mod create_user; +mod create_user_warren; mod delete_user; +mod delete_user_warren; mod edit_user; +mod edit_user_warren; mod list_all_users_and_warrens; mod list_users; use create_user::create_user; +use create_user_warren::create_user_warren; use delete_user::delete_user; +use delete_user_warren::delete_user_warren; use edit_user::edit_user; +use edit_user_warren::edit_user_warren; use list_all_users_and_warrens::list_all_users_and_warrens; use list_users::list_users; @@ -27,4 +33,7 @@ pub fn routes() -> Router> .route("/users", post(create_user)) .route("/users", patch(edit_user)) .route("/users", delete(delete_user)) + .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/inbound/http/handlers/auth/fetch_session.rs b/backend/src/lib/inbound/http/handlers/auth/fetch_session.rs index e113883..b1efabc 100644 --- a/backend/src/lib/inbound/http/handlers/auth/fetch_session.rs +++ b/backend/src/lib/inbound/http/handlers/auth/fetch_session.rs @@ -35,6 +35,7 @@ impl From for FetchSessionResponseBody { } } +/// TODO: Remove the `WarrenAuth ` bit and let the service handle that pub async fn fetch_session( State(state): State>, headers: HeaderMap, @@ -43,7 +44,7 @@ pub async fn fetch_session( h.to_str() .map(|h| AuthSessionId::new(&h["WarrenAuth ".len()..])) }) else { - return Err(ApiError::BadRequest( + return Err(ApiError::Unauthorized( "No authorization header set".to_string(), )); }; diff --git a/backend/src/lib/inbound/http/handlers/mod.rs b/backend/src/lib/inbound/http/handlers/mod.rs index 1f15873..2cab43e 100644 --- a/backend/src/lib/inbound/http/handlers/mod.rs +++ b/backend/src/lib/inbound/http/handlers/mod.rs @@ -35,12 +35,10 @@ impl From for UserData { pub(super) struct UserWarrenData { user_id: Uuid, warren_id: Uuid, - can_create_children: bool, can_list_files: bool, can_read_files: bool, can_modify_files: bool, can_delete_files: bool, - can_delete_warren: bool, } impl From for UserWarrenData { @@ -48,12 +46,10 @@ impl From for UserWarrenData { Self { user_id: *value.user_id(), warren_id: *value.warren_id(), - can_create_children: value.can_create_children(), can_list_files: value.can_list_files(), can_read_files: value.can_read_files(), can_modify_files: value.can_modify_files(), can_delete_files: value.can_delete_files(), - can_delete_warren: value.can_delete_warren(), } } } @@ -74,3 +70,22 @@ impl From for WarrenData { } } } + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +/// A warren with admin data that can be safely sent to the client +pub(super) struct AdminWarrenData { + id: Uuid, + name: String, + path: String, +} + +impl From for AdminWarrenData { + fn from(value: Warren) -> Self { + Self { + id: *value.id(), + name: value.name().to_string(), + path: value.path().to_string(), + } + } +} diff --git a/backend/src/lib/inbound/http/handlers/warrens/list_warrens.rs b/backend/src/lib/inbound/http/handlers/warrens/list_warrens.rs index 1140a5c..e020dd3 100644 --- a/backend/src/lib/inbound/http/handlers/warrens/list_warrens.rs +++ b/backend/src/lib/inbound/http/handlers/warrens/list_warrens.rs @@ -4,9 +4,7 @@ use uuid::Uuid; use crate::{ domain::warren::{ - models::{ - auth_session::AuthRequest, user_warren::requests::ListWarrensRequest, warren::Warren, - }, + models::{auth_session::AuthRequest, warren::Warren}, ports::{AuthService, WarrenService}, }, inbound::http::{ @@ -48,7 +46,7 @@ pub async fn list_warrens( State(state): State>, SessionIdHeader(session): SessionIdHeader, ) -> Result, ApiError> { - let domain_request = AuthRequest::new(session, ListWarrensRequest::new()); + let domain_request = AuthRequest::new(session, ()); state .auth_service diff --git a/backend/src/lib/outbound/metrics_debug_logger.rs b/backend/src/lib/outbound/metrics_debug_logger.rs index a08eb2a..b325487 100644 --- a/backend/src/lib/outbound/metrics_debug_logger.rs +++ b/backend/src/lib/outbound/metrics_debug_logger.rs @@ -13,7 +13,6 @@ impl WarrenMetrics for MetricsDebugLogger { async fn record_warren_list_success(&self) { tracing::debug!("[Metrics] Warren list succeeded"); } - async fn record_warren_list_failure(&self) { tracing::debug!("[Metrics] Warren list failed"); } @@ -21,11 +20,17 @@ impl WarrenMetrics for MetricsDebugLogger { async fn record_warren_fetch_success(&self) { tracing::debug!("[Metrics] Warren fetch succeeded"); } - async fn record_warren_fetch_failure(&self) { tracing::debug!("[Metrics] Warren fetch failed"); } + async fn record_warrens_fetch_success(&self) { + tracing::debug!("[Metrics] Fetch warrens succeeded"); + } + async fn record_warrens_fetch_failure(&self) { + tracing::debug!("[Metrics] Fetch warrens failed"); + } + async fn record_list_warren_files_success(&self) { tracing::debug!("[Metrics] Warren list files succeeded"); } @@ -187,6 +192,27 @@ impl AuthMetrics for MetricsDebugLogger { tracing::debug!("[Metrics] Auth session fetch failed"); } + async fn record_auth_user_warren_creation_success(&self) { + tracing::debug!("[Metrics] User warren creation succeeded"); + } + async fn record_auth_user_warren_creation_failure(&self) { + tracing::debug!("[Metrics] User warren creation failed"); + } + + async fn record_auth_user_warren_edit_success(&self) { + tracing::debug!("[Metrics] User warren edit succeeded"); + } + async fn record_auth_user_warren_edit_failure(&self) { + tracing::debug!("[Metrics] User warren edit failed"); + } + + async fn record_auth_user_warren_deletion_success(&self) { + tracing::debug!("[Metrics] User warren deletion succeeded"); + } + async fn record_auth_user_warren_deletion_failure(&self) { + tracing::debug!("[Metrics] User warren deletion failed"); + } + async fn record_auth_fetch_user_warren_list_success(&self) { tracing::debug!("[Metrics] Auth warren list succeeded"); } diff --git a/backend/src/lib/outbound/notifier_debug_logger.rs b/backend/src/lib/outbound/notifier_debug_logger.rs index 10a5909..638af13 100644 --- a/backend/src/lib/outbound/notifier_debug_logger.rs +++ b/backend/src/lib/outbound/notifier_debug_logger.rs @@ -24,6 +24,10 @@ impl NotifierDebugLogger { } impl WarrenNotifier for NotifierDebugLogger { + async fn warrens_fetched(&self, warrens: &Vec) { + tracing::debug!("[Notifier] Fetched {} warren(s)", warrens.len()); + } + async fn warrens_listed(&self, warrens: &Vec) { tracing::debug!("[Notifier] Listed {} warren(s)", warrens.len()); } @@ -182,6 +186,33 @@ impl AuthNotifier for NotifierDebugLogger { ); } + async fn auth_user_warren_created(&self, creator: &User, user_warren: &UserWarren) { + tracing::debug!( + "[Notifier] Admin user {} added user {} to warren {}", + creator.id(), + user_warren.user_id(), + user_warren.warren_id(), + ); + } + + async fn auth_user_warren_edited(&self, editor: &User, user_warren: &UserWarren) { + tracing::debug!( + "[Notifier] Admin user {} edited the access of user {} to warren {}", + editor.id(), + user_warren.user_id(), + user_warren.warren_id(), + ); + } + + async fn auth_user_warren_deleted(&self, deleter: &User, user_warren: &UserWarren) { + tracing::debug!( + "[Notifier] Admin user {} added removed {} from warren {}", + deleter.id(), + user_warren.user_id(), + user_warren.warren_id(), + ); + } + async fn auth_user_warrens_fetched(&self, user_id: &Uuid, warren_ids: &Vec) { tracing::debug!( "[Notifier] Fetched {} user warrens for authenticated user {}", diff --git a/backend/src/lib/outbound/postgres.rs b/backend/src/lib/outbound/postgres.rs index 8f7d158..1b7ef7b 100644 --- a/backend/src/lib/outbound/postgres.rs +++ b/backend/src/lib/outbound/postgres.rs @@ -33,12 +33,15 @@ use crate::domain::warren::{ user_warren::{ UserWarren, requests::{ + CreateUserWarrenError, CreateUserWarrenRequest, DeleteUserWarrenError, + DeleteUserWarrenRequest, EditUserWarrenError, EditUserWarrenRequest, FetchUserWarrenError, FetchUserWarrenRequest, FetchUserWarrensError, FetchUserWarrensRequest, ListUserWarrensError, ListUserWarrensRequest, }, }, warren::{ - FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, Warren, + FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, + ListWarrensError, ListWarrensRequest, Warren, }, }, ports::{AuthRepository, WarrenRepository, WarrenService}, @@ -109,7 +112,7 @@ impl Postgres { Ok(warren) } - async fn list_warrens( + async fn fetch_warrens( &self, connection: &mut PgConnection, ids: &[Uuid], @@ -131,6 +134,24 @@ impl Postgres { Ok(warrens) } + async fn fetch_all_warrens( + &self, + connection: &mut PgConnection, + ) -> Result, sqlx::Error> { + let warrens: Vec = sqlx::query_as::( + " + SELECT + * + FROM + warrens + ", + ) + .fetch_all(&mut *connection) + .await?; + + Ok(warrens) + } + async fn create_user( &self, connection: &mut PgConnection, @@ -470,10 +491,110 @@ impl Postgres { Ok(users) } + + async fn add_user_to_warren( + &self, + connection: &mut PgConnection, + user_warren: &UserWarren, + ) -> Result { + let user_warren: UserWarren = sqlx::query_as( + " + INSERT INTO user_warrens ( + user_id, + warren_id, + can_list_files, + can_read_files, + can_modify_files, + can_delete_files + ) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6 + ) + RETURNING + * + ", + ) + .bind(user_warren.user_id()) + .bind(user_warren.warren_id()) + .bind(user_warren.can_list_files()) + .bind(user_warren.can_read_files()) + .bind(user_warren.can_modify_files()) + .bind(user_warren.can_delete_files()) + .fetch_one(connection) + .await?; + + Ok(user_warren) + } + + async fn update_user_warren( + &self, + connection: &mut PgConnection, + user_warren: &UserWarren, + ) -> Result { + let user_warren: UserWarren = sqlx::query_as( + " + UPDATE + user_warrens + SET + can_list_files = $3, + can_read_files = $4, + can_modify_files = $5, + can_delete_files = $6 + WHERE + user_id = $1 AND + warren_id = $2 + RETURNING + * + ", + ) + .bind(user_warren.user_id()) + .bind(user_warren.warren_id()) + .bind(user_warren.can_list_files()) + .bind(user_warren.can_read_files()) + .bind(user_warren.can_modify_files()) + .bind(user_warren.can_delete_files()) + .fetch_one(connection) + .await?; + + Ok(user_warren) + } + + async fn remove_user_from_warren( + &self, + connection: &mut PgConnection, + user_id: &Uuid, + warren_id: &Uuid, + ) -> Result { + let mut tx = connection.begin().await?; + + let user_warren = sqlx::query_as( + " + DELETE FROM + user_warrens + WHERE + user_id = $1 AND + warren_id = $2 + RETURNING + * + ", + ) + .bind(user_id) + .bind(warren_id) + .fetch_one(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(user_warren) + } } impl WarrenRepository for Postgres { - async fn list_warrens( + async fn fetch_warrens( &self, request: FetchWarrensRequest, ) -> Result, FetchWarrensError> { @@ -484,9 +605,27 @@ impl WarrenRepository for Postgres { .context("Failed to get a PostgreSQL connection")?; let warrens = self - .list_warrens(&mut connection, request.ids()) + .fetch_warrens(&mut connection, request.ids()) .await - .map_err(|err| anyhow!(err).context("Failed to list warrens"))?; + .map_err(|err| anyhow!(err).context("Failed to fetch warrens"))?; + + Ok(warrens) + } + + async fn list_warrens( + &self, + _request: ListWarrensRequest, + ) -> Result, ListWarrensError> { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let warrens = self + .fetch_all_warrens(&mut connection) + .await + .map_err(|err| anyhow!(err).context("Failed to list all warrens"))?; Ok(warrens) } @@ -649,6 +788,66 @@ impl AuthRepository for Postgres { Ok(FetchAuthSessionResponse::new(session, user)) } + async fn create_user_warren( + &self, + request: CreateUserWarrenRequest, + ) -> Result { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let user_warren = self + .add_user_to_warren(&mut connection, request.user_warren()) + .await + .context("Failed to create user warren")?; + + Ok(user_warren) + } + + async fn edit_user_warren( + &self, + request: EditUserWarrenRequest, + ) -> Result { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let user_warren = self + .update_user_warren(&mut connection, request.user_warren()) + .await + .context("Failed to edit user warren")?; + + Ok(user_warren) + } + + async fn delete_user_warren( + &self, + request: DeleteUserWarrenRequest, + ) -> Result { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + let user_warren = self + .remove_user_from_warren(&mut connection, request.user_id(), request.warren_id()) + .await + .map_err(|e| { + if is_not_found_error(&e) { + DeleteUserWarrenError::NotFound + } else { + anyhow!("Failed to delete user warren: {e:?}").into() + } + })?; + + Ok(user_warren) + } + async fn fetch_user_warrens( &self, request: FetchUserWarrensRequest, @@ -741,14 +940,9 @@ impl AuthRepository for Postgres { .await .context("Failed to fetch all user warrens")?; let warrens = warren_service - .list_warrens(FetchWarrensRequest::new( - user_warrens - .iter() - .map(|uw| uw.warren_id().clone()) - .collect(), - )) + .list_warrens(ListWarrensRequest::new()) .await - .context("Failed to get warrens")?; + .context("Failed to get all warrens")?; Ok(ListAllUsersAndWarrensResponse::new( users, diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index e68a7b8..edbec0a 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -42,6 +42,24 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--reka-accordion-content-height); + } + } + @keyframes accordion-up { + from { + height: var(--reka-accordion-content-height); + } + to { + height: 0; + } + } } :root { diff --git a/frontend/components/SidebarAdminMenu.vue b/frontend/components/SidebarAdminMenu.vue index 42080aa..24101ed 100644 --- a/frontend/components/SidebarAdminMenu.vue +++ b/frontend/components/SidebarAdminMenu.vue @@ -20,34 +20,6 @@ const route = useRoute(); Administration - - - - - - Users - - - - - - Warrens - - - - - diff --git a/frontend/components/admin/AddUserWarrenDialog.vue b/frontend/components/admin/AddUserWarrenDialog.vue new file mode 100644 index 0000000..103626a --- /dev/null +++ b/frontend/components/admin/AddUserWarrenDialog.vue @@ -0,0 +1,248 @@ + + + diff --git a/frontend/components/admin/CreateUserDialog.vue b/frontend/components/admin/CreateUserDialog.vue index 1efcbcb..378d925 100644 --- a/frontend/components/admin/CreateUserDialog.vue +++ b/frontend/components/admin/CreateUserDialog.vue @@ -121,11 +121,14 @@ const onSubmit = form.handleSubmit(async (values) => { - + Cancel - Create diff --git a/frontend/components/admin/DeleteUserDialog.vue b/frontend/components/admin/DeleteUserDialog.vue index 24f3920..8acf505 100644 --- a/frontend/components/admin/DeleteUserDialog.vue +++ b/frontend/components/admin/DeleteUserDialog.vue @@ -97,7 +97,7 @@ async function submit() { - + Cancel +import { Accordion } from '@/components/ui/accordion'; import { AlertDialog, AlertDialogAction, @@ -14,14 +15,14 @@ import { useForm } from 'vee-validate'; import { toTypedSchema } from '@vee-validate/yup'; import { editUserSchema } from '~/lib/schemas/admin'; import { editUser } from '~/lib/api/admin/editUser'; -import type { AuthUser } from '~/shared/types/auth'; +import type { AuthUserWithWarrens } from '~/shared/types/admin'; const adminStore = useAdminStore(); const isValid = computed(() => Object.keys(form.errors.value).length < 1); // We'll only update this value if there is a user to prevent layout shifts on close -const user = ref(); +const user = ref(); const editing = ref(false); const isChanged = computed(() => { @@ -52,8 +53,13 @@ const form = useForm({ adminStore.$subscribe((_mutation, state) => { if (state.editUserDialog != null && !editing.value) { - user.value = state.editUserDialog.user; - form.setValues(user.value); + user.value = state.resources.users.find( + (u) => u.id === state.editUserDialog?.user.id + ); + + if (user.value != null) { + form.setValues(user.value); + } } }); @@ -73,7 +79,14 @@ const onSubmit = form.handleSubmit(async (values) => { }); if (result.success) { - close(); + const newUser: AuthUserWithWarrens = { + id: result.user.id, + name: result.user.name, + email: result.user.email, + admin: result.user.admin, + warrens: user.value.warrens, + }; + adminStore.setEditUserDialog(newUser); } editing.value = false; @@ -83,102 +96,141 @@ const onSubmit = form.handleSubmit(async (values) => {