diff --git a/dashboard/src/pages/Dashboard/UserManagment.tsx b/dashboard/src/pages/Dashboard/UserManagment.tsx index 524daa6ebf..a05a96a805 100644 --- a/dashboard/src/pages/Dashboard/UserManagment.tsx +++ b/dashboard/src/pages/Dashboard/UserManagment.tsx @@ -9,6 +9,7 @@ import { import { InviteUserModal } from "../../components/InviteUserModal"; import { EditUserModal } from "../../components/EditUserModal"; import { createToast } from "../../components/ShowToasts"; +import { FaRegularTrashCan } from "solid-icons/fa"; export const UserManagement = () => { const apiHost = import.meta.env.VITE_API_HOST as unknown as string; @@ -96,6 +97,46 @@ export const UserManagement = () => { }); }; + const removeUser = (id: string) => { + const confirm = window.confirm( + "Are you sure you want to remove this user?", + ); + if (!confirm) { + return; + } + + fetch( + `${apiHost}/organization/${ + userContext.selectedOrganizationId?.() as string + }/user/${id}`, + { + method: "DELETE", + headers: { + "TR-Organization": userContext.selectedOrganizationId?.() as string, + }, + credentials: "include", + }, + ) + .then((res) => { + if (res.ok) { + getUsers(); + createToast({ + title: "Success", + type: "success", + message: "User removed successfully!", + }); + } + }) + .catch((err) => { + console.error(err); + createToast({ + title: "Error", + type: "error", + message: "Error removing user!", + }); + }); + }; + const deleteInvitation = (id: string) => { fetch(`${apiHost}/invitation/${id}`, { method: "DELETE", @@ -228,10 +269,16 @@ export const UserManagement = () => { Edit + + Delete + @@ -247,7 +294,7 @@ export const UserManagement = () => { {fromI32ToUserRole(user.user_orgs[0].role) as string} - + + + + )} diff --git a/server/src/handlers/organization_handler.rs b/server/src/handlers/organization_handler.rs index 91d37a637c..12a6fe2881 100644 --- a/server/src/handlers/organization_handler.rs +++ b/server/src/handlers/organization_handler.rs @@ -6,7 +6,7 @@ use crate::{ create_organization_query, delete_organization_query, get_org_from_id_query, get_org_usage_by_id_query, get_org_users_by_id_query, update_organization_query, }, - user_operator::add_user_to_organization, + user_operator::{add_user_to_organization, remove_user_from_org_query}, }, }; use actix_web::{web, HttpRequest, HttpResponse}; @@ -255,3 +255,42 @@ pub async fn get_organization_users( Ok(HttpResponse::Ok().json(usage)) } + +#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)] +pub struct RemoveUserFromOrgData { + /// The id of the organization to remove the user from. + organization_id: uuid::Uuid, + /// The id of the user to remove from the organization. + user_id: uuid::Uuid, +} + +/// Remove User From Organization +/// +/// Remove a user from an organization. The auth'ed user must be an admin or owner of the organization to remove a user. +#[utoipa::path( + delete, + path = "/organization/{organization_id}/user/{user_id}", + context_path = "/api", + tag = "organization", + responses( + (status = 204, description = "Confirmation that the user was removed from the organization"), + (status = 400, description = "Service error relating to removing the user from the organization", body = ErrorResponseBody), + ), + params( + ("TR-Organization" = String, Header, description = "The organization id to use for the request"), + ("user_id" = Option, Path, description = "The id of the user you want to remove from the organization."), + ), + security( + ("ApiKey" = ["readonly"]), + ) +)] +pub async fn remove_user_from_org( + data: web::Path, + pool: web::Data, + redis_pool: web::Data, + _admin: OwnerOnly, +) -> Result { + remove_user_from_org_query(data.user_id, data.organization_id, pool, redis_pool).await?; + + Ok(HttpResponse::NoContent().finish()) +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 94f3d0fc0b..67af399aab 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -782,6 +782,10 @@ pub fn main() -> std::io::Result<()> { handlers::organization_handler::get_organization_users, )), ) + .service( + web::resource("/{organization_id}/user/{user_id}") + .route(web::delete().to(handlers::organization_handler::remove_user_from_org)), + ) .service( web::resource("/{organization_id}") .route( diff --git a/server/src/operators/user_operator.rs b/server/src/operators/user_operator.rs index 5f47633a0b..e9c02553e5 100644 --- a/server/src/operators/user_operator.rs +++ b/server/src/operators/user_operator.rs @@ -559,3 +559,33 @@ pub async fn create_default_user(api_key: &str, pool: web::Data) -> Result Ok(()) } + +pub async fn remove_user_from_org_query( + user_id: uuid::Uuid, + organization_id: uuid::Uuid, + pool: web::Data, + redis_pool: web::Data, +) -> Result<(), ServiceError> { + use crate::data::schema::user_organizations::dsl as user_organizations_columns; + + let mut conn = pool.get().await.unwrap(); + + diesel::delete( + user_organizations_columns::user_organizations + .filter(user_organizations_columns::user_id.eq(user_id)) + .filter(user_organizations_columns::organization_id.eq(organization_id)), + ) + .execute(&mut conn) + .await + .map_err(|_| ServiceError::BadRequest("Error removing user from org".to_string()))?; + + let mut redis_conn = redis_pool.get().await.map_err(|_| { + ServiceError::InternalServerError("Failed to get redis connection".to_string()) + })?; + + redis_conn.del(user_id.to_string()).await.map_err(|_| { + ServiceError::InternalServerError("Failed to delete user from redis".to_string()) + })?; + + Ok(()) +}