Use browser router for Account Console (#22192)

Closes #27442

Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Jon Koops 2024-03-04 13:38:28 +01:00 committed by GitHub
parent be3e2fabc4
commit 7afd75ba08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 152 additions and 115 deletions

View file

@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test";
import { getRootPath } from "./src/utils/getRootPath";
/**
* See https://playwright.dev/docs/test-configuration.
@ -11,9 +12,7 @@ export default defineConfig({
workers: 1,
reporter: process.env.CI ? [["github"], ["html"]] : "list",
use: {
baseURL: process.env.CI
? "http://localhost:8080/realms/master/account/"
: "http://localhost:8080/",
baseURL: `http://localhost:8080${getRootPath()}`,
trace: "on-first-retry",
},

View file

@ -0,0 +1,2 @@
export const DEFAULT_REALM = "master";
export const ROOT_PATH = "/realms/:realm/account";

View file

@ -1,3 +1,6 @@
import { matchPath } from "react-router-dom";
import { DEFAULT_REALM, ROOT_PATH } from "./constants";
export type Feature = {
isRegistrationEmailAsUsername: boolean;
isEditUserNameAllowed: boolean;
@ -31,11 +34,12 @@ export type Environment = {
features: Feature;
};
// The default environment, used during development.
const realm = new URLSearchParams(window.location.search).get("realm");
// Detect the current realm from the URL.
const match = matchPath(ROOT_PATH, location.pathname);
const defaultEnvironment: Environment = {
authUrl: "http://localhost:8180",
realm: realm || "master",
realm: match?.params.realm ?? DEFAULT_REALM,
clientId: "security-admin-console-v2",
resourceUrl: "http://localhost:8080",
logo: "/logo.svg",

View file

@ -3,18 +3,22 @@ import "@patternfly/patternfly/patternfly-addons.css";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createHashRouter, RouterProvider } from "react-router-dom";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { environment } from "./environment";
import { i18n } from "./i18n";
import { routes } from "./routes";
import { getRootPath } from "./utils/getRootPath";
// Initialize required components before rendering app.
await i18n.init();
const router = createHashRouter(routes);
const container = document.getElementById("app");
const root = createRoot(container!);
const basename = getRootPath(environment.realm);
const router = createBrowserRouter(routes, { basename });
root.render(
<StrictMode>
<RouterProvider router={router} />

View file

@ -0,0 +1,5 @@
import { generatePath } from "react-router-dom";
import { DEFAULT_REALM, ROOT_PATH } from "../constants";
export const getRootPath = (realm = DEFAULT_REALM) =>
generatePath(ROOT_PATH, { realm });

View file

@ -1,18 +1,20 @@
import { expect, test } from "@playwright/test";
import {
createIdentityProvider,
deleteIdentityProvider,
createClient,
deleteClient,
inRealm,
findClientByClientId,
createRandomUserWithPassword,
deleteUser,
} from "../admin-client";
import groupsIdPClient from "../realms/groups-idp.json" assert { type: "json" };
import ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import { randomUUID } from "crypto";
import { expect, test } from "@playwright/test";
import { randomUUID } from "node:crypto";
import {
createClient,
createIdentityProvider,
createRandomUserWithPassword,
deleteClient,
deleteIdentityProvider,
deleteUser,
findClientByClientId,
inRealm,
} from "../admin-client";
import groupsIdPClient from "../realms/groups-idp.json" assert { type: "json" };
import { getBaseUrl } from "../utils";
const realm = "groups";
@ -30,7 +32,7 @@ test.describe("Account linking", () => {
groupIdPClientId = await createClient(
groupsIdPClient as ClientRepresentation,
);
const kc = process.env.KEYCLOAK_SERVER || "http://localhost:8080";
const baseUrl = getBaseUrl();
const idp: IdentityProviderRepresentation = {
alias: "master-idp",
providerId: "oidc",
@ -39,12 +41,12 @@ test.describe("Account linking", () => {
clientId: "groups-idp",
clientSecret: "H0JaTc7VBu3HJR26vrzMxgidfJmgI5Dw",
validateSignature: "false",
tokenUrl: `${kc}/realms/master/protocol/openid-connect/token`,
jwksUrl: `${kc}/realms/master/protocol/openid-connect/certs`,
issuer: `${kc}/realms/master`,
authorizationUrl: `${kc}/realms/master/protocol/openid-connect/auth`,
logoutUrl: `${kc}/realms/master/protocol/openid-connect/logout`,
userInfoUrl: `${kc}/realms/master/protocol/openid-connect/userinfo`,
tokenUrl: `${baseUrl}/realms/master/protocol/openid-connect/token`,
jwksUrl: `${baseUrl}/realms/master/protocol/openid-connect/certs`,
issuer: `${baseUrl}/realms/master`,
authorizationUrl: `${baseUrl}/realms/master/protocol/openid-connect/auth`,
logoutUrl: `${baseUrl}/realms/master/protocol/openid-connect/logout`,
userInfoUrl: `${baseUrl}/realms/master/protocol/openid-connect/userinfo`,
},
};

View file

@ -5,9 +5,12 @@ import RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmR
import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { DEFAULT_REALM } from "../src/constants";
import { getBaseUrl } from "./utils";
const adminClient = new KeycloakAdminClient({
baseUrl: process.env.KEYCLOAK_SERVER || "http://127.0.0.1:8080",
realmName: "master",
baseUrl: getBaseUrl(),
realmName: DEFAULT_REALM,
});
await adminClient.auth({
@ -18,9 +21,12 @@ await adminClient.auth({
});
export async function useTheme() {
const masterRealm = await adminClient.realms.findOne({ realm: "master" });
const masterRealm = await adminClient.realms.findOne({
realm: DEFAULT_REALM,
});
await adminClient.realms.update(
{ realm: "master" },
{ realm: DEFAULT_REALM },
{ ...masterRealm, accountTheme: "keycloak.v3" },
);
}
@ -76,7 +82,7 @@ export async function importUserProfile(
await adminClient.users.updateProfile({ ...userProfile, realm });
}
export async function enableLocalization(realm: string) {
export async function enableLocalization(realm = DEFAULT_REALM) {
const realmRepresentation = await adminClient.realms.findOne({ realm });
await adminClient.realms.update(
{ realm },
@ -121,9 +127,9 @@ export async function getUserByUsername(username: string, realm: string) {
export async function deleteUser(username: string) {
try {
const users = await adminClient.users.find({ username, realm });
const users = await adminClient.users.find({ username });
const { id } = users[0];
await adminClient.users.del({ id: id!, realm });
await adminClient.users.del({ id: id! });
} catch (error) {
console.error(error);
}

View file

@ -1,10 +1,12 @@
import { expect, test } from "@playwright/test";
import { getRootPath } from "../src/utils/getRootPath";
import { login } from "./login";
import { getAccountUrl, getAdminUrl } from "./utils";
test.describe("Applications test", () => {
test.beforeEach(async ({ page }) => {
// Sign out all devices before each test
await login(page, "admin", "admin", "master");
await login(page, "admin", "admin");
await page.getByTestId("accountSecurity").click();
await page.getByTestId("account-security/device-activity").click();
@ -19,7 +21,7 @@ test.describe("Applications test", () => {
});
test("Single application", async ({ page }) => {
await login(page, "admin", "admin", "master");
await login(page, "admin", "admin");
await page.getByTestId("applications").click();
@ -39,8 +41,8 @@ test.describe("Applications test", () => {
const page1 = await context1.newPage();
const page2 = await context2.newPage();
await login(page1, "admin", "admin", "master");
await login(page2, "admin", "admin", "master");
await login(page1, "admin", "admin");
await login(page2, "admin", "admin");
await page1.getByTestId("applications").click();
@ -62,15 +64,15 @@ test.describe("Applications test", () => {
"Skip this test if not running with regular Keycloak",
);
await login(page, "admin", "admin", "master");
await login(page, "admin", "admin");
// go to admin console
await page.goto("/");
await expect(page).toHaveURL("http://localhost:8080/admin/master/console/");
await page.waitForURL("http://localhost:8080/admin/master/console/");
await expect(page).toHaveURL(getAdminUrl());
await page.waitForURL(getAdminUrl());
await page.goto("/realms/master/account");
await page.waitForURL("http://localhost:8080/realms/master/account/");
await page.goto(getRootPath());
await page.waitForURL(getAccountUrl());
await page.getByTestId("applications").click();

View file

@ -1,15 +1,16 @@
import { Page } from "@playwright/test";
import { DEFAULT_REALM } from "../src/constants";
import { getRootPath } from "../src/utils/getRootPath";
export const login = async (
page: Page,
username: string,
password: string,
realm?: string,
realm = DEFAULT_REALM,
) => {
if (realm)
await page.goto(
process.env.CI ? `/realms/${realm}/account` : `/?realm=${realm}`,
);
const rootPath = getRootPath(realm);
await page.goto(rootPath);
await page.getByLabel("Username").fill(username);
await page.getByLabel("Password", { exact: true }).fill(password);
await page.getByRole("button", { name: "Sign In" }).click();

View file

@ -18,7 +18,7 @@ test.describe("Personal info page", () => {
test("sets basic information", async ({ page }) => {
user = await createRandomUserWithPassword("user-" + randomUUID(), "pwd");
await login(page, user, "pwd", "master");
await login(page, user, "pwd");
await page.getByTestId("email").fill(`${user}@somewhere.com`);
await page.getByTestId("firstName").fill("Erik");
@ -84,7 +84,7 @@ test.describe("Personal info with userprofile enabled", async () => {
// skip currently the locale is not part of the response
test.describe.skip("Realm localization", async () => {
test.beforeAll(() => enableLocalization("master"));
test.beforeAll(() => enableLocalization());
test("change locale", async ({ page }) => {
const user = await createRandomUserWithPassword(
@ -92,7 +92,7 @@ test.describe.skip("Realm localization", async () => {
"pwd",
);
await login(page, user, "pwd", "master");
await login(page, user, "pwd");
await page
.locator("div")
.filter({ hasText: /^Deutsch$/ })

View file

@ -0,0 +1,13 @@
import { getRootPath } from "../src/utils/getRootPath";
export function getBaseUrl(): string {
return process.env.KEYCLOAK_SERVER ?? "http://localhost:8080";
}
export function getAccountUrl() {
return getBaseUrl() + getRootPath();
}
export function getAdminUrl() {
return getBaseUrl() + "/admin/master/console/";
}

View file

@ -4,6 +4,8 @@ import { defineConfig, loadEnv } from "vite";
import { checker } from "vite-plugin-checker";
import dts from "vite-plugin-dts";
import { getRootPath } from "./src/utils/getRootPath";
// https://vitejs.dev/config/
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
@ -26,6 +28,7 @@ export default defineConfig(({ mode }) => {
base: "",
server: {
port: 8080,
open: getRootPath(),
},
build: {
...lib,

View file

@ -18,7 +18,7 @@ describe("Masthead tests", () => {
it("Go to account console and back to admin console", () => {
sidebarPage.waitForPageLoad();
masthead.accountManagement();
cy.url().should("contain", "/realms/master/account/");
cy.url().should("contain", "/realms/master/account");
});
it("Sign out reachs to log in screen", () => {

View file

@ -92,15 +92,12 @@ public class AccountConsole implements AccountResourceProvider {
@GET
@NoCache
@Path("{any:.*}")
public Response getMainPage() throws IOException, FreeMarkerException {
UriInfo uriInfo = session.getContext().getUri(UrlType.FRONTEND);
URI accountBaseUrl = uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(realm.getName())
.path(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).path("/").build(realm);
if (!session.getContext().getUri().getRequestUri().getPath().endsWith("/")) {
UriBuilder redirectUri = session.getContext().getUri().getRequestUriBuilder().uri(accountBaseUrl);
return Response.status(302).location(redirectUri.build()).build();
} else {
Map<String, Object> map = new HashMap<>();
URI adminBaseUri = session.getContext().getUri(UrlType.ADMIN).getBaseUri();
@ -162,7 +159,6 @@ public class AccountConsole implements AccountResourceProvider {
Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result);
return builder.build();
}
}
private Map<String, String> supportedLocales(Properties messages) {
return realm.getSupportedLocalesStream()