warren creation / edit / deletion

This commit is contained in:
2025-07-22 22:01:43 +02:00
parent 2ed69ae498
commit b3e68deb38
27 changed files with 1345 additions and 25 deletions

View File

@@ -8,7 +8,7 @@ use crate::domain::warren::models::file::{
RelativeFilePath, RenameEntryError, RenameEntryRequest, RelativeFilePath, RenameEntryError, RenameEntryRequest,
}; };
use super::Warren; use super::{Warren, WarrenName};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FetchWarrenRequest { pub struct FetchWarrenRequest {
@@ -517,3 +517,87 @@ pub enum ListWarrensError {
#[error(transparent)] #[error(transparent)]
Unknown(#[from] anyhow::Error), 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),
}

View File

@@ -1,4 +1,13 @@
pub trait WarrenMetrics: Clone + Send + Sync + 'static { pub trait WarrenMetrics: Clone + Send + Sync + 'static {
fn record_warren_creation_success(&self) -> impl Future<Output = ()> + Send;
fn record_warren_creation_failure(&self) -> impl Future<Output = ()> + Send;
fn record_warren_edit_success(&self) -> impl Future<Output = ()> + Send;
fn record_warren_edit_failure(&self) -> impl Future<Output = ()> + Send;
fn record_warren_deletion_success(&self) -> impl Future<Output = ()> + Send;
fn record_warren_deletion_failure(&self) -> impl Future<Output = ()> + Send;
fn record_warren_fetch_success(&self) -> impl Future<Output = ()> + Send; fn record_warren_fetch_success(&self) -> impl Future<Output = ()> + Send;
fn record_warren_fetch_failure(&self) -> impl Future<Output = ()> + Send; fn record_warren_fetch_failure(&self) -> impl Future<Output = ()> + Send;
@@ -53,6 +62,15 @@ pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
} }
pub trait AuthMetrics: Clone + Send + Sync + 'static { pub trait AuthMetrics: Clone + Send + Sync + 'static {
fn record_auth_warren_creation_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_warren_creation_failure(&self) -> impl Future<Output = ()> + Send;
fn record_auth_warren_edit_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_warren_edit_failure(&self) -> impl Future<Output = ()> + Send;
fn record_auth_warren_deletion_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_warren_deletion_failure(&self) -> impl Future<Output = ()> + Send;
fn record_user_registration_success(&self) -> impl Future<Output = ()> + Send; fn record_user_registration_success(&self) -> impl Future<Output = ()> + Send;
fn record_user_registration_failure(&self) -> impl Future<Output = ()> + Send; fn record_user_registration_failure(&self) -> impl Future<Output = ()> + Send;

View File

@@ -35,8 +35,10 @@ use super::models::{
}, },
warren::{ warren::{
CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, CreateWarrenDirectoryResponse, CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, CreateWarrenDirectoryResponse,
DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest, DeleteWarrenDirectoryError,
DeleteWarrenFileError, DeleteWarrenFileRequest, DeleteWarrenFileResponse, FetchWarrenError, DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, DeleteWarrenError,
DeleteWarrenFileError, DeleteWarrenFileRequest, DeleteWarrenFileResponse,
DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrenError,
FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, ListWarrenFilesError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, ListWarrenFilesError,
ListWarrenFilesRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest, ListWarrenFilesRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest,
RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse, RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse,
@@ -45,6 +47,19 @@ use super::models::{
}; };
pub trait WarrenService: Clone + Send + Sync + 'static { pub trait WarrenService: Clone + Send + Sync + 'static {
fn create_warren(
&self,
request: CreateWarrenRequest,
) -> impl Future<Output = Result<Warren, CreateWarrenError>> + Send;
fn edit_warren(
&self,
request: EditWarrenRequest,
) -> impl Future<Output = Result<Warren, EditWarrenError>> + Send;
fn delete_warren(
&self,
request: DeleteWarrenRequest,
) -> impl Future<Output = Result<Warren, DeleteWarrenError>> + Send;
fn fetch_warrens( fn fetch_warrens(
&self, &self,
request: FetchWarrensRequest, request: FetchWarrensRequest,
@@ -119,6 +134,25 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
} }
pub trait AuthService: Clone + Send + Sync + 'static { pub trait AuthService: Clone + Send + Sync + 'static {
/// MUST REQUIRE ADMIN PRIVILEGES
fn create_warren<WS: WarrenService>(
&self,
request: AuthRequest<CreateWarrenRequest>,
warren_service: &WS,
) -> impl Future<Output = Result<Warren, AuthError<CreateWarrenError>>> + Send;
/// MUST REQUIRE ADMIN PRIVILEGES
fn edit_warren<WS: WarrenService>(
&self,
request: AuthRequest<EditWarrenRequest>,
warren_service: &WS,
) -> impl Future<Output = Result<Warren, AuthError<EditWarrenError>>> + Send;
/// MUST REQUIRE ADMIN PRIVILEGES
fn delete_warren<WS: WarrenService>(
&self,
request: AuthRequest<DeleteWarrenRequest>,
warren_service: &WS,
) -> impl Future<Output = Result<Warren, AuthError<DeleteWarrenError>>> + Send;
fn register_user( fn register_user(
&self, &self,
request: RegisterUserRequest, request: RegisterUserRequest,

View File

@@ -12,6 +12,10 @@ use crate::domain::warren::models::{
}; };
pub trait WarrenNotifier: Clone + Send + Sync + 'static { pub trait WarrenNotifier: Clone + Send + Sync + 'static {
fn warren_created(&self, warren: &Warren) -> impl Future<Output = ()> + Send;
fn warren_edited(&self, warren: &Warren) -> impl Future<Output = ()> + Send;
fn warren_deleted(&self, warren: &Warren) -> impl Future<Output = ()> + Send;
fn warrens_fetched(&self, warrens: &Vec<Warren>) -> impl Future<Output = ()> + Send; fn warrens_fetched(&self, warrens: &Vec<Warren>) -> impl Future<Output = ()> + Send;
fn warren_fetched(&self, warren: &Warren) -> impl Future<Output = ()> + Send; fn warren_fetched(&self, warren: &Warren) -> impl Future<Output = ()> + Send;
fn warrens_listed(&self, warrens: &Vec<Warren>) -> impl Future<Output = ()> + Send; fn warrens_listed(&self, warrens: &Vec<Warren>) -> impl Future<Output = ()> + Send;
@@ -72,6 +76,19 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
} }
pub trait AuthNotifier: Clone + Send + Sync + 'static { pub trait AuthNotifier: Clone + Send + Sync + 'static {
fn auth_warren_created(
&self,
creator: &User,
warren: &Warren,
) -> impl Future<Output = ()> + Send;
fn auth_warren_edited(&self, editor: &User, warren: &Warren)
-> impl Future<Output = ()> + Send;
fn auth_warren_deleted(
&self,
deleter: &User,
warren: &Warren,
) -> impl Future<Output = ()> + Send;
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send; fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send; fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send;
fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send; fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send;

View File

@@ -27,14 +27,28 @@ use crate::domain::warren::models::{
}, },
}, },
warren::{ warren::{
FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
ListWarrensError, ListWarrensRequest, Warren, EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
FetchWarrensError, FetchWarrensRequest, ListWarrensError, ListWarrensRequest, Warren,
}, },
}; };
use super::WarrenService; use super::WarrenService;
pub trait WarrenRepository: Clone + Send + Sync + 'static { pub trait WarrenRepository: Clone + Send + Sync + 'static {
fn create_warren(
&self,
request: CreateWarrenRequest,
) -> impl Future<Output = Result<Warren, CreateWarrenError>> + Send;
fn edit_warren(
&self,
request: EditWarrenRequest,
) -> impl Future<Output = Result<Warren, EditWarrenError>> + Send;
fn delete_warren(
&self,
request: DeleteWarrenRequest,
) -> impl Future<Output = Result<Warren, DeleteWarrenError>> + Send;
fn fetch_warrens( fn fetch_warrens(
&self, &self,
request: FetchWarrensRequest, request: FetchWarrensRequest,

View File

@@ -26,13 +26,15 @@ use crate::{
}, },
warren::{ warren::{
CreateWarrenDirectoryError, CreateWarrenDirectoryRequest, CreateWarrenDirectoryError, CreateWarrenDirectoryRequest,
CreateWarrenDirectoryResponse, DeleteWarrenDirectoryError, CreateWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest,
DeleteWarrenDirectoryRequest, DeleteWarrenDirectoryResponse, DeleteWarrenFileError, DeleteWarrenDirectoryError, DeleteWarrenDirectoryRequest,
DeleteWarrenFileRequest, DeleteWarrenFileResponse, FetchWarrenError, DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileError,
FetchWarrenRequest, FetchWarrensRequest, ListWarrenFilesError, DeleteWarrenFileRequest, DeleteWarrenFileResponse, DeleteWarrenRequest,
ListWarrenFilesRequest, ListWarrenFilesResponse, RenameWarrenEntryError, EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
RenameWarrenEntryRequest, RenameWarrenEntryResponse, UploadWarrenFilesError, FetchWarrensRequest, ListWarrenFilesError, ListWarrenFilesRequest,
UploadWarrenFilesRequest, UploadWarrenFilesResponse, Warren, ListWarrenFilesResponse, RenameWarrenEntryError, RenameWarrenEntryRequest,
RenameWarrenEntryResponse, UploadWarrenFilesError, UploadWarrenFilesRequest,
UploadWarrenFilesResponse, Warren,
}, },
}, },
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService}, ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
@@ -102,6 +104,100 @@ where
M: AuthMetrics, M: AuthMetrics,
N: AuthNotifier, N: AuthNotifier,
{ {
async fn create_warren<WS: WarrenService>(
&self,
request: AuthRequest<CreateWarrenRequest>,
warren_service: &WS,
) -> Result<Warren, AuthError<CreateWarrenError>> {
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<WS: WarrenService>(
&self,
request: AuthRequest<EditWarrenRequest>,
warren_service: &WS,
) -> Result<Warren, AuthError<EditWarrenError>> {
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<WS: WarrenService>(
&self,
request: AuthRequest<DeleteWarrenRequest>,
warren_service: &WS,
) -> Result<Warren, AuthError<DeleteWarrenError>> {
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<User, RegisterUserError> { async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
let result = self.repository.create_user(request.into()).await; let result = self.repository.create_user(request.into()).await;

View File

@@ -1,9 +1,11 @@
use crate::domain::warren::{ use crate::domain::warren::{
models::warren::{ models::warren::{
CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse, CreateWarrenDirectoryResponse, CreateWarrenError, CreateWarrenRequest,
FetchWarrensError, FetchWarrensRequest, ListWarrenFilesResponse, ListWarrensError, DeleteWarrenDirectoryResponse, DeleteWarrenError, DeleteWarrenFileResponse,
ListWarrensRequest, RenameWarrenEntryError, RenameWarrenEntryRequest, DeleteWarrenRequest, EditWarrenError, EditWarrenRequest, FetchWarrensError,
RenameWarrenEntryResponse, UploadWarrenFilesResponse, FetchWarrensRequest, ListWarrenFilesResponse, ListWarrensError, ListWarrensRequest,
RenameWarrenEntryError, RenameWarrenEntryRequest, RenameWarrenEntryResponse,
UploadWarrenFilesResponse,
}, },
ports::FileSystemService, ports::FileSystemService,
}; };
@@ -56,6 +58,49 @@ where
N: WarrenNotifier, N: WarrenNotifier,
FSS: FileSystemService, FSS: FileSystemService,
{ {
async fn create_warren(
&self,
request: CreateWarrenRequest,
) -> Result<Warren, CreateWarrenError> {
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<Warren, EditWarrenError> {
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<Warren, DeleteWarrenError> {
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( async fn fetch_warrens(
&self, &self,
request: FetchWarrensRequest, request: FetchWarrensRequest,

View File

@@ -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<ParseCreateWarrenHttpRequestError> 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<CreateWarrenRequest, ParseCreateWarrenHttpRequestError> {
let name = WarrenName::new(&self.name)?;
let path = FilePath::new(&self.path)?.try_into()?;
Ok(CreateWarrenRequest::new(name, path))
}
}
pub async fn create_warren<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>,
SessionIdHeader(session): SessionIdHeader,
Json(request): Json<CreateWarrenHttpRequestBody>,
) -> Result<ApiSuccess<AdminWarrenData>, 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)
}

View File

@@ -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<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>,
SessionIdHeader(session): SessionIdHeader,
Json(request): Json<DeleteWarrenHttpRequestBody>,
) -> Result<ApiSuccess<AdminWarrenData>, 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)
}

View File

@@ -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<ParseEditWarrenHttpRequestError> 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<EditWarrenRequest, ParseEditWarrenHttpRequestError> {
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<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>,
SessionIdHeader(session): SessionIdHeader,
Json(request): Json<EditWarrenHttpRequestBody>,
) -> Result<ApiSuccess<AdminWarrenData>, 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)
}

View File

@@ -1,18 +1,24 @@
mod create_user; mod create_user;
mod create_user_warren; mod create_user_warren;
mod create_warren;
mod delete_user; mod delete_user;
mod delete_user_warren; mod delete_user_warren;
mod delete_warren;
mod edit_user; mod edit_user;
mod edit_user_warren; mod edit_user_warren;
mod edit_warren;
mod list_all_users_and_warrens; mod list_all_users_and_warrens;
mod list_users; mod list_users;
use create_user::create_user; use create_user::create_user;
use create_user_warren::create_user_warren; use create_user_warren::create_user_warren;
use create_warren::create_warren;
use delete_user::delete_user; use delete_user::delete_user;
use delete_user_warren::delete_user_warren; use delete_user_warren::delete_user_warren;
use delete_warren::delete_warren;
use edit_user::edit_user; use edit_user::edit_user;
use edit_user_warren::edit_user_warren; 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_all_users_and_warrens::list_all_users_and_warrens;
use list_users::list_users; use list_users::list_users;
@@ -33,6 +39,9 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
.route("/users", post(create_user)) .route("/users", post(create_user))
.route("/users", patch(edit_user)) .route("/users", patch(edit_user))
.route("/users", delete(delete_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", post(create_user_warren))
.route("/user-warrens", patch(edit_user_warren)) .route("/user-warrens", patch(edit_user_warren))
.route("/user-warrens", delete(delete_user_warren)) .route("/user-warrens", delete(delete_user_warren))

View File

@@ -10,6 +10,27 @@ impl MetricsDebugLogger {
} }
impl WarrenMetrics for 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) { async fn record_warren_list_success(&self) {
tracing::debug!("[Metrics] Warren list succeeded"); tracing::debug!("[Metrics] Warren list succeeded");
} }
@@ -129,6 +150,27 @@ impl FileSystemMetrics for MetricsDebugLogger {
} }
impl AuthMetrics 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) { async fn record_user_registration_success(&self) {
tracing::debug!("[Metrics] User registration succeeded"); tracing::debug!("[Metrics] User registration succeeded");
} }

View File

@@ -24,6 +24,16 @@ impl NotifierDebugLogger {
} }
impl WarrenNotifier for 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<Warren>) { async fn warrens_fetched(&self, warrens: &Vec<Warren>) {
tracing::debug!("[Notifier] Fetched {} warren(s)", warrens.len()); tracing::debug!("[Notifier] Fetched {} warren(s)", warrens.len());
} }
@@ -121,6 +131,28 @@ impl FileSystemNotifier for NotifierDebugLogger {
} }
impl AuthNotifier 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) { async fn user_registered(&self, user: &User) {
tracing::debug!("[Notifier] Registered user {}", user.name()); tracing::debug!("[Notifier] Registered user {}", user.name());
} }

View File

@@ -1,11 +1,16 @@
use anyhow::{Context as _, anyhow}; use anyhow::{Context as _, anyhow};
use sqlx::PgConnection; use sqlx::{Acquire as _, PgConnection};
use uuid::Uuid; use uuid::Uuid;
use crate::domain::warren::{ use crate::domain::warren::{
models::warren::{ models::{
FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, file::AbsoluteFilePath,
ListWarrensError, ListWarrensRequest, Warren, warren::{
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
FetchWarrensError, FetchWarrensRequest, ListWarrensError, ListWarrensRequest, Warren,
WarrenName,
},
}, },
ports::WarrenRepository, ports::WarrenRepository,
}; };
@@ -13,6 +18,62 @@ use crate::domain::warren::{
use super::{Postgres, is_not_found_error}; use super::{Postgres, is_not_found_error};
impl WarrenRepository for Postgres { impl WarrenRepository for Postgres {
async fn create_warren(
&self,
request: CreateWarrenRequest,
) -> Result<Warren, CreateWarrenError> {
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<Warren, EditWarrenError> {
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<Warren, DeleteWarrenError> {
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( async fn fetch_warrens(
&self, &self,
request: FetchWarrensRequest, request: FetchWarrensRequest,
@@ -74,6 +135,96 @@ impl WarrenRepository for Postgres {
} }
impl Postgres { impl Postgres {
async fn create_warren(
&self,
connection: &mut PgConnection,
name: &WarrenName,
path: &AbsoluteFilePath,
) -> Result<Warren, sqlx::Error> {
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<Warren, sqlx::Error> {
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<Warren, sqlx::Error> {
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( async fn get_warren(
&self, &self,
connection: &mut PgConnection, connection: &mut PgConnection,

View File

@@ -45,10 +45,16 @@ function onKeyDown(e: KeyboardEvent) {
submit(); submit();
} }
} }
function onOpenChange(state: boolean) {
if (!state) {
dialog.reset();
}
}
</script> </script>
<template> <template>
<Dialog v-model:open="dialog.open"> <Dialog v-model:open="dialog.open" @update:open="onOpenChange">
<DialogTrigger as-child> <DialogTrigger as-child>
<slot /> <slot />
</DialogTrigger> </DialogTrigger>

View File

@@ -0,0 +1,104 @@
<script setup lang="ts">
import { toTypedSchema } from '@vee-validate/yup';
import { useForm } from 'vee-validate';
import { createWarren } from '~/lib/api/admin/createWarren';
import { createWarrenSchema } from '~/lib/schemas/admin';
const adminStore = useAdminStore();
const creating = ref(false);
function close() {
adminStore.closeCreateWarrenDialog();
form.resetForm();
}
const form = useForm({
validationSchema: toTypedSchema(createWarrenSchema),
});
const onSubmit = form.handleSubmit(async (values) => {
if (creating.value) {
return;
}
creating.value = true;
const result = await createWarren(values.name, values.path);
creating.value = false;
if (result.success) {
close();
}
});
</script>
<template>
<AlertDialog :open="adminStore.createWarrenDialogOpen">
<AlertDialogTrigger><slot /></AlertDialogTrigger>
<AlertDialogContent @escape-key-down="close">
<AlertDialogHeader>
<AlertDialogTitle>Create warren</AlertDialogTitle>
<AlertDialogDescription>
Enter a name and an absolute file path to create a new
warren
</AlertDialogDescription>
</AlertDialogHeader>
<form
id="create-warren-form"
class="flex flex-col gap-2"
@submit.prevent="onSubmit"
>
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
v-bind="componentField"
id="name"
type="text"
placeholder="my-warren"
autocomplete="off"
data-1p-ignore
data-protonpass-ignore
data-bwignore
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="path">
<FormItem>
<FormLabel>File path</FormLabel>
<FormControl>
<Input
v-bind="componentField"
id="path"
type="text"
placeholder="/mywarren"
autocomplete="off"
data-1p-ignore
data-protonpass-ignore
data-bwignore
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</form>
<AlertDialogFooter class="gap-y-0">
<AlertDialogCancel variant="outline" @click="close">
Cancel
</AlertDialogCancel>
<AlertDialogAction
type="submit"
form="create-warren-form"
:disabled="creating"
>Create</AlertDialogAction
>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>

View File

@@ -64,7 +64,7 @@ async function submit() {
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription class="space-y-1"> <AlertDialogDescription class="space-y-1">
<p ref="test"> <p>
This action cannot be undone. This will permanently This action cannot be undone. This will permanently
delete the user and remove their data from the database delete the user and remove their data from the database
</p> </p>

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import { Input } from '@/components/ui/input';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { deleteWarren } from '~/lib/api/admin/deleteWarren';
import type { AdminWarrenData } from '~/shared/types/warrens';
const adminStore = useAdminStore();
// We'll only update this value if there is a warren to prevent layout shifts on close
const warren = ref<AdminWarrenData>();
const deleting = ref(false);
const confirmPath = ref<string>('');
const confirmPathInput = ref<InstanceType<typeof Input>>();
const pathMatches = computed(
() => warren.value != null && warren.value.path === confirmPath.value
);
adminStore.$subscribe(async (_mutation, state) => {
if (state.deleteWarrenDialog != null) {
warren.value = state.deleteWarrenDialog?.warren;
setTimeout(() => confirmPathInput.value?.domRef?.focus(), 25);
}
});
function close() {
confirmPath.value = '';
adminStore.closeDeleteWarrenDialog();
}
async function submit() {
if (deleting.value || adminStore.deleteWarrenDialog == null) {
return;
}
deleting.value = true;
const { success } = await deleteWarren(
adminStore.deleteWarrenDialog.warren.id
);
if (success) {
close();
}
deleting.value = false;
}
</script>
<template>
<AlertDialog :open="adminStore.deleteWarrenDialog != null">
<AlertDialogTrigger as-child>
<slot />
</AlertDialogTrigger>
<AlertDialogContent @escape-key-down="close">
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription class="space-y-1">
<p>
This action cannot be undone. This will permanently
delete the warren. The contained files will be left
unchanged.
</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlterDialogContent v-if="warren != null">
<div class="flex flex-col gap-4">
<AdminWarrenListing :warren />
<div class="flex flex-col gap-1">
<p
:class="[
'tight text-sm',
pathMatches
? 'text-muted-foreground'
: 'text-destructive-foreground',
]"
>
Enter the warren's path to continue
</p>
<Input
ref="confirmPathInput"
v-model="confirmPath"
type="text"
:placeholder="warren.path"
autocomplete="off"
data-1p-ignore
data-protonpass-ignore
data-bwignore
/>
</div>
</div>
</AlterDialogContent>
<AlertDialogFooter class="gap-y-0">
<AlertDialogCancel @click="close">Cancel</AlertDialogCancel>
<AlertDialogAction
:disabled="!pathMatches || deleting"
@click="submit"
>Delete</AlertDialogAction
>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>

View File

@@ -0,0 +1,158 @@
<script setup lang="ts">
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/yup';
import { editWarrenSchema } from '~/lib/schemas/admin';
import { editWarren } from '~/lib/api/admin/editWarren';
import type { AdminWarrenData } from '~/shared/types/warrens';
const adminStore = useAdminStore();
const isValid = computed(() => Object.keys(form.errors.value).length < 1);
// We'll only update this value if there is a warren to prevent layout shifts on close
const warren = ref<AdminWarrenData>();
const editing = ref(false);
const isChanged = computed(() => {
if (warren.value == null) {
return false;
}
try {
const values = editWarrenSchema.validateSync(
form.controlledValues.value
);
return (
values.name !== warren.value.name ||
values.path !== warren.value.path
);
} catch {
return true;
}
});
function close() {
adminStore.closeEditWarrenDialog();
}
const form = useForm({
validationSchema: toTypedSchema(editWarrenSchema),
});
adminStore.$subscribe((_mutation, state) => {
if (state.editWarrenDialog != null && !editing.value) {
warren.value = Object.values(state.resources.warrens).find(
(w) => w.id === state.editWarrenDialog?.warren.id
);
if (warren.value != null) {
form.setValues(warren.value);
}
}
});
const onSubmit = form.handleSubmit(async (values) => {
if (warren.value == null || !isChanged.value || editing.value) {
return;
}
editing.value = true;
const result = await editWarren(warren.value.id, values.name, values.path);
if (result.success) {
const newWarren: AdminWarrenData = {
id: result.warren.id,
name: result.warren.name,
path: result.warren.path,
};
adminStore.openEditWarrenDialog(newWarren);
}
editing.value = false;
});
</script>
<template>
<AlertDialog :open="adminStore.editWarrenDialog != null">
<AlertDialogTrigger><slot /></AlertDialogTrigger>
<AlertDialogContent
v-if="warren != null"
class="flex max-h-[95vh] flex-col"
@escape-key-down="close"
>
<AlertDialogHeader class="shrink">
<AlertDialogTitle>Edit warren</AlertDialogTitle>
<AlertDialogDescription>
Modify the warren's name and path
</AlertDialogDescription>
</AlertDialogHeader>
<form
id="edit-warren-form"
class="flex flex-col gap-2"
@submit.prevent="onSubmit"
>
<input type="hidden" name="id" :value="warren.id" />
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
v-bind="componentField"
id="name"
type="text"
placeholder="my-warren"
autocomplete="off"
data-1p-ignore
data-protonpass-ignore
data-bwignore
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="path">
<FormItem>
<FormLabel>Path</FormLabel>
<FormControl>
<Input
v-bind="componentField"
id="path"
type="text"
placeholder="/mywarren"
autocomplete="off"
data-1p-ignore
data-protonpass-ignore
data-bwignore
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</form>
<AlertDialogFooter class="gap-y-0">
<AlertDialogCancel @click="close">Close</AlertDialogCancel>
<AlertDialogAction
type="submit"
form="edit-warren-form"
:disabled="!isChanged || !isValid"
>Save</AlertDialogAction
>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>

View File

@@ -17,6 +17,11 @@ await useAsyncData('admin-resources', async () => {
<AdminCreateUserDialog /> <AdminCreateUserDialog />
<AdminEditUserDialog /> <AdminEditUserDialog />
<AdminDeleteUserDialog /> <AdminDeleteUserDialog />
<AdminCreateWarrenDialog />
<AdminEditWarrenDialog />
<AdminDeleteWarrenDialog />
<slot /> <slot />
</NuxtLayout> </NuxtLayout>
</template> </template>

View File

@@ -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<ApiResponse<AdminWarrenData>>(
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,
};
}

View File

@@ -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<ApiResponse<AdminWarrenData>>(
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,
};
}

View File

@@ -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<ApiResponse<AdminWarrenData>>(
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,
};
}

View File

@@ -1,5 +1,6 @@
export async function logout() { export async function logout() {
useAuthSession().value = null; useAuthSession().value = null;
useAdminStore().$reset();
await navigateTo({ await navigateTo({
path: '/login', path: '/login',
}); });

View File

@@ -26,3 +26,13 @@ export const userWarrenSchema = object({
canModifyFiles: boolean().required(), canModifyFiles: boolean().required(),
canDeleteFiles: 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'),
});

View File

@@ -15,7 +15,7 @@ const adminStore = useAdminStore();
<CardTitle>Users</CardTitle> <CardTitle>Users</CardTitle>
<CardDescription>Create or manage users</CardDescription> <CardDescription>Create or manage users</CardDescription>
</CardHeader> </CardHeader>
<CardContent class="max-h-64 overflow-hidden"> <CardContent class="max-h-96 overflow-hidden">
<ScrollArea class="h-full w-full overflow-hidden"> <ScrollArea class="h-full w-full overflow-hidden">
<div class="flex w-full flex-col gap-2 overflow-hidden"> <div class="flex w-full flex-col gap-2 overflow-hidden">
<AdminUserListing <AdminUserListing
@@ -66,7 +66,7 @@ const adminStore = useAdminStore();
<CardTitle>Warrens</CardTitle> <CardTitle>Warrens</CardTitle>
<CardDescription>Create or manage warrens</CardDescription> <CardDescription>Create or manage warrens</CardDescription>
</CardHeader> </CardHeader>
<CardContent class="max-h-64 grow overflow-hidden"> <CardContent class="max-h-96 grow overflow-hidden">
<ScrollArea class="h-full w-full overflow-hidden"> <ScrollArea class="h-full w-full overflow-hidden">
<div class="flex w-full flex-col gap-2 overflow-hidden"> <div class="flex w-full flex-col gap-2 overflow-hidden">
<AdminWarrenListing <AdminWarrenListing
@@ -79,12 +79,24 @@ const adminStore = useAdminStore();
class="m-1" class="m-1"
variant="outline" variant="outline"
size="icon" size="icon"
@click="
() =>
adminStore.openEditWarrenDialog(
warren
)
"
><Icon name="lucide:pencil" ><Icon name="lucide:pencil"
/></Button> /></Button>
<Button <Button
class="m-1" class="m-1"
variant="destructive" variant="destructive"
size="icon" size="icon"
@click="
() =>
adminStore.openDeleteWarrenDialog(
warren
)
"
><Icon name="lucide:trash-2" ><Icon name="lucide:trash-2"
/></Button> /></Button>
</template> </template>
@@ -94,7 +106,7 @@ const adminStore = useAdminStore();
</CardContent> </CardContent>
<CardFooter> <CardFooter>
<div class="mt-4 flex grow flex-row justify-end"> <div class="mt-4 flex grow flex-row justify-end">
<Button @click="adminStore.openCreateUserDialog" <Button @click="adminStore.openCreateWarrenDialog"
>Create</Button >Create</Button
> >
</div> </div>

View File

@@ -1,5 +1,6 @@
import type { AuthUser } from '#shared/types/auth'; import type { AuthUser } from '#shared/types/auth';
import type { AdminResources, AuthUserWithWarrens } from '~/shared/types/admin'; import type { AdminResources, AuthUserWithWarrens } from '~/shared/types/admin';
import type { AdminWarrenData } from '~/shared/types/warrens';
export const useAdminStore = defineStore('admin', { export const useAdminStore = defineStore('admin', {
state: () => ({ state: () => ({
@@ -7,11 +8,37 @@ export const useAdminStore = defineStore('admin', {
users: [], users: [],
warrens: {}, warrens: {},
} as AdminResources, } as AdminResources,
createWarrenDialogOpen: false,
editWarrenDialog: null as { warren: AdminWarrenData } | null,
deleteWarrenDialog: null as { warren: AdminWarrenData } | null,
createUserDialogOpen: false, createUserDialogOpen: false,
editUserDialog: null as { user: AuthUserWithWarrens } | null, editUserDialog: null as { user: AuthUserWithWarrens } | null,
deleteUserDialog: null as { user: AuthUser } | null, deleteUserDialog: null as { user: AuthUser } | null,
}), }),
actions: { 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() { openCreateUserDialog() {
this.createUserDialogOpen = true; this.createUserDialogOpen = true;
}, },