Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: update next-auth #394

Merged
merged 6 commits into from Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions compose.prod.yaml
Expand Up @@ -16,6 +16,8 @@ services:
build:
context: ./pwa
target: prod
args:
AUTH_SECRET: ${AUTH_SECRET}
environment:
AUTH_SECRET: ${AUTH_SECRET}

Expand Down
2 changes: 2 additions & 0 deletions pwa/Dockerfile
Expand Up @@ -41,6 +41,8 @@ RUN pnpm fetch --prod

COPY --link . .

ARG AUTH_SECRET

RUN pnpm install --frozen-lockfile --offline --prod && \
pnpm run build

Expand Down
15 changes: 0 additions & 15 deletions pwa/app/auth.tsx
@@ -1,5 +1,4 @@
import { type TokenSet } from "@auth/core/types";
import { signOut as logout, type SignOutParams } from "next-auth/react";
import NextAuth, { type Session as DefaultSession, type User } from "next-auth";
import KeycloakProvider from "next-auth/providers/keycloak";

Expand Down Expand Up @@ -27,20 +26,6 @@ interface Account {
refresh_token: string
}

interface SignOutResponse {
url: string
}

export async function signOut<R extends boolean = true>(
session: DefaultSession,
options?: SignOutParams<R>
): Promise<R extends true ? undefined : SignOutResponse> {
return await logout({
// @ts-ignore
callbackUrl: `${OIDC_SERVER_URL}/protocol/openid-connect/logout?id_token_hint=${session.idToken}&post_logout_redirect_uri=${options?.callbackUrl ?? window.location.origin}`,
});
}

export const { handlers: { GET, POST }, auth } = NextAuth({
callbacks: {
// @ts-ignore
Expand Down
2 changes: 2 additions & 0 deletions pwa/components/admin/Admin.tsx
@@ -1,3 +1,5 @@
"use client";

import Head from "next/head";
import { useContext, useRef, useState } from "react";
import { type DataProvider, Layout, type LayoutProps, localStorageStore, resolveBrowserLocale } from "react-admin";
Expand Down
43 changes: 39 additions & 4 deletions pwa/components/admin/AppBar.tsx
@@ -1,13 +1,16 @@
import { useContext, useState } from "react";
import { AppBar, AppBarClasses, UserMenu, Logout, useStore } from "react-admin";
import {ForwardedRef, 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);
Expand Down Expand Up @@ -62,11 +65,43 @@ const DocTypeMenuButton = () => {
);
};

const Logout = forwardRef((props, ref: ForwardedRef<any>) => {
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 (
<MenuItem
className="logout"
onClick={handleClick}
ref={ref}
component="li"
{...props}
>
<ListItemIcon className={LogoutClasses.icon}>
<ExitIcon fontSize="small" />
</ListItemIcon>
<ListItemText>
{translate('ra.auth.logout', { _: 'Logout' })}
</ListItemText>
</MenuItem>
);
});
Logout.displayName = "Logout";

const CustomAppBar = ({ ...props }: AppBarProps) => {
return (
<AppBar userMenu={
<UserMenu>
<Logout redirectTo={`${window.location.origin}/books`}/>
<Logout/>
</UserMenu>
} {...props}>
<Typography
Expand Down
21 changes: 14 additions & 7 deletions pwa/components/admin/authProvider.tsx
@@ -1,21 +1,26 @@
import { AuthProvider } from "react-admin";
import { signIn } from "next-auth/react";
import { signIn, signOut, useSession } from "next-auth/react";

import { auth, signOut } from "../../app/auth";
import { OIDC_SERVER_URL } from "../../config/keycloak";

const authProvider: AuthProvider = {
// Nothing to do here, this function will never be called
login: async () => Promise.resolve(),
logout: async () => {
const session = await auth();
// eslint-disable-next-line react-hooks/rules-of-hooks
const { data: session } = useSession();
if (!session) {
return;
}

await signOut(session, {callbackUrl: window.location.origin});
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();
// eslint-disable-next-line react-hooks/rules-of-hooks
const { data: session } = useSession();
const status = error.status;
// @ts-ignore
if (!session || session?.error === "RefreshAccessTokenError" || status === 401) {
Expand All @@ -29,7 +34,8 @@ const authProvider: AuthProvider = {
}
},
checkAuth: async () => {
const session = await auth();
// eslint-disable-next-line react-hooks/rules-of-hooks
const { data: session } = useSession();
// @ts-ignore
if (!session || session?.error === "RefreshAccessTokenError") {
await signIn("keycloak");
Expand All @@ -42,7 +48,8 @@ const authProvider: AuthProvider = {
getPermissions: () => Promise.resolve(),
// @ts-ignore
getIdentity: async () => {
const session = await auth();
// eslint-disable-next-line react-hooks/rules-of-hooks
const { data: session } = useSession();

return session ? Promise.resolve(session.user) : Promise.reject();
},
Expand Down
9 changes: 6 additions & 3 deletions pwa/components/common/Header.tsx
@@ -1,12 +1,12 @@
"use client";

import { signIn, useSession } from "next-auth/react";
import { signIn, signOut, useSession } from "next-auth/react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import PersonOutlineIcon from "@mui/icons-material/PersonOutline";
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder";

import { signOut } from "../../app/auth";
import { OIDC_SERVER_URL } from "../../config/keycloak";

export const Header = () => {
const pathname = usePathname();
Expand All @@ -31,7 +31,10 @@ export const Header = () => {
{status === "authenticated" && (
<a href="#" className="font-semibold text-gray-900" role="menuitem" onClick={(e) => {
e.preventDefault();
signOut(session, {callbackUrl: `${window.location.origin}/books`});
signOut({
// @ts-ignore
callbackUrl: `${OIDC_SERVER_URL}/protocol/openid-connect/logout?id_token_hint=${session.idToken}&post_logout_redirect_uri=${window.location.origin}/books`,
});
}}>
Sign out
</a>
Expand Down
3 changes: 2 additions & 1 deletion pwa/package.json
Expand Up @@ -23,7 +23,8 @@
"autoprefixer": "^10.4.19",
"formik": "^2.4.5",
"next": "^14.2.2",
"next-auth": "5.0.0-beta.15",
"next-auth": "5.0.0-beta.16",
"picocolors": "^1.0.0",
"postcss": "^8.4.38",
"ra-i18n-polyglot": "^4.16.15",
"ra-language-english": "^4.16.15",
Expand Down
21 changes: 12 additions & 9 deletions pwa/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pwa/tests/User.spec.ts
Expand Up @@ -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);

Expand All @@ -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();
Expand Down
24 changes: 24 additions & 0 deletions 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");
});
});
4 changes: 4 additions & 0 deletions pwa/tests/admin/pages/UserPage.ts
@@ -0,0 +1,4 @@
import { AbstractPage } from "./AbstractPage";

export class UserPage extends AbstractPage {
}
23 changes: 22 additions & 1 deletion 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<Test>({
Expand All @@ -16,6 +34,9 @@ export const test = playwrightTest.extend<Test>({
reviewPage: async ({ page }, use) => {
await use(new ReviewPage(page));
},
userPage: async ({ page }, use) => {
await use(new UserPage(page));
},
});

export { expect };