edit users
This commit is contained in:
60
backend/src/lib/domain/warren/models/user/requests/edit.rs
Normal file
60
backend/src/lib/domain/warren/models/user/requests/edit.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::models::user::{UserEmail, UserName, UserPassword};
|
||||
|
||||
/// An admin request to edit an existing user
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct EditUserRequest {
|
||||
user_id: Uuid,
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
password: Option<UserPassword>,
|
||||
admin: bool,
|
||||
}
|
||||
|
||||
impl EditUserRequest {
|
||||
pub fn new(
|
||||
user_id: Uuid,
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
password: Option<UserPassword>,
|
||||
admin: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_id,
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
admin,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_id(&self) -> &Uuid {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &UserName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn email(&self) -> &UserEmail {
|
||||
&self.email
|
||||
}
|
||||
|
||||
pub fn password(&self) -> Option<&UserPassword> {
|
||||
self.password.as_ref()
|
||||
}
|
||||
|
||||
pub fn admin(&self) -> bool {
|
||||
self.admin
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EditUserError {
|
||||
#[error("There is no user with this id")]
|
||||
NotFound,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::domain::warren::models::{
|
||||
user::User,
|
||||
user_warren::{UserWarren, requests::ListUserWarrensError},
|
||||
warren::{FetchWarrensError, Warren},
|
||||
};
|
||||
|
||||
use super::ListUsersError;
|
||||
|
||||
/// An admin request to list all users, user warrens and warrens
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListAllUsersAndWarrensRequest {}
|
||||
|
||||
impl ListAllUsersAndWarrensRequest {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListAllUsersAndWarrensError {
|
||||
#[error(transparent)]
|
||||
ListUsers(#[from] ListUsersError),
|
||||
#[error(transparent)]
|
||||
ListUserWarrens(#[from] ListUserWarrensError),
|
||||
#[error(transparent)]
|
||||
FetchWarrens(#[from] FetchWarrensError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListAllUsersAndWarrensResponse {
|
||||
users: Vec<User>,
|
||||
user_warrens: Vec<UserWarren>,
|
||||
warrens: Vec<Warren>,
|
||||
}
|
||||
|
||||
impl ListAllUsersAndWarrensResponse {
|
||||
pub fn new(users: Vec<User>, user_warrens: Vec<UserWarren>, warrens: Vec<Warren>) -> Self {
|
||||
Self {
|
||||
users,
|
||||
user_warrens,
|
||||
warrens,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn users(&self) -> &Vec<User> {
|
||||
&self.users
|
||||
}
|
||||
|
||||
pub fn user_warrens(&self) -> &Vec<UserWarren> {
|
||||
&self.user_warrens
|
||||
}
|
||||
|
||||
pub fn warrens(&self) -> &Vec<Warren> {
|
||||
&self.warrens
|
||||
}
|
||||
|
||||
pub fn unpack(self) -> (Vec<User>, Vec<UserWarren>, Vec<Warren>) {
|
||||
(self.users, self.user_warrens, self.warrens)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
mod create;
|
||||
mod delete;
|
||||
mod edit;
|
||||
mod list;
|
||||
mod list_all;
|
||||
mod login;
|
||||
mod register;
|
||||
mod verify_password;
|
||||
|
||||
pub use create::*;
|
||||
pub use delete::*;
|
||||
pub use edit::*;
|
||||
pub use list::*;
|
||||
pub use list_all::*;
|
||||
pub use login::*;
|
||||
pub use register::*;
|
||||
pub use verify_password::*;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FetchUserWarrenRequest {
|
||||
user_id: Uuid,
|
||||
warren_id: Uuid,
|
||||
}
|
||||
|
||||
impl FetchUserWarrenRequest {
|
||||
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 FetchUserWarrenError {
|
||||
#[error("This user warren does not exist")]
|
||||
NotFound,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -42,31 +42,3 @@ pub enum ListWarrensError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FetchUserWarrenRequest {
|
||||
user_id: Uuid,
|
||||
warren_id: Uuid,
|
||||
}
|
||||
|
||||
impl FetchUserWarrenRequest {
|
||||
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 FetchUserWarrenError {
|
||||
#[error("This user warren does not exist")]
|
||||
NotFound,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use thiserror::Error;
|
||||
|
||||
/// A request to list all user warrens (admin only)
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ListUserWarrensRequest {}
|
||||
|
||||
impl ListUserWarrensRequest {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ListUserWarrensError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod fetch_user_warren;
|
||||
mod fetch_user_warrens;
|
||||
mod list_user_warrens;
|
||||
pub use fetch_user_warren::*;
|
||||
pub use fetch_user_warrens::*;
|
||||
pub use list_user_warrens::*;
|
||||
@@ -59,9 +59,15 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_user_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_user_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_user_edit_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_user_edit_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_user_list_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_user_list_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_list_all_users_and_warrens_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_list_all_users_and_warrens_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_user_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_user_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
|
||||
@@ -20,9 +20,10 @@ use super::models::{
|
||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
||||
ListUsersRequest, LoginUserError, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
||||
RegisterUserRequest, User,
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, LoginUserError,
|
||||
LoginUserRequest, LoginUserResponse, RegisterUserError, RegisterUserRequest, User,
|
||||
},
|
||||
user_warren::{
|
||||
UserWarren,
|
||||
@@ -121,11 +122,17 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
request: LoginUserRequest,
|
||||
) -> impl Future<Output = Result<LoginUserResponse, LoginUserError>> + Send;
|
||||
|
||||
/// An action that creates a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||
fn create_user(
|
||||
&self,
|
||||
request: AuthRequest<CreateUserRequest>,
|
||||
) -> impl Future<Output = Result<User, AuthError<CreateUserError>>> + Send;
|
||||
/// An action that edits a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||
fn edit_user(
|
||||
&self,
|
||||
request: AuthRequest<EditUserRequest>,
|
||||
) -> impl Future<Output = Result<User, AuthError<EditUserError>>> + Send;
|
||||
/// An action that deletes a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||
fn delete_user(
|
||||
&self,
|
||||
@@ -137,6 +144,15 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
request: AuthRequest<ListUsersRequest>,
|
||||
) -> impl Future<Output = Result<Vec<User>, AuthError<ListUsersError>>> + Send;
|
||||
|
||||
/// An action that lists all users, user warrens and warrens (MUST REQUIRE ADMIN PRIVILEGES)
|
||||
fn list_all_users_and_warrens<WS: WarrenService>(
|
||||
&self,
|
||||
request: AuthRequest<ListAllUsersAndWarrensRequest>,
|
||||
warren_service: &WS,
|
||||
) -> impl Future<
|
||||
Output = Result<ListAllUsersAndWarrensResponse, AuthError<ListAllUsersAndWarrensError>>,
|
||||
> + Send;
|
||||
|
||||
fn create_auth_session(
|
||||
&self,
|
||||
request: CreateAuthSessionRequest,
|
||||
|
||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
||||
use crate::domain::warren::models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{File, FilePath},
|
||||
user::{LoginUserResponse, User},
|
||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||
user_warren::UserWarren,
|
||||
warren::{
|
||||
CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse,
|
||||
@@ -74,12 +74,18 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||
fn user_registered(&self, user: &User) -> 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_edited(&self, editor: &User, edited: &User) -> impl Future<Output = ()> + Send;
|
||||
fn user_deleted(&self, deleter: &User, user: &User) -> impl Future<Output = ()> + Send;
|
||||
/// Lists all the users (admin action)
|
||||
///
|
||||
/// * `user`: The user who requested the list
|
||||
/// * `users`: The users from the list
|
||||
fn users_listed(&self, user: &User, users: &Vec<User>) -> impl Future<Output = ()> + Send;
|
||||
fn all_users_and_warrens_listed(
|
||||
&self,
|
||||
user: &User,
|
||||
response: &ListAllUsersAndWarrensResponse,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn auth_session_created(&self, user_id: &Uuid) -> impl Future<Output = ()> + Send;
|
||||
fn auth_session_fetched(
|
||||
|
||||
@@ -12,14 +12,16 @@ use crate::domain::warren::models::{
|
||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
||||
ListUsersRequest, User, VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User,
|
||||
VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||
},
|
||||
user_warren::{
|
||||
UserWarren,
|
||||
requests::{
|
||||
FetchUserWarrenError, FetchUserWarrenRequest, FetchUserWarrensError,
|
||||
FetchUserWarrensRequest,
|
||||
FetchUserWarrensRequest, ListUserWarrensError, ListUserWarrensRequest,
|
||||
},
|
||||
},
|
||||
warren::{
|
||||
@@ -27,6 +29,8 @@ use crate::domain::warren::models::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::WarrenService;
|
||||
|
||||
pub trait WarrenRepository: Clone + Send + Sync + 'static {
|
||||
fn list_warrens(
|
||||
&self,
|
||||
@@ -74,6 +78,11 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
request: CreateUserRequest,
|
||||
) -> impl Future<Output = Result<User, CreateUserError>> + Send;
|
||||
/// An action that edits a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||
fn edit_user(
|
||||
&self,
|
||||
request: EditUserRequest,
|
||||
) -> impl Future<Output = Result<User, EditUserError>> + Send;
|
||||
fn list_users(
|
||||
&self,
|
||||
request: ListUsersRequest,
|
||||
@@ -83,11 +92,23 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
|
||||
request: DeleteUserRequest,
|
||||
) -> impl Future<Output = Result<User, DeleteUserError>> + Send;
|
||||
|
||||
/// An action that lists all users, user warrens and warrens (MUST REQUIRE ADMIN PRIVILEGES)
|
||||
fn list_all_users_and_warrens<WS: WarrenService>(
|
||||
&self,
|
||||
request: ListAllUsersAndWarrensRequest,
|
||||
warren_service: &WS,
|
||||
) -> impl Future<Output = Result<ListAllUsersAndWarrensResponse, ListAllUsersAndWarrensError>> + Send;
|
||||
|
||||
fn verify_user_password(
|
||||
&self,
|
||||
request: VerifyUserPasswordRequest,
|
||||
) -> impl Future<Output = Result<User, VerifyUserPasswordError>> + Send;
|
||||
|
||||
fn list_user_warrens(
|
||||
&self,
|
||||
request: ListUserWarrensRequest,
|
||||
) -> impl Future<Output = Result<Vec<UserWarren>, ListUserWarrensError>> + Send;
|
||||
|
||||
fn create_auth_session(
|
||||
&self,
|
||||
request: CreateAuthSessionRequest,
|
||||
|
||||
@@ -11,8 +11,10 @@ use crate::{
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||
ListUsersError, ListUsersRequest, LoginUserError, LoginUserRequest,
|
||||
LoginUserResponse, RegisterUserError, RegisterUserRequest, User,
|
||||
EditUserError, EditUserRequest, ListAllUsersAndWarrensError,
|
||||
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
|
||||
ListUsersRequest, LoginUserError, LoginUserRequest, LoginUserResponse,
|
||||
RegisterUserError, RegisterUserRequest, User,
|
||||
},
|
||||
user_warren::{
|
||||
UserWarren,
|
||||
@@ -166,6 +168,32 @@ where
|
||||
result.map_err(AuthError::Custom)
|
||||
}
|
||||
|
||||
async fn edit_user(
|
||||
&self,
|
||||
request: AuthRequest<EditUserRequest>,
|
||||
) -> Result<User, AuthError<EditUserError>> {
|
||||
let (session, request) = request.unpack();
|
||||
|
||||
let response = self
|
||||
.fetch_auth_session(FetchAuthSessionRequest::new(session.session_id().clone()))
|
||||
.await?;
|
||||
|
||||
if !response.user().admin() {
|
||||
return Err(AuthError::InsufficientPermissions);
|
||||
}
|
||||
|
||||
let result = self.repository.edit_user(request).await;
|
||||
|
||||
if let Ok(user) = result.as_ref() {
|
||||
self.metrics.record_user_edit_success().await;
|
||||
self.notifier.user_edited(response.user(), user).await;
|
||||
} else {
|
||||
self.metrics.record_user_edit_failure().await;
|
||||
}
|
||||
|
||||
result.map_err(AuthError::Custom)
|
||||
}
|
||||
|
||||
async fn delete_user(
|
||||
&self,
|
||||
request: AuthRequest<DeleteUserRequest>,
|
||||
@@ -222,6 +250,42 @@ where
|
||||
result.map_err(AuthError::Custom)
|
||||
}
|
||||
|
||||
async fn list_all_users_and_warrens<WS: WarrenService>(
|
||||
&self,
|
||||
request: AuthRequest<ListAllUsersAndWarrensRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<ListAllUsersAndWarrensResponse, AuthError<ListAllUsersAndWarrensError>> {
|
||||
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 = self
|
||||
.repository
|
||||
.list_all_users_and_warrens(request, warren_service)
|
||||
.await;
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics
|
||||
.record_list_all_users_and_warrens_success()
|
||||
.await;
|
||||
self.notifier
|
||||
.all_users_and_warrens_listed(session_response.user(), response)
|
||||
.await;
|
||||
} else {
|
||||
self.metrics
|
||||
.record_list_all_users_and_warrens_failure()
|
||||
.await;
|
||||
}
|
||||
|
||||
result.map_err(AuthError::Custom)
|
||||
}
|
||||
|
||||
async fn create_auth_session(
|
||||
&self,
|
||||
request: CreateAuthSessionRequest,
|
||||
|
||||
103
backend/src/lib/inbound/http/handlers/admin/edit_user.rs
Normal file
103
backend/src/lib/inbound/http/handlers/admin/edit_user.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use axum::{Json, extract::State, http::StatusCode};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::{
|
||||
auth_session::AuthRequest,
|
||||
user::{
|
||||
EditUserRequest, UserEmail, UserEmailError, UserName, UserNameError, UserPassword,
|
||||
UserPasswordError,
|
||||
},
|
||||
},
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
handlers::{UserData, extractors::SessionIdHeader},
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub(super) enum ParseEditUserHttpRequestError {
|
||||
#[error(transparent)]
|
||||
UserName(#[from] UserNameError),
|
||||
#[error(transparent)]
|
||||
UserEmail(#[from] UserEmailError),
|
||||
#[error(transparent)]
|
||||
UserPassword(#[from] UserPasswordError),
|
||||
}
|
||||
|
||||
impl From<ParseEditUserHttpRequestError> for ApiError {
|
||||
fn from(value: ParseEditUserHttpRequestError) -> Self {
|
||||
match value {
|
||||
ParseEditUserHttpRequestError::UserName(err) => match err {
|
||||
UserNameError::Empty => {
|
||||
Self::BadRequest("The username must not be empty".to_string())
|
||||
}
|
||||
},
|
||||
ParseEditUserHttpRequestError::UserEmail(err) => match err {
|
||||
UserEmailError::Invalid => Self::BadRequest("The email is invalid".to_string()),
|
||||
UserEmailError::Empty => {
|
||||
Self::BadRequest("The email must not be empty".to_string())
|
||||
}
|
||||
},
|
||||
ParseEditUserHttpRequestError::UserPassword(err) => Self::BadRequest(
|
||||
match err {
|
||||
UserPasswordError::Empty => "The provided password is empty",
|
||||
// Best not give a potential bad actor any hints since this is the login and
|
||||
// not the registration
|
||||
UserPasswordError::LeadingWhitespace
|
||||
| UserPasswordError::TrailingWhitespace
|
||||
| UserPasswordError::TooShort
|
||||
| UserPasswordError::TooLong => "",
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct EditUserHttpRequestBody {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
email: String,
|
||||
password: Option<String>,
|
||||
admin: bool,
|
||||
}
|
||||
|
||||
impl EditUserHttpRequestBody {
|
||||
fn try_into_domain(self) -> Result<EditUserRequest, ParseEditUserHttpRequestError> {
|
||||
let name = UserName::new(&self.name)?;
|
||||
let email = UserEmail::new(&self.email)?;
|
||||
let password = if let Some(password) = self.password.as_ref() {
|
||||
Some(UserPassword::new(password)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(EditUserRequest::new(
|
||||
self.id, name, email, password, self.admin,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn edit_user<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
SessionIdHeader(session): SessionIdHeader,
|
||||
Json(request): Json<EditUserHttpRequestBody>,
|
||||
) -> Result<ApiSuccess<UserData>, ApiError> {
|
||||
let domain_request = request.try_into_domain()?;
|
||||
|
||||
state
|
||||
.auth_service
|
||||
.edit_user(AuthRequest::new(session, domain_request))
|
||||
.await
|
||||
.map(|user| ApiSuccess::new(StatusCode::CREATED, user.into()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use axum::{extract::State, http::StatusCode};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::{
|
||||
auth_session::AuthRequest,
|
||||
user::{ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse},
|
||||
},
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
handlers::{UserData, UserWarrenData, WarrenData, extractors::SessionIdHeader},
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct ListAllUsersAndWarrensHttpResponseBody {
|
||||
users: Vec<UserData>,
|
||||
user_warrens: Vec<UserWarrenData>,
|
||||
warrens: Vec<WarrenData>,
|
||||
}
|
||||
|
||||
impl From<ListAllUsersAndWarrensResponse> for ListAllUsersAndWarrensHttpResponseBody {
|
||||
fn from(value: ListAllUsersAndWarrensResponse) -> Self {
|
||||
let (users, user_warrens, warrens) = value.unpack();
|
||||
|
||||
Self {
|
||||
users: users.into_iter().map(Into::into).collect(),
|
||||
user_warrens: user_warrens.into_iter().map(Into::into).collect(),
|
||||
warrens: warrens.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_all_users_and_warrens<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
SessionIdHeader(session): SessionIdHeader,
|
||||
) -> Result<ApiSuccess<ListAllUsersAndWarrensHttpResponseBody>, ApiError> {
|
||||
state
|
||||
.auth_service
|
||||
.list_all_users_and_warrens(
|
||||
AuthRequest::new(session, ListAllUsersAndWarrensRequest::new()),
|
||||
state.warren_service.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map(|response| ApiSuccess::new(StatusCode::OK, response.into()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
mod create_user;
|
||||
mod delete_user;
|
||||
mod edit_user;
|
||||
mod list_all_users_and_warrens;
|
||||
mod list_users;
|
||||
|
||||
use create_user::create_user;
|
||||
use delete_user::delete_user;
|
||||
use edit_user::edit_user;
|
||||
use list_all_users_and_warrens::list_all_users_and_warrens;
|
||||
use list_users::list_users;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, post},
|
||||
routing::{delete, get, patch, post},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -18,7 +22,9 @@ use crate::{
|
||||
|
||||
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||
Router::new()
|
||||
.route("/all", get(list_all_users_and_warrens))
|
||||
.route("/users", get(list_users))
|
||||
.route("/users", post(create_user))
|
||||
.route("/users", patch(edit_user))
|
||||
.route("/users", delete(delete_user))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::models::user::User;
|
||||
use crate::domain::warren::models::{user::User, user_warren::UserWarren, warren::Warren};
|
||||
|
||||
pub mod admin;
|
||||
pub mod auth;
|
||||
@@ -28,3 +28,49 @@ impl From<User> for UserData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// A user warren that can be safely sent to the client
|
||||
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 {
|
||||
fn from(value: UserWarren) -> Self {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// A warren that can be safely sent to the client
|
||||
pub(super) struct WarrenData {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl From<Warren> for WarrenData {
|
||||
fn from(value: Warren) -> Self {
|
||||
Self {
|
||||
id: *value.id(),
|
||||
name: value.name().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,13 @@ impl AuthMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] User creation failed");
|
||||
}
|
||||
|
||||
async fn record_user_edit_success(&self) {
|
||||
tracing::debug!("[Metrics] User edit succeeded");
|
||||
}
|
||||
async fn record_user_edit_failure(&self) {
|
||||
tracing::debug!("[Metrics] User edit failed");
|
||||
}
|
||||
|
||||
async fn record_user_deletion_success(&self) {
|
||||
tracing::debug!("[Metrics] User deletion succeeded");
|
||||
}
|
||||
@@ -152,13 +159,20 @@ impl AuthMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] User deletion failed");
|
||||
}
|
||||
|
||||
async fn record_user_list_success(&self) -> () {
|
||||
async fn record_user_list_success(&self) {
|
||||
tracing::debug!("[Metrics] User list succeeded");
|
||||
}
|
||||
async fn record_user_list_failure(&self) -> () {
|
||||
async fn record_user_list_failure(&self) {
|
||||
tracing::debug!("[Metrics] User list failed");
|
||||
}
|
||||
|
||||
async fn record_list_all_users_and_warrens_success(&self) {
|
||||
tracing::debug!("[Metrics] Users, warrens and user warrens list succeeded");
|
||||
}
|
||||
async fn record_list_all_users_and_warrens_failure(&self) {
|
||||
tracing::debug!("[Metrics] Users, warrens and user warrens list failed");
|
||||
}
|
||||
|
||||
async fn record_auth_session_creation_success(&self) {
|
||||
tracing::debug!("[Metrics] Auth session creation succeeded");
|
||||
}
|
||||
@@ -180,10 +194,10 @@ impl AuthMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] Auth warren list failed");
|
||||
}
|
||||
|
||||
async fn record_auth_fetch_user_warrens_success(&self) -> () {
|
||||
async fn record_auth_fetch_user_warrens_success(&self) {
|
||||
tracing::debug!("[Metrics] Auth user warren id fetch succeeded");
|
||||
}
|
||||
async fn record_auth_fetch_user_warrens_failure(&self) -> () {
|
||||
async fn record_auth_fetch_user_warrens_failure(&self) {
|
||||
tracing::debug!("[Metrics] Auth user warren id fetch failed");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::domain::warren::{
|
||||
models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{File, FilePath},
|
||||
user::{LoginUserResponse, User},
|
||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||
user_warren::UserWarren,
|
||||
warren::{
|
||||
CreateWarrenDirectoryResponse, DeleteWarrenDirectoryResponse, DeleteWarrenFileResponse,
|
||||
@@ -129,6 +129,14 @@ impl AuthNotifier for NotifierDebugLogger {
|
||||
);
|
||||
}
|
||||
|
||||
async fn user_edited(&self, editor: &User, user: &User) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Admin user {} edited user {}",
|
||||
editor.name(),
|
||||
user.id()
|
||||
);
|
||||
}
|
||||
|
||||
async fn user_deleted(&self, deleter: &User, user: &User) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Admin user {} deleted user {}",
|
||||
@@ -145,6 +153,20 @@ impl AuthNotifier for NotifierDebugLogger {
|
||||
);
|
||||
}
|
||||
|
||||
async fn all_users_and_warrens_listed(
|
||||
&self,
|
||||
user: &User,
|
||||
response: &ListAllUsersAndWarrensResponse,
|
||||
) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Admin user {} listed {} user(s), {} warren(s) and {} user warren(s)",
|
||||
user.name(),
|
||||
response.users().len(),
|
||||
response.warrens().len(),
|
||||
response.user_warrens().len(),
|
||||
);
|
||||
}
|
||||
|
||||
async fn user_logged_in(&self, response: &LoginUserResponse) {
|
||||
tracing::debug!("[Notifier] Logged in user {}", response.user().name());
|
||||
}
|
||||
|
||||
@@ -25,22 +25,23 @@ use crate::domain::warren::{
|
||||
},
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
||||
ListUsersRequest, User, UserEmail, UserName, UserPassword, VerifyUserPasswordError,
|
||||
VerifyUserPasswordRequest,
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User, UserEmail,
|
||||
UserName, UserPassword, VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||
},
|
||||
user_warren::{
|
||||
UserWarren,
|
||||
requests::{
|
||||
FetchUserWarrenError, FetchUserWarrenRequest, FetchUserWarrensError,
|
||||
FetchUserWarrensRequest,
|
||||
FetchUserWarrensRequest, ListUserWarrensError, ListUserWarrensRequest,
|
||||
},
|
||||
},
|
||||
warren::{
|
||||
FetchWarrenError, FetchWarrenRequest, FetchWarrensError, FetchWarrensRequest, Warren,
|
||||
},
|
||||
},
|
||||
ports::{AuthRepository, WarrenRepository},
|
||||
ports::{AuthRepository, WarrenRepository, WarrenService},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -138,12 +139,8 @@ impl Postgres {
|
||||
password: &UserPassword,
|
||||
is_admin: bool,
|
||||
) -> anyhow::Result<User> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let password_hash = argon2
|
||||
.hash_password(password.as_str().as_bytes(), &salt)
|
||||
.map_err(|_| anyhow!("Failed to hash password"))?
|
||||
.to_string();
|
||||
let password_hash =
|
||||
hash_password(password).map_err(|e| anyhow!("Failed to hash password: {e:?}"))?;
|
||||
|
||||
let mut tx = connection.begin().await?;
|
||||
|
||||
@@ -176,6 +173,73 @@ impl Postgres {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn edit_user(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
id: &Uuid,
|
||||
name: &UserName,
|
||||
email: &UserEmail,
|
||||
password: Option<&UserPassword>,
|
||||
is_admin: bool,
|
||||
) -> anyhow::Result<User> {
|
||||
let password_hash = if let Some(password) = password {
|
||||
Some(hash_password(password).map_err(|e| anyhow!("Failed to hash password: {e:?}"))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut tx = connection.begin().await?;
|
||||
|
||||
let user: User = sqlx::query_as(
|
||||
"UPDATE
|
||||
users
|
||||
SET
|
||||
name = $2,
|
||||
email = $3,
|
||||
hash = COALESCE($4, hash),
|
||||
admin = $5
|
||||
WHERE
|
||||
id = $1
|
||||
RETURNING
|
||||
*
|
||||
",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(name)
|
||||
.bind(email)
|
||||
.bind(password_hash)
|
||||
.bind(is_admin)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
self.delete_user_sessions(&mut *tx, id).await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn delete_user_sessions(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
user_id: &Uuid,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
let rows_affected = sqlx::query(
|
||||
"
|
||||
DELETE FROM
|
||||
auth_sessions
|
||||
WHERE
|
||||
user_id = $1
|
||||
",
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(connection)
|
||||
.await?
|
||||
.rows_affected();
|
||||
|
||||
Ok(rows_affected)
|
||||
}
|
||||
|
||||
async fn delete_user_from_database(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
@@ -307,7 +371,7 @@ impl Postgres {
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
session_id: &AuthSessionId,
|
||||
) -> anyhow::Result<AuthSession> {
|
||||
) -> Result<AuthSession, sqlx::Error> {
|
||||
let session: AuthSession = sqlx::query_as(
|
||||
"
|
||||
SELECT
|
||||
@@ -330,7 +394,7 @@ impl Postgres {
|
||||
connection: &mut PgConnection,
|
||||
user_id: &Uuid,
|
||||
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
||||
let ids: Vec<UserWarren> = sqlx::query_as(
|
||||
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
||||
"
|
||||
SELECT
|
||||
*
|
||||
@@ -344,7 +408,25 @@ impl Postgres {
|
||||
.fetch_all(connection)
|
||||
.await?;
|
||||
|
||||
Ok(ids)
|
||||
Ok(user_warrens)
|
||||
}
|
||||
|
||||
async fn get_all_user_warrens(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
) -> Result<Vec<UserWarren>, sqlx::Error> {
|
||||
let user_warrens: Vec<UserWarren> = sqlx::query_as(
|
||||
"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
user_warrens
|
||||
",
|
||||
)
|
||||
.fetch_all(connection)
|
||||
.await?;
|
||||
|
||||
Ok(user_warrens)
|
||||
}
|
||||
|
||||
async fn get_user_warren(
|
||||
@@ -379,6 +461,8 @@ impl Postgres {
|
||||
*
|
||||
FROM
|
||||
users
|
||||
ORDER BY
|
||||
created_at ASC
|
||||
",
|
||||
)
|
||||
.fetch_all(connection)
|
||||
@@ -453,6 +537,28 @@ impl AuthRepository for Postgres {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn edit_user(&self, request: EditUserRequest) -> Result<User, EditUserError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
.acquire()
|
||||
.await
|
||||
.context("Failed to get a PostgreSQL connection")?;
|
||||
|
||||
let user = self
|
||||
.edit_user(
|
||||
&mut connection,
|
||||
request.user_id(),
|
||||
request.name(),
|
||||
request.email(),
|
||||
request.password(),
|
||||
request.admin(),
|
||||
)
|
||||
.await
|
||||
.context(format!("Failed to edit user"))?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn delete_user(&self, request: DeleteUserRequest) -> Result<User, DeleteUserError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
@@ -528,7 +634,13 @@ impl AuthRepository for Postgres {
|
||||
let session = self
|
||||
.get_auth_session(&mut connection, request.session_id())
|
||||
.await
|
||||
.context("Failed to get auth session")?;
|
||||
.map_err(|e| {
|
||||
if is_not_found_error(&e) {
|
||||
FetchAuthSessionError::NotFound
|
||||
} else {
|
||||
anyhow!("Failed to get auth session: {e:?}").into()
|
||||
}
|
||||
})?;
|
||||
let user = self
|
||||
.get_user_from_id(&mut connection, session.user_id())
|
||||
.await
|
||||
@@ -547,12 +659,30 @@ impl AuthRepository for Postgres {
|
||||
.await
|
||||
.context("Failed to get a PostgreSQL connection")?;
|
||||
|
||||
let warren_ids = self
|
||||
let user_warrens = self
|
||||
.get_user_warrens(&mut connection, request.user_id())
|
||||
.await
|
||||
.context("Failed to get user warrens")?;
|
||||
|
||||
Ok(warren_ids)
|
||||
Ok(user_warrens)
|
||||
}
|
||||
|
||||
async fn list_user_warrens(
|
||||
&self,
|
||||
_request: ListUserWarrensRequest,
|
||||
) -> Result<Vec<UserWarren>, ListUserWarrensError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
.acquire()
|
||||
.await
|
||||
.context("Failed to get a PostgreSQL connection")?;
|
||||
|
||||
let user_warrens = self
|
||||
.get_all_user_warrens(&mut connection)
|
||||
.await
|
||||
.context("Failed to get all user warrens")?;
|
||||
|
||||
Ok(user_warrens)
|
||||
}
|
||||
|
||||
async fn fetch_user_warren(
|
||||
@@ -590,8 +720,53 @@ impl AuthRepository for Postgres {
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
async fn list_all_users_and_warrens<WS: WarrenService>(
|
||||
&self,
|
||||
_request: ListAllUsersAndWarrensRequest,
|
||||
warren_service: &WS,
|
||||
) -> Result<ListAllUsersAndWarrensResponse, ListAllUsersAndWarrensError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
.acquire()
|
||||
.await
|
||||
.context("Failed to get a PostgreSQL connection")?;
|
||||
|
||||
let users = self
|
||||
.fetch_users(&mut connection)
|
||||
.await
|
||||
.context("Failed to fetch all users")?;
|
||||
let user_warrens = self
|
||||
.get_all_user_warrens(&mut connection)
|
||||
.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(),
|
||||
))
|
||||
.await
|
||||
.context("Failed to get warrens")?;
|
||||
|
||||
Ok(ListAllUsersAndWarrensResponse::new(
|
||||
users,
|
||||
user_warrens,
|
||||
warrens,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_not_found_error(err: &sqlx::Error) -> bool {
|
||||
matches!(err, sqlx::Error::RowNotFound)
|
||||
}
|
||||
|
||||
fn hash_password(password: &UserPassword) -> Result<String, argon2::password_hash::Error> {
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
argon2
|
||||
.hash_password(password.as_str().as_bytes(), &salt)
|
||||
.map(|h| h.to_string())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user