diff --git a/src/clients/ClientsSection.tsx b/src/clients/ClientsSection.tsx index ed28fdcbf3..1eedc51c26 100644 --- a/src/clients/ClientsSection.tsx +++ b/src/clients/ClientsSection.tsx @@ -21,7 +21,7 @@ import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation"; import { formattedLinkTableCell } from "../components/external-link/FormattedLink"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; -import { InitialAccessTokenList } from "./InitialAccessTokenList"; +import { InitialAccessTokenList } from "./initial-access/InitialAccessTokenList"; export const ClientsSection = () => { const { t } = useTranslation("clients"); diff --git a/src/clients/InitialAccessTokenList.tsx b/src/clients/InitialAccessTokenList.tsx deleted file mode 100644 index 1a7f70bfdb..0000000000 --- a/src/clients/InitialAccessTokenList.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from "react"; -import moment from "moment"; - -import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; -import { useAdminClient } from "../context/auth/AdminClient"; -import { useRealm } from "../context/realm-context/RealmContext"; - -export const InitialAccessTokenList = () => { - const adminClient = useAdminClient(); - const { realm } = useRealm(); - const loader = async () => - await adminClient.realms.getClientsInitialAccess({ realm }); - - return ( - moment(row.timestamp * 1000).fromNow(), - }, - { - name: "expiration", - cellRenderer: (row) => - moment(moment.now() - row.expiration * 1000).fromNow(), - }, - { - name: "count", - }, - { - name: "remainingCount", - }, - ]} - /> - ); -}; diff --git a/src/clients/help.json b/src/clients/help.json index ade986a0f7..85fe0f6dcd 100644 --- a/src/clients/help.json +++ b/src/clients/help.json @@ -4,6 +4,9 @@ "adminURL": "URL to the admin interface of the client. Set this if the client supports the adapter REST API. This REST API allows the auth server to push revocation policies and other administrative tasks. Usually this is set to the base URL of the client.", "downloadType": "this is information about the download type", "details": "this is information about the details", + "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.", diff --git a/src/clients/initial-access/AccessTokenDialog.tsx b/src/clients/initial-access/AccessTokenDialog.tsx new file mode 100644 index 0000000000..b03bde8cde --- /dev/null +++ b/src/clients/initial-access/AccessTokenDialog.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { + Alert, + AlertVariant, + ClipboardCopy, + Form, + FormGroup, + Modal, + ModalVariant, +} from "@patternfly/react-core"; + +type AccessTokenDialogProps = { + token: string; + toggleDialog: () => void; +}; + +export const AccessTokenDialog = ({ + token, + toggleDialog, +}: AccessTokenDialogProps) => { + const { t } = useTranslation("clients"); + return ( + + +
+ + + {token} + + +
+
+ ); +}; diff --git a/src/clients/initial-access/CreateInitialAccessToken.tsx b/src/clients/initial-access/CreateInitialAccessToken.tsx new file mode 100644 index 0000000000..11d1ab273c --- /dev/null +++ b/src/clients/initial-access/CreateInitialAccessToken.tsx @@ -0,0 +1,129 @@ +import React, { FormEvent, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Controller, useForm } from "react-hook-form"; +import { + ActionGroup, + AlertVariant, + Button, + FormGroup, + NumberInput, + PageSection, +} from "@patternfly/react-core"; + +import ClientInitialAccessPresentation from "keycloak-admin/lib/defs/clientInitialAccessPresentation"; +import { FormAccess } from "../../components/form-access/FormAccess"; +import { ViewHeader } from "../../components/view-header/ViewHeader"; +import { HelpItem } from "../../components/help-enabler/HelpItem"; +import { TimeSelector } from "../../components/time-selector/TimeSelector"; +import { useHistory } from "react-router-dom"; +import { useRealm } from "../../context/realm-context/RealmContext"; +import { useAdminClient } from "../../context/auth/AdminClient"; +import { useAlerts } from "../../components/alert/Alerts"; +import { AccessTokenDialog } from "./AccessTokenDialog"; + +export const CreateInitialAccessToken = () => { + const { t } = useTranslation("clients"); + const { handleSubmit, control } = useForm(); + + const adminClient = useAdminClient(); + const { realm } = useRealm(); + const { addAlert } = useAlerts(); + + const history = useHistory(); + const [token, setToken] = useState(""); + + const save = async (clientToken: ClientInitialAccessPresentation) => { + try { + const access = await adminClient.realms.createClientsInitialAccess( + { realm }, + clientToken + ); + setToken(access.token); + } catch (error) { + addAlert(t("tokenSaveError", { error }), AlertVariant.danger); + } + }; + + return ( + <> + {token && ( + setToken("")} /> + )} + + + + + } + > + ( + + )} + /> + + + } + > + ( + onChange(value + 1)} + onMinus={() => onChange(value - 1)} + onChange={(event) => + onChange(Number((event.target as HTMLInputElement).value)) + } + /> + )} + /> + + + + + + + + + ); +}; diff --git a/src/clients/initial-access/InitialAccessTokenList.tsx b/src/clients/initial-access/InitialAccessTokenList.tsx new file mode 100644 index 0000000000..f8e3f46a63 --- /dev/null +++ b/src/clients/initial-access/InitialAccessTokenList.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { useHistory, useRouteMatch } from "react-router-dom"; +import moment from "moment"; +import { useTranslation } from "react-i18next"; +import { Button } from "@patternfly/react-core"; + +import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; +import { useAdminClient } from "../../context/auth/AdminClient"; +import { useRealm } from "../../context/realm-context/RealmContext"; + +export const InitialAccessTokenList = () => { + const { t } = useTranslation("clients"); + const adminClient = useAdminClient(); + const { realm } = useRealm(); + + const history = useHistory(); + const { url } = useRouteMatch(); + + const loader = async () => + await adminClient.realms.getClientsInitialAccess({ realm }); + + return ( + + + + } + columns={[ + { + name: "id", + displayKey: "clients:id", + }, + { + name: "timestamp", + displayKey: "clients:timestamp", + cellRenderer: (row) => moment(row.timestamp * 1000).format("LLL"), + }, + { + name: "expiration", + displayKey: "clients:expires", + cellRenderer: (row) => + moment(row.timestamp * 1000 + row.expiration * 1000).fromNow(), + }, + { + name: "count", + displayKey: "clients:count", + }, + { + name: "remainingCount", + displayKey: "clients:remainingCount", + }, + ]} + /> + ); +}; diff --git a/src/clients/messages.json b/src/clients/messages.json index 420348cbc7..97c0357847 100644 --- a/src/clients/messages.json +++ b/src/clients/messages.json @@ -74,6 +74,16 @@ "disableConfirm": "If you disable this client, you cannot initiate a login or obtain access tokens.", "clientDeleteConfirm": "If you delete this client, all associated data will be removed.", "searchInitialAccessToken": "Search token", + "createToken": "Create initial access token", + "id": "ID", + "timestamp": "Created date", + "expirs": "Expires", + "count": "Count", + "remainingCount": "Remaining count", + "expiration": "Expiration", + "tokenSaveError": "Could not create initial access token {{error}}", + "initialAccessTokenDetails": "Initial access token details", + "copyInitialAccessToken": "Please copy and paste the initial access token before closing as it can not be retrieved later.", "clientAuthentication": "Client authentication", "authentication": "Authentication", "authenticationFlow": "Authentication flow", diff --git a/src/route-config.ts b/src/route-config.ts index 85d75730a7..cd5908f4f8 100644 --- a/src/route-config.ts +++ b/src/route-config.ts @@ -27,6 +27,7 @@ import { UserFederationLdapSettings } from "./user-federation/UserFederationLdap import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm"; import { RealmRoleTabs } from "./realm-roles/RealmRoleTabs"; import { SearchGroups } from "./groups/SearchGroups"; +import { CreateInitialAccessToken } from "./clients/initial-access/CreateInitialAccessToken"; export type RouteDef = BreadcrumbsRoute & { access: AccessType; @@ -54,6 +55,12 @@ export const routes: RoutesFn = (t: TFunction) => [ breadcrumb: null, access: "query-clients", }, + { + path: "/:realm/clients/initialAccessToken/create", + component: CreateInitialAccessToken, + breadcrumb: t("clients:createToken"), + access: "manage-clients", + }, { path: "/:realm/clients/add-client", component: NewClientForm,