Run as theme (#840)
* Build for Keycloak theme * Update based on lastest index.html changes. * Fix realm dropdown when home realm is not master. * Fix readme. * Fix linting errors. * Try to fix tests. * Address Jon's comments.
This commit is contained in:
parent
0930778390
commit
d606dc6bee
13 changed files with 172 additions and 132 deletions
2
.github/workflows/cypress.yml
vendored
2
.github/workflows/cypress.yml
vendored
|
@ -30,7 +30,7 @@ jobs:
|
|||
run: ./start.js & sleep 40
|
||||
|
||||
- name: Run Admin Console
|
||||
run: npx http-server ./build -P http://localhost:8180/ & sleep 30
|
||||
run: npx http-server ./build/src/main/resources/admin/resources -P http://localhost:8180/ & sleep 30
|
||||
|
||||
- name: Admin Console client
|
||||
run: ./import.js
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -128,8 +128,9 @@ sketch
|
|||
|
||||
# snowpack
|
||||
web_modules/
|
||||
build/
|
||||
.build/
|
||||
build/src
|
||||
build/target
|
||||
.build/target
|
||||
public/assets/
|
||||
|
||||
# server-install
|
||||
|
|
27
build/README.md
Normal file
27
build/README.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Keycloak Admin Console V2 Maven Build
|
||||
|
||||
The Maven build prepares the admin console to be deployed as a theme on the Keycloak server.
|
||||
|
||||
# Build Instructions
|
||||
|
||||
```bash
|
||||
$> npm run build
|
||||
$> cd build
|
||||
$> mvn install
|
||||
```
|
||||
|
||||
# Deployment
|
||||
|
||||
The jar created with `mvn install` needs to be deployed to a Maven repository. From there, it will become part of the Keycloak server build.
|
||||
|
||||
For development, you can also just copy the contents of `build/target/classes` to `<keycloak server>/themes/keycloak.v2`. Then restart the server.
|
||||
|
||||
# To Run
|
||||
|
||||
Until New Admin Console becomes the default, you will need to start Keycloak server like this:
|
||||
|
||||
```bash
|
||||
$> bin/standalone.sh -Dprofile.feature.newadmin=enabled
|
||||
```
|
||||
|
||||
Then go to `Realm Settings --> Themes` and set Admin Console Theme to `keycloak.v2`.
|
92
build/pom.xml
Normal file
92
build/pom.xml
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
JBoss, Home of Professional Open Source
|
||||
Copyright 2016, Red Hat, Inc. and/or its affiliates, and individual
|
||||
contributors by the @authors tag. See the copyright.txt in the
|
||||
distribution for a full listing of individual contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-admin-ui</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>Keycloak Administration UI</name>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>Apache License, Version 2.0.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jboss</id>
|
||||
<url>https://repository.jboss.org/nexus/content/groups/public/</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.google.code.maven-replacer-plugin</groupId>
|
||||
<artifactId>maven-replacer-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>replace</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<file>target/classes/admin/resources/index.html</file>
|
||||
<outputFile>target/classes/admin/index.ftl</outputFile>
|
||||
<regex>false</regex>
|
||||
<replacements>
|
||||
<replacement>
|
||||
<token>src="/_dist_</token>
|
||||
<value>src="${resourceUrl}/_dist_</value>
|
||||
</replacement>
|
||||
<replacement>
|
||||
<token>href="./</token>
|
||||
<value>href="${resourceUrl}/</value>
|
||||
</replacement>
|
||||
<replacement>
|
||||
<token><head></token>
|
||||
<value>
|
||||
<head>
|
||||
<script type="text/javascript">
|
||||
var loginRealm = "${loginRealm}";
|
||||
var authServerUrl = "${authServerUrl}";
|
||||
var authUrl = "${authUrl}";
|
||||
var consoleBaseUrl = "${consoleBaseUrl}";
|
||||
var resourceUrl = "${resourceUrl}";
|
||||
var masterRealm = "${masterRealm}";
|
||||
var resourceVersion = "${resourceVersion}";
|
||||
</script>
|
||||
</value>
|
||||
</replacement>
|
||||
</replacements>
|
||||
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -1,67 +0,0 @@
|
|||
{
|
||||
"clientId": "security-admin-console-v2",
|
||||
"rootUrl": "",
|
||||
"adminUrl": "",
|
||||
"baseUrl": "",
|
||||
"surrogateAuthRequired": false,
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"/adminv2/*",
|
||||
"http://localhost:8080/*",
|
||||
"http://localhost/*"
|
||||
],
|
||||
"webOrigins": [
|
||||
"*"
|
||||
],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
"standardFlowEnabled": true,
|
||||
"implicitFlowEnabled": false,
|
||||
"directAccessGrantsEnabled": true,
|
||||
"serviceAccountsEnabled": false,
|
||||
"publicClient": true,
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"saml.assertion.signature": "false",
|
||||
"saml.force.post.binding": "false",
|
||||
"saml.multivalued.roles": "false",
|
||||
"saml.encrypt": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false",
|
||||
"saml.server.signature": "false",
|
||||
"saml.server.signature.keyinfo.ext": "false",
|
||||
"exclude.session.state.from.auth.response": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"client_credentials.use_refresh_token": "false",
|
||||
"saml_force_name_id_format": "false",
|
||||
"saml.client.signature": "false",
|
||||
"tls.client.certificate.bound.access.tokens": "false",
|
||||
"saml.authnstatement": "false",
|
||||
"display.on.consent.screen": "false",
|
||||
"saml.onetimeuse.condition": "false"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": true,
|
||||
"nodeReRegistrationTimeout": -1,
|
||||
"defaultClientScopes": [
|
||||
"web-origins",
|
||||
"role_list",
|
||||
"roles",
|
||||
"profile",
|
||||
"email"
|
||||
],
|
||||
"optionalClientScopes": [
|
||||
"address",
|
||||
"phone",
|
||||
"offline_access",
|
||||
"microprofile-jwt"
|
||||
],
|
||||
"access": {
|
||||
"view": true,
|
||||
"configure": true,
|
||||
"manage": true
|
||||
}
|
||||
}
|
|
@ -9,7 +9,10 @@ module.exports = {
|
|||
"@snowpack/plugin-typescript",
|
||||
],
|
||||
buildOptions: {
|
||||
baseUrl: "/adminv2",
|
||||
baseUrl: "./",
|
||||
clean: true,
|
||||
},
|
||||
devOptions: {
|
||||
out: "build/src/main/resources/admin/resources", // For snowpack 3, "out" goes under buildOptions.
|
||||
},
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ import { WhoAmIContext } from "./context/whoami/WhoAmI";
|
|||
import { HelpContext, HelpHeader } from "./components/help-enabler/HelpHeader";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { useAdminClient } from "./context/auth/AdminClient";
|
||||
import { resourceUri } from "./util";
|
||||
|
||||
export const Header = () => {
|
||||
const adminClient = useAdminClient();
|
||||
|
@ -126,7 +127,7 @@ export const Header = () => {
|
|||
<UserDropdown />
|
||||
</PageHeaderToolsItem>
|
||||
</PageHeaderToolsGroup>
|
||||
<Avatar src="./img_avatar.svg" alt="Avatar image" />
|
||||
<Avatar src={resourceUri + "/img_avatar.svg"} alt="Avatar image" />
|
||||
</PageHeaderTools>
|
||||
);
|
||||
};
|
||||
|
@ -180,7 +181,7 @@ export const Header = () => {
|
|||
logo={
|
||||
<Link to="/">
|
||||
<Brand
|
||||
src="./logo.svg"
|
||||
src={resourceUri + "/logo.svg"}
|
||||
id="masthead-logo"
|
||||
alt="Logo"
|
||||
className="keycloak__pageheader_brand"
|
||||
|
|
|
@ -16,7 +16,7 @@ describe("FormAccess", () => {
|
|||
<WhoAmIContext.Provider
|
||||
value={{
|
||||
refresh: () => {},
|
||||
whoAmI: new WhoAmI("master", whoami as WhoAmIRepresentation),
|
||||
whoAmI: new WhoAmI(whoami as WhoAmIRepresentation),
|
||||
}}
|
||||
>
|
||||
<RealmContext.Provider
|
||||
|
|
|
@ -1,51 +1,25 @@
|
|||
import KcAdminClient from "keycloak-admin";
|
||||
import { homeRealm, isDevMode, authUri } from "../../util";
|
||||
|
||||
export default async function (): Promise<KcAdminClient> {
|
||||
const realm =
|
||||
new URLSearchParams(window.location.search).get("realm") || "master";
|
||||
|
||||
const kcAdminClient = new KcAdminClient();
|
||||
|
||||
try {
|
||||
await kcAdminClient.init(
|
||||
{ onLoad: "check-sso", pkceMethod: "S256" },
|
||||
{
|
||||
url: keycloakAuthUrl(),
|
||||
realm: realm,
|
||||
clientId: "security-admin-console-v2",
|
||||
url: authUri(),
|
||||
realm: homeRealm(),
|
||||
clientId: isDevMode
|
||||
? "security-admin-console-v2"
|
||||
: "security-admin-console",
|
||||
}
|
||||
);
|
||||
kcAdminClient.setConfig({ realmName: realm });
|
||||
kcAdminClient.setConfig({ realmName: homeRealm() });
|
||||
|
||||
// we can get rid of devMode once developers upgrade to Keycloak 13
|
||||
const devMode = !window.location.pathname.startsWith("/adminv2");
|
||||
kcAdminClient.baseUrl = devMode ? "/auth" : keycloakAuthUrl();
|
||||
kcAdminClient.baseUrl = authUri();
|
||||
} catch (error) {
|
||||
alert("failed to initialize keycloak");
|
||||
}
|
||||
|
||||
return kcAdminClient;
|
||||
}
|
||||
|
||||
const keycloakAuthUrl = () => {
|
||||
// Eventually, authContext should not be hard-coded.
|
||||
// You are allowed to change this context on your keycloak server,
|
||||
// but it is rarely done.
|
||||
const authContext = "/auth";
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
// passed in as query param
|
||||
const authUrlFromParam = searchParams.get("keycloak-server");
|
||||
if (authUrlFromParam) return authUrlFromParam + authContext;
|
||||
|
||||
// dev mode
|
||||
if (!window.location.pathname.startsWith("/adminv2"))
|
||||
return "http://localhost:8180" + authContext;
|
||||
|
||||
// demo mode
|
||||
if (searchParams.get("demo")) return "http://localhost:8080" + authContext;
|
||||
|
||||
// admin console served from keycloak server
|
||||
return window.location.origin + authContext;
|
||||
};
|
||||
|
|
|
@ -4,12 +4,10 @@ import i18n from "../../i18n";
|
|||
import type WhoAmIRepresentation from "keycloak-admin/lib/defs/whoAmIRepresentation";
|
||||
import type { AccessType } from "keycloak-admin/lib/defs/whoAmIRepresentation";
|
||||
import { useAdminClient, useFetch } from "../auth/AdminClient";
|
||||
import { homeRealm } from "../../util";
|
||||
|
||||
export class WhoAmI {
|
||||
constructor(
|
||||
private homeRealm?: string | undefined,
|
||||
private me?: WhoAmIRepresentation | undefined
|
||||
) {
|
||||
constructor(private me?: WhoAmIRepresentation) {
|
||||
if (this.me !== undefined && this.me.locale) {
|
||||
i18n.changeLanguage(this.me.locale, (error) => {
|
||||
if (error) console.error("Unable to set locale to", this.me?.locale);
|
||||
|
@ -33,11 +31,7 @@ export class WhoAmI {
|
|||
* Return the realm I am signed in to.
|
||||
*/
|
||||
public getHomeRealm(): string {
|
||||
let realm: string | undefined = this.homeRealm;
|
||||
if (realm === undefined) realm = this.me?.realm;
|
||||
if (realm === undefined) realm = "master"; // this really can't happen in the real world
|
||||
|
||||
return realm;
|
||||
return homeRealm();
|
||||
}
|
||||
|
||||
public canCreateRealm(): boolean {
|
||||
|
@ -72,7 +66,7 @@ export const WhoAmIContextProvider = ({ children }: WhoAmIProviderProps) => {
|
|||
useFetch(
|
||||
() => adminClient.whoAmI.find(),
|
||||
(me) => {
|
||||
const whoAmI = new WhoAmI(adminClient.keycloak?.realm, me);
|
||||
const whoAmI = new WhoAmI(me);
|
||||
setWhoAmI(whoAmI);
|
||||
},
|
||||
[key]
|
||||
|
|
|
@ -3,24 +3,22 @@ import { WhoAmI } from "../WhoAmI";
|
|||
import type WhoAmIRepresentation from "keycloak-admin/lib/defs/whoAmIRepresentation";
|
||||
|
||||
test("returns display name", () => {
|
||||
const whoami = new WhoAmI("master", whoamiMock as WhoAmIRepresentation);
|
||||
const whoami = new WhoAmI(whoamiMock as WhoAmIRepresentation);
|
||||
expect(whoami.getDisplayName()).toEqual("Stan Silvert");
|
||||
});
|
||||
|
||||
test("returns correct home realm", () => {
|
||||
let whoami = new WhoAmI("myrealm", whoamiMock as WhoAmIRepresentation);
|
||||
expect(whoami.getHomeRealm()).toEqual("myrealm");
|
||||
whoami = new WhoAmI(undefined, whoamiMock as WhoAmIRepresentation);
|
||||
test("returns correct home realm in dev mode", () => {
|
||||
const whoami = new WhoAmI(whoamiMock as WhoAmIRepresentation);
|
||||
expect(whoami.getHomeRealm()).toEqual("master");
|
||||
});
|
||||
|
||||
test("can not create realm", () => {
|
||||
const whoami = new WhoAmI("master", whoamiMock as WhoAmIRepresentation);
|
||||
const whoami = new WhoAmI(whoamiMock as WhoAmIRepresentation);
|
||||
expect(whoami.canCreateRealm()).toEqual(false);
|
||||
});
|
||||
|
||||
test("getRealmAccess", () => {
|
||||
const whoami = new WhoAmI("master", whoamiMock as WhoAmIRepresentation);
|
||||
const whoami = new WhoAmI(whoamiMock as WhoAmIRepresentation);
|
||||
expect(Object.keys(whoami.getRealmAccess()).length).toEqual(3);
|
||||
expect(whoami.getRealmAccess()["master"].length).toEqual(18);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
Brand,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
|
@ -29,17 +28,17 @@ import { useRealm } from "../context/realm-context/RealmContext";
|
|||
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||
|
||||
import "./dashboard.css";
|
||||
import { toUpperCase } from "../util";
|
||||
import { toUpperCase, resourceUri } from "../util";
|
||||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||
|
||||
const EmptyDashboard = () => {
|
||||
const { t } = useTranslation("dashboard");
|
||||
const { realm, setRealm } = useRealm();
|
||||
const { realm } = useRealm();
|
||||
return (
|
||||
<PageSection variant="light">
|
||||
<EmptyState variant="large">
|
||||
<Brand
|
||||
src="./icon.svg"
|
||||
src={resourceUri + "/icon.svg"}
|
||||
alt="Keycloak icon"
|
||||
className="keycloak__dashboard_icon"
|
||||
/>
|
||||
|
@ -50,9 +49,6 @@ const EmptyDashboard = () => {
|
|||
{realm}
|
||||
</Title>
|
||||
<EmptyStateBody>{t("introduction")}</EmptyStateBody>
|
||||
<Button variant="link" onClick={() => setRealm("master")}>
|
||||
{t("common:realmInfo")}
|
||||
</Button>
|
||||
</EmptyState>
|
||||
</PageSection>
|
||||
);
|
||||
|
|
21
src/util.ts
21
src/util.ts
|
@ -5,6 +5,27 @@ import type ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentat
|
|||
import type { ProviderRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
|
||||
import type KeycloakAdminClient from "keycloak-admin";
|
||||
|
||||
// if we are running on Keycloak server, resourceUrl will be passed from index.ftl
|
||||
declare const resourceUrl: string;
|
||||
export const isDevMode = typeof resourceUrl === "undefined";
|
||||
export const resourceUri = isDevMode ? "." : resourceUrl;
|
||||
|
||||
// if we are running on Keycloak server, loginRealm will be passed from index.ftl
|
||||
declare const loginRealm: string;
|
||||
export const homeRealm = () => {
|
||||
if (typeof loginRealm !== "undefined") return loginRealm;
|
||||
|
||||
return new URLSearchParams(window.location.search).get("realm") || "master";
|
||||
};
|
||||
|
||||
// if we are running on Keycloak server, authUrl will be passed from index.ftl
|
||||
declare const authUrl: string;
|
||||
export const authUri = () => {
|
||||
if (typeof authUrl !== "undefined") return authUrl;
|
||||
|
||||
return "http://localhost:8180/auth";
|
||||
};
|
||||
|
||||
export const sortProviders = (providers: {
|
||||
[index: string]: ProviderRepresentation;
|
||||
}) => {
|
||||
|
|
Loading…
Reference in a new issue