diff --git a/backend/src/lib/domain/warren/models/user/mod.rs b/backend/src/lib/domain/warren/models/user/mod.rs index 33d8258..3f4d3e4 100644 --- a/backend/src/lib/domain/warren/models/user/mod.rs +++ b/backend/src/lib/domain/warren/models/user/mod.rs @@ -105,3 +105,54 @@ impl UserEmail { Ok(Self(raw.to_string())) } } + +/// A valid user password +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct UserPassword(String); + +#[derive(Clone, Debug, Error)] +pub enum UserPasswordError { + #[error("A user's password must not be empty")] + Empty, + #[error("A user's password must not start with a whitespace")] + LeadingWhitespace, + #[error("A user's password must not end with a whitespace")] + TrailingWhitespace, + #[error("A user's password must be longer")] + TooShort, + #[error("A user's password must be shorter")] + TooLong, +} + +impl UserPassword { + const MIN_LENGTH: usize = 12; + const MAX_LENGTH: usize = 32; + + pub fn new(raw: &str) -> Result { + if raw.is_empty() { + return Err(UserPasswordError::Empty); + } + + if raw.trim_start().len() != raw.len() { + return Err(UserPasswordError::LeadingWhitespace); + } + + if raw.trim_end().len() != raw.len() { + return Err(UserPasswordError::TrailingWhitespace); + } + + if raw.len() < Self::MIN_LENGTH { + return Err(UserPasswordError::TooShort); + } + + if raw.len() > Self::MAX_LENGTH { + return Err(UserPasswordError::TooLong); + } + + Ok(Self(raw.to_string())) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} diff --git a/backend/src/lib/domain/warren/models/user/requests.rs b/backend/src/lib/domain/warren/models/user/requests.rs deleted file mode 100644 index 665a0f6..0000000 --- a/backend/src/lib/domain/warren/models/user/requests.rs +++ /dev/null @@ -1,246 +0,0 @@ -use thiserror::Error; - -use crate::domain::warren::models::auth_session::{AuthSession, requests::CreateAuthSessionError}; - -use super::{User, UserEmail, UserName}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RegisterUserRequest { - name: UserName, - email: UserEmail, - password: UserPassword, -} - -impl From for CreateUserRequest { - fn from(value: RegisterUserRequest) -> Self { - Self::new(value.name, value.email, value.password, false) - } -} - -#[derive(Debug, Error)] -pub enum RegisterUserError { - #[error(transparent)] - CreateUser(#[from] CreateUserError), - #[error(transparent)] - Unknown(#[from] anyhow::Error), -} - -/// A valid user password -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct UserPassword(String); - -#[derive(Clone, Debug, Error)] -pub enum UserPasswordError { - #[error("A user's password must not be empty")] - Empty, - #[error("A user's password must not start with a whitespace")] - LeadingWhitespace, - #[error("A user's password must not end with a whitespace")] - TrailingWhitespace, - #[error("A user's password must be longer")] - TooShort, - #[error("A user's password must be shorter")] - TooLong, -} - -impl UserPassword { - const MIN_LENGTH: usize = 12; - const MAX_LENGTH: usize = 32; - - pub fn new(raw: &str) -> Result { - if raw.is_empty() { - return Err(UserPasswordError::Empty); - } - - if raw.trim_start().len() != raw.len() { - return Err(UserPasswordError::LeadingWhitespace); - } - - if raw.trim_end().len() != raw.len() { - return Err(UserPasswordError::TrailingWhitespace); - } - - if raw.len() < Self::MIN_LENGTH { - return Err(UserPasswordError::TooShort); - } - - if raw.len() > Self::MAX_LENGTH { - return Err(UserPasswordError::TooLong); - } - - Ok(Self(raw.to_string())) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl RegisterUserRequest { - pub fn new(name: UserName, email: UserEmail, password: UserPassword) -> Self { - Self { - name, - email, - password, - } - } - - pub fn name(&self) -> &UserName { - &self.name - } - - pub fn email(&self) -> &UserEmail { - &self.email - } - - pub fn password(&self) -> &UserPassword { - &self.password - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct VerifyUserPasswordRequest { - email: UserEmail, - password: UserPassword, -} - -impl VerifyUserPasswordRequest { - pub fn new(email: UserEmail, password: UserPassword) -> Self { - Self { email, password } - } - - pub fn email(&self) -> &UserEmail { - &self.email - } - - pub fn password(&self) -> &UserPassword { - &self.password - } -} - -impl From for VerifyUserPasswordRequest { - fn from(value: LoginUserRequest) -> Self { - Self { - email: value.email, - password: value.password, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct LoginUserRequest { - email: UserEmail, - password: UserPassword, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct LoginUserResponse { - session: AuthSession, - user: User, -} - -impl LoginUserResponse { - pub fn new(session: AuthSession, user: User) -> Self { - Self { session, user } - } - - pub fn session(&self) -> &AuthSession { - &self.session - } - - pub fn user(&self) -> &User { - &self.user - } -} - -impl LoginUserRequest { - pub fn new(email: UserEmail, password: UserPassword) -> Self { - Self { email, password } - } - - pub fn email(&self) -> &UserEmail { - &self.email - } - - pub fn password(&self) -> &UserPassword { - &self.password - } -} - -#[derive(Debug, Error)] -pub enum VerifyUserPasswordError { - #[error("There is no user with this email: {0}")] - NotFound(UserEmail), - #[error("The password is incorrect")] - IncorrectPassword, - #[error(transparent)] - Unknown(#[from] anyhow::Error), -} - -#[derive(Debug, Error)] -pub enum LoginUserError { - #[error(transparent)] - VerifyUser(#[from] VerifyUserPasswordError), - #[error(transparent)] - CreateAuthToken(#[from] CreateAuthSessionError), - #[error(transparent)] - Unknown(#[from] anyhow::Error), -} - -/// An admin request to create a new user -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct CreateUserRequest { - name: UserName, - email: UserEmail, - password: UserPassword, - admin: bool, -} - -impl CreateUserRequest { - pub fn new(name: UserName, email: UserEmail, password: UserPassword, admin: bool) -> Self { - Self { - name, - email, - password, - admin, - } - } - - pub fn name(&self) -> &UserName { - &self.name - } - - pub fn email(&self) -> &UserEmail { - &self.email - } - - pub fn password(&self) -> &UserPassword { - &self.password - } - - pub fn admin(&self) -> bool { - self.admin - } -} - -#[derive(Debug, Error)] -pub enum CreateUserError { - #[error(transparent)] - Unknown(#[from] anyhow::Error), -} - -/// An admin request to list all users -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ListUsersRequest {} - -impl ListUsersRequest { - pub fn new() -> Self { - Self {} - } -} - -#[derive(Debug, Error)] -pub enum ListUsersError { - #[error(transparent)] - Unknown(#[from] anyhow::Error), -} diff --git a/backend/src/lib/domain/warren/models/user/requests/create.rs b/backend/src/lib/domain/warren/models/user/requests/create.rs new file mode 100644 index 0000000..67ec8a4 --- /dev/null +++ b/backend/src/lib/domain/warren/models/user/requests/create.rs @@ -0,0 +1,45 @@ +use thiserror::Error; + +use crate::domain::warren::models::user::{UserEmail, UserName, UserPassword}; + +/// An admin request to create a new user +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CreateUserRequest { + name: UserName, + email: UserEmail, + password: UserPassword, + admin: bool, +} + +impl CreateUserRequest { + pub fn new(name: UserName, email: UserEmail, password: UserPassword, admin: bool) -> Self { + Self { + name, + email, + password, + admin, + } + } + + pub fn name(&self) -> &UserName { + &self.name + } + + pub fn email(&self) -> &UserEmail { + &self.email + } + + pub fn password(&self) -> &UserPassword { + &self.password + } + + pub fn admin(&self) -> bool { + self.admin + } +} + +#[derive(Debug, Error)] +pub enum CreateUserError { + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/models/user/requests/delete.rs b/backend/src/lib/domain/warren/models/user/requests/delete.rs new file mode 100644 index 0000000..c7a01f5 --- /dev/null +++ b/backend/src/lib/domain/warren/models/user/requests/delete.rs @@ -0,0 +1,28 @@ +use thiserror::Error; +use uuid::Uuid; + +/// An admin request to delete a user +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeleteUserRequest { + user_id: Uuid, +} + +impl DeleteUserRequest { + pub fn new(user_id: Uuid) -> Self { + Self { user_id } + } + + pub fn user_id(&self) -> &Uuid { + &self.user_id + } +} + +#[derive(Debug, Error)] +pub enum DeleteUserError { + #[error("A user must not delete themself")] + NotSelf, + #[error("The user does not exist")] + NotFound, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/models/user/requests/list.rs b/backend/src/lib/domain/warren/models/user/requests/list.rs new file mode 100644 index 0000000..308b5f1 --- /dev/null +++ b/backend/src/lib/domain/warren/models/user/requests/list.rs @@ -0,0 +1,17 @@ +use thiserror::Error; + +/// An admin request to list all users +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ListUsersRequest {} + +impl ListUsersRequest { + pub fn new() -> Self { + Self {} + } +} + +#[derive(Debug, Error)] +pub enum ListUsersError { + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/models/user/requests/login.rs b/backend/src/lib/domain/warren/models/user/requests/login.rs new file mode 100644 index 0000000..8aec9b1 --- /dev/null +++ b/backend/src/lib/domain/warren/models/user/requests/login.rs @@ -0,0 +1,58 @@ +use thiserror::Error; + +use crate::domain::warren::models::{ + auth_session::{AuthSession, requests::CreateAuthSessionError}, + user::{User, UserEmail, UserPassword}, +}; + +use super::verify_password::VerifyUserPasswordError; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LoginUserRequest { + pub(super) email: UserEmail, + pub(super) password: UserPassword, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LoginUserResponse { + session: AuthSession, + user: User, +} + +impl LoginUserResponse { + pub fn new(session: AuthSession, user: User) -> Self { + Self { session, user } + } + + pub fn session(&self) -> &AuthSession { + &self.session + } + + pub fn user(&self) -> &User { + &self.user + } +} + +impl LoginUserRequest { + pub fn new(email: UserEmail, password: UserPassword) -> Self { + Self { email, password } + } + + pub fn email(&self) -> &UserEmail { + &self.email + } + + pub fn password(&self) -> &UserPassword { + &self.password + } +} + +#[derive(Debug, Error)] +pub enum LoginUserError { + #[error(transparent)] + VerifyUser(#[from] VerifyUserPasswordError), + #[error(transparent)] + CreateAuthToken(#[from] CreateAuthSessionError), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/models/user/requests/mod.rs b/backend/src/lib/domain/warren/models/user/requests/mod.rs new file mode 100644 index 0000000..e93aab7 --- /dev/null +++ b/backend/src/lib/domain/warren/models/user/requests/mod.rs @@ -0,0 +1,13 @@ +mod create; +mod delete; +mod list; +mod login; +mod register; +mod verify_password; + +pub use create::*; +pub use delete::*; +pub use list::*; +pub use login::*; +pub use register::*; +pub use verify_password::*; diff --git a/backend/src/lib/domain/warren/models/user/requests/register.rs b/backend/src/lib/domain/warren/models/user/requests/register.rs new file mode 100644 index 0000000..055aadd --- /dev/null +++ b/backend/src/lib/domain/warren/models/user/requests/register.rs @@ -0,0 +1,48 @@ +use thiserror::Error; + +use crate::domain::warren::models::user::{UserEmail, UserName, UserPassword}; + +use super::create::{CreateUserError, CreateUserRequest}; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RegisterUserRequest { + name: UserName, + email: UserEmail, + password: UserPassword, +} + +impl RegisterUserRequest { + pub fn new(name: UserName, email: UserEmail, password: UserPassword) -> Self { + Self { + name, + email, + password, + } + } + + pub fn name(&self) -> &UserName { + &self.name + } + + pub fn email(&self) -> &UserEmail { + &self.email + } + + pub fn password(&self) -> &UserPassword { + &self.password + } +} + +impl From for CreateUserRequest { + fn from(value: RegisterUserRequest) -> Self { + Self::new(value.name, value.email, value.password, false) + } +} + +#[derive(Debug, Error)] +pub enum RegisterUserError { + #[error(transparent)] + CreateUser(#[from] CreateUserError), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/models/user/requests/verify_password.rs b/backend/src/lib/domain/warren/models/user/requests/verify_password.rs new file mode 100644 index 0000000..a71b0a4 --- /dev/null +++ b/backend/src/lib/domain/warren/models/user/requests/verify_password.rs @@ -0,0 +1,44 @@ +use thiserror::Error; + +use crate::domain::warren::models::user::{UserEmail, UserPassword}; + +use super::login::LoginUserRequest; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct VerifyUserPasswordRequest { + email: UserEmail, + password: UserPassword, +} + +impl VerifyUserPasswordRequest { + pub fn new(email: UserEmail, password: UserPassword) -> Self { + Self { email, password } + } + + pub fn email(&self) -> &UserEmail { + &self.email + } + + pub fn password(&self) -> &UserPassword { + &self.password + } +} + +impl From for VerifyUserPasswordRequest { + fn from(value: LoginUserRequest) -> Self { + Self { + email: value.email, + password: value.password, + } + } +} + +#[derive(Debug, Error)] +pub enum VerifyUserPasswordError { + #[error("There is no user with this email: {0}")] + NotFound(UserEmail), + #[error("The password is incorrect")] + IncorrectPassword, + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} diff --git a/backend/src/lib/domain/warren/ports/metrics.rs b/backend/src/lib/domain/warren/ports/metrics.rs index a9e4578..655c057 100644 --- a/backend/src/lib/domain/warren/ports/metrics.rs +++ b/backend/src/lib/domain/warren/ports/metrics.rs @@ -62,6 +62,9 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static { fn record_user_list_success(&self) -> impl Future + Send; fn record_user_list_failure(&self) -> impl Future + Send; + fn record_user_deletion_success(&self) -> impl Future + Send; + fn record_user_deletion_failure(&self) -> impl Future + Send; + fn record_auth_session_creation_success(&self) -> impl Future + Send; fn record_auth_session_creation_failure(&self) -> impl Future + Send; diff --git a/backend/src/lib/domain/warren/ports/mod.rs b/backend/src/lib/domain/warren/ports/mod.rs index e6e23b9..9745e09 100644 --- a/backend/src/lib/domain/warren/ports/mod.rs +++ b/backend/src/lib/domain/warren/ports/mod.rs @@ -20,8 +20,9 @@ use super::models::{ FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest, }, user::{ - CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, LoginUserError, - LoginUserRequest, LoginUserResponse, RegisterUserError, RegisterUserRequest, User, + CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError, + ListUsersRequest, LoginUserError, LoginUserRequest, LoginUserResponse, RegisterUserError, + RegisterUserRequest, User, }, user_warren::{ UserWarren, @@ -125,6 +126,12 @@ pub trait AuthService: Clone + Send + Sync + 'static { &self, request: AuthRequest, ) -> impl Future>> + Send; + /// An action that deletes a user (MUST REQUIRE ADMIN PRIVILEGES) + fn delete_user( + &self, + request: AuthRequest, + ) -> impl Future>> + Send; + /// An action that lists all users (MUST REQUIRE ADMIN PRIVILEGES) fn list_users( &self, request: AuthRequest, diff --git a/backend/src/lib/domain/warren/ports/notifier.rs b/backend/src/lib/domain/warren/ports/notifier.rs index c0db56d..72e37fe 100644 --- a/backend/src/lib/domain/warren/ports/notifier.rs +++ b/backend/src/lib/domain/warren/ports/notifier.rs @@ -74,6 +74,7 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static { fn user_registered(&self, user: &User) -> impl Future + Send; fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future + Send; fn user_created(&self, creator: &User, created: &User) -> impl Future + Send; + fn user_deleted(&self, deleter: &User, user: &User) -> impl Future + Send; /// Lists all the users (admin action) /// /// * `user`: The user who requested the list diff --git a/backend/src/lib/domain/warren/ports/repository.rs b/backend/src/lib/domain/warren/ports/repository.rs index 3734215..5c4b233 100644 --- a/backend/src/lib/domain/warren/ports/repository.rs +++ b/backend/src/lib/domain/warren/ports/repository.rs @@ -12,8 +12,8 @@ use crate::domain::warren::models::{ FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest, }, user::{ - CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, User, - VerifyUserPasswordError, VerifyUserPasswordRequest, + CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError, + ListUsersRequest, User, VerifyUserPasswordError, VerifyUserPasswordRequest, }, user_warren::{ UserWarren, @@ -78,6 +78,10 @@ pub trait AuthRepository: Clone + Send + Sync + 'static { &self, request: ListUsersRequest, ) -> impl Future, ListUsersError>> + Send; + fn delete_user( + &self, + request: DeleteUserRequest, + ) -> impl Future> + Send; fn verify_user_password( &self, diff --git a/backend/src/lib/domain/warren/service/auth.rs b/backend/src/lib/domain/warren/service/auth.rs index c7031d8..7bbf7bb 100644 --- a/backend/src/lib/domain/warren/service/auth.rs +++ b/backend/src/lib/domain/warren/service/auth.rs @@ -10,9 +10,9 @@ use crate::{ }, }, user::{ - CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, - LoginUserError, LoginUserRequest, LoginUserResponse, RegisterUserError, - RegisterUserRequest, User, + CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, + ListUsersError, ListUsersRequest, LoginUserError, LoginUserRequest, + LoginUserResponse, RegisterUserError, RegisterUserRequest, User, }, user_warren::{ UserWarren, @@ -166,6 +166,36 @@ where result.map_err(AuthError::Custom) } + async fn delete_user( + &self, + request: AuthRequest, + ) -> Result> { + 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); + } + + if request.user_id() == response.user().id() { + return Err(AuthError::Custom(DeleteUserError::NotSelf)); + } + + let result = self.repository.delete_user(request).await; + + if let Ok(user) = result.as_ref() { + self.metrics.record_user_deletion_success().await; + self.notifier.user_deleted(response.user(), user).await; + } else { + self.metrics.record_user_deletion_failure().await; + } + + result.map_err(AuthError::Custom) + } + async fn list_users( &self, request: AuthRequest, diff --git a/backend/src/lib/inbound/http/handlers/admin/delete_user.rs b/backend/src/lib/inbound/http/handlers/admin/delete_user.rs new file mode 100644 index 0000000..fd866b8 --- /dev/null +++ b/backend/src/lib/inbound/http/handlers/admin/delete_user.rs @@ -0,0 +1,42 @@ +use axum::{Json, extract::State, http::StatusCode}; +use serde::Deserialize; +use uuid::Uuid; + +use crate::{ + domain::warren::{ + models::{auth_session::AuthRequest, user::DeleteUserRequest}, + ports::{AuthService, WarrenService}, + }, + inbound::http::{ + AppState, + handlers::{UserData, extractors::SessionIdHeader}, + responses::{ApiError, ApiSuccess}, + }, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct DeleteUserHttpRequestBody { + user_id: Uuid, +} + +impl DeleteUserHttpRequestBody { + pub fn into_domain(self) -> DeleteUserRequest { + DeleteUserRequest::new(self.user_id) + } +} + +pub async fn delete_user( + State(state): State>, + SessionIdHeader(session): SessionIdHeader, + Json(request): Json, +) -> Result, ApiError> { + let request = request.into_domain(); + + state + .auth_service + .delete_user(AuthRequest::new(session, request)) + .await + .map(|user| ApiSuccess::new(StatusCode::OK, user.into())) + .map_err(ApiError::from) +} diff --git a/backend/src/lib/inbound/http/handlers/admin/mod.rs b/backend/src/lib/inbound/http/handlers/admin/mod.rs index 57cf33d..75dcf14 100644 --- a/backend/src/lib/inbound/http/handlers/admin/mod.rs +++ b/backend/src/lib/inbound/http/handlers/admin/mod.rs @@ -1,12 +1,14 @@ mod create_user; +mod delete_user; mod list_users; use create_user::create_user; +use delete_user::delete_user; use list_users::list_users; use axum::{ Router, - routing::{get, post}, + routing::{delete, get, post}, }; use crate::{ @@ -18,4 +20,5 @@ pub fn routes() -> Router> Router::new() .route("/users", get(list_users)) .route("/users", post(create_user)) + .route("/users", delete(delete_user)) } diff --git a/backend/src/lib/outbound/metrics_debug_logger.rs b/backend/src/lib/outbound/metrics_debug_logger.rs index c6037cf..0277bc0 100644 --- a/backend/src/lib/outbound/metrics_debug_logger.rs +++ b/backend/src/lib/outbound/metrics_debug_logger.rs @@ -145,6 +145,13 @@ impl AuthMetrics for MetricsDebugLogger { tracing::debug!("[Metrics] User creation failed"); } + async fn record_user_deletion_success(&self) { + tracing::debug!("[Metrics] User deletion succeeded"); + } + async fn record_user_deletion_failure(&self) { + tracing::debug!("[Metrics] User deletion failed"); + } + async fn record_user_list_success(&self) -> () { tracing::debug!("[Metrics] User list succeeded"); } diff --git a/backend/src/lib/outbound/notifier_debug_logger.rs b/backend/src/lib/outbound/notifier_debug_logger.rs index b2564c6..5a24d01 100644 --- a/backend/src/lib/outbound/notifier_debug_logger.rs +++ b/backend/src/lib/outbound/notifier_debug_logger.rs @@ -121,11 +121,19 @@ impl AuthNotifier for NotifierDebugLogger { tracing::debug!("[Notifier] Registered user {}", user.name()); } - async fn user_created(&self, creator: &User, created: &User) { + async fn user_created(&self, creator: &User, user: &User) { tracing::debug!( "[Notifier] Admin user {} created user {}", creator.name(), - created.name() + user.name() + ); + } + + async fn user_deleted(&self, deleter: &User, user: &User) { + tracing::debug!( + "[Notifier] Admin user {} deleted user {}", + deleter.name(), + user.name() ); } diff --git a/backend/src/lib/outbound/postgres.rs b/backend/src/lib/outbound/postgres.rs index 86d0f55..5f12e0f 100644 --- a/backend/src/lib/outbound/postgres.rs +++ b/backend/src/lib/outbound/postgres.rs @@ -25,8 +25,9 @@ use crate::domain::warren::{ }, }, user::{ - CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, User, UserEmail, - UserName, UserPassword, VerifyUserPasswordError, VerifyUserPasswordRequest, + CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError, + ListUsersRequest, User, UserEmail, UserName, UserPassword, VerifyUserPasswordError, + VerifyUserPasswordRequest, }, user_warren::{ UserWarren, @@ -175,6 +176,28 @@ impl Postgres { Ok(user) } + async fn delete_user_from_database( + &self, + connection: &mut PgConnection, + user_id: &Uuid, + ) -> Result { + let user: User = sqlx::query_as( + " + DELETE FROM + users + WHERE + id = $1 + RETURNING + * + ", + ) + .bind(user_id) + .fetch_one(connection) + .await?; + + Ok(user) + } + async fn get_user_from_id( &self, connection: &mut PgConnection, @@ -430,6 +453,24 @@ impl AuthRepository for Postgres { Ok(user) } + async fn delete_user(&self, request: DeleteUserRequest) -> Result { + let mut connection = self + .pool + .acquire() + .await + .context("Failed to get a PostgreSQL connection")?; + + self.delete_user_from_database(&mut connection, request.user_id()) + .await + .map_err(|e| { + if is_not_found_error(&e) { + DeleteUserError::NotFound + } else { + DeleteUserError::Unknown(anyhow!(e)) + } + }) + } + async fn verify_user_password( &self, request: VerifyUserPasswordRequest, diff --git a/frontend/components/admin/DeleteUserDialog.vue b/frontend/components/admin/DeleteUserDialog.vue index 52d97f5..86926c2 100644 --- a/frontend/components/admin/DeleteUserDialog.vue +++ b/frontend/components/admin/DeleteUserDialog.vue @@ -12,12 +12,15 @@ import { AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import type { AuthUser } from '#shared/types/auth'; +import { deleteUser } from '~/lib/api/admin/deleteUser'; const adminStore = useAdminStore(); // We'll only update this value if there is a user to prevent layout shifts on close const user = ref(); -const confirmEmailInput = ref>(); +const deleting = ref(false); + const confirmEmail = ref(''); +const confirmEmailInput = ref>(); const emailMatches = computed( () => user.value != null && user.value.email === confirmEmail.value @@ -30,11 +33,26 @@ adminStore.$subscribe(async (_mutation, state) => { } }); -function cancel() { +function close() { + confirmEmail.value = ''; adminStore.clearDeleteUserDialog(); } -function submit() {} +async function submit() { + if (deleting.value || adminStore.deleteUserDialog == null) { + return; + } + + deleting.value = true; + + const { success } = await deleteUser(adminStore.deleteUserDialog.user.id); + + if (success) { + close(); + } + + deleting.value = false; +}