add / edit / delete user warrens

This commit is contained in:
2025-07-21 19:27:41 +02:00
parent 50e066f794
commit 2c26002507
86 changed files with 2197 additions and 300 deletions

View File

@@ -0,0 +1 @@
ALTER TABLE user_warrens DROP COLUMN can_create_children, DROP COLUMN can_delete_warren;

View File

@@ -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
}
}

View File

@@ -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),
}

View File

@@ -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),
}

View File

@@ -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),
}

View File

@@ -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),
}

View File

@@ -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::*;

View File

@@ -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),
}

View File

@@ -1,10 +1,13 @@
pub trait WarrenMetrics: Clone + Send + Sync + 'static {
fn record_warren_list_success(&self) -> impl Future<Output = ()> + Send;
fn record_warren_list_failure(&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_warrens_fetch_success(&self) -> impl Future<Output = ()> + Send;
fn record_warrens_fetch_failure(&self) -> impl Future<Output = ()> + Send;
fn record_warren_list_success(&self) -> impl Future<Output = ()> + Send;
fn record_warren_list_failure(&self) -> impl Future<Output = ()> + Send;
fn record_list_warren_files_success(&self) -> impl Future<Output = ()> + Send;
fn record_list_warren_files_failure(&self) -> impl Future<Output = ()> + Send;
@@ -77,6 +80,15 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
fn record_auth_session_fetch_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_session_fetch_failure(&self) -> impl Future<Output = ()> + Send;
fn record_auth_user_warren_creation_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_user_warren_creation_failure(&self) -> impl Future<Output = ()> + Send;
fn record_auth_user_warren_edit_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_user_warren_edit_failure(&self) -> impl Future<Output = ()> + Send;
fn record_auth_user_warren_deletion_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_user_warren_deletion_failure(&self) -> impl Future<Output = ()> + Send;
fn record_auth_fetch_user_warrens_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_fetch_user_warrens_failure(&self) -> impl Future<Output = ()> + Send;

View File

@@ -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<Output = Result<Vec<Warren>, FetchWarrensError>> + Send;
fn list_warrens(
&self,
request: ListWarrensRequest,
) -> impl Future<Output = Result<Vec<Warren>, ListWarrensError>> + Send;
fn fetch_warren(
&self,
request: FetchWarrenRequest,
@@ -164,9 +169,25 @@ pub trait AuthService: Clone + Send + Sync + 'static {
fn list_warrens<WS: WarrenService>(
&self,
request: AuthRequest<ListWarrensRequest>,
request: AuthRequest<()>,
warren_service: &WS,
) -> impl Future<Output = Result<Vec<Warren>, AuthError<ListWarrensError>>> + Send;
) -> impl Future<Output = Result<Vec<Warren>, AuthError<FetchUserWarrensError>>> + Send;
/// An action that creates an association between a user and a warren (MUST REQUIRE ADMIN PRIVILEGES)
fn create_user_warren(
&self,
request: AuthRequest<CreateUserWarrenRequest>,
) -> impl Future<Output = Result<UserWarren, AuthError<CreateUserWarrenError>>> + Send;
/// An action that edits an association between a user and a warren (MUST REQUIRE ADMIN PRIVILEGES)
fn edit_user_warren(
&self,
request: AuthRequest<EditUserWarrenRequest>,
) -> impl Future<Output = Result<UserWarren, AuthError<EditUserWarrenError>>> + Send;
/// An action that deletes an association between a user and a warren (MUST REQUIRE ADMIN PRIVILEGES)
fn delete_user_warren(
&self,
request: AuthRequest<DeleteUserWarrenRequest>,
) -> impl Future<Output = Result<UserWarren, AuthError<DeleteUserWarrenError>>> + Send;
fn fetch_user_warrens(
&self,

View File

@@ -12,8 +12,9 @@ use crate::domain::warren::models::{
};
pub trait WarrenNotifier: Clone + Send + Sync + 'static {
fn warrens_listed(&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 warrens_listed(&self, warrens: &Vec<Warren>) -> impl Future<Output = ()> + Send;
fn warren_files_listed(
&self,
response: &ListWarrenFilesResponse,
@@ -93,6 +94,21 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
response: &FetchAuthSessionResponse,
) -> impl Future<Output = ()> + Send;
fn auth_user_warren_created(
&self,
creator: &User,
user_warren: &UserWarren,
) -> impl Future<Output = ()> + Send;
fn auth_user_warren_edited(
&self,
editor: &User,
user_warren: &UserWarren,
) -> impl Future<Output = ()> + Send;
fn auth_user_warren_deleted(
&self,
deleter: &User,
user_warren: &UserWarren,
) -> impl Future<Output = ()> + Send;
fn auth_user_warrens_listed(
&self,
user: &User,

View File

@@ -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<Output = Result<Vec<Warren>, FetchWarrensError>> + Send;
fn list_warrens(
&self,
request: ListWarrensRequest,
) -> impl Future<Output = Result<Vec<Warren>, ListWarrensError>> + Send;
fn fetch_warren(
&self,
request: FetchWarrenRequest,
@@ -104,6 +112,19 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
request: VerifyUserPasswordRequest,
) -> impl Future<Output = Result<User, VerifyUserPasswordError>> + Send;
fn create_user_warren(
&self,
request: CreateUserWarrenRequest,
) -> impl Future<Output = Result<UserWarren, CreateUserWarrenError>> + Send;
fn edit_user_warren(
&self,
request: EditUserWarrenRequest,
) -> impl Future<Output = Result<UserWarren, EditUserWarrenError>> + Send;
fn delete_user_warren(
&self,
request: DeleteUserWarrenRequest,
) -> impl Future<Output = Result<UserWarren, DeleteUserWarrenError>> + Send;
fn list_user_warrens(
&self,
request: ListUserWarrensRequest,

View File

@@ -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<CreateUserWarrenRequest>,
) -> Result<UserWarren, AuthError<CreateUserWarrenError>> {
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<EditUserWarrenRequest>,
) -> Result<UserWarren, AuthError<EditUserWarrenError>> {
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<DeleteUserWarrenRequest>,
) -> Result<UserWarren, AuthError<DeleteUserWarrenError>> {
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<WS: WarrenService>(
&self,
request: AuthRequest<ListWarrensRequest>,
request: AuthRequest<()>,
warren_service: &WS,
) -> Result<Vec<Warren>, AuthError<ListWarrensError>> {
) -> Result<Vec<Warren>, AuthError<FetchUserWarrensError>> {
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;

View File

@@ -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<Vec<Warren>, 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<Vec<Warren>, ListWarrensError> {
let result = self.repository.list_warrens(request).await;
if let Ok(warren) = result.as_ref() {

View File

@@ -164,10 +164,10 @@ impl From<FetchAuthSessionError> 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()),
}

View File

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

View File

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

View File

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

View File

@@ -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<UserData>,
user_warrens: Vec<UserWarrenData>,
warrens: Vec<WarrenData>,
warrens: Vec<AdminWarrenData>,
}
impl From<ListAllUsersAndWarrensResponse> for ListAllUsersAndWarrensHttpResponseBody {

View File

@@ -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<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
.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))
}

View File

@@ -35,6 +35,7 @@ impl From<FetchAuthSessionResponse> for FetchSessionResponseBody {
}
}
/// TODO: Remove the `WarrenAuth ` bit and let the service handle that
pub async fn fetch_session<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>,
headers: HeaderMap,
@@ -43,7 +44,7 @@ pub async fn fetch_session<WS: WarrenService, AS: AuthService>(
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(),
));
};

View File

@@ -35,12 +35,10 @@ impl From<User> 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<UserWarren> for UserWarrenData {
@@ -48,12 +46,10 @@ impl From<UserWarren> 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<Warren> 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<Warren> for AdminWarrenData {
fn from(value: Warren) -> Self {
Self {
id: *value.id(),
name: value.name().to_string(),
path: value.path().to_string(),
}
}
}

View File

@@ -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<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>,
SessionIdHeader(session): SessionIdHeader,
) -> Result<ApiSuccess<ListWarrensResponseData>, ApiError> {
let domain_request = AuthRequest::new(session, ListWarrensRequest::new());
let domain_request = AuthRequest::new(session, ());
state
.auth_service

View File

@@ -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");
}

View File

@@ -24,6 +24,10 @@ impl NotifierDebugLogger {
}
impl WarrenNotifier for NotifierDebugLogger {
async fn warrens_fetched(&self, warrens: &Vec<Warren>) {
tracing::debug!("[Notifier] Fetched {} warren(s)", warrens.len());
}
async fn warrens_listed(&self, warrens: &Vec<Warren>) {
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<UserWarren>) {
tracing::debug!(
"[Notifier] Fetched {} user warrens for authenticated user {}",

View File

@@ -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<Vec<Warren>, sqlx::Error> {
let warrens: Vec<Warren> = sqlx::query_as::<sqlx::Postgres, Warren>(
"
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<UserWarren, sqlx::Error> {
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<UserWarren, sqlx::Error> {
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<UserWarren, sqlx::Error> {
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<Vec<Warren>, 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<Vec<Warren>, 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<UserWarren, CreateUserWarrenError> {
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<UserWarren, EditUserWarrenError> {
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<UserWarren, DeleteUserWarrenError> {
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,