Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,13 @@ class API {
return res;
}

async deleteCentralIdentityUser(uuid: string) {
const res = await axios.delete<ConductorBaseResponse>(
`/central-identity/users/${uuid}`
);
return res;
}

async reEnableCentralIdentityUser(uuid: string) {
const res = await axios.patch<ConductorBaseResponse>(
`/central-identity/users/${uuid}/re-enable`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useState } from "react";
import { Button, Icon, Modal, ModalProps, Input, Message } from "semantic-ui-react";

interface ConfirmDeleteUserModalProps extends ModalProps {
open: boolean;
userName: string;
userUuid: string;
onClose: () => void;
onConfirmDelete: () => void;
loading?: boolean;
}

const ConfirmDeleteUserModal: React.FC<ConfirmDeleteUserModalProps> = ({
open,
onClose,
userName,
userUuid,
onConfirmDelete,
loading = false,
...rest
}) => {
const [confirmText, setConfirmText] = useState("");
const isDeleteConfirmed = confirmText.toLowerCase() === "delete";

const handleConfirmDelete = () => {
if (isDeleteConfirmed) {
onConfirmDelete();
}
};

const handleClose = () => {
setConfirmText("");
onClose();
};

return (
<Modal open={open} onClose={handleClose} size="large" {...rest}>
<Modal.Header>
<Icon name="warning sign" color="red" />
Delete User
</Modal.Header>
<Modal.Content>
<Message negative>
<Message.Header>Warning: This action cannot be undone!</Message.Header>
<p>
You are about to permanently delete the entire LibreOne record for{" "}
<strong>{userName}</strong> (UUID: {userUuid}).
</p>
</Message>
<div className="mb-4">
<p><strong>This will permanently:</strong></p>
<ul className="list-disc list-inside ml-4 space-y-1">
<li>Delete the user's LibreOne account</li>
<li>Remove all associated application licenses</li>
<li>Remove all organization memberships</li>
<li>Delete all internal notes</li>
<li>Remove all access to LibreTexts services</li>
</ul>
</div>
<div className="mb-4">
<p className="font-semibold mb-2">
To confirm this action, please type "delete" in the box below:
</p>
<Input
fluid
placeholder="Type 'delete' to confirm"
value={confirmText}
onChange={(e, { value }) => setConfirmText(value)}
/>
</div>
</Modal.Content>
<Modal.Actions>
<Button onClick={handleClose} disabled={loading}>
<Icon name="cancel" />
Cancel
</Button>
<Button
color="red"
onClick={handleConfirmDelete}
disabled={!isDeleteConfirmed || loading}
loading={loading}
>
<Icon name="trash" />
Delete User
</Button>
</Modal.Actions>
</Modal>
);
};

export default ConfirmDeleteUserModal;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, lazy, useMemo } from "react";
import { Link, useParams } from "react-router-dom";
import { Link, useHistory, useParams } from "react-router-dom";
import {
Header,
Segment,
Expand Down Expand Up @@ -67,9 +67,12 @@ import { useModals } from "../../../../context/ModalContext";
import ConfirmModal from "../../../../components/ConfirmModal";
import AddUserAppLicenseModal from "../../../../components/controlpanel/CentralIdentity/AddUserAppLicenseModal";
import ChangeUserEmailModal from "../../../../components/controlpanel/CentralIdentity/ChangeUserEmailModal";
import { useTypedSelector } from "../../../../state/hooks";
import ConfirmDeleteUserModal from "../../../../components/controlpanel/CentralIdentity/ConfirmDeleteUserModal";

const CentralIdentityUserView = () => {
const { uuid } = useParams<{ uuid: string }>();
const history = useHistory();
const DEFAULT_AVATAR_URL =
"https://cdn.libretexts.net/DefaultImages/avatar.png";

Expand All @@ -80,6 +83,8 @@ const CentralIdentityUserView = () => {
const [showAcademyAccessModal, setShowAcademyAccessModal] =
useState<boolean>(false);
const [showAddOrgModal, setShowAddOrgModal] = useState<boolean>(false);
const [showDeleteUserModal, setShowDeleteUserModal] = useState<boolean>(false);
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const [userApps, setUserApps] = useState<CentralIdentityApp[]>([]);
const [userAppLicenses, setUserAppLicenses] = useState<
CentralIdentityUserLicenseResult[]
Expand All @@ -99,6 +104,32 @@ const CentralIdentityUserView = () => {
const { handleGlobalError } = useGlobalError();
const { addNotification } = useNotifications();
const { openModal, closeAllModals } = useModals();
const isSuperAdmin = useTypedSelector((state) => state.user.isSuperAdmin);

const handleDeleteUser = async () => {
try {
setDeleteLoading(true);
const res = await api.deleteCentralIdentityUser(uuid!);

if (res.data.err) {
throw new Error("Failed to delete user");
}

addNotification({
type: "success",
message: "User deleted successfully",
});

// Navigate back to users list
history.push("/controlpanel/libreone/users");
} catch (err) {
handleGlobalError(err);
} finally {
setDeleteLoading(false);
setShowDeleteUserModal(false);
}
};

const { control, formState, reset, watch, getValues, setValue } =
useForm<CentralIdentityUser>({
defaultValues: {
Expand Down Expand Up @@ -477,6 +508,16 @@ const CentralIdentityUserView = () => {
<Icon name="ban" /> Disable User
</Button>
)}
{isSuperAdmin && (
<Button
color="red"
size="small"
onClick={() => setShowDeleteUserModal(true)}
style={{ backgroundColor: "#d32f2f" }}
>
<Icon name="trash" /> Delete User
</Button>
)}
</div>
</div>
<div style={{ marginBottom: "1.25rem", width: "100%" }}>
Expand Down Expand Up @@ -971,6 +1012,14 @@ const CentralIdentityUserView = () => {
handleAcademyAccessModalClose(true);
}}
/>
<ConfirmDeleteUserModal
open={showDeleteUserModal}
userName={`${getValues("first_name")} ${getValues("last_name")}`}
userUuid={getValues("uuid")}
onClose={() => setShowDeleteUserModal(false)}
onConfirmDelete={handleDeleteUser}
loading={deleteLoading}
/>
</Grid>
);
};
Expand Down
9 changes: 9 additions & 0 deletions server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ router
centralIdentityAPI.validate("updateUser"),
middleware.checkValidationErrors,
centralIdentityAPI.updateUser
)
.delete(
middleware.checkCentralIdentityConfig,
authAPI.verifyRequest,
authAPI.getUserAttributes,
authAPI.checkHasRoleMiddleware("libretexts", "superadmin"),
middleware.validateZod(centralIdentityValidators.DeleteUserValidator),
middleware.checkValidationErrors,
centralIdentityAPI.deleteUser
);

router
Expand Down
33 changes: 31 additions & 2 deletions server/api/central-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
CheckUserApplicationAccessValidator,
VerificationStatusUpdateWebhookValidator,
GetVerificationRequestsSchema,
CheckUsersApplicationAccessValidator
CheckUsersApplicationAccessValidator,
DeleteUserValidator
} from "./validators/central-identity.js";
import Project, { ProjectInterface } from "../models/project.js";
import { getSubdomainFromLibrary } from "../util/librariesclient.js";
Expand Down Expand Up @@ -1797,6 +1798,33 @@ async function reEnableUser(
}
}

async function deleteUser(
req: ZodReqWithUser<z.infer<typeof DeleteUserValidator>>,
res: Response
) {
try {
if (!req.params.id) {
return conductor400Err(res);
}

console.log("DELETING USER", req.params.id);
const deleteRes = await centralIdentityService.deleteUser(req.params.id);

if (deleteRes.data.err || deleteRes.data.errMsg) {
return conductor500Err(res);
}

return res.send({
err: false,
msg: "User successfully deleted.",
meta: {},
});
} catch (err) {
debugError(err);
return conductor500Err(res);
}
}

/**
* Middleware(s) to verify that requests contain necessary and/or valid fields.
*/
Expand Down Expand Up @@ -2018,5 +2046,6 @@ export default {
updateUserNote,
deleteUserNote,
disableUser,
reEnableUser
reEnableUser,
deleteUser
};
5 changes: 5 additions & 0 deletions server/api/services/central-identity-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ export default class CentralIdentityService {
return this.instance.patch(`/users/${userId}/re-enable`);
}

async deleteUser(userId: string) {
console.log("Deleting user", userId);
return this.instance.delete(`/users/${userId}`);
}

async updateUserAdminRole(userId: string, orgId: string, adminRole: string) {
return this.instance.post(`/users/${userId}/organizations/${orgId}/admin-role`, { admin_role: adminRole });
}
Expand Down
6 changes: 6 additions & 0 deletions server/api/validators/central-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,10 @@ export const GetVerificationRequestsSchema = z.object({
query: z.object({
status: z.enum(["open", "closed"]).optional()
}).merge(PaginationSchema),
});

export const DeleteUserValidator = z.object({
params: z.object({
id: z.string().uuid(),
}),
});