diff --git a/pwa/components/admin/AppBar.tsx b/pwa/components/admin/AppBar.tsx index 5e8385a0..b948f568 100644 --- a/pwa/components/admin/AppBar.tsx +++ b/pwa/components/admin/AppBar.tsx @@ -1,13 +1,16 @@ -import { useContext, useState } from "react"; -import { AppBar, AppBarClasses, UserMenu, Logout, useStore } from "react-admin"; +import { forwardRef, useContext, useState } from "react"; +import { AppBar, AppBarClasses, LogoutClasses, UserMenu, useTranslate, useStore } from "react-admin"; import { type AppBarProps } from "react-admin"; -import { Button, Menu, MenuItem, Typography } from "@mui/material"; +import {Button, ListItemIcon, ListItemText, Menu, MenuItem, Typography} from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ExitIcon from "@mui/icons-material/PowerSettingsNew"; +import { signOut, useSession } from "next-auth/react"; import DocContext from "../../components/admin/DocContext"; import HydraLogo from "../../components/admin/HydraLogo"; import OpenApiLogo from "../../components/admin/OpenApiLogo"; import Logo from "../../components/admin/Logo"; +import {OIDC_SERVER_URL} from "../../config/keycloak"; const DocTypeMenuButton = () => { const [anchorEl, setAnchorEl] = useState(null); @@ -62,11 +65,42 @@ const DocTypeMenuButton = () => { ); }; +const Logout = forwardRef((props, ref) => { + const { data: session } = useSession(); + const translate = useTranslate(); + + if (!session) { + return; + } + + const handleClick = () => signOut({ + // @ts-ignore + callbackUrl: `${OIDC_SERVER_URL}/protocol/openid-connect/logout?id_token_hint=${session.idToken}&post_logout_redirect_uri=${window.location.origin}`, + }); + + return ( + + + + + + {translate('ra.auth.logout', { _: 'Logout' })} + + + ); +}); + const CustomAppBar = ({ ...props }: AppBarProps) => { return ( - + } {...props}> Promise.resolve(), logout: async () => { - const session = await auth(); + const { data: session } = useSession(); if (!session) { return; } - await signOut(/*{ + await signOut({ // @ts-ignore callbackUrl: `${OIDC_SERVER_URL}/protocol/openid-connect/logout?id_token_hint=${session.idToken}&post_logout_redirect_uri=${window.location.origin}`, - }*/); + }); }, checkError: async (error) => { - const session = await auth(); + const { data: session } = useSession(); const status = error.status; // @ts-ignore if (!session || session?.error === "RefreshAccessTokenError" || status === 401) { @@ -33,7 +32,7 @@ const authProvider: AuthProvider = { } }, checkAuth: async () => { - const session = await auth(); + const { data: session } = useSession(); // @ts-ignore if (!session || session?.error === "RefreshAccessTokenError") { await signIn("keycloak"); @@ -46,7 +45,7 @@ const authProvider: AuthProvider = { getPermissions: () => Promise.resolve(), // @ts-ignore getIdentity: async () => { - const session = await auth(); + const { data: session } = useSession(); return session ? Promise.resolve(session.user) : Promise.reject(); }, diff --git a/pwa/tests/User.spec.ts b/pwa/tests/User.spec.ts index a03bd1a1..2ea33234 100644 --- a/pwa/tests/User.spec.ts +++ b/pwa/tests/User.spec.ts @@ -5,7 +5,7 @@ test.describe("User authentication", () => { await bookPage.gotoList(); }); - test("I can log in @login", async ({ userPage, page }) => { + test("I can log in Books Store @login", async ({ userPage, page }) => { await expect(page.getByText("Log in")).toBeVisible(); await expect(page.getByText("Sign out")).toHaveCount(0); @@ -22,7 +22,7 @@ test.describe("User authentication", () => { await expect(page.getByText("Sign out")).toBeVisible(); }); - test("I can sign out @login", async ({ userPage, page }) => { + test("I can sign out of Books Store @login", async ({ userPage, page }) => { await page.getByText("Log in").click(); await userPage.login(); await page.getByText("Sign out").click(); diff --git a/pwa/tests/admin/User.spec.ts b/pwa/tests/admin/User.spec.ts new file mode 100644 index 00000000..dd5920a9 --- /dev/null +++ b/pwa/tests/admin/User.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from "./test"; + +test.describe("User authentication", () => { + test.beforeEach(async ({ bookPage }) => { + await bookPage.gotoList(); + }); + + test("I can sign out of Admin @login", async ({ userPage, page }) => { + await page.getByLabel("Profile").click(); + await page.getByRole("menu").getByText("Logout").waitFor({ state: "visible" }); + await page.getByRole("menu").getByText("Logout").click(); + + await expect(page).toHaveURL(/\/$/); + + // I should be logged out from Keycloak also + await page.goto("/admin"); + await page.waitForURL(/\/oidc\/realms\/demo\/protocol\/openid-connect\/auth/); + // @ts-ignore assert declared on test.ts + await expect(page).toBeOnLoginPage(); + await expect(page.locator("#kc-header-wrapper")).toContainText("API Platform - Demo"); + await expect(page.locator("#kc-form-login")).toContainText("Login as user: john.doe@example.com"); + await expect(page.locator("#kc-form-login")).toContainText("Login as admin: chuck.norris@example.com"); + }); +}); diff --git a/pwa/tests/admin/pages/UserPage.ts b/pwa/tests/admin/pages/UserPage.ts new file mode 100644 index 00000000..d6196b12 --- /dev/null +++ b/pwa/tests/admin/pages/UserPage.ts @@ -0,0 +1,4 @@ +import { AbstractPage } from "./AbstractPage"; + +export class UserPage extends AbstractPage { +} diff --git a/pwa/tests/admin/test.ts b/pwa/tests/admin/test.ts index a5acafa9..0c86fb58 100644 --- a/pwa/tests/admin/test.ts +++ b/pwa/tests/admin/test.ts @@ -1,12 +1,30 @@ -import { test as playwrightTest } from "@playwright/test"; +import { Page, test as playwrightTest } from "@playwright/test"; import { expect } from "../test"; import { BookPage } from "./pages/BookPage"; import { ReviewPage } from "./pages/ReviewPage"; +import { UserPage } from "./pages/UserPage"; + +expect.extend({ + toBeOnLoginPage(page: Page) { + if (page.url().match(/\/oidc\/realms\/demo\/protocol\/openid-connect\/auth/)) { + return { + message: () => "passed", + pass: true, + }; + } + + return { + message: () => `toBeOnLoginPage() assertion failed.\nExpected "/oidc/realms/demo/protocol/openid-connect/auth", got "${page.url()}".`, + pass: false, + }; + }, +}); type Test = { bookPage: BookPage, reviewPage: ReviewPage, + userPage: UserPage, } export const test = playwrightTest.extend({ @@ -16,6 +34,9 @@ export const test = playwrightTest.extend({ reviewPage: async ({ page }, use) => { await use(new ReviewPage(page)); }, + userPage: async ({ page }, use) => { + await use(new UserPage(page)); + }, }); export { expect };