Added the credentials tab (#196)
* initial version credentials tab * added signed jwt * submit token endpoint signing alg * added x509 * show tab when public client * fixed test default confimnation dailog size is small * pr review comments
This commit is contained in:
parent
6b3990e1b9
commit
2393b40c9b
15 changed files with 711 additions and 247 deletions
199
src/clients/ClientDetails.tsx
Normal file
199
src/clients/ClientDetails.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
import React, { useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
AlertVariant,
|
||||
ButtonVariant,
|
||||
DropdownItem,
|
||||
PageSection,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabTitleText,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
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 { Credentials } from "./credentials/Credentials";
|
||||
import { ClientRepresentation } from "../realm/models/Realm";
|
||||
import {
|
||||
convertFormValuesToObject,
|
||||
convertToFormValues,
|
||||
exportClient,
|
||||
} from "../util";
|
||||
import {
|
||||
convertToMultiline,
|
||||
toValue,
|
||||
} from "../components/multi-line-input/MultiLineInput";
|
||||
|
||||
export const ClientDetails = () => {
|
||||
const { t } = useTranslation("clients");
|
||||
const httpClient = useContext(HttpClientContext)!;
|
||||
const { realm } = useContext(RealmContext);
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const form = useForm();
|
||||
const publicClient = useWatch({
|
||||
control: form.control,
|
||||
name: "publicClient",
|
||||
defaultValue: false,
|
||||
});
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
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: () => {
|
||||
try {
|
||||
httpClient.doDelete(`/admin/realms/${realm}/clients/${id}`);
|
||||
addAlert(t("clientDeletedSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(`${t("clientDeleteError")} ${error}`, AlertVariant.danger);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [toggleDownloadDialog, DownloadDialog] = useDownloadDialog({
|
||||
id,
|
||||
protocol: form.getValues("protocol"),
|
||||
});
|
||||
|
||||
const setupForm = (client: ClientRepresentation) => {
|
||||
form.reset(client);
|
||||
Object.entries(client).map((entry) => {
|
||||
if (entry[0] === "redirectUris") {
|
||||
form.setValue(entry[0], convertToMultiline(entry[1]));
|
||||
} else if (entry[0] === "attributes") {
|
||||
convertToFormValues(entry[1], "attributes", form.setValue);
|
||||
} else {
|
||||
form.setValue(entry[0], entry[1]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const fetchedClient = await httpClient.doGet<ClientRepresentation>(url);
|
||||
if (fetchedClient.data) {
|
||||
setName(fetchedClient.data.clientId);
|
||||
setupForm(fetchedClient.data);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const save = async () => {
|
||||
if (await form.trigger()) {
|
||||
const redirectUris = toValue(form.getValues()["redirectUris"]);
|
||||
const attributes = form.getValues()["attributes"]
|
||||
? convertFormValuesToObject(form.getValues()["attributes"])
|
||||
: {};
|
||||
|
||||
try {
|
||||
const client = {
|
||||
...form.getValues(),
|
||||
redirectUris,
|
||||
attributes,
|
||||
};
|
||||
await httpClient.doPut(url, client);
|
||||
setupForm(client as ClientRepresentation);
|
||||
addAlert(t("clientSaveSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(`${t("clientSaveError")} '${error}'`, AlertVariant.danger);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
<DownloadDialog />
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={form.control}
|
||||
defaultValue={true}
|
||||
render={({ onChange, value }) => {
|
||||
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:disableConfirmTitle",
|
||||
messageKey: "clients:disableConfirm",
|
||||
continueButtonLabel: "common:disable",
|
||||
onConfirm: () => {
|
||||
onChange(!value);
|
||||
save();
|
||||
},
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<DisableConfirm />
|
||||
<ViewHeader
|
||||
titleKey={name}
|
||||
subKey="clients:clientsExplain"
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="download"
|
||||
onClick={() => toggleDownloadDialog()}
|
||||
>
|
||||
{t("downloadAdapterConfig")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="export"
|
||||
onClick={() => exportClient(form.getValues())}
|
||||
>
|
||||
{t("common:export")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
onClick={() => toggleDeleteDialog()}
|
||||
>
|
||||
{t("common:delete")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
isEnabled={value}
|
||||
onToggle={(value) => {
|
||||
if (!value) {
|
||||
toggleDisableDialog();
|
||||
} else {
|
||||
onChange(value);
|
||||
save();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onSelect={(_, key) => setActiveTab(key as number)}
|
||||
isBox
|
||||
>
|
||||
<Tab
|
||||
eventKey={0}
|
||||
title={<TabTitleText>{t("settings")}</TabTitleText>}
|
||||
>
|
||||
<ClientSettings form={form} save={save} />
|
||||
</Tab>
|
||||
{publicClient && (
|
||||
<Tab
|
||||
eventKey={1}
|
||||
title={<TabTitleText>{t("credentials")}</TabTitleText>}
|
||||
>
|
||||
<Credentials clientId={id} form={form} save={save} />
|
||||
</Tab>
|
||||
)}
|
||||
</Tabs>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext, useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
FormGroup,
|
||||
|
@ -6,152 +6,27 @@ import {
|
|||
Form,
|
||||
Switch,
|
||||
TextArea,
|
||||
PageSection,
|
||||
ActionGroup,
|
||||
Button,
|
||||
AlertVariant,
|
||||
ButtonVariant,
|
||||
DropdownItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, UseFormMethods } from "react-hook-form";
|
||||
|
||||
import { ScrollForm } from "../components/scroll-form/ScrollForm";
|
||||
import { ClientDescription } from "./ClientDescription";
|
||||
import { CapabilityConfig } from "./add/CapabilityConfig";
|
||||
import { RealmContext } from "../context/realm-context/RealmContext";
|
||||
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
||||
import { ClientRepresentation } from "../realm/models/Realm";
|
||||
import {
|
||||
convertToMultiline,
|
||||
MultiLineInput,
|
||||
toValue,
|
||||
} from "../components/multi-line-input/MultiLineInput";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { exportClient } from "../util";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { useDownloadDialog } from "../components/download-dialog/DownloadDialog";
|
||||
import { MultiLineInput } from "../components/multi-line-input/MultiLineInput";
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
|
||||
export const ClientSettings = () => {
|
||||
type ClientSettingsProps = {
|
||||
form: UseFormMethods;
|
||||
save: () => void;
|
||||
};
|
||||
|
||||
export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const httpClient = useContext(HttpClientContext)!;
|
||||
const { realm } = useContext(RealmContext);
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [name, setName] = useState("");
|
||||
const form = useForm();
|
||||
const url = `/admin/realms/${realm}/clients/${id}`;
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const fetchedClient = await httpClient.doGet<ClientRepresentation>(url);
|
||||
if (fetchedClient.data) {
|
||||
setName(fetchedClient.data.clientId);
|
||||
Object.entries(fetchedClient.data).map((entry) => {
|
||||
if (entry[0] !== "redirectUris") {
|
||||
form.setValue(entry[0], entry[1]);
|
||||
} else if (entry[1] && entry[1].length > 0) {
|
||||
form.setValue(entry[0], convertToMultiline(entry[1]));
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:clientDeleteConfirmTitle",
|
||||
messageKey: "clients:clientDeleteConfirm",
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: () => {
|
||||
try {
|
||||
httpClient.doDelete(`/admin/realms/${realm}/clients/${id}`);
|
||||
addAlert(t("clientDeletedSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(`${t("clientDeleteError")} ${error}`, AlertVariant.danger);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [toggleDownloadDialog, DownloadDialog] = useDownloadDialog({
|
||||
id,
|
||||
protocol: form.getValues("protocol"),
|
||||
});
|
||||
|
||||
const save = async () => {
|
||||
if (await form.trigger()) {
|
||||
const redirectUris = toValue(form.getValues()["redirectUris"]);
|
||||
try {
|
||||
httpClient.doPut(url, { ...form.getValues(), redirectUris });
|
||||
addAlert(t("clientSaveSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(`${t("clientSaveError")} '${error}'`, AlertVariant.danger);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
<DownloadDialog />
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={form.control}
|
||||
defaultValue={true}
|
||||
render={({ onChange, value }) => {
|
||||
const [toggleDisableDialog, DisableConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:disableConfirmTitle",
|
||||
messageKey: "clients:disableConfirm",
|
||||
continueButtonLabel: "common:disable",
|
||||
onConfirm: () => {
|
||||
onChange(!value);
|
||||
save();
|
||||
},
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<DisableConfirm />
|
||||
<ViewHeader
|
||||
titleKey={name}
|
||||
subKey="clients:clientsExplain"
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="download"
|
||||
onClick={() => toggleDownloadDialog()}
|
||||
>
|
||||
{t("downloadAdapterConfig")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="export"
|
||||
onClick={() => exportClient(form.getValues())}
|
||||
>
|
||||
{t("common:export")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
onClick={() => toggleDeleteDialog()}
|
||||
>
|
||||
{t("common:delete")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
isEnabled={value}
|
||||
onToggle={(value) => {
|
||||
if (!value) {
|
||||
toggleDisableDialog();
|
||||
} else {
|
||||
onChange(value);
|
||||
save();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<ScrollForm
|
||||
sections={[
|
||||
t("capabilityConfig"),
|
||||
|
@ -244,7 +119,6 @@ export const ClientSettings = () => {
|
|||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</ScrollForm>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
34
src/clients/credentials/ClientSecret.tsx
Normal file
34
src/clients/credentials/ClientSecret.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
ClipboardCopy,
|
||||
FormGroup,
|
||||
Split,
|
||||
SplitItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
export type ClientSecretProps = {
|
||||
secret: string;
|
||||
toggle: () => void;
|
||||
};
|
||||
|
||||
export const ClientSecret = ({ secret, toggle }: ClientSecretProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
return (
|
||||
<FormGroup label={t("clientSecret")} fieldId="kc-client-secret">
|
||||
<Split hasGutter>
|
||||
<SplitItem isFilled>
|
||||
<ClipboardCopy id="kc-client-secret" isReadOnly>
|
||||
{secret}
|
||||
</ClipboardCopy>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Button variant="secondary" onClick={toggle}>
|
||||
{t("regenerate")}
|
||||
</Button>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
230
src/clients/credentials/Credentials.tsx
Normal file
230
src/clients/credentials/Credentials.tsx
Normal file
|
@ -0,0 +1,230 @@
|
|||
import {
|
||||
ActionGroup,
|
||||
AlertVariant,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
ClipboardCopy,
|
||||
Divider,
|
||||
Form,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Split,
|
||||
SplitItem,
|
||||
} from "@patternfly/react-core";
|
||||
import React, { useContext, 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 { ClientSecret } from "./ClientSecret";
|
||||
import { SignedJWT } from "./SignedJWT";
|
||||
import { X509 } from "./X509";
|
||||
|
||||
type ClientAuthenticatorProviders = {
|
||||
id: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
type Secret = {
|
||||
type: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type AccessToken = {
|
||||
registrationAccessToken: string;
|
||||
};
|
||||
|
||||
export type CredentialsProps = {
|
||||
clientId: string;
|
||||
form: UseFormMethods;
|
||||
save: () => void;
|
||||
};
|
||||
|
||||
export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const httpClient = useContext(HttpClientContext)!;
|
||||
const { realm } = useContext(RealmContext);
|
||||
const { addAlert } = useAlerts();
|
||||
const clientAuthenticatorType = useWatch({
|
||||
control: form.control,
|
||||
name: "clientAuthenticatorType",
|
||||
});
|
||||
|
||||
const [providers, setProviders] = useState<ClientAuthenticatorProviders[]>(
|
||||
[]
|
||||
);
|
||||
const [secret, setSecret] = useState("");
|
||||
const [accessToken, setAccessToken] = useState("");
|
||||
const [open, isOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await httpClient.doGet<ClientAuthenticatorProviders[]>(
|
||||
`/admin/realms/${realm}/authentication/client-authenticator-providers`
|
||||
);
|
||||
setProviders(response.data!);
|
||||
|
||||
const secretResponse = await httpClient.doGet<Secret>(
|
||||
`/admin/realms/${realm}/clients/${clientId}/client-secret`
|
||||
);
|
||||
setSecret(secretResponse.data!.value);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function regenerate<T>(
|
||||
endpoint: string,
|
||||
message: string
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
const response = await httpClient.doPost<T>(
|
||||
`/admin/realms/${realm}/clients/${clientId}/${endpoint}`,
|
||||
{ client: clientId, realm }
|
||||
);
|
||||
addAlert(t(`${message}Success`), AlertVariant.success);
|
||||
return response.data!;
|
||||
} catch (error) {
|
||||
addAlert(t(`${message}Error`, { error }), AlertVariant.danger);
|
||||
}
|
||||
}
|
||||
|
||||
const regenerateClientSecret = async () => {
|
||||
const secret = await regenerate<Secret>("client-secret", "clientSecret");
|
||||
setSecret(secret?.value || "");
|
||||
};
|
||||
|
||||
const [toggleClientSecretConfirm, ClientSecretConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:confirmClientSecretTitle",
|
||||
messageKey: "clients:confirmClientSecretBody",
|
||||
continueButtonLabel: "common:yes",
|
||||
cancelButtonLabel: "common:no",
|
||||
onConfirm: regenerateClientSecret,
|
||||
});
|
||||
|
||||
const regenerateAccessToken = async () => {
|
||||
const accessToken = await regenerate<AccessToken>(
|
||||
"registration-access-token",
|
||||
"accessToken"
|
||||
);
|
||||
setAccessToken(accessToken?.registrationAccessToken || "");
|
||||
};
|
||||
|
||||
const [toggleAccessTokenConfirm, AccessTokenConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:confirmAccessTokenTitle",
|
||||
messageKey: "clients:confirmAccessTokenBody",
|
||||
continueButtonLabel: "common:yes",
|
||||
cancelButtonLabel: "common:no",
|
||||
onConfirm: regenerateAccessToken,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form isHorizontal className="pf-u-mt-md">
|
||||
<ClientSecretConfirm />
|
||||
<AccessTokenConfirm />
|
||||
<Card isFlat>
|
||||
<CardBody>
|
||||
<FormGroup
|
||||
label={t("clientAuthenticator")}
|
||||
fieldId="kc-client-authenticator-type"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:client-authenticator-type"
|
||||
forLabel={t("clientAuthenticator")}
|
||||
forID="kc-client-authenticator-type"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="clientAuthenticatorType"
|
||||
control={form.control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="kc-client-authenticator-type"
|
||||
required
|
||||
onToggle={() => isOpen(!open)}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value as string);
|
||||
isOpen(false);
|
||||
}}
|
||||
selections={value}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("clientAuthenticator")}
|
||||
isOpen={open}
|
||||
>
|
||||
{providers.map((option) => (
|
||||
<SelectOption
|
||||
selected={option.id === value}
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
>
|
||||
{option.displayName}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{clientAuthenticatorType === "client-jwt" && (
|
||||
<SignedJWT form={form} />
|
||||
)}
|
||||
{clientAuthenticatorType === "client-x509" && <X509 form={form} />}
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => save()}
|
||||
isDisabled={!form.formState.isDirty}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
{(clientAuthenticatorType === "client-secret" ||
|
||||
clientAuthenticatorType === "client-secret-jwt") && (
|
||||
<>
|
||||
<Divider className="pf-u-mb-md" />
|
||||
<ClientSecret
|
||||
secret={secret}
|
||||
toggle={toggleClientSecretConfirm}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card isFlat>
|
||||
<CardBody>
|
||||
<FormGroup
|
||||
label={t("registrationAccessToken")}
|
||||
fieldId="kc-access-token"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:registration-access-token"
|
||||
forLabel={t("registrationAccessToken")}
|
||||
forID="kc-access-token"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Split hasGutter>
|
||||
<SplitItem isFilled>
|
||||
<ClipboardCopy id="kc-access-token" isReadOnly>
|
||||
{accessToken}
|
||||
</ClipboardCopy>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Button variant="secondary" onClick={toggleAccessTokenConfirm}>
|
||||
{t("regenerate")}
|
||||
</Button>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</FormGroup>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Form>
|
||||
);
|
||||
};
|
69
src/clients/credentials/SignedJWT.tsx
Normal file
69
src/clients/credentials/SignedJWT.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, UseFormMethods } from "react-hook-form";
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { sortProviders } from "../../util";
|
||||
|
||||
export type SignedJWTProps = {
|
||||
form: UseFormMethods;
|
||||
};
|
||||
|
||||
export const SignedJWT = ({ form }: SignedJWTProps) => {
|
||||
const providers = sortProviders(
|
||||
useServerInfo().providers.clientSignature.providers
|
||||
);
|
||||
const { t } = useTranslation("clients");
|
||||
|
||||
const [open, isOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("signatureAlgorithm")}
|
||||
fieldId="kc-signature-algorithm"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:signature-algorithm"
|
||||
forLabel={t("signatureAlgorithm")}
|
||||
forID="kc-signature-algorithm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.token_endpoint_auth_signing_alg"
|
||||
defaultValue={providers[0]}
|
||||
control={form.control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="kc-signature-algorithm"
|
||||
onToggle={() => isOpen(!open)}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value as string);
|
||||
isOpen(false);
|
||||
}}
|
||||
selections={value}
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("signatureAlgorithm")}
|
||||
isOpen={open}
|
||||
>
|
||||
{providers.map((option) => (
|
||||
<SelectOption
|
||||
selected={option === value}
|
||||
key={option}
|
||||
value={option}
|
||||
/>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
);
|
||||
};
|
33
src/clients/credentials/X509.tsx
Normal file
33
src/clients/credentials/X509.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UseFormMethods } from "react-hook-form";
|
||||
import { FormGroup, TextInput } from "@patternfly/react-core";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
|
||||
export type X509Props = {
|
||||
form: UseFormMethods;
|
||||
};
|
||||
|
||||
export const X509 = ({ form }: X509Props) => {
|
||||
const { t } = useTranslation("clients");
|
||||
return (
|
||||
<FormGroup
|
||||
label={t("subject")}
|
||||
fieldId="kc-subject"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:subject"
|
||||
forLabel={t("subject")}
|
||||
forID="kc-subject"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
ref={form.register()}
|
||||
type="text"
|
||||
id="kc-subject"
|
||||
name="attributes.x509_subjectdn"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,10 @@
|
|||
{
|
||||
"clients-help": {
|
||||
"downloadType": "this is information about the download type",
|
||||
"details": "this is information about the details"
|
||||
"details": "this is information about the details",
|
||||
"client-authenticator-type": "Client Authenticator used for authentication of this client against Keycloak server",
|
||||
"registration-access-token": "The registration access token provides access for clients to the client registration service.",
|
||||
"signature-algorithm": "JWA algorithm, which the client needs to use when signing a JWT for authentication. If left blank, the client is allowed to use any algorithm.",
|
||||
"subject": "A regular expression for validating Subject DN in the Client Certificate. Use \"(.*?)(?:$)\" to match all kind of expressions."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
"name": "Name",
|
||||
"formatOption": "Format option",
|
||||
"downloadAdaptorTitle": "Download adaptor configs",
|
||||
"settings": "Settings",
|
||||
"credentials": "Credentials",
|
||||
"details": "Details",
|
||||
"clientList": "Client list",
|
||||
"clientSettings": "Client details",
|
||||
|
@ -46,6 +48,20 @@
|
|||
"validRedirectUri": "Valid redirect URIs",
|
||||
"loginTheme": "Login theme",
|
||||
"consentRequired": "Consent required",
|
||||
"clientAuthenticator": "Client Authenticator",
|
||||
"clientSecret": "Client secret",
|
||||
"regenerate": "Regenerate",
|
||||
"confirmClientSecretTitle": "Regenerate secret for this client?",
|
||||
"confirmClientSecretBody": "If you regenerate secret, the Keycloak database will be updated and you will need to download a new adapter for this client.",
|
||||
"confirmAccessTokenTitle": "Regenerate registration access token?",
|
||||
"confirmAccessTokenBody": "If you regenerate registration access token, the access data regarding the client registration service will be updated.",
|
||||
"clientSecretSuccess": "Client secret regenerated",
|
||||
"clientSecretError": "Could not regenerate client secret due to: {{error}}",
|
||||
"registrationAccessToken": "Registration access token",
|
||||
"accessTokenSuccess": "Access token regenerated",
|
||||
"accessTokenError": "Could not regenerate access token due to: {{error}}",
|
||||
"signatureAlgorithm": "Signature algorithm",
|
||||
"subject": "Subject DN",
|
||||
"searchForClient": "Search for client"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
"unknownUser": "Anonymous",
|
||||
|
||||
"add": "Add",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"create": "Create",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
@ -54,7 +54,7 @@ export const ConfirmDialogModal = ({
|
|||
onCancel,
|
||||
children,
|
||||
open = true,
|
||||
variant = ModalVariant.default,
|
||||
variant = ModalVariant.small,
|
||||
toggleDialog,
|
||||
}: ConfirmDialogModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
|
@ -48,7 +48,7 @@ exports[`Confirmation dialog renders simple confirm dialog 1`] = `
|
|||
ouiaSafe={true}
|
||||
showClose={true}
|
||||
title="Delete app02?"
|
||||
variant="default"
|
||||
variant="small"
|
||||
>
|
||||
<Portal
|
||||
containerInfo={
|
||||
|
@ -63,8 +63,8 @@ exports[`Confirmation dialog renders simple confirm dialog 1`] = `
|
|||
aria-describedby="pf-modal-part-3"
|
||||
aria-labelledby="pf-modal-part-2"
|
||||
aria-modal="true"
|
||||
class="pf-c-modal-box"
|
||||
data-ouia-component-id="OUIA-Generated-Modal-default-2"
|
||||
class="pf-c-modal-box pf-m-sm"
|
||||
data-ouia-component-id="OUIA-Generated-Modal-small-2"
|
||||
data-ouia-component-type="PF4/ModalContent"
|
||||
data-ouia-safe="true"
|
||||
id="pf-modal-part-1"
|
||||
|
@ -170,11 +170,11 @@ exports[`Confirmation dialog renders simple confirm dialog 1`] = `
|
|||
isOpen={true}
|
||||
labelId="pf-modal-part-2"
|
||||
onClose={[Function]}
|
||||
ouiaId="OUIA-Generated-Modal-default-2"
|
||||
ouiaId="OUIA-Generated-Modal-small-2"
|
||||
ouiaSafe={true}
|
||||
showClose={true}
|
||||
title="Delete app02?"
|
||||
variant="default"
|
||||
variant="small"
|
||||
>
|
||||
<Backdrop>
|
||||
<div
|
||||
|
@ -198,20 +198,20 @@ exports[`Confirmation dialog renders simple confirm dialog 1`] = `
|
|||
aria-label=""
|
||||
aria-labelledby="pf-modal-part-2"
|
||||
className=""
|
||||
data-ouia-component-id="OUIA-Generated-Modal-default-2"
|
||||
data-ouia-component-id="OUIA-Generated-Modal-small-2"
|
||||
data-ouia-component-type="PF4/ModalContent"
|
||||
data-ouia-safe={true}
|
||||
id="pf-modal-part-1"
|
||||
style={Object {}}
|
||||
variant="default"
|
||||
variant="small"
|
||||
>
|
||||
<div
|
||||
aria-describedby="pf-modal-part-3"
|
||||
aria-label={null}
|
||||
aria-labelledby="pf-modal-part-2"
|
||||
aria-modal="true"
|
||||
className="pf-c-modal-box"
|
||||
data-ouia-component-id="OUIA-Generated-Modal-default-2"
|
||||
className="pf-c-modal-box pf-m-sm"
|
||||
data-ouia-component-id="OUIA-Generated-Modal-small-2"
|
||||
data-ouia-component-type="PF4/ModalContent"
|
||||
data-ouia-safe={true}
|
||||
id="pf-modal-part-1"
|
||||
|
|
|
@ -15,7 +15,7 @@ type MultiLine = {
|
|||
};
|
||||
|
||||
export function convertToMultiline(fields: string[]): MultiLine[] {
|
||||
return fields.map((field) => {
|
||||
return (fields && fields.length > 0 ? fields : [""]).map((field) => {
|
||||
return { value: field };
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { createContext, ReactNode, useContext } from "react";
|
||||
import { ServerInfoRepresentation } from "./server-info";
|
||||
import { HttpClientContext } from "../http-service/HttpClientContext";
|
||||
import { sortProvider } from "../../util";
|
||||
import { sortProviders } from "../../util";
|
||||
import { DataLoader } from "../../components/data-loader/DataLoader";
|
||||
|
||||
export const ServerInfoContext = createContext<ServerInfoRepresentation>(
|
||||
|
@ -11,10 +11,7 @@ export const ServerInfoContext = createContext<ServerInfoRepresentation>(
|
|||
export const useServerInfo = () => useContext(ServerInfoContext);
|
||||
|
||||
export const useLoginProviders = () => {
|
||||
const serverInfo = Object.entries(
|
||||
useServerInfo().providers["login-protocol"].providers
|
||||
);
|
||||
return [...new Map(serverInfo.sort(sortProvider)).keys()];
|
||||
return sortProviders(useServerInfo().providers["login-protocol"].providers);
|
||||
};
|
||||
|
||||
export const ServerInfoProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { AuthenticationSection } from "./authentication/AuthenticationSection";
|
|||
import { ClientScopeForm } from "./client-scopes/form/ClientScopeForm";
|
||||
import { ClientScopesSection } from "./client-scopes/ClientScopesSection";
|
||||
import { NewClientForm } from "./clients/add/NewClientForm";
|
||||
import { ClientSettings } from "./clients/ClientSettings";
|
||||
import { ClientsSection } from "./clients/ClientsSection";
|
||||
import { ImportForm } from "./clients/import/ImportForm";
|
||||
import { EventsSection } from "./events/EventsSection";
|
||||
|
@ -20,6 +19,7 @@ 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 = {
|
||||
path: string;
|
||||
|
@ -39,7 +39,7 @@ export const routes: RoutesFn = (t: TFunction) => [
|
|||
},
|
||||
{
|
||||
path: "/clients/:id",
|
||||
component: ClientSettings,
|
||||
component: ClientDetails,
|
||||
breadcrumb: t("clients:clientSettings"),
|
||||
access: "view-clients",
|
||||
},
|
||||
|
|
|
@ -3,7 +3,13 @@ import FileSaver from "file-saver";
|
|||
import { ClientRepresentation } from "./clients/models/client-model";
|
||||
import { ProviderRepresentation } from "./context/server-info/server-info";
|
||||
|
||||
export const sortProvider = (
|
||||
export const sortProviders = (providers: {
|
||||
[index: string]: ProviderRepresentation;
|
||||
}) => {
|
||||
return [...new Map(Object.entries(providers).sort(sortProvider)).keys()];
|
||||
};
|
||||
|
||||
const sortProvider = (
|
||||
a: [string, ProviderRepresentation],
|
||||
b: [string, ProviderRepresentation]
|
||||
) => {
|
||||
|
|
Loading…
Reference in a new issue