Use keycloak-admin with axios instead of fetch wrapper (#212)

* changed to use the admin client

* added helper to always set realm

* fixed merge

* no need to polyfill anymore

* updated to use keycloak-admin-client

* updated to release version

* fixed types

* added user federation

* update test

* lint
This commit is contained in:
Erik Jan de Wit 2020-11-12 13:55:52 +01:00 committed by GitHub
parent 42bb5cfe3f
commit dcb18c5488
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1074 additions and 1245 deletions

View file

@ -22,7 +22,7 @@
"@patternfly/react-table": "4.16.20",
"file-saver": "^2.0.2",
"i18next": "^19.6.2",
"keycloak-js": "^11.0.0",
"keycloak-admin": "^1.14.1",
"react": "^16.8.5",
"react-dom": "^16.8.5",
"react-hook-form": "^6.8.2",

View file

@ -6,7 +6,6 @@ import { Header } from "./PageHeader";
import { PageNav } from "./PageNav";
import { Help } from "./components/help-enabler/HelpHeader";
import { RealmContextProvider } from "./context/realm-context/RealmContext";
import { WhoAmIContextProvider } from "./context/whoami/WhoAmI";
import { ServerInfoProvider } from "./context/server-info/ServerInfoProvider";
import { AlertProvider } from "./components/alert/Alerts";
@ -18,7 +17,6 @@ import { ForbiddenSection } from "./ForbiddenSection";
const AppContexts = ({ children }: { children: ReactNode }) => (
<WhoAmIContextProvider>
<RealmContextProvider>
<AccessContextProvider>
<Help>
<AlertProvider>
@ -26,7 +24,6 @@ const AppContexts = ({ children }: { children: ReactNode }) => (
</AlertProvider>
</Help>
</AccessContextProvider>
</RealmContextProvider>
</WhoAmIContextProvider>
);

View file

@ -14,41 +14,32 @@ import {
PageHeaderToolsGroup,
} from "@patternfly/react-core";
import { HelpIcon } from "@patternfly/react-icons";
import { KeycloakContext } from "./context/auth/KeycloakContext";
import { WhoAmIContext } from "./context/whoami/WhoAmI";
import { HelpHeader } from "./components/help-enabler/HelpHeader";
import { Link } from "react-router-dom";
import { useAdminClient } from "./context/auth/AdminClient";
export const Header = () => {
return (
<PageHeader
showNavToggle
logo={
<Link to="/">
<Brand src="/logo.svg" alt="Logo" />
</Link>
}
logoComponent="div"
headerTools={headerTools()}
/>
);
};
const adminClient = useAdminClient();
const { t } = useTranslation();
const ManageAccountDropdownItem = () => {
const keycloak = useContext(KeycloakContext);
const { t } = useTranslation();
return (
<DropdownItem key="manage account" onClick={() => keycloak?.account()}>
<DropdownItem
key="manage account"
onClick={() => adminClient.keycloak.accountManagement()}
>
{t("manageAccount")}
</DropdownItem>
);
};
const SignOutDropdownItem = () => {
const keycloak = useContext(KeycloakContext);
const { t } = useTranslation();
return (
<DropdownItem key="sign out" onClick={() => keycloak?.logout()}>
<DropdownItem
key="sign out"
onClick={() => adminClient.keycloak.logout({ redirectUri: "" })}
>
{t("signOut")}
</DropdownItem>
);
@ -68,14 +59,14 @@ const kebabDropdownItems = [
<ManageAccountDropdownItem key="kebab Manage Account" />,
<ServerInfoDropdownItem key="kebab Server Info" />,
<HelpDropdownItem key="kebab Help" />,
<DropdownSeparator key="kebab sign out seperator" />,
<DropdownSeparator key="kebab sign out separator" />,
<SignOutDropdownItem key="kebab Sign out" />,
];
const userDropdownItems = [
<ManageAccountDropdownItem key="Manage Account" />,
<ServerInfoDropdownItem key="Server info" />,
<DropdownSeparator key="sign out seperator" />,
<DropdownSeparator key="sign out separator" />,
<SignOutDropdownItem key="Sign out" />,
];
@ -134,7 +125,6 @@ const KebabDropdown = () => {
};
const UserDropdown = () => {
const keycloak = useContext(KeycloakContext);
const whoami = useContext(WhoAmIContext);
const [isDropdownOpen, setDropdownOpen] = useState(false);
@ -156,3 +146,17 @@ const UserDropdown = () => {
/>
);
};
return (
<PageHeader
showNavToggle
logo={
<Link to="/">
<Brand src="/logo.svg" alt="Logo" />
</Link>
}
logoComponent="div"
headerTools={headerTools()}
/>
);
};

View file

@ -1,4 +1,4 @@
import React, { useState, useContext } from "react";
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
@ -10,20 +10,16 @@ import {
} from "@patternfly/react-core";
import { RealmSelector } from "./components/realm-selector/RealmSelector";
import { DataLoader } from "./components/data-loader/DataLoader";
import { HttpClientContext } from "./context/http-service/HttpClientContext";
import { useAdminClient } from "./context/auth/AdminClient";
import { useAccess } from "./context/access/Access";
import { RealmRepresentation } from "./realm/models/Realm";
import { routes } from "./route-config";
export const PageNav: React.FunctionComponent = () => {
const { t } = useTranslation("common");
const { hasAccess, hasSomeAccess } = useAccess();
const httpClient = useContext(HttpClientContext)!;
const adminClient = useAdminClient();
const realmLoader = async () => {
const response = await httpClient.doGet<RealmRepresentation[]>(
"/admin/realms"
);
return response.data;
return await adminClient.realms.find();
};
const history = useHistory();

View file

@ -1,14 +1,13 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { Button, PageSection, Spinner } from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import { RealmContext } from "../context/realm-context/RealmContext";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { ClientRepresentation } from "../realm/models/Realm";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
import { ClientScopeList } from "./ClientScopesList";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAdminClient } from "../context/auth/AdminClient";
export const ClientScopesSection = () => {
const { t } = useTranslation("client-scopes");
@ -16,25 +15,22 @@ export const ClientScopesSection = () => {
const [rawData, setRawData] = useState<ClientRepresentation[]>();
const [filteredData, setFilteredData] = useState<ClientRepresentation[]>();
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
useEffect(() => {
(async () => {
if (filteredData) {
return filteredData;
}
const result = await httpClient.doGet<ClientRepresentation[]>(
`/admin/realms/${realm}/client-scopes`
);
setRawData(result.data!);
const result = await adminClient.clientScopes.find();
setRawData(result);
})();
}, []);
const filterData = (search: string) => {
setFilteredData(
rawData!.filter((group) =>
group.name.toLowerCase().includes(search.toLowerCase())
group.name!.toLowerCase().includes(search.toLowerCase())
)
);
};

View file

@ -19,12 +19,10 @@ import {
TableVariant,
} from "@patternfly/react-table";
import { useTranslation } from "react-i18next";
import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation";
import { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import {
ProtocolMapperRepresentation,
ProtocolMapperTypeRepresentation,
} from "../../context/server-info/server-info";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
export type AddMapperDialogModalProps = {
@ -45,8 +43,8 @@ export const AddMapperDialog = (props: AddMapperDialogProps) => {
const serverInfo = useServerInfo();
const protocol = props.protocol;
const protocolMappers = serverInfo.protocolMapperTypes[protocol];
const builtInMappers = serverInfo.builtinProtocolMappers[protocol];
const protocolMappers = serverInfo.protocolMapperTypes![protocol];
const builtInMappers = serverInfo.builtinProtocolMappers![protocol];
const [filter, setFilter] = useState<ProtocolMapperRepresentation[]>([]);
const allRows = builtInMappers.map((mapper) => {

View file

@ -0,0 +1,222 @@
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import {
ActionGroup,
AlertVariant,
Button,
Form,
FormGroup,
PageSection,
Select,
SelectOption,
SelectVariant,
Switch,
TextInput,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import { ClientScopeRepresentation } from "../models/client-scope";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useAlerts } from "../../components/alert/Alerts";
import { useAdminClient } from "../../context/auth/AdminClient";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
export const NewClientScopeForm = () => {
const { t } = useTranslation("client-scopes");
const helpText = useTranslation("client-scopes-help").t;
const { register, control, handleSubmit, errors } = useForm<
ClientScopeRepresentation
>();
const history = useHistory();
const adminClient = useAdminClient();
const providers = useLoginProviders();
const [open, isOpen] = useState(false);
const { addAlert } = useAlerts();
const save = async (clientScopes: ClientScopeRepresentation) => {
try {
const keyValues = Object.keys(clientScopes.attributes!).map((key) => {
const newKey = key.replace(/_/g, ".");
return { [newKey]: clientScopes.attributes![key] };
});
clientScopes.attributes = Object.assign({}, ...keyValues);
await adminClient.clientScopes.create({ ...clientScopes });
addAlert(t("createClientScopeSuccess"), AlertVariant.success);
} catch (error) {
addAlert(
`${t("createClientScopeError")} '${error}'`,
AlertVariant.danger
);
}
};
return (
<>
<ViewHeader
titleKey="client-scopes:createClientScope"
subKey="client-scopes:clientScopeExplain"
/>
<PageSection variant="light">
<Form isHorizontal onSubmit={handleSubmit(save)}>
<FormGroup
label={t("name")}
labelIcon={
<HelpItem
helpText={helpText("name")}
forLabel={t("name")}
forID="kc-name"
/>
}
fieldId="kc-name"
isRequired
validated={errors.name ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register({ required: true })}
type="text"
id="kc-name"
name="name"
/>
</FormGroup>
<FormGroup
label={t("description")}
labelIcon={
<HelpItem
helpText={helpText("description")}
forLabel={t("description")}
forID="kc-description"
/>
}
fieldId="kc-description"
>
<TextInput
ref={register}
type="text"
id="kc-description"
name="description"
/>
</FormGroup>
<FormGroup
label={t("protocol")}
labelIcon={
<HelpItem
helpText={helpText("protocol")}
forLabel="protocol"
forID="kc-protocol"
/>
}
fieldId="kc-protocol"
>
<Controller
name="protocol"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Select
toggleId="kc-protocol"
required
onToggle={() => isOpen(!open)}
onSelect={(_, value, isPlaceholder) => {
onChange(isPlaceholder ? "" : (value as string));
isOpen(false);
}}
selections={value}
variant={SelectVariant.single}
aria-label={t("selectEncryptionType")}
placeholderText={t("common:selectOne")}
isOpen={open}
>
{providers.map((option) => (
<SelectOption
selected={option === value}
key={option}
value={option}
/>
))}
</Select>
)}
/>
</FormGroup>
<FormGroup
hasNoPaddingTop
label={t("displayOnConsentScreen")}
labelIcon={
<HelpItem
helpText={helpText("displayOnConsentScreen")}
forLabel={t("displayOnConsentScreen")}
forID="kc-display.on.consent.screen"
/>
}
fieldId="kc-display.on.consent.screen"
>
<Controller
name="attributes.display_on_consent_screen"
control={control}
defaultValue={false}
render={({ onChange, value }) => (
<Switch
id="kc-display.on.consent.screen"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value}
onChange={onChange}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("consentScreenText")}
labelIcon={
<HelpItem
helpText={helpText("consentScreenText")}
forLabel={t("consentScreenText")}
forID="kc-consent-screen-text"
/>
}
fieldId="kc-consent-screen-text"
>
<TextInput
ref={register}
type="text"
id="kc-consent-screen-text"
name="attributes.consent_screen_text"
/>
</FormGroup>
<FormGroup
label={t("guiOrder")}
labelIcon={
<HelpItem
helpText={helpText("guiOrder")}
forLabel={t("guiOrder")}
forID="kc-gui-order"
/>
}
fieldId="kc-gui-order"
>
<TextInput
ref={register}
type="number"
id="kc-gui-order"
name="attributes.gui_order"
/>
</FormGroup>
<ActionGroup>
<Button variant="primary" type="submit">
{t("common:save")}
</Button>
<Button variant="link" onClick={() => history.push("..")}>
{t("common:cancel")}
</Button>
</ActionGroup>
</Form>
</PageSection>
</>
);
};

View file

@ -19,16 +19,14 @@ import {
ValidatedOptions,
} from "@patternfly/react-core";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { useAlerts } from "../../components/alert/Alerts";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { useAdminClient } from "../../context/auth/AdminClient";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import {
ClientRepresentation,
RoleRepresentation,
} from "../../realm/models/Realm";
import { ProtocolMapperRepresentation } from "../models/client-scope";
export type RoleMappingFormProps = {
@ -36,8 +34,8 @@ export type RoleMappingFormProps = {
};
export const RoleMappingForm = ({ clientScopeId }: RoleMappingFormProps) => {
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const history = useHistory();
const { addAlert } = useAlerts();
@ -54,18 +52,13 @@ export const RoleMappingForm = ({ clientScopeId }: RoleMappingFormProps) => {
useEffect(() => {
(async () => {
const response = await httpClient.doGet<RoleRepresentation[]>(
`/admin/realms/${realm}/roles`
);
setRoles(response.data!);
const clientResponse = await httpClient.doGet<ClientRepresentation[]>(
`/admin/realms/${realm}/clients`
);
const clients = clientResponse.data!;
const roles = await adminClient.roles.find();
setRoles(roles);
const clients = await adminClient.clients.find();
clients.map(
(client) =>
(client.toString = function () {
return this.name;
return this.name!;
})
);
setClients(clients);
@ -76,19 +69,15 @@ export const RoleMappingForm = ({ clientScopeId }: RoleMappingFormProps) => {
(async () => {
const client = selectedClient as ClientRepresentation;
if (client && client.name !== "realmRoles") {
const response = await httpClient.doGet<RoleRepresentation[]>(
`/admin/realms/master/clients/${client.id}/roles`
);
setClientRoles(response.data!);
setClientRoles(await adminClient.clients.listRoles({ id: client.id! }));
}
})();
}, [selectedClient]);
const save = async (mapping: ProtocolMapperRepresentation) => {
try {
await httpClient.doPost(
`/admin/realms/${realm}/client-scopes/${clientScopeId}/protocol-mappers/models`,
await adminClient.clientScopes.addProtocolMapper(
{ id: clientScopeId },
mapping
);
addAlert(t("mapperCreateSuccess"));
@ -219,8 +208,8 @@ export const RoleMappingForm = ({ clientScopeId }: RoleMappingFormProps) => {
} else {
return createSelectGroup(
clients.filter((client) =>
client.name
.toLowerCase()
client
.name!.toLowerCase()
.includes(textInput.toLowerCase())
)
);

View file

@ -1,4 +1,4 @@
import React, { useContext, useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import {
@ -16,22 +16,16 @@ import {
} from "@patternfly/react-table";
import { CaretDownIcon } from "@patternfly/react-icons";
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation";
import { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import {
ProtocolMapperRepresentation as ServerInfoProtocolMapper,
ProtocolMapperTypeRepresentation,
} from "../../context/server-info/server-info";
import {
ClientScopeRepresentation,
ProtocolMapperRepresentation,
} from "../models/client-scope";
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { useAlerts } from "../../components/alert/Alerts";
import { AddMapperDialog } from "../add/MapperDialog";
import { useAdminClient } from "../../context/auth/AdminClient";
type MapperListProps = {
clientScope: ClientScopeRepresentation;
@ -47,8 +41,7 @@ type Row = {
export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
const { t } = useTranslation("client-scopes");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const [filteredData, setFilteredData] = useState<
@ -56,7 +49,7 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
>();
const [mapperAction, setMapperAction] = useState(false);
const mapperList = clientScope.protocolMappers!;
const mapperTypes = useServerInfo().protocolMapperTypes[
const mapperTypes = useServerInfo().protocolMapperTypes![
clientScope.protocol!
];
@ -67,9 +60,9 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
mappers: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
) => {
try {
await httpClient.doPost(
`/admin/realms/${realm}/client-scopes/${clientScope.id}/protocol-mappers/add-models`,
mappers
await adminClient.clientScopes.addMultipleProtocolMappers(
{ id: clientScope.id! },
mappers as ProtocolMapperRepresentation[]
);
refresh();
addAlert(t("mappingCreatedSuccess"), AlertVariant.success);
@ -83,7 +76,7 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
<>
<AddMapperDialog
protocol={clientScope.protocol!}
filter={(mapperList as ServerInfoProtocolMapper[]) || []}
filter={mapperList || []}
onConfirm={addMappers}
open={builtInDialogOpen}
toggleDialog={toggleBuiltInMapperDialog}
@ -168,7 +161,7 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
>
<AddMapperDialog
protocol={clientScope.protocol!}
filter={(mapperList as ServerInfoProtocolMapper[]) || []}
filter={mapperList || []}
onConfirm={addMappers}
open={builtInDialogOpen}
toggleDialog={toggleBuiltInMapperDialog}
@ -185,9 +178,10 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
title: t("common:delete"),
onClick: async (_, rowId) => {
try {
await httpClient.doDelete(
`/admin/realms/${realm}/client-scopes/${clientScope.id}/protocol-mappers/models/${data[rowId].mapper.id}`
);
await adminClient.clientScopes.delProtocolMapper({
id: clientScope.id!,
mapperId: data[rowId].mapper.id!,
});
refresh();
addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
} catch (error) {

View file

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
@ -19,23 +19,21 @@ import {
Switch,
TextInput,
} from "@patternfly/react-core";
import { ConfigPropertyRepresentation } from "keycloak-admin/lib/defs/configPropertyRepresentation";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { useAdminClient } from "../../context/auth/AdminClient";
import { ProtocolMapperRepresentation } from "../models/client-scope";
import { Controller, useForm } from "react-hook-form";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../../components/alert/Alerts";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { ConfigPropertyRepresentation } from "../../context/server-info/server-info";
import { convertFormValuesToObject, convertToFormValues } from "../../util";
export const MappingDetails = () => {
const { t } = useTranslation("client-scopes");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const { scopeId, id } = useParams<{ scopeId: string; id: string }>();
@ -47,28 +45,27 @@ export const MappingDetails = () => {
>();
const serverInfo = useServerInfo();
const url = `/admin/realms/${realm}/client-scopes/${scopeId}/protocol-mappers/models/${id}`;
const history = useHistory();
useEffect(() => {
(async () => {
const response = await httpClient.doGet<ProtocolMapperRepresentation>(
url
);
if (response.data) {
Object.entries(response.data).map((entry) => {
const data = await adminClient.clientScopes.findProtocolMapper({
id: scopeId,
mapperId: id,
});
if (data) {
Object.entries(data).map((entry) => {
if (entry[0] === "config") {
convertToFormValues(entry[1], "config", setValue);
}
setValue(entry[0], entry[1]);
});
}
setMapping(response.data);
const mapperTypes =
serverInfo.protocolMapperTypes[response.data!.protocol!];
setMapping(data);
const mapperTypes = serverInfo.protocolMapperTypes![data!.protocol!];
const properties = mapperTypes.find(
(type) => type.id === response.data!.protocolMapper
)?.properties;
(type) => type.id === data.protocolMapper
)?.properties!;
setConfigProperties(properties);
})();
}, []);
@ -78,9 +75,12 @@ export const MappingDetails = () => {
messageKey: "client-scopes:deleteMappingConfirm",
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: () => {
onConfirm: async () => {
try {
httpClient.doDelete(url);
await adminClient.clientScopes.delClientScopeMappings(
{ client: scopeId, id },
[]
);
addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
history.push(`/client-scopes/${scopeId}`);
} catch (error) {
@ -93,7 +93,10 @@ export const MappingDetails = () => {
const config = convertFormValuesToObject(formMapping.config);
const map = { ...mapping, config };
try {
await httpClient.doPut(url, map);
await adminClient.clientScopes.updateProtocolMapper(
{ id: scopeId, mapperId: id },
map
);
addAlert(t("mappingUpdatedSuccess"), AlertVariant.success);
} catch (error) {
addAlert(t("mappingUpdatedError", { error }), AlertVariant.danger);
@ -211,8 +214,8 @@ export const MappingDetails = () => {
>
{configProperties &&
configProperties
.find((property) => property.name === "jsonType.label")
?.options.map((option) => (
.find((property) => property.name! === "jsonType.label")
?.options!.map((option) => (
<SelectOption
selected={option === value}
key={option}

View file

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import {
ActionGroup,
@ -22,8 +22,7 @@ import { Controller, useForm } from "react-hook-form";
import { ClientScopeRepresentation } from "../models/client-scope";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
import { ViewHeader } from "../../components/view-header/ViewHeader";
@ -39,8 +38,7 @@ export const ClientScopeForm = () => {
const [clientScope, setClientScope] = useState<ClientScopeRepresentation>();
const [activeTab, setActiveTab] = useState(0);
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const providers = useLoginProviders();
const { id } = useParams<{ id: string }>();
@ -49,11 +47,9 @@ export const ClientScopeForm = () => {
const load = async () => {
if (id) {
const response = await httpClient.doGet<ClientScopeRepresentation>(
`/admin/realms/${realm}/client-scopes/${id}`
);
if (response.data) {
Object.entries(response.data).map((entry) => {
const data = await adminClient.clientScopes.findOne({ id });
if (data) {
Object.entries(data).map((entry) => {
if (entry[0] === "attributes") {
convertToFormValues(entry[1], "attributes", setValue);
}
@ -61,7 +57,7 @@ export const ClientScopeForm = () => {
});
}
setClientScope(response.data);
setClientScope(data);
}
};
@ -75,11 +71,10 @@ export const ClientScopeForm = () => {
clientScopes.attributes!
);
const url = `/admin/realms/${realm}/client-scopes/`;
if (id) {
await httpClient.doPut(url + id, clientScopes);
await adminClient.clientScopes.update({ id }, clientScopes);
} else {
await httpClient.doPost(url, clientScopes);
await adminClient.clientScopes.create(clientScopes);
}
addAlert(t((id ? "update" : "create") + "Success"), AlertVariant.success);
} catch (error) {

View file

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import {
AlertVariant,
ButtonVariant,
@ -11,16 +11,15 @@ import {
import { useTranslation } from "react-i18next";
import { Controller, useForm, useWatch } from "react-hook-form";
import { useParams } from "react-router-dom";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { ClientSettings } from "./ClientSettings";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useDownloadDialog } from "../components/download-dialog/DownloadDialog";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { RealmContext } from "../context/realm-context/RealmContext";
import { useAdminClient } from "../context/auth/AdminClient";
import { Credentials } from "./credentials/Credentials";
import { ClientRepresentation } from "../realm/models/Realm";
import {
convertFormValuesToObject,
convertToFormValues,
@ -33,8 +32,7 @@ import {
export const ClientDetails = () => {
const { t } = useTranslation("clients");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const form = useForm();
@ -47,16 +45,15 @@ export const ClientDetails = () => {
const [activeTab, setActiveTab] = useState(0);
const [name, setName] = useState("");
const url = `/admin/realms/${realm}/clients/${id}`;
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "clients:clientDeleteConfirmTitle",
messageKey: "clients:clientDeleteConfirm",
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: () => {
onConfirm: async () => {
try {
httpClient.doDelete(`/admin/realms/${realm}/clients/${id}`);
await adminClient.clients.del({ id });
addAlert(t("clientDeletedSuccess"), AlertVariant.success);
} catch (error) {
addAlert(`${t("clientDeleteError")} ${error}`, AlertVariant.danger);
@ -84,10 +81,10 @@ export const ClientDetails = () => {
useEffect(() => {
(async () => {
const fetchedClient = await httpClient.doGet<ClientRepresentation>(url);
if (fetchedClient.data) {
setName(fetchedClient.data.clientId);
setupForm(fetchedClient.data);
const fetchedClient = await adminClient.clients.findOne({ id });
if (fetchedClient) {
setName(fetchedClient.clientId!);
setupForm(fetchedClient);
}
})();
}, []);
@ -105,7 +102,7 @@ export const ClientDetails = () => {
redirectUris,
attributes,
};
await httpClient.doPut(url, client);
await adminClient.clients.update({ id }, client);
setupForm(client as ClientRepresentation);
addAlert(t("clientSaveSuccess"), AlertVariant.success);
} catch (error) {

View file

@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import {
@ -10,12 +10,11 @@ import {
IFormatterValueType,
} from "@patternfly/react-table";
import { Badge, AlertVariant } from "@patternfly/react-core";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { ExternalLink } from "../components/external-link/ExternalLink";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { useAlerts } from "../components/alert/Alerts";
import { ClientRepresentation } from "./models/client-model";
import { RealmContext } from "../context/realm-context/RealmContext";
import { useAdminClient } from "../context/auth/AdminClient";
import { exportClient } from "../util";
type ClientListProps = {
@ -33,8 +32,7 @@ const columns: (keyof ClientRepresentation)[] = [
export const ClientList = ({ baseUrl, clients, refresh }: ClientListProps) => {
const { t } = useTranslation("clients");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const emptyFormatter = (): IFormatter => (data?: IFormatterValueType) => {
@ -108,9 +106,9 @@ export const ClientList = ({ baseUrl, clients, refresh }: ClientListProps) => {
title: t("common:delete"),
onClick: async (_, rowId) => {
try {
await httpClient.doDelete(
`/admin/realms/${realm}/clients/${data[rowId].client.id}`
);
await adminClient.clients.del({
id: data[rowId].client.id!,
});
refresh();
addAlert(t("clientDeletedSuccess"), AlertVariant.success);
} catch (error) {

View file

@ -1,15 +1,13 @@
import React, { useState, useContext, useEffect } from "react";
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Button, PageSection, Spinner } from "@patternfly/react-core";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { ClientList } from "./ClientList";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { KeycloakContext } from "../context/auth/KeycloakContext";
import { ClientRepresentation } from "./models/client-model";
import { RealmContext } from "../context/realm-context/RealmContext";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { PaginatingTableToolbar } from "../components/table-toolbar/PaginatingTableToolbar";
import { useAdminClient } from "../context/auth/AdminClient";
export const ClientsSection = () => {
const { t } = useTranslation("clients");
@ -18,10 +16,8 @@ export const ClientsSection = () => {
const [max, setMax] = useState(10);
const [first, setFirst] = useState(0);
const [search, setSearch] = useState("");
const adminClient = useAdminClient();
const [clients, setClients] = useState<ClientRepresentation[]>();
const httpClient = useContext(HttpClientContext)!;
const keycloak = useContext(KeycloakContext);
const { realm } = useContext(RealmContext);
const loader = async () => {
const params: { [name: string]: string | number } = { first, max };
@ -29,11 +25,8 @@ export const ClientsSection = () => {
params.clientId = search;
params.search = "true";
}
const result = await httpClient.doGet<ClientRepresentation[]>(
`/admin/realms/${realm}/clients`,
{ params: params }
);
setClients(result.data);
const result = await adminClient.clients.find({ ...params });
setClients(result);
};
useEffect(() => {
@ -84,7 +77,7 @@ export const ClientsSection = () => {
<ClientList
clients={clients}
refresh={loader}
baseUrl={keycloak!.authServerUrl()!}
baseUrl={adminClient.keycloak.authServerUrl!}
/>
</PaginatingTableToolbar>
)}

View file

@ -1,18 +1,28 @@
import React from "react";
import { MemoryRouter } from "react-router-dom";
import { render } from "@testing-library/react";
import KeycloakAdminClient from "keycloak-admin";
import clientMock from "./mock-clients.json";
import { ClientList } from "../ClientList";
import { AdminClient } from "../../context/auth/AdminClient";
test("renders ClientList", () => {
const container = render(
<MemoryRouter>
<AdminClient.Provider
value={
({
setConfig: () => {},
} as unknown) as KeycloakAdminClient
}
>
<ClientList
clients={clientMock}
baseUrl="http://blog.nerdin.ch"
refresh={() => {}}
/>
</AdminClient.Provider>
</MemoryRouter>
);
expect(container).toMatchSnapshot();

View file

@ -1,4 +1,4 @@
import React, { useState, useContext } from "react";
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import {
PageSection,
@ -11,18 +11,16 @@ import {
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { GeneralSettings } from "./GeneralSettings";
import { CapabilityConfig } from "./CapabilityConfig";
import { ClientRepresentation } from "../models/client-model";
import { useAlerts } from "../../components/alert/Alerts";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
export const NewClientForm = () => {
const { t } = useTranslation("clients");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const history = useHistory();
const [client, setClient] = useState<ClientRepresentation>({
@ -42,7 +40,7 @@ export const NewClientForm = () => {
const save = async () => {
try {
await httpClient.doPost(`/admin/realms/${realm}/clients`, client);
await adminClient.clients.create({ ...client });
addAlert(t("createSuccess"), AlertVariant.success);
} catch (error) {
addAlert(t("createError", { error }), AlertVariant.danger);

View file

@ -14,15 +14,15 @@ import {
Split,
SplitItem,
} from "@patternfly/react-core";
import React, { useContext, useEffect, useState } from "react";
import CredentialRepresentation from "keycloak-admin/lib/defs/credentialRepresentation";
import React, { useEffect, useState } from "react";
import { Controller, UseFormMethods, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { useAdminClient } from "../../context/auth/AdminClient";
import { ClientSecret } from "./ClientSecret";
import { SignedJWT } from "./SignedJWT";
import { X509 } from "./X509";
@ -49,8 +49,7 @@ export type CredentialsProps = {
export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
const { t } = useTranslation("clients");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const clientAuthenticatorType = useWatch({
control: form.control,
@ -66,36 +65,37 @@ export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
useEffect(() => {
(async () => {
const response = await httpClient.doGet<ClientAuthenticatorProviders[]>(
`/admin/realms/${realm}/authentication/client-authenticator-providers`
const providers = await adminClient.authenticationManagement.getClientAuthenticatorProviders(
{ id: clientId }
);
setProviders(response.data!);
setProviders(providers);
const secretResponse = await httpClient.doGet<Secret>(
`/admin/realms/${realm}/clients/${clientId}/client-secret`
);
setSecret(secretResponse.data!.value);
const secret = await adminClient.clients.getClientSecret({
id: clientId,
});
setSecret(secret.value!);
})();
}, []);
async function regenerate<T>(
endpoint: string,
call: (clientId: string) => Promise<T>,
message: string
): Promise<T | undefined> {
try {
const response = await httpClient.doPost<T>(
`/admin/realms/${realm}/clients/${clientId}/${endpoint}`,
{ client: clientId, realm }
);
const data = await call(clientId);
addAlert(t(`${message}Success`), AlertVariant.success);
return response.data!;
return data;
} catch (error) {
addAlert(t(`${message}Error`, { error }), AlertVariant.danger);
}
}
const regenerateClientSecret = async () => {
const secret = await regenerate<Secret>("client-secret", "clientSecret");
const secret = await regenerate<CredentialRepresentation>(
(clientId) =>
adminClient.clients.generateNewClientSecret({ id: clientId }),
"clientSecret"
);
setSecret(secret?.value || "");
};
@ -109,7 +109,8 @@ export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
const regenerateAccessToken = async () => {
const accessToken = await regenerate<AccessToken>(
"registration-access-token",
(clientId) =>
adminClient.clients.generateRegistrationAccessToken({ id: clientId }),
"accessToken"
);
setAccessToken(accessToken?.registrationAccessToken || "");

View file

@ -18,7 +18,7 @@ export type SignedJWTProps = {
export const SignedJWT = ({ form }: SignedJWTProps) => {
const providers = sortProviders(
useServerInfo().providers.clientSignature.providers
useServerInfo().providers!.clientSignature.providers
);
const { t } = useTranslation("clients");

View file

@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React from "react";
import {
PageSection,
Form,
@ -11,18 +11,16 @@ import {
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { ClientRepresentation } from "../models/client-model";
import { ClientDescription } from "../ClientDescription";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { JsonFileUpload } from "../../components/json-file-upload/JsonFileUpload";
import { useAlerts } from "../../components/alert/Alerts";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
export const ImportForm = () => {
const { t } = useTranslation("clients");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const form = useForm<ClientRepresentation>();
const { register, handleSubmit, setValue } = form;
@ -44,7 +42,7 @@ export const ImportForm = () => {
const save = async (client: ClientRepresentation) => {
try {
await httpClient.doPost(`/admin/realms/${realm}/clients`, client);
await adminClient.clients.create({ ...client });
addAlert(t("clientImportSuccess"), AlertVariant.success);
} catch (error) {
addAlert(`${t("clientImportError")} '${error}'`, AlertVariant.danger);

View file

@ -1,4 +1,4 @@
import React, { useContext, useState, useEffect, ReactElement } from "react";
import React, { useState, useEffect, ReactElement, useContext } from "react";
import {
Alert,
AlertVariant,
@ -15,11 +15,10 @@ import {
import FileSaver from "file-saver";
import { ConfirmDialogModal } from "../confirm-dialog/ConfirmDialog";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { HelpItem } from "../help-enabler/HelpItem";
import { useTranslation } from "react-i18next";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { useAdminClient } from "../../context/auth/AdminClient";
import { HelpContext } from "../help-enabler/HelpHeader";
export type DownloadDialogProps = {
@ -53,13 +52,12 @@ export const DownloadDialog = ({
toggleDialog,
protocol = "openid-connect",
}: DownloadDialogModalProps) => {
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { t } = useTranslation("common");
const { enabled } = useContext(HelpContext);
const serverInfo = useServerInfo();
const configFormats = serverInfo.clientInstallations[protocol];
const configFormats = serverInfo.clientInstallations![protocol];
const [selected, setSelected] = useState(
configFormats[configFormats.length - 1].id
);
@ -69,11 +67,16 @@ export const DownloadDialog = ({
useEffect(() => {
let isMounted = true;
(async () => {
const response = await httpClient.doGet<string>(
`/admin/realms/${realm}/clients/${id}/installation/providers/${selected}`
);
const snippet = await adminClient.clients.getInstallationProviders({
id,
providerId: selected,
});
if (isMounted) {
setSnippet(await response.text());
if (typeof snippet === "string") {
setSnippet(snippet);
} else {
setSnippet(JSON.stringify(snippet, undefined, 3));
}
}
})();
return () => {

View file

@ -14,9 +14,9 @@ import {
GridItem,
TextArea,
} from "@patternfly/react-core";
import { AccessType } from "keycloak-admin/lib/defs/whoAmIRepresentation";
import { useAccess } from "../../context/access/Access";
import { AccessType } from "../../context/whoami/who-am-i-model";
export type FormAccessProps = FormProps & {
/**

View file

@ -15,7 +15,7 @@ import {
} from "@patternfly/react-core";
import { CheckIcon } from "@patternfly/react-icons";
import { RealmRepresentation } from "../../realm/models/Realm";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
@ -60,7 +60,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
search === ""
? realmList
: realmList.filter(
(r) => r.realm.toLowerCase().indexOf(search.toLowerCase()) !== -1
(r) => r.realm!.toLowerCase().indexOf(search.toLowerCase()) !== -1
);
setFilteredItems(filtered || []);
};
@ -73,11 +73,11 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
<DropdownItem
key={`realm-dropdown-item-${r.realm}`}
onClick={() => {
setRealm(r.realm);
setRealm(r.realm!);
setOpen(!open);
}}
>
<RealmText value={r.realm} />
<RealmText value={r.realm!} />
</DropdownItem>
));
@ -114,7 +114,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
>
{filteredItems.map((item) => (
<ContextSelectorItem key={item.id}>
<RealmText value={item.realm} />
<RealmText value={item.realm!} />
</ContextSelectorItem>
))}
<ContextSelectorItem key="add">

View file

@ -1,8 +1,8 @@
import React, { createContext, useContext } from "react";
import { AccessType } from "keycloak-admin/lib/defs/whoAmIRepresentation";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
import { AccessType } from "../../context/whoami/who-am-i-model";
type AccessContextProps = {
hasAccess: (...types: AccessType[]) => boolean;

View file

@ -0,0 +1,18 @@
import { createContext, useContext } from "react";
import KeycloakAdminClient from "keycloak-admin";
import { RealmContext } from "../realm-context/RealmContext";
export const AdminClient = createContext<KeycloakAdminClient | undefined>(
undefined
);
export const useAdminClient = () => {
const adminClient = useContext(AdminClient)!;
const { realm } = useContext(RealmContext);
adminClient.setConfig({
realmName: realm,
});
return adminClient;
};

View file

@ -1,6 +0,0 @@
import * as React from "react";
import { KeycloakService } from "./keycloak.service";
export const KeycloakContext = React.createContext<KeycloakService | undefined>(
undefined
);

View file

@ -1,84 +0,0 @@
import { KeycloakLoginOptions } from "keycloak-js";
import { useTranslation } from "react-i18next";
export type KeycloakClient = Keycloak.KeycloakInstance;
type Token = {
given_name: string;
family_name: string;
preferred_username: string;
};
export class KeycloakService {
private keycloakAuth: KeycloakClient;
public constructor(keycloak: KeycloakClient) {
this.keycloakAuth = keycloak;
}
public authenticated(): boolean {
return this.keycloakAuth.authenticated
? this.keycloakAuth.authenticated
: false;
}
public login(options?: KeycloakLoginOptions): void {
this.keycloakAuth.login(options);
}
public logout(redirectUri: string = ""): void {
this.keycloakAuth.logout({ redirectUri: redirectUri });
}
public account(): void {
this.keycloakAuth.accountManagement();
}
public authServerUrl(): string | undefined {
const authServerUrl = this.keycloakAuth.authServerUrl;
return authServerUrl!.charAt(authServerUrl!.length - 1) === "/"
? authServerUrl
: authServerUrl + "/";
}
public realm(): string | undefined {
return this.keycloakAuth.realm;
}
public get loggedInUser(): string {
const { t } = useTranslation();
return this.loggedInUserName(t, this.keycloakAuth.tokenParsed as Token);
}
private loggedInUserName = (t: Function, tokenParsed: Token) => {
let userName = t("unknownUser");
if (tokenParsed) {
const givenName = tokenParsed.given_name;
const familyName = tokenParsed.family_name;
const preferredUsername = tokenParsed.preferred_username;
if (givenName && familyName) {
userName = t("fullName", { givenName, familyName });
} else {
userName = givenName || familyName || preferredUsername || userName;
}
}
return userName;
};
public getToken(): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (this.keycloakAuth.token) {
this.keycloakAuth
.updateToken(5)
.then(() => {
resolve(this.keycloakAuth.token as string);
})
.catch(() => {
reject("Failed to refresh token");
});
} else {
reject("Not logged in");
}
});
}
}

View file

@ -1,17 +1,24 @@
import Keycloak, { KeycloakInstance } from "keycloak-js";
import KcAdminClient from "keycloak-admin";
export default async function (): Promise<KcAdminClient> {
const realm =
new URLSearchParams(window.location.search).get("realm") || "master";
const keycloak: KeycloakInstance = Keycloak({
const kcAdminClient = new KcAdminClient();
try {
await kcAdminClient.init(
{ onLoad: "check-sso", pkceMethod: "S256" },
{
url: "http://localhost:8180/auth/",
realm: realm,
clientId: "security-admin-console-v2",
});
export default async function (): Promise<KeycloakInstance> {
await keycloak.init({ onLoad: "check-sso", pkceMethod: "S256" }).catch(() => {
alert("failed to initialize keycloak");
});
return keycloak;
}
);
kcAdminClient.baseUrl = "";
} catch (error) {
alert("failed to initialize keycloak");
}
return kcAdminClient;
}

View file

@ -1,6 +0,0 @@
import { createContext } from "react";
import { HttpClient } from "./http-client";
export const HttpClientContext = createContext<HttpClient | undefined>(
undefined
);

View file

@ -1,150 +0,0 @@
import { KeycloakService } from "../auth/keycloak.service";
type ConfigResolve = (config: RequestInit) => void;
export interface HttpResponse<T = {}> extends Response {
data?: T;
}
export interface RequestInitWithParams extends RequestInit {
params?: { [name: string]: string | number };
}
export class AccountServiceError extends Error {
constructor(message: string) {
super(message);
}
}
/**
*
* @author Stan Silvert ssilvert@redhat.com (C) 2018 Red Hat Inc.
*/
export class HttpClient {
private kcSvc: KeycloakService;
public constructor(keycloakService: KeycloakService) {
this.kcSvc = keycloakService;
}
public async doGet<T>(
endpoint: string,
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, { ...config, method: "get" });
}
public async doDelete<T>(
endpoint: string,
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, { ...config, method: "delete" });
}
public async doPost<T>(
endpoint: string,
body: string | {},
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, {
...config,
body: JSON.stringify(body),
method: "post",
});
}
public async doPut<T>(
endpoint: string,
body: string | {},
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
return this.doRequest(endpoint, {
...config,
body: JSON.stringify(body),
method: "put",
});
}
public async doRequest<T>(
endpoint: string,
config?: RequestInitWithParams
): Promise<HttpResponse<T>> {
const response: HttpResponse<T> = await fetch(
this.makeUrl(endpoint, config).toString(),
await this.makeConfig(config)
);
const contentType = response.headers.get("content-type");
if (contentType && contentType.indexOf("application/json") !== -1) {
try {
response.data = await response.json();
} catch (e) {
console.warn(e);
}
}
if (!response.ok) {
this.handleError(response);
}
return response;
}
private handleError(response: HttpResponse): void {
if (response != null && response.status === 401) {
// session timed out?
this.kcSvc.login();
}
if (response != null && response.data != null) {
throw new AccountServiceError((response.data as any).errorMessage);
} else {
throw new AccountServiceError(response.statusText);
}
}
private makeUrl(url: string, config?: RequestInitWithParams): string {
const searchParams = new URLSearchParams();
// add request params
if (config && {}.hasOwnProperty.call(config, "params")) {
const params: { [name: string]: string } = (config.params as {}) || {};
Object.keys(params).forEach((key) =>
searchParams.append(key, params[key])
);
}
return url + "?" + searchParams.toString();
}
private makeConfig(config: RequestInit = {}): Promise<RequestInit> {
return new Promise((resolve: ConfigResolve) => {
this.kcSvc
.getToken()
.then((token: string) => {
resolve({
...config,
headers: {
"Content-Type": "application/json",
...config.headers,
Authorization: "Bearer " + token,
},
});
})
.catch(() => {
this.kcSvc.login();
});
});
}
}
window.addEventListener(
"unhandledrejection",
(event: PromiseRejectionEvent) => {
event.promise.catch((error) => {
if (error instanceof AccountServiceError) {
// We already handled the error. Ignore unhandled rejection.
event.preventDefault();
}
});
}
);

View file

@ -1,8 +1,9 @@
import React, { createContext, ReactNode, useContext } from "react";
import { ServerInfoRepresentation } from "./server-info";
import { HttpClientContext } from "../http-service/HttpClientContext";
import { ServerInfoRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
import { sortProviders } from "../../util";
import { DataLoader } from "../../components/data-loader/DataLoader";
import { useAdminClient } from "../auth/AdminClient";
export const ServerInfoContext = createContext<ServerInfoRepresentation>(
{} as ServerInfoRepresentation
@ -11,16 +12,13 @@ export const ServerInfoContext = createContext<ServerInfoRepresentation>(
export const useServerInfo = () => useContext(ServerInfoContext);
export const useLoginProviders = () => {
return sortProviders(useServerInfo().providers["login-protocol"].providers);
return sortProviders(useServerInfo().providers!["login-protocol"].providers);
};
export const ServerInfoProvider = ({ children }: { children: ReactNode }) => {
const httpClient = useContext(HttpClientContext)!;
const adminClient = useAdminClient();
const loader = async () => {
const response = await httpClient.doGet<ServerInfoRepresentation>(
"/admin/serverinfo"
);
return response.data!;
return await adminClient.serverInfo.find();
};
return (
<DataLoader loader={loader}>

View file

@ -1,123 +0,0 @@
export interface ServerInfoRepresentation {
systemInfo: SystemInfoRepresentation;
memoryInfo: MemoryInfoRepresentation;
profileInfo: ProfileInfoRepresentation;
themes: { [index: string]: ThemeInfoRepresentation[] };
socialProviders: { [index: string]: string }[];
identityProviders: { [index: string]: string }[];
clientImporters: { [index: string]: string }[];
providers: { [index: string]: SpiInfoRepresentation };
protocolMapperTypes: { [index: string]: ProtocolMapperTypeRepresentation[] };
builtinProtocolMappers: { [index: string]: ProtocolMapperRepresentation[] };
clientInstallations: { [index: string]: ClientInstallationRepresentation[] };
componentTypes: { [index: string]: ComponentTypeRepresentation[] };
passwordPolicies: PasswordPolicyTypeRepresentation[];
enums: { [index: string]: string[] };
}
export interface SystemInfoRepresentation {
version: string;
serverTime: string;
uptime: string;
uptimeMillis: number;
javaVersion: string;
javaVendor: string;
javaVm: string;
javaVmVersion: string;
javaRuntime: string;
javaHome: string;
osName: string;
osArchitecture: string;
osVersion: string;
fileEncoding: string;
userName: string;
userDir: string;
userTimezone: string;
userLocale: string;
}
export interface MemoryInfoRepresentation {
total: number;
totalFormated: string;
used: number;
usedFormated: string;
free: number;
freePercentage: number;
freeFormated: string;
}
export interface ProfileInfoRepresentation {
name: string;
disabledFeatures: string[];
previewFeatures: string[];
experimentalFeatures: string[];
}
export interface ThemeInfoRepresentation {
name: string;
locales: string[];
}
export interface SpiInfoRepresentation {
internal: boolean;
providers: { [index: string]: ProviderRepresentation };
}
export interface ProtocolMapperTypeRepresentation {
id: string;
name: string;
category: string;
helpText: string;
priority: number;
properties: ConfigPropertyRepresentation[];
}
export interface ProtocolMapperRepresentation {
id: string;
name: string;
protocol: string;
protocolMapper: string;
consentRequired: boolean;
consentText: string;
config: { [index: string]: string };
}
export interface ClientInstallationRepresentation {
id: string;
protocol: string;
downloadOnly: boolean;
displayType: string;
helpText: string;
filename: string;
mediaType: string;
}
export interface ComponentTypeRepresentation {
id: string;
helpText: string;
properties: ConfigPropertyRepresentation[];
metadata: { [index: string]: any };
}
export interface PasswordPolicyTypeRepresentation {
id: string;
displayName: string;
configType: string;
defaultValue: string;
multipleSupported: boolean;
}
export interface ProviderRepresentation {
order: number;
operationalInfo: { [index: string]: string };
}
export interface ConfigPropertyRepresentation {
name: string;
label: string;
helpText: string;
type: string;
defaultValue: any;
options: string[];
secret: boolean;
}

View file

@ -1,11 +1,11 @@
import React, { useContext } from "react";
import React from "react";
import i18n from "../../i18n";
import WhoAmIRepresentation, { AccessType } from "./who-am-i-model";
import { HttpClientContext } from "../http-service/HttpClientContext";
import { KeycloakContext } from "../auth/KeycloakContext";
import { DataLoader } from "../../components/data-loader/DataLoader";
import { useAdminClient } from "../auth/AdminClient";
import WhoAmIRepresentation, {
AccessType,
} from "keycloak-admin/lib/defs/whoAmIRepresentation";
export class WhoAmI {
constructor(
@ -53,24 +53,19 @@ export const WhoAmIContext = React.createContext(new WhoAmI());
type WhoAmIProviderProps = { children: React.ReactNode };
export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => {
const httpClient = useContext(HttpClientContext)!;
const keycloak = useContext(KeycloakContext);
const adminClient = useAdminClient();
const whoAmILoader = async () => {
if (keycloak === undefined) return undefined;
if (adminClient.keycloak === undefined) return undefined;
const realm = keycloak.realm();
return await httpClient
.doGet(`/admin/${realm}/console/whoami/`)
.then((r) => r.data as WhoAmIRepresentation);
return await adminClient.whoAmI.find();
};
return (
<DataLoader loader={whoAmILoader}>
{(whoamirep) => (
<WhoAmIContext.Provider
value={new WhoAmI(keycloak?.realm(), whoamirep.data)}
value={new WhoAmI(adminClient.keycloak?.realm, whoamirep.data)}
>
{children}
</WhoAmIContext.Provider>

View file

@ -1,29 +0,0 @@
export type AccessType =
| "view-realm"
| "view-identity-providers"
| "manage-identity-providers"
| "impersonation"
| "create-client"
| "manage-users"
| "query-realms"
| "view-authorization"
| "query-clients"
| "query-users"
| "manage-events"
| "manage-realm"
| "view-events"
| "view-users"
| "view-clients"
| "manage-authorization"
| "manage-clients"
| "query-groups"
| "anyone";
export default interface WhoAmIRepresentation {
userId: string;
realm: string;
displayName: string;
locale: string;
createRealm: boolean;
realm_access: { [key: string]: AccessType[] };
}

View file

@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React from "react";
import {
AlertVariant,
Button,
@ -10,8 +10,7 @@ import {
ValidatedOptions,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { RealmContext } from "../context/realm-context/RealmContext";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
import { useForm } from "react-hook-form";
@ -33,8 +32,7 @@ export const GroupsCreateModal = ({
refresh,
}: GroupsCreateModalProps) => {
const { t } = useTranslation("groups");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const form = useForm();
const { register, errors } = form;
@ -46,9 +44,7 @@ export const GroupsCreateModal = ({
const submitForm = async () => {
if (await form.trigger()) {
try {
await httpClient.doPost(`/admin/realms/${realm}/groups`, {
name: createGroupName,
});
await adminClient.groups.create({ name: createGroupName });
refresh();
setIsCreateModalOpen(false);
setCreateGroupName("");

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext } from "react";
import React, { useState, useEffect } from "react";
import {
Table,
TableHeader,
@ -9,8 +9,7 @@ import { Button, AlertVariant } from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { GroupRepresentation } from "./models/groups";
import { UsersIcon } from "@patternfly/react-icons";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { RealmContext } from "../context/realm-context/RealmContext";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
export type GroupsListProps = {
@ -32,10 +31,9 @@ export const GroupsList = ({
setTableRowSelectedArray,
}: GroupsListProps) => {
const { t } = useTranslation("groups");
const httpClient = useContext(HttpClientContext)!;
const adminClient = useAdminClient();
const columnGroupName: keyof GroupRepresentation = "name";
const columnGroupNumber: keyof GroupRepresentation = "membersLength";
const { realm } = useContext(RealmContext);
const { addAlert } = useAlerts();
const [formattedData, setFormattedData] = useState<FormattedData[]>([]);
@ -107,9 +105,7 @@ export const GroupsList = ({
rowId: number
) => {
try {
await httpClient.doDelete(
`/admin/realms/${realm}/groups/${list![rowId].id}`
);
await adminClient.groups.del({ id: list![rowId].id! });
refresh();
setTableRowSelectedArray([]);
addAlert(t("Group deleted"), AlertVariant.success);

View file

@ -1,17 +1,11 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { GroupsList } from "./GroupsList";
import { GroupsCreateModal } from "./GroupsCreateModal";
import { GroupRepresentation } from "./models/groups";
import {
ServerGroupsArrayRepresentation,
ServerGroupMembersRepresentation,
} from "./models/server-info";
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { RealmContext } from "../context/realm-context/RealmContext";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
import {
Button,
@ -25,10 +19,11 @@ import {
AlertVariant,
} from "@patternfly/react-core";
import "./GroupsSection.css";
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
export const GroupsSection = () => {
const { t } = useTranslation("groups");
const httpClient = useContext(HttpClientContext)!;
const adminClient = useAdminClient();
const [rawData, setRawData] = useState<{ [key: string]: any }[]>();
const [filteredData, setFilteredData] = useState<{ [key: string]: any }[]>();
const [isKebabOpen, setIsKebabOpen] = useState(false);
@ -38,22 +33,16 @@ export const GroupsSection = () => {
Array<number>
>([]);
const columnID: keyof GroupRepresentation = "id";
const membersLength: keyof GroupRepresentation = "membersLength";
const columnGroupName: keyof GroupRepresentation = "name";
const { addAlert } = useAlerts();
const { realm } = useContext(RealmContext);
const membersLength = "membersLength";
const loader = async () => {
const groups = await httpClient.doGet<ServerGroupsArrayRepresentation[]>(
`/admin/realms/${realm}/groups`
);
const groupsData = groups.data!;
const getMembers = async (id: number) => {
const response = await httpClient.doGet<
ServerGroupMembersRepresentation[]
>(`/admin/realms/${realm}/groups/${id}/members`);
const responseData = response.data!;
return responseData.length;
const groupsData = await adminClient.groups.find();
const getMembers = async (id: string) => {
const response = await adminClient.groups.listMembers({ id });
return response.length;
};
const memberPromises = groupsData.map((group: { [key: string]: any }) =>
@ -101,11 +90,9 @@ export const GroupsSection = () => {
if (tableRowSelectedArray.length !== 0) {
const deleteGroup = async (rowId: number) => {
try {
await httpClient.doDelete(
`/admin/realms/${realm}/groups/${
filteredData ? filteredData![rowId].id : rawData![rowId].id
}`
);
await adminClient.groups.del({
id: filteredData ? filteredData![rowId].id : rawData![rowId].id,
});
loader();
} catch (error) {
addAlert(`${t("groupDeleteError")} ${error}`, AlertVariant.danger);

View file

@ -2,22 +2,20 @@ import React from "react";
import ReactDom from "react-dom";
import i18n from "./i18n";
import { App } from "./App";
import { AdminClient } from "./context/auth/AdminClient";
import init from "./context/auth/keycloak";
import { KeycloakContext } from "./context/auth/KeycloakContext";
import { KeycloakService } from "./context/auth/keycloak.service";
import { HttpClientContext } from "./context/http-service/HttpClientContext";
import { HttpClient } from "./context/http-service/http-client";
import { App } from "./App";
import { RealmContextProvider } from "./context/realm-context/RealmContext";
console.info("supported languages", ...i18n.languages);
init().then((keycloak) => {
const keycloakService = new KeycloakService(keycloak);
init().then((adminClient) => {
ReactDom.render(
<KeycloakContext.Provider value={keycloakService}>
<HttpClientContext.Provider value={new HttpClient(keycloakService)}>
<RealmContextProvider>
<AdminClient.Provider value={adminClient}>
<App />
</HttpClientContext.Provider>
</KeycloakContext.Provider>,
</AdminClient.Provider>
</RealmContextProvider>,
document.getElementById("app")
);
});

View file

@ -1,14 +1,13 @@
import React, { useContext, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Button, PageSection } from "@patternfly/react-core";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { RoleRepresentation } from "../model/role-model";
import { RolesList } from "./RoleList";
import { RealmContext } from "../context/realm-context/RealmContext";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAdminClient } from "../context/auth/AdminClient";
import { PaginatingTableToolbar } from "../components/table-toolbar/PaginatingTableToolbar";
import { ViewHeader } from "../components/view-header/ViewHeader";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
export const RealmRolesSection = () => {
@ -16,19 +15,11 @@ export const RealmRolesSection = () => {
const [first, setFirst] = useState(0);
const { t } = useTranslation("roles");
const history = useHistory();
const httpClient = useContext(HttpClientContext)!;
const adminClient = useAdminClient();
const [roles, setRoles] = useState<RoleRepresentation[]>();
const { realm } = useContext(RealmContext);
const loader = async () => {
const params: { [name: string]: string | number } = { first, max };
const result = await httpClient.doGet<RoleRepresentation[]>(
`/admin/realms/${realm}/roles`,
{ params: params }
);
setRoles(result.data);
};
const loader = async () => setRoles(await adminClient.roles.find(params));
useEffect(() => {
loader();

View file

@ -1,4 +1,4 @@
import React, { useContext, useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
@ -11,11 +11,10 @@ import {
} from "@patternfly/react-table";
import { ExternalLink } from "../components/external-link/ExternalLink";
import { RoleRepresentation } from "../model/role-model";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { AlertVariant, ButtonVariant } from "@patternfly/react-core";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
import { RealmContext } from "../context/realm-context/RealmContext";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
type RolesListProps = {
@ -31,8 +30,7 @@ const columns: (keyof RoleRepresentation)[] = [
export const RolesList = ({ roles, refresh }: RolesListProps) => {
const { t } = useTranslation("roles");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const [selectedRowId, setSelectedRowId] = useState(-1);
@ -71,9 +69,9 @@ export const RolesList = ({ roles, refresh }: RolesListProps) => {
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await httpClient.doDelete(
`/admin/realms/${realm}/roles/${data[selectedRowId].role.name}`
);
await adminClient.roles.delByName({
name: data[selectedRowId].role.name!,
});
refresh();
addAlert(t("roleDeletedSuccess"), AlertVariant.success);
} catch (error) {

View file

@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import {
Text,
@ -15,17 +15,15 @@ import {
ValidatedOptions,
} from "@patternfly/react-core";
import { RoleRepresentation } from "../../model/role-model";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { useAlerts } from "../../components/alert/Alerts";
import { Controller, useForm } from "react-hook-form";
import { RealmContext } from "../../context/realm-context/RealmContext";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
export const NewRoleForm = () => {
const { t } = useTranslation("roles");
const httpClient = useContext(HttpClientContext)!;
const { addAlert } = useAlerts();
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const { register, control, errors, handleSubmit } = useForm<
RoleRepresentation
@ -33,7 +31,7 @@ export const NewRoleForm = () => {
const save = async (role: RoleRepresentation) => {
try {
await httpClient.doPost(`admin/realms/${realm}/roles`, role);
await adminClient.roles.create(role);
addAlert(t("roleCreated"), AlertVariant.success);
} catch (error) {
addAlert(`${t("roleCreateError")} '${error}'`, AlertVariant.danger);

View file

@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import {
PageSection,
@ -12,15 +12,15 @@ import {
} from "@patternfly/react-core";
import { JsonFileUpload } from "../../components/json-file-upload/JsonFileUpload";
import { RealmRepresentation } from "../models/Realm";
import { HttpClientContext } from "../../context/http-service/HttpClientContext";
import { useAlerts } from "../../components/alert/Alerts";
import { useForm, Controller } from "react-hook-form";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
export const NewRealmForm = () => {
const { t } = useTranslation("realm");
const httpClient = useContext(HttpClientContext)!;
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const { register, handleSubmit, setValue, control } = useForm<
@ -38,7 +38,7 @@ export const NewRealmForm = () => {
const save = async (realm: RealmRepresentation) => {
try {
await httpClient.doPost("/admin/realms", realm);
await adminClient.realms.create(realm);
addAlert(t("Realm created"), AlertVariant.success);
} catch (error) {
addAlert(

View file

@ -1,4 +1,6 @@
import { TFunction } from "i18next";
import { AccessType } from "keycloak-admin/lib/defs/whoAmIRepresentation";
import { AuthenticationSection } from "./authentication/AuthenticationSection";
import { ClientScopeForm } from "./client-scopes/form/ClientScopeForm";
import { ClientScopesSection } from "./client-scopes/ClientScopesSection";
@ -17,8 +19,6 @@ import { SessionsSection } from "./sessions/SessionsSection";
import { UserFederationSection } from "./user-federation/UserFederationSection";
import { UsersSection } from "./user/UsersSection";
import { MappingDetails } from "./client-scopes/details/MappingDetails";
import { AccessType } from "./context/whoami/who-am-i-model";
import { ClientDetails } from "./clients/ClientDetails";
export type RouteDef = {

View file

@ -7,8 +7,8 @@ import roles from "../realm-roles/__tests__/mock-roles.json";
import { ServerInfoContext } from "../context/server-info/ServerInfoProvider";
import { RoleMappingForm } from "../client-scopes/add/RoleMappingForm";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { HttpClient } from "../context/http-service/http-client";
import { AdminClient } from "../context/auth/AdminClient";
import KeycloakAdminClient from "keycloak-admin";
export default {
title: "Role Mapping Form",
@ -17,18 +17,24 @@ export default {
export const RoleMappingFormExample = () => (
<ServerInfoContext.Provider value={serverInfo}>
<HttpClientContext.Provider
<AdminClient.Provider
value={
({
doGet: () => {
return { data: roles };
setConfig: () => {},
roles: {
find: () => {
return roles;
},
} as unknown) as HttpClient
},
clients: {
find: () => roles,
},
} as unknown) as KeycloakAdminClient
}
>
<Page>
<RoleMappingForm clientScopeId="dummy" />
</Page>
</HttpClientContext.Provider>
</AdminClient.Provider>
</ServerInfoContext.Provider>
);

View file

@ -15,45 +15,43 @@ import {
TextVariants,
} from "@patternfly/react-core";
import ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation";
import { KeycloakCard } from "../components/keycloak-card/KeycloakCard";
import { useAlerts } from "../components/alert/Alerts";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { DatabaseIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next";
import { RealmContext } from "../context/realm-context/RealmContext";
import { HttpClientContext } from "../context/http-service/HttpClientContext";
import { UserFederationRepresentation } from "./model/userFederation";
import { useAdminClient } from "../context/auth/AdminClient";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import "./user-federation.css";
type Config = {
enabled: string[];
};
export const UserFederationSection = () => {
const [userFederations, setUserFederations] = useState<
UserFederationRepresentation[]
ComponentRepresentation[]
>();
const { addAlert } = useAlerts();
const { t } = useTranslation("user-federation");
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const loader = async () => {
const testParams: { [name: string]: string | number } = {
parentId: realm,
type: "org.keycloak.storage.UserStorageProvider", // MF note that this is providerType in the output, but API call is still type
};
const result = await httpClient.doGet<UserFederationRepresentation[]>(
`/admin/realms/${realm}/components`,
{
params: testParams,
}
);
setUserFederations(result.data);
const userFederations = await adminClient.components.find(testParams);
setUserFederations(userFederations);
};
useEffect(() => {
loader();
}, []);
const { t } = useTranslation("user-federation");
const httpClient = useContext(HttpClientContext)!;
const { realm } = useContext(RealmContext);
const ufAddProviderDropdownItems = [
<DropdownItem key="itemLDAP">LDAP</DropdownItem>,
<DropdownItem key="itemKerberos">Kerberos</DropdownItem>,
@ -75,9 +73,8 @@ export const UserFederationSection = () => {
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
httpClient
.doDelete(`/admin/realms/${realm}/components/${currentCard}`)
.then(() => loader());
await adminClient.components.del({ id: currentCard });
await loader();
addAlert(t("userFedDeletedSuccess"), AlertVariant.success);
} catch (error) {
addAlert(t("userFedDeleteError", { error }), AlertVariant.danger);
@ -96,7 +93,7 @@ export const UserFederationSection = () => {
<DropdownItem
key={`${index}-cardDelete`}
onClick={() => {
toggleDeleteForCard(userFederation.id);
toggleDeleteForCard(userFederation.id!);
}}
>
{t("common:delete")}
@ -105,14 +102,14 @@ export const UserFederationSection = () => {
return (
<GalleryItem key={index}>
<KeycloakCard
id={userFederation.id}
id={userFederation.id!}
dropdownItems={ufCardDropdownItems}
title={userFederation.name}
title={userFederation.name!}
footerText={
userFederation.providerId === "ldap" ? "LDAP" : "Kerberos"
}
labelText={
userFederation.config.enabled
(userFederation.config as Config)!.enabled[0] !== "false"
? `${t("common:enabled")}`
: `${t("common:disabled")}`
}

View file

@ -1,8 +0,0 @@
export interface UserFederationRepresentation {
id: string;
name: string;
providerId: string;
providerType: string;
parentId: string;
config: { [index: string]: any };
}

View file

@ -1,7 +1,6 @@
import FileSaver from "file-saver";
import { ClientRepresentation } from "./clients/models/client-model";
import { ProviderRepresentation } from "./context/server-info/server-info";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { ProviderRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
export const sortProviders = (providers: {
[index: string]: ProviderRepresentation;

View file

@ -5482,6 +5482,13 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
axios@^0.21.0:
version "0.21.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
dependencies:
follow-redirects "^1.10.0"
axobject-query@^2.0.2:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@ -6567,6 +6574,11 @@ camelcase@^6.0.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e"
integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==
camelize@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
can-use-dom@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/can-use-dom/-/can-use-dom-0.1.0.tgz#22cc4a34a0abc43950f42c6411024a3f6366b45a"
@ -9415,6 +9427,11 @@ follow-redirects@^1.0.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6"
integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg==
follow-redirects@^1.10.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
for-in@^0.1.3:
version "0.1.8"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
@ -12472,10 +12489,23 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3, jsx-ast-utils@^2.4.1:
array-includes "^3.1.1"
object.assign "^4.1.0"
keycloak-js@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-11.0.0.tgz#2bc3126e192e3c5279ca157c33f4cc657467540a"
integrity sha512-hjpIrO+ujaRsSJC76xEpVlZVAPpXm3OZYruxGa/cZ/PF7lwp9kRmIinqCxdg0jttr4dogCbOZ2YrFpqPx4a8mw==
keycloak-admin@^1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.1.tgz#d30507b1b89270e42dc2e1ab73842afe3df5c0af"
integrity sha512-duQYY+I8iSeSOYtsJnyfQ9LUuKeY5oZCp2rkfH4tJ71VYuk0bon4liN2OBGmLO8PBsZ2TXW95rSydZumysuP/g==
dependencies:
axios "^0.21.0"
camelize "^1.0.0"
keycloak-js "^11.0.3"
lodash "^4.17.20"
query-string "^6.13.7"
url-join "^4.0.0"
url-template "^2.0.8"
keycloak-js@^11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-11.0.3.tgz#5f22f22662211e2bfa5327d3d2eb83020a5baa23"
integrity sha512-e2OVyCiru25UhJz3aPj5irf//+vJzvAhHdcsCIWAcvF8Te22iUoZqEdNFji8D3zNzDehX4VpuIJwQOYCj6rqTA==
dependencies:
base64-js "1.3.1"
js-sha256 "0.9.0"
@ -12771,7 +12801,7 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.5, lodash@~4.17.10, lodash@~4.17.5:
"lodash@>=3.5 <5", lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@~4.17.10, lodash@~4.17.5:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@ -15492,6 +15522,15 @@ query-string@^4.1.0:
object-assign "^4.1.0"
strict-uri-encode "^1.0.0"
query-string@^6.13.7:
version "6.13.7"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.7.tgz#af53802ff6ed56f3345f92d40a056f93681026ee"
integrity sha512-CsGs8ZYb39zu0WLkeOhe0NMePqgYdAuCqxOYKDR5LVCytDZYMGx3Bb+xypvQvPHVPijRXB0HZNFllCzHRe4gEA==
dependencies:
decode-uri-component "^0.2.0"
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"
querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@ -17270,6 +17309,11 @@ spdy@^4.0.1:
select-hose "^2.0.0"
spdy-transport "^3.0.0"
split-on-first@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
split-string@^3.0.1, split-string@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@ -17399,6 +17443,11 @@ strict-uri-encode@^1.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
string-length@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
@ -18421,6 +18470,11 @@ urix@^0.1.0:
resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
url-join@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7"
integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==
url-loader@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-2.3.0.tgz#e0e2ef658f003efb8ca41b0f3ffbf76bab88658b"
@ -18447,6 +18501,11 @@ url-parse@^1.4.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
url-template@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE=
url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"