mailslurp-examples - next-auth-example

https://github.com/mailslurp/examples

Table of Contents

next-auth-example/tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  },
  "include": [
    "next-env.d.ts",
    "next-auth.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": ["node_modules"]
}

next-auth-example/prestart.js

/**
 * Set env variables before start for tests
 */
const API_KEY = process.env.API_KEY;
if (!API_KEY) {
    throw new Error("Must provide API_KEY for MailSlurp prestart.js")
}
// create mailslurp instance
const Mailslurp = require('mailslurp-client').default;
const fs = require("fs");
const mailslurp = new Mailslurp({ apiKey: API_KEY })

mailslurp.createInboxWithOptions({ inboxType: 'SMTP_INBOX' }).then((inbox)=> {
    return mailslurp.getImapSmtpAccessDetails(inbox.id).then(access => {
        const content = `# from prestart.js
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=63e3a8eb680c38655f67b07422563daf5231240fcb1c4de3a8c2aef4dec506cb
EMAIL_SERVER_HOST=${access.secureSmtpServerHost}
EMAIL_SERVER_PORT=${access.secureSmtpServerPort}
EMAIL_SERVER_USER=${access.secureSmtpUsername}
EMAIL_SERVER_PASSWORD=${access.secureSmtpPassword}
EMAIL_FROM=${inbox.emailAddress}
`
// write .env file
        fs.writeFileSync(__dirname + '/.env.local', content, { encoding: 'utf-8' })
    })
}).catch(err => { throw err })

next-auth-example/playwright.config.ts

import { defineConfig, devices } from '@playwright/test';

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// require('dotenv').config();

/**
 * See https://playwright.dev/docs/test-configuration.
 */
export default defineConfig({
  testDir: './tests',
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  retries: process.env.CI ? 2 : 0,
  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : undefined,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: 'html',
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
    // baseURL: 'http://127.0.0.1:3000',

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
    trace: 'on-first-retry',
  },

  /* Configure projects for major browsers */
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],

  /* Run your local dev server before starting the tests */
  webServer: {
    command: 'npm run start',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
  },
});

next-auth-example/package.json

{
  "private": true,
  "description": "An example project for NextAuth.js with Next.js",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "prestart": "node prestart.js && npm run build",
    "start": "next start",
    "test": "playwright test"
  },
  "dependencies": {
    "@auth/sequelize-adapter": "^1.0.1",
    "mailslurp-client": "^15.17.2",
    "next": "^13.4.19",
    "next-auth": "^4.23.1",
    "nodemailer": "^6.9.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "sequelize": "^6.32.1",
    "sqlite3": "^5.1.6"
  },
  "devDependencies": {
    "@playwright/test": "^1.37.1",
    "@types/node": "^18.16.2",
    "@types/react": "^18.2.0",
    "playwright": "^1.37.1",
    "typescript": "5.2.2"
  }
}

next-auth-example/next.config.js

/** @type {import("next").NextConfig} */
module.exports = {
  reactStrictMode: true,
}

next-auth-example/next-auth.d.ts

import "next-auth/jwt"

// Read more at: https://next-auth.js.org/getting-started/typescript#module-augmentation

declare module "next-auth/jwt" {
  interface JWT {
    /** The user's role. */
    userRole?: "admin"
  }
}

next-auth-example/middleware.ts

import { withAuth } from "next-auth/middleware"

// More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware
export default withAuth({
  callbacks: {
    authorized({ req, token }) {
      // `/admin` requires admin role
      if (req.nextUrl.pathname === "/admin") {
        return token?.userRole === "admin"
      }
      // `/me` only requires the user to be logged in
      return !!token
    },
  },
})

export const config = { matcher: ["/admin", "/me"] }

next-auth-example/README.md

# Next.js Auth magic link example 
Example project demonstrating end-to-end testing of next-auth email access links using Playwright and MailSlurp.

## Setup from scratch
Following the [NextAuth email guide](https://next-auth.js.org/providers/email):

```
npm install --save next next-auth nodemailer mailslurp-client
```

Email provider requires a database, let's use [sequelize](https://authjs.dev/reference/adapter/sequelize):

```
npm install --save sequelize sqlite3 @auth/sequelize-adapter
```

For testing:

```
npm install --save-dev playwright
```

next-auth-example/Makefile

-include ../.env

node_modules:
	npm install

fmt:
	npm run fmt

test: node_modules
	API_KEY=$(API_KEY) npm t

next-auth-example/LICENSE

ISC License

Copyright (c) 2022-2023, Balázs Orbán

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

next-auth-example/.env.local.example

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=63e3a8eb680c38655f67b07422563daf5231240fcb1c4de3a8c2aef4dec506cb
# see https://docs.mailslurp.com/imap-smtp/
EMAIL_SERVER_HOST=
EMAIL_SERVER_PORT=
EMAIL_SERVER_USER=
EMAIL_SERVER_PASSWORD=
EMAIL_FROM=

next-auth-example/tests/login.spec.ts

import { test, expect } from '@playwright/test';
import { MailSlurp } from 'mailslurp-client';
import { writeFile } from 'fs/promises'
import path from 'path';
const apiKey = process.env.API_KEY ?? '';
const absolutePath = path.resolve(`${__dirname}/body.html`)

test('can login using magic link', async ({ page }) => {
  expect(apiKey, "MailSlurp API_KEY env should be set").toBeTruthy()
  // create an inbox for sign up
  const mailslurp = new MailSlurp({ apiKey })
  const userInbox = await mailslurp.createInbox()

  await page.goto('http://localhost:3000/');
  await page.screenshot({ path: `screenshots/01-welcome.jpeg` });

  // load the app and try access
  await page.goto('http://localhost:3000/protected/');
  await page.waitForSelector('[data-id="access-denied"]')
  await page.screenshot({ path: `screenshots/02-access-denied.jpeg` });

  // now login
  await page.click('[data-id="access-link"]')
  await page.waitForURL(/signin/);

  // try sign in with email
  await page.fill('#input-email-for-email-provider', userInbox.emailAddress)
  await page.screenshot({ path: `screenshots/03-sign-in.jpeg` });
  await page.click('#submitButton')
  await page.waitForURL(/verify-request/)
  await page.screenshot({ path: `screenshots/04-verify.jpeg` });

  // wait for the login link and extract it
  const email = await mailslurp.waitForLatestEmail(userInbox.id)
  await writeFile(absolutePath, email.body!!)
  const { links: [loginLink] } = await mailslurp.emailController.getEmailLinks({
    emailId: email.id,
  })
  expect(loginLink).toContain('localhost');
  await page.goto('file://' + absolutePath)
  await page.screenshot({ path: `screenshots/05-email.jpeg` });

  // now go to link
  await page.goto(loginLink)
  await page.waitForSelector('[data-id="access-permitted"]')
  await page.screenshot({ path: `screenshots/06-access.jpeg` });
});

next-auth-example/pages/styles.css

body {
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
    "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
    "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  padding: 0 1rem 1rem 1rem;
  max-width: 680px;
  margin: 0 auto;
  background: #fff;
  color: #333;
}

li,
p {
  line-height: 1.5rem;
}

a {
  font-weight: 500;
}

hr {
  border: 1px solid #ddd;
}

iframe {
  background: #ccc;
  border: 1px solid #ccc;
  height: 10rem;
  width: 100%;
  border-radius: 0.5rem;
  filter: invert(1);
}

next-auth-example/pages/server.tsx

import { getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"

import type { GetServerSidePropsContext } from "next"
import { useSession } from "next-auth/react"

export default function ServerSidePage() {
  const { data: session } = useSession()
  // As this page uses Server Side Rendering, the `session` will be already
  // populated on render without needing to go through a loading stage.
  return (
    <Layout>
      <h1>Server Side Rendering</h1>
      <p>
        This page uses the <strong>getServerSession()</strong> method in{" "}
        <strong>getServerSideProps()</strong>.
      </p>
      <p>
        Using <strong>getServerSession()</strong> in{" "}
        <strong>getServerSideProps()</strong> is the recommended approach if you
        need to support Server Side Rendering with authentication.
      </p>
      <p>
        The advantage of Server Side Rendering is this page does not require
        client side JavaScript.
      </p>
      <p>
        The disadvantage of Server Side Rendering is that this page is slower to
        render.
      </p>
      <pre>{JSON.stringify(session, null, 2)}</pre>
    </Layout>
  )
}

// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps(context: GetServerSidePropsContext) {
  return {
    props: {
      session: await getServerSession(context.req, context.res, authOptions),
    },
  }
}

next-auth-example/pages/protected.tsx

import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"

export default function ProtectedPage() {
  const { data: session } = useSession()
  const [content, setContent] = useState()

  // Fetch content from protected route
  useEffect(() => {
    const fetchData = async () => {
      const res = await fetch("/api/examples/protected")
      const json = await res.json()
      if (json.content) {
        setContent(json.content)
      }
    }
    fetchData()
  }, [session])

  // If no session exists, display access denied message
  if (!session) {
    return (
      <Layout>
        <AccessDenied />
      </Layout>
    )
  }

  // If session exists, display content
  return (
    <Layout>
      <h1 data-id={"access-permitted"}>Protected Page</h1>
      <p>
        <strong>{content ?? "\u00a0"}</strong>
      </p>
    </Layout>
  )
}

next-auth-example/pages/policy.tsx

import Layout from "../components/layout"

export default function PolicyPage() {
  return (
    <Layout>
      <p>
        This is an example site to demonstrate how to use{" "}
        <a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
      </p>
      <h2>Terms of Service</h2>
      <p>
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
        OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
        MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
        IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
        CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
        TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
        SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
      </p>
      <h2>Privacy Policy</h2>
      <p>
        This site uses JSON Web Tokens and an in-memory database which resets
        every ~2 hours.
      </p>
      <p>
        Data provided to this site is exclusively used to support signing in and
        is not passed to any third party services, other than via SMTP or OAuth
        for the purposes of authentication.
      </p>
    </Layout>
  )
}

next-auth-example/pages/me.tsx

import { useSession } from "next-auth/react"
import Layout from "../components/layout"

export default function MePage() {
  const { data } = useSession()

  return (
    <Layout>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </Layout>
  )
}

next-auth-example/pages/index.tsx

import Layout from "../components/layout"

export default function IndexPage() {
  return (
    <Layout>
      <h1>NextAuth.js Example</h1>
      <p>
        This is an example site to demonstrate how to use{" "}
        <a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
      </p>
    </Layout>
  )
}

next-auth-example/pages/client.tsx

import Layout from "../components/layout"

export default function ClientPage() {
  return (
    <Layout>
      <h1>Client Side Rendering</h1>
      <p>
        This page uses the <strong>useSession()</strong> React Hook in the{" "}
        <strong>&lt;Header/&gt;</strong> component.
      </p>
      <p>
        The <strong>useSession()</strong> React Hook is easy to use and allows
        pages to render very quickly.
      </p>
      <p>
        The advantage of this approach is that session state is shared between
        pages by using the <strong>Provider</strong> in <strong>_app.js</strong>{" "}
        so that navigation between pages using <strong>useSession()</strong> is
        very fast.
      </p>
      <p>
        The disadvantage of <strong>useSession()</strong> is that it requires
        client side JavaScript.
      </p>
    </Layout>
  )
}

next-auth-example/pages/api-example.tsx

import Layout from "../components/layout"

export default function ApiExamplePage() {
  return (
    <Layout>
      <h1>API Example</h1>
      <p>The examples below show responses from the example API endpoints.</p>
      <p>
        <em>You must be signed in to see responses.</em>
      </p>
      <h2>Session</h2>
      <p>/api/examples/session</p>
      <iframe src="/api/examples/session" />
      <h2>JSON Web Token</h2>
      <p>/api/examples/jwt</p>
      <iframe src="/api/examples/jwt" />
    </Layout>
  )
}

next-auth-example/pages/admin.tsx

import Layout from "../components/layout"

export default function Page() {
  return (
    <Layout>
      <h1>This page is protected by Middleware</h1>
      <p>Only admin users can see this page.</p>
      <p>
        To learn more about the NextAuth middleware see&nbsp;
        <a href="https://next-auth.js.org/configuration/nextjs#middleware">
          the docs
        </a>
        .
      </p>
    </Layout>
  )
}

next-auth-example/pages/_app.tsx

import { SessionProvider } from "next-auth/react"
import "./styles.css"

import type { AppProps } from "next/app"
import type { Session } from "next-auth"

// Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({
  Component,
  pageProps: { session, ...pageProps },
}: AppProps<{ session: Session }>) {
  return (
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}

next-auth-example/pages/api/examples/session.ts

// This is an example of how to access a session from an API route
import { getServerSession } from "next-auth"
import { authOptions } from "../auth/[...nextauth]"

import type { NextApiRequest, NextApiResponse } from "next"

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getServerSession(req, res, authOptions)
  res.send(JSON.stringify(session, null, 2))
}

next-auth-example/pages/api/examples/protected.ts

// This is an example of to protect an API route
import { getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"

import type { NextApiRequest, NextApiResponse } from "next"

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getServerSession(req, res, authOptions)

  if (session) {
    return res.send({
      content:
        "This is protected content. You can access this content because you are signed in.",
    })
  }

  res.send({
    error: "You must be signed in to view the protected content on this page.",
  })
}

next-auth-example/pages/api/examples/jwt.ts

// This is an example of how to read a JSON Web Token from an API route
import { getToken } from "next-auth/jwt"

import type { NextApiRequest, NextApiResponse } from "next"

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // If you don't have the NEXTAUTH_SECRET environment variable set,
  // you will have to pass your secret as `secret` to `getToken`
  const token = await getToken({ req })
  res.send(JSON.stringify(token, null, 2))
}

next-auth-example/pages/api/auth/[...nextauth].ts

import NextAuth, { NextAuthOptions } from "next-auth"
import EmailProvider from "next-auth/providers/email"
import SequelizeAdapter from "@auth/sequelize-adapter";
import {Sequelize} from "sequelize";
const sequelize = new Sequelize("sqlite::memory:");
const adapter= SequelizeAdapter(sequelize);
// create tables
sequelize.sync();

export const authOptions: NextAuthOptions = {
  providers: [
    EmailProvider({
      server: {
        host: process.env.EMAIL_SERVER_HOST,
        port: process.env.EMAIL_SERVER_PORT,
        auth: {
          user: process.env.EMAIL_SERVER_USER,
          pass: process.env.EMAIL_SERVER_PASSWORD
        }
      },
      from: process.env.EMAIL_FROM,
    }),
  ],
  adapter: adapter as any,
  callbacks: {
    async jwt({ token }) {
      token.userRole = "admin"
      return token
    },
  },
}

export default NextAuth(authOptions)

next-auth-example/components/layout.tsx

import Header from "./header"
import Footer from "./footer"
import type { ReactNode } from "react"

export default function Layout({ children }: { children: ReactNode }) {
  return (
    <>
      <Header />
      <main>{children}</main>
      <Footer />
    </>
  )
}

next-auth-example/components/header.tsx

import Link from "next/link"
import { signIn, signOut, useSession } from "next-auth/react"
import styles from "./header.module.css"

// The approach used in this component shows how to build a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header() {
  const { data: session, status } = useSession()
  const loading = status === "loading"

  return (
    <header>
      <noscript>
        <style>{`.nojs-show { opacity: 1; top: 0; }`}</style>
      </noscript>
      <div className={styles.signedInStatus}>
        <p
          className={`nojs-show ${
            !session && loading ? styles.loading : styles.loaded
          }`}
        >
          {!session && (
            <>
              <span className={styles.notSignedInText}>
                You are not signed in
              </span>
              <a
                href={`/api/auth/signin`}
                className={styles.buttonPrimary}
                onClick={(e) => {
                  e.preventDefault()
                  signIn()
                }}
              >
                Sign in
              </a>
            </>
          )}
          {session?.user && (
            <>
              {session.user.image && (
                <span
                  style={{ backgroundImage: `url('${session.user.image}')` }}
                  className={styles.avatar}
                />
              )}
              <span className={styles.signedInText}>
                <small>Signed in as</small>
                <br />
                <strong>{session.user.email ?? session.user.name}</strong>
              </span>
              <a
                href={`/api/auth/signout`}
                className={styles.button}
                onClick={(e) => {
                  e.preventDefault()
                  signOut()
                }}
              >
                Sign out
              </a>
            </>
          )}
        </p>
      </div>
      <nav>
        <ul className={styles.navItems}>
          <li className={styles.navItem}>
            <Link href="/">Home</Link>
          </li>
          <li className={styles.navItem}>
            <Link href="/client">Client</Link>
          </li>
          <li className={styles.navItem}>
            <Link href="/server">Server</Link>
          </li>
          <li className={styles.navItem}>
            <Link href="/protected">Protected</Link>
          </li>
          <li className={styles.navItem}>
            <Link href="/api-example">API</Link>
          </li>
          <li className={styles.navItem}>
            <Link href="/admin">Admin</Link>
          </li>
          <li className={styles.navItem}>
            <Link href="/me">Me</Link>
          </li>
        </ul>
      </nav>
    </header>
  )
}

next-auth-example/components/header.module.css

/* Set min-height to avoid page reflow while session loading */
.signedInStatus {
  display: block;
  min-height: 4rem;
  width: 100%;
}

.loading,
.loaded {
  position: relative;
  top: 0;
  opacity: 1;
  overflow: hidden;
  border-radius: 0 0 0.6rem 0.6rem;
  padding: 0.6rem 1rem;
  margin: 0;
  background-color: rgba(0, 0, 0, 0.05);
  transition: all 0.2s ease-in;
}

.loading {
  top: -2rem;
  opacity: 0;
}

.signedInText,
.notSignedInText {
  position: absolute;
  padding-top: 0.8rem;
  left: 1rem;
  right: 6.5rem;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  display: inherit;
  z-index: 1;
  line-height: 1.3rem;
}

.signedInText {
  padding-top: 0rem;
  left: 4.6rem;
}

.avatar {
  border-radius: 2rem;
  float: left;
  height: 2.8rem;
  width: 2.8rem;
  background-color: white;
  background-size: cover;
  background-repeat: no-repeat;
}

.button,
.buttonPrimary {
  float: right;
  margin-right: -0.4rem;
  font-weight: 500;
  border-radius: 0.3rem;
  cursor: pointer;
  font-size: 1rem;
  line-height: 1.4rem;
  padding: 0.7rem 0.8rem;
  position: relative;
  z-index: 10;
  background-color: transparent;
  color: #555;
}

.buttonPrimary {
  background-color: #346df1;
  border-color: #346df1;
  color: #fff;
  text-decoration: none;
  padding: 0.7rem 1.4rem;
}

.buttonPrimary:hover {
  box-shadow: inset 0 0 5rem rgba(0, 0, 0, 0.2);
}

.navItems {
  margin-bottom: 2rem;
  padding: 0;
  list-style: none;
}

.navItem {
  display: inline-block;
  margin-right: 1rem;
}
import Link from "next/link"
import styles from "./footer.module.css"
import packageJSON from "../package.json"

export default function Footer() {
  return (
    <footer className={styles.footer}>
      <hr />
      <ul className={styles.navItems}>
        <li className={styles.navItem}>
          <a href="https://next-auth.js.org">Documentation</a>
        </li>
        <li className={styles.navItem}>
          <a href="https://www.npmjs.com/package/next-auth">NPM</a>
        </li>
        <li className={styles.navItem}>
          <a href="https://github.com/nextauthjs/next-auth-example">GitHub</a>
        </li>
        <li className={styles.navItem}>
          <Link href="/policy">Policy</Link>
        </li>
        <li className={styles.navItem}>
          <em>next-auth@{packageJSON.dependencies["next-auth"]}</em>
        </li>
      </ul>
    </footer>
  )
}
.footer {
  margin-top: 2rem;
}

.navItems {
  margin-bottom: 1rem;
  padding: 0;
  list-style: none;
}

.navItem {
  display: inline-block;
  margin-right: 1rem;
}

next-auth-example/components/access-denied.tsx

import { signIn } from "next-auth/react"

export default function AccessDenied() {
  return (
    <>
      <h1 data-id={"access-denied"}>Access Denied</h1>
      <p>
        <a
          data-id={"access-link"}
          href="/api/auth/signin"
          onClick={(e) => {
            e.preventDefault()
            signIn()
          }}
        >
          You must be signed in to view this page
        </a>
      </p>
    </>
  )
}