delete users
This commit is contained in:
@@ -105,3 +105,54 @@ impl UserEmail {
|
|||||||
Ok(Self(raw.to_string()))
|
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<Self, UserPasswordError> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<RegisterUserRequest> 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<Self, UserPasswordError> {
|
|
||||||
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<LoginUserRequest> 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),
|
|
||||||
}
|
|
||||||
45
backend/src/lib/domain/warren/models/user/requests/create.rs
Normal file
45
backend/src/lib/domain/warren/models/user/requests/create.rs
Normal file
@@ -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),
|
||||||
|
}
|
||||||
28
backend/src/lib/domain/warren/models/user/requests/delete.rs
Normal file
28
backend/src/lib/domain/warren/models/user/requests/delete.rs
Normal file
@@ -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),
|
||||||
|
}
|
||||||
17
backend/src/lib/domain/warren/models/user/requests/list.rs
Normal file
17
backend/src/lib/domain/warren/models/user/requests/list.rs
Normal file
@@ -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),
|
||||||
|
}
|
||||||
58
backend/src/lib/domain/warren/models/user/requests/login.rs
Normal file
58
backend/src/lib/domain/warren/models/user/requests/login.rs
Normal file
@@ -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),
|
||||||
|
}
|
||||||
13
backend/src/lib/domain/warren/models/user/requests/mod.rs
Normal file
13
backend/src/lib/domain/warren/models/user/requests/mod.rs
Normal file
@@ -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::*;
|
||||||
@@ -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<RegisterUserRequest> 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),
|
||||||
|
}
|
||||||
@@ -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<LoginUserRequest> 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),
|
||||||
|
}
|
||||||
@@ -62,6 +62,9 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
|||||||
fn record_user_list_success(&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_user_list_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;
|
||||||
|
|
||||||
fn record_auth_session_creation_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_session_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_auth_session_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_session_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ use super::models::{
|
|||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, LoginUserError,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
||||||
LoginUserRequest, LoginUserResponse, RegisterUserError, RegisterUserRequest, User,
|
ListUsersRequest, LoginUserError, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
||||||
|
RegisterUserRequest, User,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
@@ -125,6 +126,12 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: AuthRequest<CreateUserRequest>,
|
request: AuthRequest<CreateUserRequest>,
|
||||||
) -> impl Future<Output = Result<User, AuthError<CreateUserError>>> + Send;
|
) -> impl Future<Output = Result<User, AuthError<CreateUserError>>> + Send;
|
||||||
|
/// An action that deletes a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||||
|
fn delete_user(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<DeleteUserRequest>,
|
||||||
|
) -> impl Future<Output = Result<User, AuthError<DeleteUserError>>> + Send;
|
||||||
|
/// An action that lists all users (MUST REQUIRE ADMIN PRIVILEGES)
|
||||||
fn list_users(
|
fn list_users(
|
||||||
&self,
|
&self,
|
||||||
request: AuthRequest<ListUsersRequest>,
|
request: AuthRequest<ListUsersRequest>,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
|||||||
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
|
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
|
||||||
fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send;
|
fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send;
|
||||||
fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send;
|
fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send;
|
||||||
|
fn user_deleted(&self, deleter: &User, user: &User) -> impl Future<Output = ()> + Send;
|
||||||
/// Lists all the users (admin action)
|
/// Lists all the users (admin action)
|
||||||
///
|
///
|
||||||
/// * `user`: The user who requested the list
|
/// * `user`: The user who requested the list
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use crate::domain::warren::models::{
|
|||||||
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
FilePath, ListFilesError, ListFilesRequest, RenameEntryError, RenameEntryRequest,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, User,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
||||||
VerifyUserPasswordError, VerifyUserPasswordRequest,
|
ListUsersRequest, User, VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
@@ -78,6 +78,10 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: ListUsersRequest,
|
request: ListUsersRequest,
|
||||||
) -> impl Future<Output = Result<Vec<User>, ListUsersError>> + Send;
|
) -> impl Future<Output = Result<Vec<User>, ListUsersError>> + Send;
|
||||||
|
fn delete_user(
|
||||||
|
&self,
|
||||||
|
request: DeleteUserRequest,
|
||||||
|
) -> impl Future<Output = Result<User, DeleteUserError>> + Send;
|
||||||
|
|
||||||
fn verify_user_password(
|
fn verify_user_password(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||||
LoginUserError, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
ListUsersError, ListUsersRequest, LoginUserError, LoginUserRequest,
|
||||||
RegisterUserRequest, User,
|
LoginUserResponse, RegisterUserError, RegisterUserRequest, User,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
@@ -166,6 +166,36 @@ where
|
|||||||
result.map_err(AuthError::Custom)
|
result.map_err(AuthError::Custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_user(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<DeleteUserRequest>,
|
||||||
|
) -> Result<User, AuthError<DeleteUserError>> {
|
||||||
|
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(
|
async fn list_users(
|
||||||
&self,
|
&self,
|
||||||
request: AuthRequest<ListUsersRequest>,
|
request: AuthRequest<ListUsersRequest>,
|
||||||
|
|||||||
42
backend/src/lib/inbound/http/handlers/admin/delete_user.rs
Normal file
42
backend/src/lib/inbound/http/handlers/admin/delete_user.rs
Normal file
@@ -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<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
SessionIdHeader(session): SessionIdHeader,
|
||||||
|
Json(request): Json<DeleteUserHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<UserData>, 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)
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
mod create_user;
|
mod create_user;
|
||||||
|
mod delete_user;
|
||||||
mod list_users;
|
mod list_users;
|
||||||
|
|
||||||
use create_user::create_user;
|
use create_user::create_user;
|
||||||
|
use delete_user::delete_user;
|
||||||
use list_users::list_users;
|
use list_users::list_users;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
routing::{get, post},
|
routing::{delete, get, post},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -18,4 +20,5 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
|
|||||||
Router::new()
|
Router::new()
|
||||||
.route("/users", get(list_users))
|
.route("/users", get(list_users))
|
||||||
.route("/users", post(create_user))
|
.route("/users", post(create_user))
|
||||||
|
.route("/users", delete(delete_user))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,13 @@ impl AuthMetrics for MetricsDebugLogger {
|
|||||||
tracing::debug!("[Metrics] User creation failed");
|
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) -> () {
|
async fn record_user_list_success(&self) -> () {
|
||||||
tracing::debug!("[Metrics] User list succeeded");
|
tracing::debug!("[Metrics] User list succeeded");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,11 +121,19 @@ impl AuthNotifier for NotifierDebugLogger {
|
|||||||
tracing::debug!("[Notifier] Registered user {}", user.name());
|
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!(
|
tracing::debug!(
|
||||||
"[Notifier] Admin user {} created user {}",
|
"[Notifier] Admin user {} created user {}",
|
||||||
creator.name(),
|
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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ use crate::domain::warren::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, ListUsersError, ListUsersRequest, User, UserEmail,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, ListUsersError,
|
||||||
UserName, UserPassword, VerifyUserPasswordError, VerifyUserPasswordRequest,
|
ListUsersRequest, User, UserEmail, UserName, UserPassword, VerifyUserPasswordError,
|
||||||
|
VerifyUserPasswordRequest,
|
||||||
},
|
},
|
||||||
user_warren::{
|
user_warren::{
|
||||||
UserWarren,
|
UserWarren,
|
||||||
@@ -175,6 +176,28 @@ impl Postgres {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_user_from_database(
|
||||||
|
&self,
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
user_id: &Uuid,
|
||||||
|
) -> Result<User, sqlx::Error> {
|
||||||
|
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(
|
async fn get_user_from_id(
|
||||||
&self,
|
&self,
|
||||||
connection: &mut PgConnection,
|
connection: &mut PgConnection,
|
||||||
@@ -430,6 +453,24 @@ impl AuthRepository for Postgres {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_user(&self, request: DeleteUserRequest) -> Result<User, DeleteUserError> {
|
||||||
|
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(
|
async fn verify_user_password(
|
||||||
&self,
|
&self,
|
||||||
request: VerifyUserPasswordRequest,
|
request: VerifyUserPasswordRequest,
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import type { AuthUser } from '#shared/types/auth';
|
import type { AuthUser } from '#shared/types/auth';
|
||||||
|
import { deleteUser } from '~/lib/api/admin/deleteUser';
|
||||||
|
|
||||||
const adminStore = useAdminStore();
|
const adminStore = useAdminStore();
|
||||||
// We'll only update this value if there is a user to prevent layout shifts on close
|
// We'll only update this value if there is a user to prevent layout shifts on close
|
||||||
const user = ref<AuthUser>();
|
const user = ref<AuthUser>();
|
||||||
const confirmEmailInput = ref<InstanceType<typeof Input>>();
|
const deleting = ref(false);
|
||||||
|
|
||||||
const confirmEmail = ref<string>('');
|
const confirmEmail = ref<string>('');
|
||||||
|
const confirmEmailInput = ref<InstanceType<typeof Input>>();
|
||||||
|
|
||||||
const emailMatches = computed(
|
const emailMatches = computed(
|
||||||
() => user.value != null && user.value.email === confirmEmail.value
|
() => 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();
|
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;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -42,7 +60,7 @@ function submit() {}
|
|||||||
<AlertDialogTrigger as-child>
|
<AlertDialogTrigger as-child>
|
||||||
<slot />
|
<slot />
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent @escape-key-down="cancel">
|
<AlertDialogContent @escape-key-down="close">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogDescription class="space-y-1">
|
<AlertDialogDescription class="space-y-1">
|
||||||
@@ -80,8 +98,10 @@ function submit() {}
|
|||||||
</div>
|
</div>
|
||||||
</AlterDialogContent>
|
</AlterDialogContent>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel @click="cancel">Cancel</AlertDialogCancel>
|
<AlertDialogCancel @click="close">Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction :disabled="!emailMatches" @click="submit"
|
<AlertDialogAction
|
||||||
|
:disabled="!emailMatches || deleting"
|
||||||
|
@click="submit"
|
||||||
>Delete</AlertDialogAction
|
>Delete</AlertDialogAction
|
||||||
>
|
>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
|
|||||||
42
frontend/lib/api/admin/deleteUser.ts
Normal file
42
frontend/lib/api/admin/deleteUser.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ApiResponse } from '~/shared/types/api';
|
||||||
|
import type { AuthUser } from '~/shared/types/auth';
|
||||||
|
import { getApiHeaders } from '..';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
|
/** Admin function to create a new user */
|
||||||
|
export async function deleteUser(
|
||||||
|
userId: string
|
||||||
|
): Promise<{ success: true; user: AuthUser } | { success: false }> {
|
||||||
|
const { data, error } = await useFetch<ApiResponse<AuthUser>>(
|
||||||
|
getApiUrl('admin/users'),
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
userId: userId,
|
||||||
|
}),
|
||||||
|
responseType: 'json',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
toast.error('Delete user', {
|
||||||
|
description: error.value?.data ?? 'Failed to delete user',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshNuxtData('users');
|
||||||
|
|
||||||
|
toast.success('Delete user', {
|
||||||
|
description: 'Successfully delete user',
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: data.value.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user