Skip to content

Commit

Permalink
chore: update next-auth (#394)
Browse files Browse the repository at this point in the history
next-auth@v5-beta.16 now detects AUTH_SECRET only on server side. Needed to revamp some code accordingly
  • Loading branch information
vincentchalamon committed Apr 19, 2024
1 parent 5b073b7 commit bd7b70c
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 62 deletions.
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 };

0 comments on commit bd7b70c

Please sign in to comment.