From e65a1effdabf92c6c946cc610b7123c5b5dc141b Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Fri, 3 Feb 2023 12:56:20 +0100 Subject: [PATCH] Add client registration page (#4250) --- .../e2e/client_registration_policies.spec.ts | 64 ++++++ .../manage/clients/ClientRegistrationPage.ts | 23 ++ .../public/resources/en/clients-help.json | 3 + .../admin-ui/public/resources/en/clients.json | 14 ++ .../admin-ui/public/resources/en/dynamic.json | 28 +++ apps/admin-ui/src/clients/ClientsSection.tsx | 9 + .../initial-access/InitialAccessTokenList.tsx | 2 +- .../registration/AddProviderDialog.tsx | 96 +++++++++ .../registration/ClientRegistration.tsx | 66 ++++++ .../registration/ClientRegistrationList.tsx | 132 ++++++++++++ .../clients/registration/DetailProvider.tsx | 204 ++++++++++++++++++ apps/admin-ui/src/clients/routes.ts | 8 + .../clients/routes/AddRegistrationProvider.ts | 36 ++++ .../src/clients/routes/ClientRegistration.ts | 24 +++ apps/admin-ui/src/clients/routes/Clients.ts | 5 +- 15 files changed, 712 insertions(+), 2 deletions(-) create mode 100644 apps/admin-ui/cypress/e2e/client_registration_policies.spec.ts create mode 100644 apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRegistrationPage.ts create mode 100644 apps/admin-ui/src/clients/registration/AddProviderDialog.tsx create mode 100644 apps/admin-ui/src/clients/registration/ClientRegistration.tsx create mode 100644 apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx create mode 100644 apps/admin-ui/src/clients/registration/DetailProvider.tsx create mode 100644 apps/admin-ui/src/clients/routes/AddRegistrationProvider.ts create mode 100644 apps/admin-ui/src/clients/routes/ClientRegistration.ts diff --git a/apps/admin-ui/cypress/e2e/client_registration_policies.spec.ts b/apps/admin-ui/cypress/e2e/client_registration_policies.spec.ts new file mode 100644 index 0000000000..34123cd7d6 --- /dev/null +++ b/apps/admin-ui/cypress/e2e/client_registration_policies.spec.ts @@ -0,0 +1,64 @@ +import ListingPage from "../support/pages/admin-ui/ListingPage"; +import { ClientRegistrationPage } from "../support/pages/admin-ui/manage/clients/ClientRegistrationPage"; +import Masthead from "../support/pages/admin-ui/Masthead"; +import SidebarPage from "../support/pages/admin-ui/SidebarPage"; +import LoginPage from "../support/pages/LoginPage"; +import { keycloakBefore } from "../support/util/keycloak_hooks"; + +describe("Client registration policies subtab", () => { + const loginPage = new LoginPage(); + const listingPage = new ListingPage(); + const masthead = new Masthead(); + const sidebarPage = new SidebarPage(); + const clientRegistrationPage = new ClientRegistrationPage(); + + before(() => { + keycloakBefore(); + loginPage.logIn(); + sidebarPage.goToClients(); + }); + + beforeEach(() => { + clientRegistrationPage.goToClientRegistrationTab(); + sidebarPage.waitForPageLoad(); + }); + + it("add anonymous client registration policy", () => { + clientRegistrationPage + .createPolicy() + .selectRow("max-clients") + .fillPolicyForm({ + name: "new policy", + }) + .formUtils() + .save(); + + masthead.checkNotificationMessage("New client policy created successfully"); + clientRegistrationPage.formUtils().cancel(); + listingPage.itemExist("new policy"); + }); + + it("edit anonymous client registration policy", () => { + listingPage.goToItemDetails("new policy"); + clientRegistrationPage + .fillPolicyForm({ + name: "policy 2", + }) + .formUtils() + .save(); + + masthead.checkNotificationMessage("Client policy updated successfully"); + clientRegistrationPage.formUtils().cancel(); + listingPage.itemExist("policy 2"); + }); + + it("delete anonymous client registration policy", () => { + listingPage.clickRowDetails("policy 2").clickDetailMenu("Delete"); + clientRegistrationPage.modalUtils().confirmModal(); + + masthead.checkNotificationMessage( + "Client registration policy deleted successfully" + ); + listingPage.itemExist("policy 2", false); + }); +}); diff --git a/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRegistrationPage.ts b/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRegistrationPage.ts new file mode 100644 index 0000000000..4e207dc99d --- /dev/null +++ b/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRegistrationPage.ts @@ -0,0 +1,23 @@ +import CommonPage from "../../../CommonPage"; + +export class ClientRegistrationPage extends CommonPage { + goToClientRegistrationTab() { + this.tabUtils().clickTab("registration"); + return this; + } + + createPolicy() { + cy.findAllByTestId("createPolicy").click(); + return this; + } + + selectRow(name: string) { + cy.findAllByTestId(name).click(); + return this; + } + + fillPolicyForm(props: { name: string }) { + cy.findAllByTestId("name").clear().type(props.name); + return this; + } +} diff --git a/apps/admin-ui/public/resources/en/clients-help.json b/apps/admin-ui/public/resources/en/clients-help.json index 9ffc4e268b..8374c7680a 100644 --- a/apps/admin-ui/public/resources/en/clients-help.json +++ b/apps/admin-ui/public/resources/en/clients-help.json @@ -46,12 +46,15 @@ "clientSignature": "Will the client sign their saml requests and responses? And should they be validated?", "downloadType": "this is information about the download type", "details": "this is information about the details", + "clientPolicyName": "Display name of the policy", "createToken": "An initial access token can only be used to create clients", "expiration": "Specifies how long the token should be valid", "count": "Specifies how many clients can be created using the token", "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.", + "anonymousAccessPolicies": "Those Policies are used when the Client Registration Service is invoked by unauthenticated request. This means that the request does not contain Initial Access Token nor Bearer Token.", + "authenticatedAccessPolicies": "Those Policies are used when Client Registration Service is invoked by authenticated request. This means that the request contains Initial Access Token or Bearer Token.", "allowRegexComparison": "If OFF, then the Subject DN from given client certificate must exactly match the given DN from the 'Subject DN' property as described in the RFC8705 specification. The Subject DN can be in the RFC2553 or RFC1779 format. If ON, then the Subject DN from given client certificate should match regex specified by 'Subject DN' property.", "subject": "A regular expression for validating Subject DN in the Client Certificate. Use \"(.*?)(?:$)\" to match all kind of expressions.", "evaluateExplain": "This page allows you to see all protocol mappers and role scope mappings", diff --git a/apps/admin-ui/public/resources/en/clients.json b/apps/admin-ui/public/resources/en/clients.json index a9a70bd5c7..a8aa45142c 100644 --- a/apps/admin-ui/public/resources/en/clients.json +++ b/apps/admin-ui/public/resources/en/clients.json @@ -313,6 +313,20 @@ "copySuccess": "Successfully copied to clipboard!", "clipboardCopyError": "Error copying to clipboard.", "copyToClipboard": "Copy to clipboard", + "clientRegistration": "Client registration", + "anonymousAccessPolicies": "Anonymous access polices", + "authenticatedAccessPolicies": "Authenticated access polices", + "provider": "Provider", + "providerId": "Provider ID", + "providerCreateSuccess": "New client policy created successfully", + "providerCreateError": "Could not create client policy due to {{error}}", + "providerUpdatedSuccess": "Client policy updated successfully", + "providerUpdatedError": "Could not update client policy due to {{error}}", + "clientRegisterPolicyDeleteConfirmTitle": "Delete client registration policy?", + "clientRegisterPolicyDeleteConfirm": "Are you sure you want to permanently delete the client registration policy {{name}}", + "clientRegisterPolicyDeleteSuccess": "Client registration policy deleted successfully", + "clientRegisterPolicyDeleteError": "Could not delete client registration policy: '{{error}}'", + "chooseAPolicyProvider": "Choose a policy provider", "clientAuthentication": "Client authentication", "authentication": "Authentication", "authenticationFlow": "Authentication flow", diff --git a/apps/admin-ui/public/resources/en/dynamic.json b/apps/admin-ui/public/resources/en/dynamic.json index df9c7270bf..98c852f33c 100644 --- a/apps/admin-ui/public/resources/en/dynamic.json +++ b/apps/admin-ui/public/resources/en/dynamic.json @@ -156,5 +156,33 @@ "client-updater-source-roles": { "label": "Updating entity role", "tooltip": "The condition is checked during client registration/update requests and it evaluates to true if the entity (usually user), who is creating/updating client is member of the specified role. For reference the realm role, you can use the realm role name like 'my_realm_role' . For reference client role, you can use the client_id.role_name for example 'my_client.my_client_role' will refer to client role 'my_client_role' of client 'my_client'." + }, + "allowed-client-scopes": { + "label": "Allowed Client Scopes", + "tooltip": "Whitelist of the client scopes, which can be used on a newly registered client. Attempt to register client with some client scope, which is not whitelisted, will be rejected. By default, the whitelist is either empty or contains just realm default client scopes (based on 'Allow Default Scopes' configuration property)" + }, + "allow-default-scopes": { + "label": "Allow Default Scopes", + "tooltip": "If on, newly registered clients will be allowed to have client scopes mentioned in realm default client scopes or realm optional client scopes" + }, + "allowed-protocol-mappers": { + "label": "Allowed Protocol Mappers", + "tooltip": "Whitelist of allowed protocol mapper providers. If there is an attempt to register client, which contains some protocol mappers, which were not whitelisted, registration request will be rejected." + }, + "max-clients": { + "label": "Max Clients Per Realm", + "tooltip": "It will not be allowed to register a new client if count of existing clients in realm is same or bigger than the configured limit." + }, + "trusted-hosts": { + "label": "Trusted Hosts", + "tooltip": "List of Hosts, which are trusted and are allowed to invoke Client Registration Service and/or be used as values of Client URIs. You can use hostnames or IP addresses. If you use star at the beginning (for example '*.example.com' ) then whole domain example.com will be trusted." + }, + "host-sending-registration-request-must-match": { + "label": "Host Sending Client Registration Request Must Match", + "tooltip": "If on, any request to Client Registration Service is allowed just if it was sent from some trusted host or domain." + }, + "client-uris-must-match": { + "label": "Client URIs Must Match", + "tooltip": "If on, all Client URIs (Redirect URIs and others) are allowed just if they match some trusted host or domain." } } diff --git a/apps/admin-ui/src/clients/ClientsSection.tsx b/apps/admin-ui/src/clients/ClientsSection.tsx index 554d53d066..3b78de8c98 100644 --- a/apps/admin-ui/src/clients/ClientsSection.tsx +++ b/apps/admin-ui/src/clients/ClientsSection.tsx @@ -38,6 +38,7 @@ import { useRoutableTab, } from "../components/routable-tabs/RoutableTabs"; import { ClientsTab, toClients } from "./routes/Clients"; +import { ClientRegistration } from "./registration/ClientRegistration"; export default function ClientsSection() { const { t } = useTranslation("clients"); @@ -69,6 +70,7 @@ export default function ClientsSection() { const listTab = useTab("list"); const initialAccessTokenTab = useTab("initial-access-token"); + const clientRegistrationTab = useTab("client-registration"); const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: t("clientDelete", { clientId: selectedClient?.clientId }), @@ -243,6 +245,13 @@ export default function ClientsSection() { > + {t("clientRegistration")}} + {...clientRegistrationTab} + > + + diff --git a/apps/admin-ui/src/clients/initial-access/InitialAccessTokenList.tsx b/apps/admin-ui/src/clients/initial-access/InitialAccessTokenList.tsx index 893b5fa41b..431dc4f0e5 100644 --- a/apps/admin-ui/src/clients/initial-access/InitialAccessTokenList.tsx +++ b/apps/admin-ui/src/clients/initial-access/InitialAccessTokenList.tsx @@ -47,7 +47,7 @@ export const InitialAccessTokenList = () => { addAlert(t("tokenDeleteSuccess"), AlertVariant.success); setToken(undefined); } catch (error) { - addError("tokenDeleteError", error); + addError("clients:tokenDeleteError", error); } }, }); diff --git a/apps/admin-ui/src/clients/registration/AddProviderDialog.tsx b/apps/admin-ui/src/clients/registration/AddProviderDialog.tsx new file mode 100644 index 0000000000..e95791350c --- /dev/null +++ b/apps/admin-ui/src/clients/registration/AddProviderDialog.tsx @@ -0,0 +1,96 @@ +import { + DataList, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + Modal, + ModalVariant, +} from "@patternfly/react-core"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; +import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort"; + +type AddProviderDialogProps = { + onConfirm: (providerId: string) => void; + toggleDialog: () => void; +}; + +export const AddProviderDialog = ({ + onConfirm, + toggleDialog, +}: AddProviderDialogProps) => { + const { t } = useTranslation("clients"); + const serverInfo = useServerInfo(); + const providers = Object.keys( + serverInfo.providers?.["client-registration-policy"].providers || [] + ); + + const descriptions = + serverInfo.componentTypes?.[ + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" + ]; + const localeSort = useLocaleSort(); + + const rows = useMemo( + () => + localeSort( + descriptions?.filter((d) => providers.includes(d.id)) || [], + mapByKey("id") + ), + [providers, descriptions] + ); + return ( + + { + onConfirm(id); + toggleDialog(); + }} + aria-label={t("addPredefinedMappers")} + isCompact + > + + + ( + + {name} + + ) + )} + /> + + + {rows.map((provider) => ( + + + + {provider.id} + , + + {provider.helpText} + , + ]} + /> + + + ))} + + + ); +}; diff --git a/apps/admin-ui/src/clients/registration/ClientRegistration.tsx b/apps/admin-ui/src/clients/registration/ClientRegistration.tsx new file mode 100644 index 0000000000..416e1c885e --- /dev/null +++ b/apps/admin-ui/src/clients/registration/ClientRegistration.tsx @@ -0,0 +1,66 @@ +import { Tab, TabTitleText } from "@patternfly/react-core"; +import { useTranslation } from "react-i18next"; +import { HelpItem } from "../../components/help-enabler/HelpItem"; +import { + RoutableTabs, + useRoutableTab, +} from "../../components/routable-tabs/RoutableTabs"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { + ClientRegistrationTab, + toClientRegistration, +} from "../routes/ClientRegistration"; +import { ClientRegistrationList } from "./ClientRegistrationList"; + +export const ClientRegistration = () => { + const { t } = useTranslation("clients"); + const { realm } = useRealm(); + + const useTab = (subTab: ClientRegistrationTab) => + useRoutableTab(toClientRegistration({ realm, subTab })); + + const anonymousTab = useTab("anonymous"); + const authenticatedTab = useTab("authenticated"); + + return ( + + + {t("anonymousAccessPolicies")}{" "} + + + } + {...anonymousTab} + > + + + + {t("authenticatedAccessPolicies")}{" "} + + + } + {...authenticatedTab} + > + + + + ); +}; diff --git a/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx b/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx new file mode 100644 index 0000000000..d214d85212 --- /dev/null +++ b/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx @@ -0,0 +1,132 @@ +import ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation"; +import { Button, ButtonVariant, ToolbarItem } from "@patternfly/react-core"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate, useParams } from "react-router-dom"; + +import { useAlerts } from "../../components/alert/Alerts"; +import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog"; +import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; +import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import useToggle from "../../utils/useToggle"; + +import { toRegistrationProvider } from "../routes/AddRegistrationProvider"; +import { ClientRegistrationParams } from "../routes/ClientRegistration"; +import { AddProviderDialog } from "./AddProviderDialog"; + +type ClientRegistrationListProps = { + subType: "anonymous" | "authenticated"; +}; + +export const ClientRegistrationList = ({ + subType, +}: ClientRegistrationListProps) => { + const { t } = useTranslation("clients"); + const { subTab } = useParams(); + const navigate = useNavigate(); + + const { adminClient } = useAdminClient(); + const { addAlert, addError } = useAlerts(); + const { realm } = useRealm(); + const [policies, setPolicies] = useState([]); + const [selectedPolicy, setSelectedPolicy] = + useState(); + const [isAddDialogOpen, toggleAddDialog] = useToggle(); + + useFetch( + () => + adminClient.components.find({ + type: "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy", + }), + (policies) => setPolicies(policies.filter((p) => p.subType === subType)), + [selectedPolicy] + ); + + const DetailLink = (comp: ComponentRepresentation) => ( + + {comp.name} + + ); + + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "clients:clientRegisterPolicyDeleteConfirmTitle", + messageKey: t("clientRegisterPolicyDeleteConfirm", { + name: selectedPolicy?.name, + }), + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.components.del({ + realm, + id: selectedPolicy?.id!, + }); + addAlert(t("clientRegisterPolicyDeleteSuccess")); + setSelectedPolicy(undefined); + } catch (error) { + addError("clients:clientRegisterPolicyDeleteError", error); + } + }, + }); + + return ( + <> + {isAddDialogOpen && ( + + navigate( + toRegistrationProvider({ + realm, + subTab: subTab || "anonymous", + providerId, + }) + ) + } + toggleDialog={toggleAddDialog} + /> + )} + + + + + } + actions={[ + { + title: t("common:delete"), + onRowClick: (policy) => { + setSelectedPolicy(policy); + toggleDeleteDialog(); + }, + }, + ]} + columns={[ + { + name: "name", + displayKey: "common:name", + cellRenderer: DetailLink, + }, + { + name: "providerId", + displayKey: "clients:providerId", + }, + ]} + /> + + ); +}; diff --git a/apps/admin-ui/src/clients/registration/DetailProvider.tsx b/apps/admin-ui/src/clients/registration/DetailProvider.tsx new file mode 100644 index 0000000000..154b28b2fb --- /dev/null +++ b/apps/admin-ui/src/clients/registration/DetailProvider.tsx @@ -0,0 +1,204 @@ +import ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation"; +import ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation"; +import { + ActionGroup, + Button, + ButtonVariant, + DropdownItem, + FormGroup, + PageSection, + ValidatedOptions, +} from "@patternfly/react-core"; +import { useState } from "react"; +import { FormProvider, useForm, useWatch } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate } from "react-router-dom"; +import { useAlerts } from "../../components/alert/Alerts"; +import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog"; +import { DynamicComponents } from "../../components/dynamic/DynamicComponents"; +import { FormAccess } from "../../components/form-access/FormAccess"; +import { HelpItem } from "../../components/help-enabler/HelpItem"; +import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; +import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput"; +import { ViewHeader } from "../../components/view-header/ViewHeader"; +import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { useParams } from "../../utils/useParams"; +import { + RegistrationProviderParams, + toRegistrationProvider, +} from "../routes/AddRegistrationProvider"; +import { toClientRegistration } from "../routes/ClientRegistration"; + +export default function DetailProvider() { + const { t } = useTranslation("clients"); + const { id, providerId, subTab } = useParams(); + const navigate = useNavigate(); + const form = useForm({ + defaultValues: { providerId }, + }); + const { + register, + control, + handleSubmit, + reset, + formState: { errors }, + } = form; + + const { adminClient } = useAdminClient(); + const { realm } = useRealm(); + const { addAlert, addError } = useAlerts(); + const [provider, setProvider] = useState(); + const [parentId, setParentId] = useState(""); + + useFetch( + async () => + await Promise.all([ + adminClient.realms.getClientRegistrationPolicyProviders({ realm }), + adminClient.realms.findOne({ realm }), + id ? adminClient.components.findOne({ id }) : Promise.resolve(), + ]), + ([providers, realm, data]) => { + setProvider(providers.find((p) => p.id === providerId)); + setParentId(realm?.id || ""); + reset(data || { providerId }); + }, + [] + ); + + const providerName = useWatch({ control, defaultValue: "", name: "name" }); + + const onSubmit = async (component: ComponentRepresentation) => { + if (component.config) + Object.entries(component.config).forEach( + ([key, value]) => + (component.config![key] = Array.isArray(value) ? value : [value]) + ); + try { + const updatedComponent = { + ...component, + subType: subTab, + parentId, + providerType: + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy", + providerId, + }; + if (id) { + await adminClient.components.update({ id }, updatedComponent); + } else { + const { id } = await adminClient.components.create(updatedComponent); + navigate(toRegistrationProvider({ id, realm, subTab, providerId })); + } + addAlert(t(`provider${id ? "Updated" : "Create"}Success`)); + } catch (error) { + addError(`clients:provider${id ? "Updated" : "Create"}Error`, error); + } + }; + + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "clients:clientRegisterPolicyDeleteConfirmTitle", + messageKey: t("clientRegisterPolicyDeleteConfirm", { + name: providerName, + }), + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.components.del({ + realm, + id: id!, + }); + addAlert(t("clientRegisterPolicyDeleteSuccess")); + navigate(toClientRegistration({ realm, subTab })); + } catch (error) { + addError("clients:clientRegisterPolicyDeleteError", error); + } + }, + }); + + if (!provider) { + return ; + } + + return ( + <> + + {t("common:delete")} + , + ] + : undefined + } + /> + + + + + + + + } + isRequired + > + + + + + + + + + + + + + ); +} diff --git a/apps/admin-ui/src/clients/routes.ts b/apps/admin-ui/src/clients/routes.ts index 330f0279c8..0143cb7e01 100644 --- a/apps/admin-ui/src/clients/routes.ts +++ b/apps/admin-ui/src/clients/routes.ts @@ -1,7 +1,12 @@ import type { RouteDef } from "../route-config"; import { AddClientRoute } from "./routes/AddClient"; +import { + AddRegistrationProviderRoute, + EditRegistrationProviderRoute, +} from "./routes/AddRegistrationProvider"; import { AuthorizationRoute } from "./routes/AuthenticationTab"; import { ClientRoute } from "./routes/Client"; +import { ClientRegistrationRoute } from "./routes/ClientRegistration"; import { ClientRoleRoute } from "./routes/ClientRole"; import { ClientsRoute, ClientsRouteWithTab } from "./routes/Clients"; import { ClientScopesRoute } from "./routes/ClientScopeTab"; @@ -32,6 +37,9 @@ import { } from "./routes/Scope"; const routes: RouteDef[] = [ + ClientRegistrationRoute, + AddRegistrationProviderRoute, + EditRegistrationProviderRoute, AddClientRoute, ImportClientRoute, ClientsRoute, diff --git a/apps/admin-ui/src/clients/routes/AddRegistrationProvider.ts b/apps/admin-ui/src/clients/routes/AddRegistrationProvider.ts new file mode 100644 index 0000000000..7d9a69c6eb --- /dev/null +++ b/apps/admin-ui/src/clients/routes/AddRegistrationProvider.ts @@ -0,0 +1,36 @@ +import { lazy } from "react"; +import type { Path } from "react-router-dom"; +import { generatePath } from "react-router-dom"; +import type { RouteDef } from "../../route-config"; +import { ClientRegistrationTab } from "./ClientRegistration"; + +export type RegistrationProviderParams = { + realm: string; + subTab: ClientRegistrationTab; + id?: string; + providerId: string; +}; + +export const AddRegistrationProviderRoute: RouteDef = { + path: "/:realm/clients/client-registration/:subTab/:providerId", + component: lazy(() => import("../registration/DetailProvider")), + breadcrumb: (t) => t("clients:clientSettings"), + access: "manage-clients", +}; + +export const EditRegistrationProviderRoute: RouteDef = { + ...AddRegistrationProviderRoute, + path: "/:realm/clients/client-registration/:subTab/:providerId/:id", +}; + +export const toRegistrationProvider = ( + params: RegistrationProviderParams +): Partial => { + const path = params.id + ? EditRegistrationProviderRoute.path + : AddRegistrationProviderRoute.path; + + return { + pathname: generatePath(path, params), + }; +}; diff --git a/apps/admin-ui/src/clients/routes/ClientRegistration.ts b/apps/admin-ui/src/clients/routes/ClientRegistration.ts new file mode 100644 index 0000000000..35cc9563fa --- /dev/null +++ b/apps/admin-ui/src/clients/routes/ClientRegistration.ts @@ -0,0 +1,24 @@ +import { lazy } from "react"; +import type { Path } from "react-router-dom"; +import { generatePath } from "react-router-dom"; +import type { RouteDef } from "../../route-config"; + +export type ClientRegistrationTab = "anonymous" | "authenticated"; + +export type ClientRegistrationParams = { + realm: string; + subTab: ClientRegistrationTab; +}; + +export const ClientRegistrationRoute: RouteDef = { + path: "/:realm/clients/client-registration/:subTab", + component: lazy(() => import("../ClientsSection")), + breadcrumb: (t) => t("clients:clientRegistration"), + access: "view-clients", +}; + +export const toClientRegistration = ( + params: ClientRegistrationParams +): Partial => ({ + pathname: generatePath(ClientRegistrationRoute.path, params), +}); diff --git a/apps/admin-ui/src/clients/routes/Clients.ts b/apps/admin-ui/src/clients/routes/Clients.ts index c69f208dfd..b096747021 100644 --- a/apps/admin-ui/src/clients/routes/Clients.ts +++ b/apps/admin-ui/src/clients/routes/Clients.ts @@ -3,7 +3,10 @@ import type { Path } from "react-router-dom"; import { generatePath } from "react-router-dom"; import type { RouteDef } from "../../route-config"; -export type ClientsTab = "list" | "initial-access-token"; +export type ClientsTab = + | "list" + | "initial-access-token" + | "client-registration"; export type ClientsParams = { realm: string;