570 lines
20 KiB
TypeScript
570 lines
20 KiB
TypeScript
|
import React, { useState } from "react";
|
||
|
import { useTranslation } from "react-i18next";
|
||
|
import { Controller, useForm, useFormContext, useWatch } from "react-hook-form";
|
||
|
import {
|
||
|
ActionGroup,
|
||
|
AlertVariant,
|
||
|
Button,
|
||
|
FormGroup,
|
||
|
NumberInput,
|
||
|
PageSection,
|
||
|
Select,
|
||
|
SelectOption,
|
||
|
SelectVariant,
|
||
|
Switch,
|
||
|
Text,
|
||
|
TextVariants,
|
||
|
} from "@patternfly/react-core";
|
||
|
|
||
|
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
|
||
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||
|
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||
|
import { FormPanel } from "../components/scroll-form/FormPanel";
|
||
|
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
||
|
import { useAlerts } from "../components/alert/Alerts";
|
||
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||
|
|
||
|
import "./RealmSettingsSection.css";
|
||
|
import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||
|
import { TimeSelector } from "../components/time-selector/TimeSelector";
|
||
|
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||
|
import {
|
||
|
convertToFormValues,
|
||
|
forHumans,
|
||
|
flatten,
|
||
|
convertFormValuesToObject,
|
||
|
interpolateTimespan,
|
||
|
} from "../util";
|
||
|
|
||
|
type RealmSettingsSessionsTabProps = {
|
||
|
realm?: RealmRepresentation;
|
||
|
user?: UserRepresentation;
|
||
|
reset?: () => void;
|
||
|
};
|
||
|
|
||
|
export const RealmSettingsTokensTab = ({
|
||
|
realm: initialRealm,
|
||
|
reset,
|
||
|
}: RealmSettingsSessionsTabProps) => {
|
||
|
const { t } = useTranslation("realm-settings");
|
||
|
const adminClient = useAdminClient();
|
||
|
const { realm: realmName } = useRealm();
|
||
|
const { addAlert, addError } = useAlerts();
|
||
|
const serverInfo = useServerInfo();
|
||
|
|
||
|
const [realm, setRealm] = useState(initialRealm);
|
||
|
const [defaultSigAlgDrpdwnIsOpen, setDefaultSigAlgDrpdwnOpen] =
|
||
|
useState(false);
|
||
|
|
||
|
const allComponentTypes =
|
||
|
serverInfo.componentTypes?.["org.keycloak.keys.KeyProvider"] ?? [];
|
||
|
|
||
|
const esOptions = ["ES256", "ES384", "ES512"];
|
||
|
|
||
|
const hmacAlgorithmOptions = allComponentTypes[2].properties[4].options;
|
||
|
|
||
|
const javaKeystoreAlgOptions = allComponentTypes[3].properties[3].options;
|
||
|
|
||
|
const defaultSigAlgOptions = esOptions.concat(
|
||
|
hmacAlgorithmOptions!,
|
||
|
javaKeystoreAlgOptions!
|
||
|
);
|
||
|
|
||
|
const form = useForm<RealmRepresentation>();
|
||
|
const { control } = useFormContext();
|
||
|
|
||
|
const offlineSessionMaxEnabled = useWatch({
|
||
|
control,
|
||
|
name: "offlineSessionMaxLifespanEnabled",
|
||
|
defaultValue: realm?.offlineSessionMaxLifespanEnabled,
|
||
|
});
|
||
|
|
||
|
const setupForm = (realm: RealmRepresentation) => {
|
||
|
const { ...formValues } = realm;
|
||
|
form.reset(formValues);
|
||
|
Object.entries(realm).map((entry) => {
|
||
|
if (entry[0] === "attributes") {
|
||
|
convertToFormValues(entry[1], "attributes", form.setValue);
|
||
|
} else {
|
||
|
form.setValue(entry[0], entry[1]);
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
useFetch(
|
||
|
() => adminClient.realms.findOne({ realm: realmName }),
|
||
|
(realm) => {
|
||
|
setRealm(realm);
|
||
|
setupForm(realm);
|
||
|
},
|
||
|
[realmName]
|
||
|
);
|
||
|
|
||
|
const save = async () => {
|
||
|
const firstInstanceOnly = true;
|
||
|
const flattenedAttributes = convertFormValuesToObject(
|
||
|
flatten(form.getValues()["attributes"]),
|
||
|
firstInstanceOnly
|
||
|
);
|
||
|
|
||
|
const attributes = { ...flattenedAttributes, ...realm?.attributes };
|
||
|
|
||
|
try {
|
||
|
const newRealm: RealmRepresentation = {
|
||
|
...realm,
|
||
|
...form.getValues(),
|
||
|
attributes,
|
||
|
};
|
||
|
|
||
|
await adminClient.realms.update({ realm: realmName }, newRealm);
|
||
|
|
||
|
setupForm(newRealm);
|
||
|
setRealm(newRealm);
|
||
|
addAlert(t("saveSuccess"), AlertVariant.success);
|
||
|
} catch (error) {
|
||
|
addError("realm-settings:saveError", error);
|
||
|
}
|
||
|
};
|
||
|
return (
|
||
|
<>
|
||
|
<PageSection variant="light">
|
||
|
<FormPanel
|
||
|
title={t("realm-settings:general")}
|
||
|
className="kc-sso-session-template"
|
||
|
>
|
||
|
<FormAccess
|
||
|
isHorizontal
|
||
|
role="manage-realm"
|
||
|
onSubmit={form.handleSubmit(save)}
|
||
|
>
|
||
|
<FormGroup
|
||
|
label={t("defaultSigAlg")}
|
||
|
fieldId="kc-default-signature-algorithm"
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:defaultSigAlg"
|
||
|
forLabel={t("defaultSigAlg")}
|
||
|
forID={t("common:helpLabel", { label: t("algorithm") })}
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
<Controller
|
||
|
name="defaultSignatureAlgorithm"
|
||
|
defaultValue={"RS256"}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<Select
|
||
|
toggleId="kc-default-sig-alg"
|
||
|
onToggle={() =>
|
||
|
setDefaultSigAlgDrpdwnOpen(!defaultSigAlgDrpdwnIsOpen)
|
||
|
}
|
||
|
onSelect={(_, value) => {
|
||
|
onChange(value.toString());
|
||
|
setDefaultSigAlgDrpdwnOpen(false);
|
||
|
}}
|
||
|
selections={[value.toString()]}
|
||
|
variant={SelectVariant.single}
|
||
|
aria-label={t("defaultSigAlg")}
|
||
|
isOpen={defaultSigAlgDrpdwnIsOpen}
|
||
|
data-testid="select-default-sig-alg"
|
||
|
>
|
||
|
{defaultSigAlgOptions!.map((p, idx) => (
|
||
|
<SelectOption
|
||
|
selected={p === value}
|
||
|
key={`default-sig-alg-${idx}`}
|
||
|
value={p}
|
||
|
></SelectOption>
|
||
|
))}
|
||
|
</Select>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
</FormAccess>
|
||
|
</FormPanel>
|
||
|
<FormPanel
|
||
|
title={t("realm-settings:refreshTokens")}
|
||
|
className="kc-client-session-template"
|
||
|
>
|
||
|
<FormAccess
|
||
|
isHorizontal
|
||
|
role="manage-realm"
|
||
|
className="pf-u-mt-lg"
|
||
|
onSubmit={form.handleSubmit(save)}
|
||
|
>
|
||
|
<FormGroup
|
||
|
hasNoPaddingTop
|
||
|
label={t("revokeRefreshToken")}
|
||
|
fieldId="kc-revoke-refresh-token"
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:revokeRefreshToken"
|
||
|
forLabel={t("revokeRefreshToken")}
|
||
|
forID="revokeRefreshToken"
|
||
|
id="revokeRefreshToken"
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
<Controller
|
||
|
name="revokeRefreshToken"
|
||
|
control={form.control}
|
||
|
defaultValue={false}
|
||
|
render={({ onChange, value }) => (
|
||
|
<Switch
|
||
|
id="kc-revoke-refresh-token"
|
||
|
data-testid="revoke-refresh-token-switch"
|
||
|
aria-label="revoke-refresh-token-switch"
|
||
|
label={t("common:enabled")}
|
||
|
labelOff={t("common:disabled")}
|
||
|
isChecked={value}
|
||
|
onChange={onChange}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
<FormGroup
|
||
|
label={t("refreshTokenMaxReuse")}
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:refreshTokenMaxReuse"
|
||
|
forLabel={t("refreshTokenMaxReuse")}
|
||
|
forID="refreshTokenMaxReuse"
|
||
|
/>
|
||
|
}
|
||
|
fieldId="refreshTokenMaxReuse"
|
||
|
>
|
||
|
<Controller
|
||
|
name="refreshTokenMaxReuse"
|
||
|
defaultValue={0}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<NumberInput
|
||
|
type="text"
|
||
|
id="refreshTokenMaxReuseMs"
|
||
|
value={value}
|
||
|
onPlus={() => onChange(value + 1)}
|
||
|
onMinus={() => onChange(value - 1)}
|
||
|
onChange={(event) =>
|
||
|
onChange(Number((event.target as HTMLInputElement).value))
|
||
|
}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
</FormAccess>
|
||
|
</FormPanel>
|
||
|
<FormPanel
|
||
|
title={t("realm-settings:accessTokens")}
|
||
|
className="kc-offline-session-template"
|
||
|
>
|
||
|
<FormAccess
|
||
|
isHorizontal
|
||
|
role="manage-realm"
|
||
|
className="pf-u-mt-lg"
|
||
|
onSubmit={form.handleSubmit(save)}
|
||
|
>
|
||
|
<FormGroup
|
||
|
label={t("accessTokenLifespan")}
|
||
|
fieldId="accessTokenLifespan"
|
||
|
helperText={`It is recommended for this value to be shorter than the SSO session idle timeout: ${interpolateTimespan(
|
||
|
forHumans(realm?.ssoSessionIdleTimeout!)
|
||
|
)}`}
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:accessTokenLifespan"
|
||
|
forLabel={t("accessTokenLifespan")}
|
||
|
forID="accessTokenLifespan"
|
||
|
id="accessTokenLifespan"
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
<Controller
|
||
|
name="accessTokenLifespan"
|
||
|
defaultValue=""
|
||
|
helperTextInvalid={t("common:required")}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
validated={
|
||
|
value > realm?.ssoSessionIdleTimeout!
|
||
|
? "warning"
|
||
|
: "default"
|
||
|
}
|
||
|
className="kc-access-token-lifespan"
|
||
|
data-testid="access-token-lifespan-input"
|
||
|
aria-label="access-token-lifespan"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
|
||
|
<FormGroup
|
||
|
label={t("accessTokenLifespanImplicitFlow")}
|
||
|
fieldId="accessTokenLifespanImplicitFlow"
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:accessTokenLifespanImplicitFlow"
|
||
|
forLabel={t("accessTokenLifespanImplicitFlow")}
|
||
|
forID="accessTokenLifespanImplicitFlow"
|
||
|
id="accessTokenLifespanImplicitFlow"
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
<Controller
|
||
|
name="accessTokenLifespanForImplicitFlow"
|
||
|
defaultValue=""
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-access-token-lifespan-implicit"
|
||
|
data-testid="access-token-lifespan-implicit-input"
|
||
|
aria-label="access-token-lifespan-implicit"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
<FormGroup
|
||
|
label={t("clientLoginTimeout")}
|
||
|
fieldId="clientLoginTimeout"
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:clientLoginTimeout"
|
||
|
forLabel={t("clientLoginTimeout")}
|
||
|
forID="clientLoginTimeout"
|
||
|
id="clientLoginTimeout"
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
<Controller
|
||
|
name="accessCodeLifespan"
|
||
|
defaultValue=""
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-client-login-timeout"
|
||
|
data-testid="client-login-timeout-input"
|
||
|
aria-label="client-login-timeout"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
|
||
|
{offlineSessionMaxEnabled && (
|
||
|
<FormGroup
|
||
|
label={t("offlineSessionMax")}
|
||
|
fieldId="offlineSessionMax"
|
||
|
id="offline-session-max-label"
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:offlineSessionMax"
|
||
|
forLabel={t("offlineSessionMax")}
|
||
|
forID="offlineSessionMax"
|
||
|
id="offlineSessionMax"
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
<Controller
|
||
|
name="offlineSessionMaxLifespan"
|
||
|
defaultValue=""
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-offline-session-max"
|
||
|
data-testid="offline-session-max-input"
|
||
|
aria-label="offline-session-max-input"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
)}
|
||
|
</FormAccess>
|
||
|
</FormPanel>
|
||
|
<FormPanel
|
||
|
className="kc-login-settings-template"
|
||
|
title={t("actionTokens")}
|
||
|
>
|
||
|
<FormAccess
|
||
|
isHorizontal
|
||
|
role="manage-realm"
|
||
|
className="pf-u-mt-lg"
|
||
|
onSubmit={form.handleSubmit(save)}
|
||
|
>
|
||
|
<FormGroup
|
||
|
label={t("userInitiatedActionLifespan")}
|
||
|
id="kc-user-initiated-action-lifespan"
|
||
|
fieldId="userInitiatedActionLifespan"
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:userInitiatedActionLifespan"
|
||
|
forLabel={t("userInitiatedActionLifespan")}
|
||
|
forID="userInitiatedActionLifespan"
|
||
|
id="userInitiatedActionLifespan"
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
<Controller
|
||
|
name="actionTokenGeneratedByUserLifespan"
|
||
|
defaultValue={""}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-user-initiated-action-lifespan"
|
||
|
data-testid="user-initiated-action-lifespan"
|
||
|
aria-label="user-initiated-action-lifespan"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
<FormGroup
|
||
|
label={t("defaultAdminInitiated")}
|
||
|
fieldId="defaultAdminInitiated"
|
||
|
id="default-admin-initiated-label"
|
||
|
labelIcon={
|
||
|
<HelpItem
|
||
|
helpText="realm-settings-help:defaultAdminInitiatedActionLifespan"
|
||
|
forLabel={t("defaultAdminInitiated")}
|
||
|
forID="defaultAdminInitiated"
|
||
|
id="defaultAdminInitiated"
|
||
|
/>
|
||
|
}
|
||
|
>
|
||
|
<Controller
|
||
|
name="actionTokenGeneratedByAdminLifespan"
|
||
|
defaultValue={""}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-default-admin-initiated"
|
||
|
data-testid="default-admin-initated-input"
|
||
|
aria-label="default-admin-initated-input"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
<Text
|
||
|
className="kc-override-action-tokens-subtitle"
|
||
|
component={TextVariants.h1}
|
||
|
>
|
||
|
{t("overrideActionTokens")}
|
||
|
</Text>
|
||
|
<FormGroup
|
||
|
label={t("emailVerification")}
|
||
|
fieldId="emailVerification"
|
||
|
id="email-verification"
|
||
|
>
|
||
|
<Controller
|
||
|
name="attributes.actionTokenGeneratedByUserLifespan-verify-email"
|
||
|
defaultValue={""}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-email-verification"
|
||
|
data-testid="email-verification-input"
|
||
|
aria-label="email-verification-input"
|
||
|
value={value}
|
||
|
onChange={(value: any) => onChange(value.toString())}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
<FormGroup
|
||
|
label={t("idpAccountEmailVerification")}
|
||
|
fieldId="idpAccountEmailVerification"
|
||
|
id="idp-acct-label"
|
||
|
>
|
||
|
<Controller
|
||
|
name="attributes.actionTokenGeneratedByUserLifespan-idp-verify-account-via-email"
|
||
|
defaultValue={""}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-idp-email-verification"
|
||
|
data-testid="idp-email-verification-input"
|
||
|
aria-label="idp-email-verification"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
<FormGroup
|
||
|
label={t("forgotPassword")}
|
||
|
fieldId="forgotPassword"
|
||
|
id="forgot-password-label"
|
||
|
>
|
||
|
<Controller
|
||
|
name="attributes.actionTokenGeneratedByUserLifespan-reset-credentials"
|
||
|
defaultValue={""}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-forgot-pw"
|
||
|
data-testid="forgot-pw-input"
|
||
|
aria-label="forgot-pw-input"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
<FormGroup
|
||
|
label={t("executeActions")}
|
||
|
fieldId="executeActions"
|
||
|
id="execute-actions"
|
||
|
>
|
||
|
<Controller
|
||
|
name="attributes.actionTokenGeneratedByUserLifespan-execute-actions"
|
||
|
defaultValue={""}
|
||
|
control={form.control}
|
||
|
render={({ onChange, value }) => (
|
||
|
<TimeSelector
|
||
|
className="kc-execute-actions"
|
||
|
data-testid="execute-actions-input"
|
||
|
aria-label="execute-actions-input"
|
||
|
value={value}
|
||
|
onChange={onChange}
|
||
|
units={["minutes", "hours", "days"]}
|
||
|
/>
|
||
|
)}
|
||
|
/>
|
||
|
</FormGroup>
|
||
|
<ActionGroup>
|
||
|
<Button
|
||
|
variant="primary"
|
||
|
type="submit"
|
||
|
data-testid="tokens-tab-save"
|
||
|
isDisabled={!form.formState.isDirty}
|
||
|
>
|
||
|
{t("common:save")}
|
||
|
</Button>
|
||
|
<Button variant="link" onClick={reset}>
|
||
|
{t("common:revert")}
|
||
|
</Button>
|
||
|
</ActionGroup>
|
||
|
</FormAccess>
|
||
|
</FormPanel>
|
||
|
</PageSection>
|
||
|
</>
|
||
|
);
|
||
|
};
|