diff --git a/src/user/UsersSection.tsx b/src/user/UsersSection.tsx index 556d594d52..fd6f071c59 100644 --- a/src/user/UsersSection.tsx +++ b/src/user/UsersSection.tsx @@ -2,6 +2,9 @@ import { AlertVariant, Button, ButtonVariant, + Dropdown, + DropdownItem, + KebabToggle, Label, PageSection, Text, @@ -28,6 +31,7 @@ import { useRealm } from "../context/realm-context/RealmContext"; import { emptyFormatter } from "../util"; import { toUser } from "./routes/User"; import "./user-section.css"; +import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; type BruteUser = UserRepresentation & { brute?: Record; @@ -41,6 +45,8 @@ export const UsersSection = () => { const history = useHistory(); const { url } = useRouteMatch(); const [listUsers, setListUsers] = useState(false); + const [realm, setRealm] = useState(); + const [kebabOpen, setKebabOpen] = useState(false); const [selectedRows, setSelectedRows] = useState([]); const [key, setKey] = useState(""); @@ -52,11 +58,15 @@ export const UsersSection = () => { type: "org.keycloak.storage.UserStorageProvider", }; - return adminClient.components.find(testParams); + return Promise.all([ + adminClient.components.find(testParams), + adminClient.realms.findOne({ realm: realmName }), + ]); }, - (response) => { + ([storageProviders, realm]) => { //should *only* list users when no user federation is configured - setListUsers(!(response.length > 0)); + setListUsers(!(storageProviders.length > 0)); + setRealm(realm); refresh(); }, [] @@ -88,7 +98,6 @@ export const UsersSection = () => { try { const users = await adminClient.users.find({ ...params }); - const realm = await adminClient.realms.findOne({ realm: realmName }); if (realm?.bruteForceProtected) { const brutes = await Promise.all( users.map((user: BruteUser) => @@ -109,6 +118,21 @@ export const UsersSection = () => { } }; + const [toggleUnlockUsersDialog, UnlockUsersConfirm] = useConfirmDialog({ + titleKey: "users:unlockAllUsers", + messageKey: "users:unlockUsersConfirm", + continueButtonLabel: "users:unlock", + onConfirm: async () => { + try { + await adminClient.attackDetection.delAll(); + refresh(); + addAlert(t("unlockUsersSuccess"), AlertVariant.success); + } catch (error) { + addError("users:unlockUsersError", error); + } + }, + }); + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: "users:deleteConfirm", messageKey: t("deleteConfirmDialog", { count: selectedRows.length }), @@ -167,6 +191,7 @@ export const UsersSection = () => { return ( <> + { {t("addUser")} - - - + {!realm?.bruteForceProtected && ( + + + + )} + {realm?.bruteForceProtected && ( + + setKebabOpen(!kebabOpen)} /> + } + isOpen={kebabOpen} + isPlain + dropdownItems={[ + { + toggleDeleteDialog(); + setKebabOpen(false); + }} + > + {t("deleteUser")} + , + + { + toggleUnlockUsersDialog(); + setKebabOpen(false); + }} + > + {t("unlockAllUsers")} + , + ]} + /> + + )} } actions={[ diff --git a/src/user/messages.ts b/src/user/messages.ts index b1398bf4bc..4248991309 100644 --- a/src/user/messages.ts +++ b/src/user/messages.ts @@ -98,5 +98,11 @@ export default { "Are you sure you want to revoke all granted client scopes for {{clientId}}?", deleteGrantsSuccess: "Grants successfully revoked.", deleteGrantsError: "Error deleting grants.", + unlockAllUsers: "Unlock all users", + unlockUsersConfirm: + "All the users that are temporarily locked will be unlocked.", + unlock: "Unlock", + unlockUsersSuccess: "Any temporarily locked users are now unlocked", + unlockUsersError: "Could not unlock all users {{error}}", }, };