Compare commits

...

17 Commits

Author SHA1 Message Date
Balázs Orbán
b2f6b8278e chore: Update package.json [skip ci] 2023-10-26 10:27:44 -07:00
Martin Schaer
c555356895 feat(adapters-surrealdb): update surrealdb.js to enable http connection strategy (#8823)
* update surrealdb.js which fixes rest/http strategy

* uncomment rest test

* add docs

* update package dependencies

* run both strategies

* Apply suggestions from code review

* add fetch to jest globals

---------

Co-authored-by: Thang Vu <hi@thvu.dev>
2023-10-26 10:32:40 +07:00
Balázs Orbán
21288ad461 chore: bump peer dependency version 2023-10-24 17:36:41 -07:00
Balázs Orbán
ae2f2f44c8 docs: add redirect 2023-10-24 17:36:14 -07:00
Balázs Orbán
81283cc4be docs: tweak v5 guide 2023-10-24 16:01:12 -07:00
Balázs Orbán
cdf7b98a60 chore: bump version [skip ci] 2023-10-24 15:49:21 -07:00
Balázs Orbán
84c69da807 fix: allow Response as return type of authorized 2023-10-24 15:49:00 -07:00
Balázs Orbán
f78b33f2fa fix: correct links 2023-10-24 15:42:44 -07:00
Balázs Orbán
fe003ec6ff chore: bump version [skip ci] 2023-10-24 15:41:17 -07:00
Balázs Orbán
46bbfef34c fix: allow middleware handling of auth actions 2023-10-24 15:40:51 -07:00
Luke
5246ca9ed0 docs: Update resources.md (#8866)
remove a broken link
2023-10-24 02:16:39 +01:00
Balázs Orbán
b077baa280 docs: change to beta tag in migration guide 2023-10-23 17:54:09 -07:00
Balázs Orbán
0909f7185a docs: cleanup getting started guides (#8927)
* fix gitignore

* auto-generate providers docs, move pages around, rewrite some docs

* move deployment to getting started

* remove sidebar position configs

* simplify landing page code examples

* add framework logos

* add cards of frameworks on getting started intro

* reword adapters and deployments docs

* update typescript guide
2023-10-24 01:53:00 +01:00
Balázs Orbán
9604cb7cd3 chore: temporarily disallow branch release 2023-10-23 17:52:38 -07:00
Balázs Orbán
5f11ff72c2 chore: bump version [skip ci] 2023-10-23 17:48:07 -07:00
Balázs Orbán
e8a6279e47 chore: next-auth release on beta 2023-10-23 17:42:14 -07:00
Balázs Orbán
65aa467c0e feat(nextjs): introduce next-auth v5 (#7443)
Next.js 13.4 [is out](https://nextjs.org/blog/next-13-4).

For discussing project-related issues, please use https://github.com/nextauthjs/next-auth/discussions/8487

The new version of NextAuth.js is based on `@auth/core`.

If you want to test it out, you can do so already, installing `next-auth@experimental`:

- **Documentation**: https://authjs.dev/reference/nextjs
- **Migration guide**: https://authjs.dev/guides/upgrade-to-v5

BREAKING CHANGE:
Follow the [migration guide](https://authjs.dev/guides/upgrade-to-v5)
2023-10-23 17:35:30 -07:00
296 changed files with 4595 additions and 18831 deletions

View File

@@ -13,10 +13,10 @@ fauna: ["packages/adapter-fauna/**/*"]
firebase: ["packages/adapter-firebase/**/*"] firebase: ["packages/adapter-firebase/**/*"]
hasura: ["packages/adapter-hasura/**/*"] hasura: ["packages/adapter-hasura/**/*"]
frameworks: ["packages/frameworks-*/**/*"] frameworks: ["packages/frameworks-*/**/*"]
legacy: ["packages/next-auth/**/*"]
mikro-orm: ["packages/adapter-mikro-orm/**/*"] mikro-orm: ["packages/adapter-mikro-orm/**/*"]
mongodb: ["packages/adapter-mongodb/**/*"] mongodb: ["packages/adapter-mongodb/**/*"]
neo4j: ["packages/adapter-neo4j/**/*"] neo4j: ["packages/adapter-neo4j/**/*"]
next-auth: ["packages/next-auth/**/*"]
pg: ["packages/adapter-pg/**/*"] pg: ["packages/adapter-pg/**/*"]
playgrounds: ["apps/playgrounds/**/*"] playgrounds: ["apps/playgrounds/**/*"]
pouchdb: ["packages/adapter-pouchdb/**/*"] pouchdb: ["packages/adapter-pouchdb/**/*"]

View File

@@ -113,33 +113,33 @@ jobs:
# with: # with:
# directory: ./coverage # directory: ./coverage
# fail_ci_if_error: false # fail_ci_if_error: false
release-branch: # release-branch:
name: Publish branch # name: Publish branch
runs-on: ubuntu-latest # runs-on: ubuntu-latest
needs: test # needs: test
if: ${{ github.event_name == 'push' }} # if: ${{ github.event_name == 'push' }}
environment: Production # environment: Production
steps: # steps:
- name: Init # - name: Init
uses: actions/checkout@v3 # uses: actions/checkout@v3
with: # with:
fetch-depth: 0 # fetch-depth: 0
# Please upvote https://github.com/orgs/community/discussions/13836 # # Please upvote https://github.com/orgs/community/discussions/13836
token: ${{ secrets.GH_PAT }} # token: ${{ secrets.GH_PAT }}
- name: Install pnpm # - name: Install pnpm
uses: pnpm/action-setup@v2.2.4 # uses: pnpm/action-setup@v2.2.4
- name: Setup Node # - name: Setup Node
uses: actions/setup-node@v3 # uses: actions/setup-node@v3
with: # with:
cache: "pnpm" # cache: "pnpm"
- name: Install dependencies # - name: Install dependencies
run: pnpm install # run: pnpm install
- name: Publish to npm and GitHub # - name: Publish to npm and GitHub
run: pnpm release # run: pnpm release
env: # env:
# Please upvote https://github.com/orgs/community/discussions/13836 # # Please upvote https://github.com/orgs/community/discussions/13836
GITHUB_TOKEN: ${{ secrets.GH_PAT }} # GITHUB_TOKEN: ${{ secrets.GH_PAT }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
release-pr: release-pr:
name: Publish PR name: Publish PR
runs-on: ubuntu-latest runs-on: ubuntu-latest

15
.gitignore vendored
View File

@@ -30,13 +30,6 @@ dist
.cache-loader .cache-loader
packages/next-auth/providers packages/next-auth/providers
packages/next-auth/src/providers/oauth-types.ts packages/next-auth/src/providers/oauth-types.ts
packages/next-auth/client
packages/next-auth/css
packages/next-auth/utils
packages/next-auth/core
packages/next-auth/jwt
packages/next-auth/react
packages/next-auth/next
packages/*/*.js packages/*/*.js
packages/*/*.d.ts packages/*/*.d.ts
packages/*/*.d.ts.map packages/*/*.d.ts.map
@@ -81,7 +74,7 @@ test.schema.gql
# docusaurus # docusaurus
docs/.docusaurus docs/.docusaurus
docs/providers.json docs/manifest.json
# Core # Core
packages/core/src/providers/oauth-types.ts packages/core/src/providers/oauth-types.ts
@@ -90,7 +83,13 @@ packages/core/providers
packages/core/src/lib/pages/styles.ts packages/core/src/lib/pages/styles.ts
docs/docs/reference/core docs/docs/reference/core
docs/docs/reference/sveltekit docs/docs/reference/sveltekit
docs/docs/reference/nextjs
# Next.js
packages/next-auth/lib
packages/next-auth/providers
# copied from @auth/core
packages/next-auth/src/providers
# SvelteKit # SvelteKit
packages/frameworks-sveltekit/index.* packages/frameworks-sveltekit/index.*

View File

@@ -23,7 +23,7 @@ build
docs/docs/reference/core docs/docs/reference/core
docs/docs/reference/sveltekit docs/docs/reference/sveltekit
static static
docs/providers.json docs/manifest.json
# --------------- Packages --------------- # --------------- Packages ---------------
@@ -31,6 +31,7 @@ coverage
dist dist
packages/**/*.cjs packages/**/*.cjs
packages/**/*.js packages/**/*.js
!packages/*/scripts/*.js
# @auth/core # @auth/core
packages/core/src/providers/oauth-types.ts packages/core/src/providers/oauth-types.ts

View File

@@ -1,8 +1,7 @@
{ {
"files.exclude": { "files.exclude": {
"packages/core/{lib,providers,*.js,*.d.ts,*.d.ts.map}": true, "packages/core/{lib,providers,*.js,*.d.ts,*.d.ts.map}": true,
"packages/next-auth/{client,core,css,jwt,next,providers,react,utils,*.js,*.d.ts}": true
}, },
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"openInGitHub.remote.branch": "main" "openInGitHub.remote.branch": "main"
} }

View File

@@ -0,0 +1,7 @@
import { auth } from "auth"
import { NextResponse } from "next/server"
export const GET = auth(function GET(req) {
if (req.auth) return NextResponse.json(req.auth)
return NextResponse.json({ message: "Not authenticated" }, { status: 401 })
})

View File

@@ -0,0 +1,15 @@
import { handlers } from "auth"
const { GET: AuthGET, POST } = handlers
export { POST }
import type { NextRequest } from "next/server"
// Showcasing advanced initialization in Route Handlers
export async function GET(request: NextRequest) {
// Do something with request
const response = await AuthGET(request)
// Do something with response
return response
}
// export const runtime = "edge"

View File

@@ -0,0 +1,16 @@
"use client"
import { signIn, useSession } from "next-auth/react"
export default function Client() {
const { data: session, update, status } = useSession()
return (
<div>
<pre>
{status === "loading" ? "Loading..." : JSON.stringify(session, null, 2)}
</pre>
<button onClick={() => signIn("github")}>Sign in</button>
<button onClick={() => update(`New Name`)}>Update session</button>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page() {
return <h1>This page is protected.</h1>
}

View File

@@ -1,12 +1,56 @@
export default function RootLayout({ import { auth, signIn, signOut, update } from "auth"
children, import Footer from "components/footer"
}: { import { Header } from "components/header"
children: React.ReactNode import styles from "components/header.module.css"
}) { import "./styles.css"
export default function RootLayout(props: { children: React.ReactNode }) {
return ( return (
<html> <html>
<head></head> <body>
<body>{children}</body> <AppHeader />
<main>{props.children}</main>
<div>
<form
action={async () => {
"use server"
update({ user: { name: "New Name" } })
}}
>
<button>Update name</button>
</form>
</div>
<Footer />
</body>
</html> </html>
) )
} }
export async function AppHeader() {
const session = await auth()
return (
<Header
session={session}
signIn={
<form
action={async () => {
"use server"
await signIn("github")
}}
>
<button className={styles.buttonPrimary}>Sign in</button>
</form>
}
signOut={
<form
action={async () => {
"use server"
await signOut()
}}
>
<button className={styles.button}>Sign out</button>
</form>
}
/>
)
}

View File

@@ -0,0 +1,23 @@
import { SessionProvider } from "next-auth/react"
import { auth } from "auth"
import Client from "./client"
export default async function Page() {
const session = await auth()
return (
<>
{/*
NOTE: The `auth()` result is not run through the `session` callback, be careful passing down data
to a client component, this will be exposed via the /api/auth/session endpoint
*/}
<SessionProvider session={session} basePath="/auth">
<Client />
</SessionProvider>
<h1>NextAuth.js Example</h1>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://nextjs.authjs.dev">NextAuth.js</a> for authentication.
</p>
</>
)
}

View File

@@ -1,6 +0,0 @@
import { unstable_getServerSession } from "next-auth/next"
export default async function Page() {
const session = await unstable_getServerSession()
return <pre>{JSON.stringify(session, null, 2)}</pre>
}

View File

@@ -6,7 +6,7 @@ body {
max-width: 680px; max-width: 680px;
margin: 0 auto; margin: 0 auto;
background: #fff; background: #fff;
color: #333; color: var(--color-text);
} }
li, li,

View File

@@ -0,0 +1,52 @@
import type { NextAuthConfig } from "next-auth"
import Auth0 from "next-auth/providers/auth0"
import Credentials from "next-auth/providers/credentials"
import Facebook from "next-auth/providers/facebook"
import GitHub from "next-auth/providers/github"
import Google from "next-auth/providers/google"
import Twitter from "next-auth/providers/twitter"
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user: {
/** The user's postal address. */
address: string
} & User
}
interface User {
foo: string
}
}
export default {
debug: false,
providers: [
GitHub({ account() {} }),
Auth0,
Facebook,
Google,
Twitter,
Credentials({
credentials: { password: { label: "Password", type: "password" } },
authorize(c) {
if (c.password !== "password") return null
return {
id: "test",
foo: "bar",
name: "Test User",
email: "test@example.com",
}
},
}),
],
callbacks: {
jwt({ token, trigger, session }) {
if (trigger === "update") token.name = session.user.name
return token
},
},
} satisfies NextAuthConfig

19
apps/dev/nextjs/auth.ts Normal file
View File

@@ -0,0 +1,19 @@
import NextAuth from "next-auth"
import Email from "next-auth/providers/email"
import authConfig from "auth.config"
import { PrismaClient } from "@prisma/client"
import { PrismaAdapter } from "@auth/prisma-adapter"
globalThis.prisma ??= new PrismaClient()
// authConfig.providers.push(
// // Start server with `pnpm email`
// // @ts-expect-error
// Email({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" })
// )
export const { handlers, auth, signIn, signOut, update } = NextAuth({
// adapter: PrismaAdapter(globalThis.prisma),
session: { strategy: "jwt" },
...authConfig,
})

View File

@@ -1,20 +0,0 @@
import { signIn } from "next-auth/react"
export default function AccessDenied() {
return (
<>
<h1>Access Denied</h1>
<p>
<a
href="/api/auth/signin"
onClick={(e) => {
e.preventDefault()
signIn()
}}
>
You must be signed in to view this page
</a>
</p>
</>
)
}

View File

@@ -1,6 +1,6 @@
import Link from "next/link" import Link from "next/link"
import styles from "./footer.module.css" import styles from "./footer.module.css"
import packageJSON from "package.json" import packageJSON from "next-auth/package.json"
export default function Footer() { export default function Footer() {
return ( return (
@@ -11,7 +11,7 @@ export default function Footer() {
<a href="https://authjs.dev">Documentation</a> <a href="https://authjs.dev">Documentation</a>
</li> </li>
<li className={styles.navItem}> <li className={styles.navItem}>
<a href="https://www.npmjs.com/package/@auth/core">NPM</a> <a href="https://www.npmjs.com/package/next-auth">NPM</a>
</li> </li>
<li className={styles.navItem}> <li className={styles.navItem}>
<a href="https://github.com/nextauthjs/next-auth-example">GitHub</a> <a href="https://github.com/nextauthjs/next-auth-example">GitHub</a>

View File

@@ -1,89 +0,0 @@
import Link from "next/link"
import { useSession } from "next-auth/react"
import styles from "./header.module.css"
// The approach used in this component shows how to built 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()
return (
<header>
<noscript>
<style>{".nojs-show { opacity: 1; top: 0; }"}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && status === "loading" ? styles.loading : styles.loaded
}`}
>
{!session && (
<>
<span className={styles.notSignedInText}>
You are not signed in
</span>
<a href="/api/auth/signin" className={styles.buttonPrimary}>
Sign in
</a>
</>
)}
{session && (
<>
{session.user.image && (
<img src={session.user.image} className={styles.avatar} />
)}
<span className={styles.signedInText}>
<small>Signed in as</small>
<br />
<strong>{session.user.email} </strong>
{session.user.name ? `(${session.user.name})` : null}
</span>
<a href="/api/auth/signout" className={styles.button}>
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="/protected-ssr">Protected(SSR)</Link>
</li>
<li className={styles.navItem}>
<Link href="/api-example">API</Link>
</li>
<li className={styles.navItem}>
<Link href="/credentials">Credentials</Link>
</li>
<li className={styles.navItem}>
<Link href="/email">Email</Link>
</li>
<li className={styles.navItem}>
<Link href="/middleware-protected">Middleware protected</Link>
</li>
<li className={styles.navItem}>
<Link href="/supabase-client-rls">Supabase RLS</Link>
</li>
<li className={styles.navItem}>
<Link href="/supabase-ssr">Supabase RLS(SSR)</Link>
</li>
</ul>
</nav>
</header>
)
}

View File

@@ -1,6 +1,7 @@
/* Set min-height to avoid page reflow while session loading */ /* Set min-height to avoid page reflow while session loading */
.signedInStatus { .signedInStatus {
display: block; display: flex;
align-items: center;
min-height: 4rem; min-height: 4rem;
width: 100%; width: 100%;
} }
@@ -25,16 +26,13 @@
.signedInText, .signedInText,
.notSignedInText { .notSignedInText {
position: absolute;
padding-top: 0.8rem; padding-top: 0.8rem;
left: 1rem;
right: 6.5rem;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
display: inherit;
z-index: 1; z-index: 1;
line-height: 1.3rem; line-height: 1.3rem;
flex: 1;
} }
.signedInText { .signedInText {
@@ -47,6 +45,7 @@
float: left; float: left;
height: 2.8rem; height: 2.8rem;
width: 2.8rem; width: 2.8rem;
margin-right: 1rem;
background-color: white; background-color: white;
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -54,10 +53,11 @@
.button, .button,
.buttonPrimary { .buttonPrimary {
float: right; justify-self: end;
margin-right: -0.4rem;
font-weight: 500; font-weight: 500;
border-radius: 0.3rem; border-radius: 0.3rem;
border: none;
font-weight: bold;
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
line-height: 1.4rem; line-height: 1.4rem;

View File

@@ -0,0 +1,64 @@
import type { Session } from "next-auth"
import Link from "next/link"
import styles from "./header.module.css"
export function Header({
session,
signIn,
signOut,
}: {
session: Session | null
signIn: any
signOut: any
}) {
return (
<header>
<div className={styles.signedInStatus}>
{!session?.user && (
<>
<span className={styles.notSignedInText}>
You are not signed in
</span>
{signIn}
</>
)}
{session?.user && (
<>
{session.user?.image && (
<img src={session.user.image} className={styles.avatar} />
)}
<span className={styles.signedInText}>
<small>Signed in as</small>
<br />
<strong>{session.user?.email} </strong>
{session.user?.name ? `(${session.user.name})` : null}
</span>
{signOut}
</>
)}
</div>
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link href="/">Home (app)</Link>
</li>
<li className={styles.navItem}>
<Link href="/dashboard">Dashboard (app)</Link>
</li>
<li className={styles.navItem}>
<Link href="/policy">Policy (pages)</Link>
</li>
<li className={styles.navItem}>
<Link href="/credentials">Credentials (pages)</Link>
</li>
<li className={styles.navItem}>
<Link href="/protected-ssr">getServerSideProps (pages)</Link>
</li>
<li className={styles.navItem}>
<Link href="/api/examples/protected">API Route (pages)</Link>
</li>
</ul>
</nav>
</header>
)
}

View File

@@ -1,12 +0,0 @@
import Header from "components/header"
import Footer from "components/footer"
export default function Layout({ children }) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
)
}

View File

@@ -1,71 +0,0 @@
module default {
type User {
property name -> str;
required property email -> str {
constraint exclusive;
}
property emailVerified -> datetime;
property image -> str;
multi link accounts := .<user[is Account];
multi link sessions := .<user[is Session];
property createdAt -> datetime {
default := datetime_current();
};
}
type Account {
required property userId := .user.id;
required property type -> str;
required property provider -> str;
required property providerAccountId -> str {
constraint exclusive;
};
property refresh_token -> str;
property access_token -> str;
property expires_at -> int64;
property token_type -> str;
property scope -> str;
property id_token -> str;
property session_state -> str;
required link user -> User {
on target delete delete source;
};
property createdAt -> datetime {
default := datetime_current();
};
constraint exclusive on ((.provider, .providerAccountId))
}
type Session {
required property sessionToken -> str {
constraint exclusive;
}
required property userId := .user.id;
required property expires -> datetime;
required link user -> User {
on target delete delete source;
};
property createdAt -> datetime {
default := datetime_current();
};
}
type VerificationToken {
required property identifier -> str;
required property token -> str {
constraint exclusive;
}
required property expires -> datetime;
property createdAt -> datetime {
default := datetime_current();
};
constraint exclusive on ((.identifier, .token))
}
}
# Disable the application of access policies within access policies
# themselves. This behavior will become the default in EdgeDB 3.0.
# See: https://www.edgedb.com/docs/reference/ddl/access_policies#nonrecursive
using future nonrecursive_access_policies;

View File

@@ -1,2 +0,0 @@
[edgedb]
server-version = "2.6"

View File

@@ -1,45 +1,8 @@
export { default } from "next-auth/middleware" import NextAuth from "next-auth"
import authConfig from "auth.config"
export const config = { matcher: ["/middleware-protected"] } export const middleware = NextAuth(authConfig).auth
// Other ways to use this middleware export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
// import withAuth from "next-auth/middleware" }
// import { withAuth } from "next-auth/middleware"
// export function middleware(req, ev) {
// return withAuth(req)
// }
// export function middleware(req, ev) {
// return withAuth(req, ev)
// }
// export function middleware(req, ev) {
// return withAuth(req, {
// callbacks: {
// authorized: ({ token }) => !!token,
// },
// })
// }
// export default withAuth(function middleware(req, ev) {
// console.log(req.nextauth.token)
// })
// export default withAuth(
// function middleware(req, ev) {
// console.log(req, ev)
// },
// {
// callbacks: {
// authorized: ({ token }) => token.name === "Balázs Orbán",
// },
// }
// )
// export default withAuth({
// callbacks: {
// authorized: ({ token }) => !!token,
// },
// })

View File

@@ -4,6 +4,5 @@ module.exports = {
config.experiments = { ...config.experiments, topLevelAwait: true } config.experiments = { ...config.experiments, topLevelAwait: true }
return config return config
}, },
experimental: { appDir: true },
typescript: { ignoreBuildErrors: true }, typescript: { ignoreBuildErrors: true },
} }

View File

@@ -20,12 +20,13 @@
"@auth/prisma-adapter": "workspace:*", "@auth/prisma-adapter": "workspace:*",
"@auth/supabase-adapter": "workspace:*", "@auth/supabase-adapter": "workspace:*",
"@auth/typeorm-adapter": "workspace:*", "@auth/typeorm-adapter": "workspace:*",
"@prisma/client": "^3", "@prisma/client": "^5",
"edgedb": "^1.0.1", "edgedb": "^1.0.1",
"@supabase/supabase-js": "^2.0.5", "@supabase/supabase-js": "^2.0.5",
"faunadb": "^4", "faunadb": "^4",
"next": "13.4.0", "next": "13.5.7-canary.17",
"nodemailer": "^6", "next-auth": "workspace:*",
"nodemailer": "^6.9.3",
"react": "^18", "react": "^18",
"react-dom": "^18" "react-dom": "^18"
}, },
@@ -33,12 +34,12 @@
"@edgedb/generate": "^0.0.4", "@edgedb/generate": "^0.0.4",
"@playwright/test": "1.29.2", "@playwright/test": "1.29.2",
"@types/jsonwebtoken": "^8.5.5", "@types/jsonwebtoken": "^8.5.5",
"@types/react": "18.0.37", "@types/react": "^18.2.23",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.2.8",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"fake-smtp-server": "^0.8.0", "fake-smtp-server": "^0.8.0",
"pg": "^8.7.3", "pg": "^8.7.3",
"prisma": "^3", "prisma": "^5",
"sqlite3": "^5.0.8", "sqlite3": "^5.0.8",
"typeorm": "0.3.17" "typeorm": "0.3.17"
} }

View File

@@ -1,10 +0,0 @@
import { SessionProvider } from "next-auth/react"
import "./styles.css"
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
)
}

View File

@@ -0,0 +1,34 @@
import { SessionProvider, signIn, signOut, useSession } from "next-auth/react"
import "./styles.css"
import { Header } from "components/header"
import styles from "components/header.module.css"
import Footer from "components/footer"
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session} basePath="/auth">
<PagesHeader />
<Component {...pageProps} />
<Footer />
</SessionProvider>
)
}
function PagesHeader() {
const { data: session } = useSession()
return (
<Header
session={session}
signIn={
<button onClick={() => signIn()} className={styles.buttonPrimary}>
Sign in
</button>
}
signOut={
<button onClick={() => signOut()} className={styles.button}>
Sign out
</button>
}
/>
)
}

View File

@@ -1,19 +0,0 @@
import Layout from "../components/layout"
export default function Page() {
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>
)
}

View File

@@ -1,172 +0,0 @@
import { Auth, type AuthConfig } from "@auth/core"
// Providers
import Apple from "@auth/core/providers/apple"
// import Asgardeo from "@auth/core/providers/asgardeo"
import Auth0 from "@auth/core/providers/auth0"
import AzureAD from "@auth/core/providers/azure-ad"
import AzureB2C from "@auth/core/providers/azure-ad-b2c"
// import BeyondIdentity from "@auth/core/providers/beyondidentity"
import BoxyHQSAML from "@auth/core/providers/boxyhq-saml"
// import Cognito from "@auth/core/providers/cognito"
import Credentials from "@auth/core/providers/credentials"
import Descope from "@auth/core/providers/descope"
import Discord from "@auth/core/providers/discord"
import DuendeIDS6 from "@auth/core/providers/duende-identity-server6"
// import Email from "@auth/core/providers/email"
import Facebook from "@auth/core/providers/facebook"
import Foursquare from "@auth/core/providers/foursquare"
import Freshbooks from "@auth/core/providers/freshbooks"
import GitHub from "@auth/core/providers/github"
import Gitlab from "@auth/core/providers/gitlab"
import Google from "@auth/core/providers/google"
// import IDS4 from "@auth/core/providers/identity-server4"
import Instagram from "@auth/core/providers/instagram"
// import Keycloak from "@auth/core/providers/keycloak"
import Line from "@auth/core/providers/line"
import LinkedIn from "@auth/core/providers/linkedin"
import Mailchimp from "@auth/core/providers/mailchimp"
import Notion from "@auth/core/providers/notion"
// import Okta from "@auth/core/providers/okta"
import Osu from "@auth/core/providers/osu"
import Patreon from "@auth/core/providers/patreon"
import Slack from "@auth/core/providers/slack"
import Spotify from "@auth/core/providers/spotify"
import Trakt from "@auth/core/providers/trakt"
import Twitch from "@auth/core/providers/twitch"
import Twitter from "@auth/core/providers/twitter"
import Yandex from "@auth/core/providers/yandex"
import Vk from "@auth/core/providers/vk"
import Wikimedia from "@auth/core/providers/wikimedia"
import WorkOS from "@auth/core/providers/workos"
import ClickUp from '@auth/core/providers/click-up'
// // Prisma
// import { PrismaClient } from "@prisma/client"
// import { PrismaAdapter } from "@auth/prisma-adapter"
// const client = globalThis.prisma || new PrismaClient()
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
// const adapter = PrismaAdapter(client)
// // Fauna
// import { Client as FaunaClient } from "faunadb"
// import { FaunaAdapter } from "@auth/fauna-adapter"
// const opts = { secret: process.env.FAUNA_SECRET, domain: process.env.FAUNA_DOMAIN }
// const client = globalThis.fauna || new FaunaClient(opts)
// if (process.env.NODE_ENV !== "production") globalThis.fauna = client
// const adapter = FaunaAdapter(client)
// // TypeORM
// import { TypeORMAdapter } from "@auth/typeorm-adapter"
// const adapter = TypeORMAdapter({
// type: "sqlite",
// name: "next-auth-test-memory",
// database: "./typeorm/dev.db",
// synchronize: true,
// })
// // Supabase
// import { SupabaseAdapter } from "@auth/supabase-adapter"
// const adapter = SupabaseAdapter({
// url: process.env.NEXT_PUBLIC_SUPABASE_URL,
// secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
// })
// // EdgeDB
// import { EdgeDBAdapter } from "@auth/edgedb-adapter"
// import { createHttpClient } from "edgedb"
// const client = createHttpClient()
// const adapter = EdgeDBAdapter(client)
export const authConfig: AuthConfig = {
// adapter,
debug: process.env.NODE_ENV !== "production",
theme: {
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
brandColor: "#1786fb",
},
providers: [
Credentials({
credentials: { password: { label: "Password", type: "password" } },
async authorize(credentials) {
if (credentials.password !== "pw") return null
return { name: "Fill Murray", email: "bill@fillmurray.com", image: "https://www.fillmurray.com/64/64", id: "1", foo: "" }
},
}),
Apple({ clientId: process.env.APPLE_ID, clientSecret: process.env.APPLE_SECRET as string }),
// Asgardeo({ clientId: process.env.ASGARDEO_CLIENT_ID, clientSecret: process.env.ASGARDEO_CLIENT_SECRET, issuer: process.env.ASGARDEO_ISSUER }),
Auth0({ clientId: process.env.AUTH0_ID, clientSecret: process.env.AUTH0_SECRET, issuer: process.env.AUTH0_ISSUER }),
AzureAD({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID,
}),
AzureB2C({ clientId: process.env.AZURE_B2C_ID, clientSecret: process.env.AZURE_B2C_SECRET, issuer: process.env.AZURE_B2C_ISSUER }),
// BeyondIdentity({
// clientId: process.env.BEYOND_IDENTITY_CLIENT_ID,
// clientSecret: process.env.BEYOND_IDENTITY_CLIENT_SECRET,
// issuer: process.env.BEYOND_IDENTITY_ISSUER,
// }),
BoxyHQSAML({ issuer: "https://jackson-demo.boxyhq.com", clientId: "tenant=boxyhq.com&product=saml-demo.boxyhq.com", clientSecret: "dummy" }),
// Cognito({ clientId: process.env.COGNITO_ID, clientSecret: process.env.COGNITO_SECRET, issuer: process.env.COGNITO_ISSUER }),
Descope({ clientId: process.env.DESCOPE_ID, clientSecret: process.env.DESCOPE_SECRET }),
Discord({ clientId: process.env.DISCORD_ID, clientSecret: process.env.DISCORD_SECRET }),
DuendeIDS6({ clientId: "interactive.confidential", clientSecret: "secret", issuer: "https://demo.duendesoftware.com" }),
Facebook({ clientId: process.env.FACEBOOK_ID, clientSecret: process.env.FACEBOOK_SECRET }),
Foursquare({ clientId: process.env.FOURSQUARE_ID, clientSecret: process.env.FOURSQUARE_SECRET }),
Freshbooks({ clientId: process.env.FRESHBOOKS_ID, clientSecret: process.env.FRESHBOOKS_SECRET }),
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, redirectProxyUrl: process.env.AUTH_REDIRECT_PROXY_URL }),
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
// IDS4({ clientId: process.env.IDS4_ID, clientSecret: process.env.IDS4_SECRET, issuer: process.env.IDS4_ISSUER }),
Instagram({ clientId: process.env.INSTAGRAM_ID, clientSecret: process.env.INSTAGRAM_SECRET }),
// Keycloak({ clientId: process.env.KEYCLOAK_ID, clientSecret: process.env.KEYCLOAK_SECRET, issuer: process.env.KEYCLOAK_ISSUER }),
Line({ clientId: process.env.LINE_ID, clientSecret: process.env.LINE_SECRET }),
LinkedIn({ clientId: process.env.LINKEDIN_ID, clientSecret: process.env.LINKEDIN_SECRET }),
Mailchimp({ clientId: process.env.MAILCHIMP_ID, clientSecret: process.env.MAILCHIMP_SECRET }),
Notion({ clientId: process.env.NOTION_ID, clientSecret: process.env.NOTION_SECRET, redirectUri: process.env.NOTION_REDIRECT_URI as string }),
// Okta({ clientId: process.env.OKTA_ID, clientSecret: process.env.OKTA_SECRET, issuer: process.env.OKTA_ISSUER }),
Osu({ clientId: process.env.OSU_CLIENT_ID, clientSecret: process.env.OSU_CLIENT_SECRET }),
Patreon({ clientId: process.env.PATREON_ID, clientSecret: process.env.PATREON_SECRET }),
Slack({ clientId: process.env.SLACK_ID, clientSecret: process.env.SLACK_SECRET }),
Spotify({ clientId: process.env.SPOTIFY_ID, clientSecret: process.env.SPOTIFY_SECRET }),
Trakt({ clientId: process.env.TRAKT_ID, clientSecret: process.env.TRAKT_SECRET }),
Twitch({ clientId: process.env.TWITCH_ID, clientSecret: process.env.TWITCH_SECRET }),
Twitter({ clientId: process.env.TWITTER_ID, clientSecret: process.env.TWITTER_SECRET }),
// TwitterLegacy({ clientId: process.env.TWITTER_LEGACY_ID, clientSecret: process.env.TWITTER_LEGACY_SECRET }),
Yandex({ clientId: process.env.YANDEX_ID, clientSecret: process.env.YANDEX_SECRET }),
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_SECRET }),
ClickUp({ clientId: process.env.CLICK_UP_ID, clientSecret: process.env.CLICK_UP_SECRET })
],
// debug: process.env.NODE_ENV !== "production",
}
if (authConfig.adapter) {
// TODO:
// authOptions.providers.unshift(
// // NOTE: You can start a fake e-mail server with `pnpm email`
// // and then go to `http://localhost:1080` in the browser
// Email({ server: "smtp://127.0.0.1:1025?tls.rejectUnauthorized=false" })
// )
}
// TODO: move to next-auth/edge
function AuthHandler(...args: any[]) {
const envSecret = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET
const envTrustHost = !!(process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL ?? process.env.NODE_ENV !== "production")
if (args.length === 1) {
return async (req: Request) => {
args[0].secret ??= envSecret
args[0].trustHost ??= envTrustHost
return Auth(req, args[0])
}
}
args[1].secret ??= envSecret
args[1].trustHost ??= envTrustHost
return Auth(args[0], args[1])
}
export default AuthHandler(authConfig)
export const config = { runtime: "edge" }

View File

@@ -1,7 +0,0 @@
// This is an example of how to read a JSON Web Token from an API route
import { getToken } from "next-auth/jwt"
export default async (req, res) => {
const token = await getToken({ req })
res.send(JSON.stringify(token, null, 2))
}

View File

@@ -1,19 +0,0 @@
// This is an example of to protect an API route
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)
if (session) {
res.send({
content:
"This is protected content. You can access this content because you are signed in.",
session,
})
} else {
res.send({
error: "You must be sign in to view the protected content on this page.",
})
}
}

View File

@@ -0,0 +1,13 @@
import type { NextApiHandler } from "next"
import { auth } from "../../../auth"
export default async function handler(...args: Parameters<NextApiHandler>) {
const session = await auth(...args)
const res = args[1]
if (session.user) {
// Do something with the session
return res.json("This is protected content.")
}
res.status(401).json("You must be signed in.")
}

View File

@@ -1,8 +0,0 @@
// This is an example of how to access a session from an API route
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)
res.json(session)
}

View File

@@ -0,0 +1,7 @@
// This is an example of how to access a session from an API route
import { auth } from "auth"
export default async (req, res) => {
const session = await auth(req, res)
res.json(session)
}

View File

@@ -1,30 +0,0 @@
// This is an example of how to query data from Supabase with RLS.
// Learn more about Row Levele Security (RLS): https://supabase.com/docs/guides/auth/row-level-security
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "../auth/[...nextauth]"
import { createClient } from "@supabase/supabase-js"
export default async (req, res) => {
const session = await unstable_getServerSession(req, res, authOptions)
if (!session)
return res.send(JSON.stringify({ error: "No session!" }, null, 2))
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${supabaseAccessToken}`,
},
},
}
)
// Now you can query with RLS enabled.
const { data, error } = await supabase.from("users").select("*")
res.send(JSON.stringify({ supabaseAccessToken, data, error }, null, 2))
}

View File

@@ -1,8 +1,6 @@
import Layout from "../components/layout"
export default function Page() { export default function Page() {
return ( return (
<Layout> <>
<h1>Client Side Rendering</h1> <h1>Client Side Rendering</h1>
<p> <p>
This page uses the <strong>useSession()</strong> React Hook in the{" "} This page uses the <strong>useSession()</strong> React Hook in the{" "}
@@ -22,6 +20,6 @@ export default function Page() {
The disadvantage of <strong>useSession()</strong> is that it requires The disadvantage of <strong>useSession()</strong> is that it requires
client side JavaScript. client side JavaScript.
</p> </p>
</Layout> </>
) )
} }

View File

@@ -1,67 +0,0 @@
// eslint-disable-next-line no-use-before-define
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
export default function Page() {
const [response, setResponse] = React.useState(null)
const handleLogin = (options) => async () => {
if (options.redirect) {
return signIn("credentials", options)
}
const response = await signIn("credentials", options)
setResponse(response)
}
const handleLogout = (options) => async () => {
if (options.redirect) {
return signOut(options)
}
const response = await signOut(options)
setResponse(response)
}
const { data: session } = useSession()
if (session) {
return (
<Layout>
<h1>Test different flows for Credentials logout</h1>
<span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button>
<br />
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}
return (
<Layout>
<h1>Test different flows for Credentials login</h1>
<span className="spacing">Default:</span>
<button onClick={handleLogin({ redirect: true, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogin({ redirect: false, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect, wrong password:</span>
<button onClick={handleLogin({ redirect: false, password: "" })}>
Login
</button>
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}

View File

@@ -0,0 +1,67 @@
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import { SignInResponse, SignOutResponse } from "next-auth/react"
export default function Page() {
const [response, setResponse] = React.useState<
SignInResponse | SignOutResponse
>()
const { data: session } = useSession()
if (session) {
return (
<>
<h1>Test different flows for Credentials logout</h1>
<span className="spacing">Default: </span>
<button onClick={() => signOut()}>Logout</button>
<br />
<span className="spacing">No redirect: </span>
<button onClick={() => signOut({ redirect: false }).then(setResponse)}>
Logout
</button>
<br />
<p>{response ? "Response:" : "Session:"}</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response ?? session, null, 2)}
</pre>
</>
)
}
return (
<>
<h1>Test different flows for Credentials login</h1>
<span className="spacing">Default: </span>
<button onClick={() => signIn("credentials", { password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect: </span>
<button
onClick={() =>
signIn("credentials", { redirect: false, password: "password" }).then(
setResponse
)
}
>
Login
</button>
<br />
<span className="spacing">No redirect, wrong password: </span>
<button
onClick={() =>
signIn("credentials", { redirect: false, password: "wrong" }).then(
setResponse
)
}
>
Login
</button>
<p>Response:</p>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</>
)
}

View File

@@ -1,10 +1,10 @@
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
import * as React from "react" import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react" import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
export default function Page() { export default function Page() {
const [response, setResponse] = React.useState(null) const [response, setResponse] =
React.useState<Awaited<ReturnType<typeof signIn>>>()
const [email, setEmail] = React.useState("") const [email, setEmail] = React.useState("")
const handleChange = (event) => { const handleChange = (event) => {
@@ -33,7 +33,7 @@ export default function Page() {
if (session) { if (session) {
return ( return (
<Layout> <>
<h1>Test different flows for Email logout</h1> <h1>Test different flows for Email logout</h1>
<span className="spacing">Default:</span> <span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button> <button onClick={handleLogout({ redirect: true })}>Logout</button>
@@ -45,12 +45,12 @@ export default function Page() {
<pre style={{ background: "#eee", padding: 16 }}> <pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)} {JSON.stringify(response, null, 2)}
</pre> </pre>
</Layout> </>
) )
} }
return ( return (
<Layout> <>
<h1>Test different flows for Email login</h1> <h1>Test different flows for Email login</h1>
<label className="spacing"> <label className="spacing">
Email address:{" "} Email address:{" "}
@@ -75,6 +75,6 @@ export default function Page() {
<pre style={{ background: "#eee", padding: 16 }}> <pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)} {JSON.stringify(response, null, 2)}
</pre> </pre>
</Layout> </>
) )
} }

View File

@@ -1,13 +0,0 @@
import Layout from "components/layout"
export default function Page() {
return (
<Layout>
<h1>NextAuth.js Example</h1>
<p>
This is an example site to demonstrate how to use{" "}
<a href="https://authjs.dev">NextAuth.js</a> for authentication.
</p>
</Layout>
)
}

View File

@@ -1,9 +0,0 @@
import Layout from "components/layout"
export default function Page() {
return (
<Layout>
<h1>Page protected by Middleware</h1>
</Layout>
)
}

View File

@@ -1,8 +1,6 @@
import Layout from "../components/layout"
export default function Page() { export default function Page() {
return ( return (
<Layout> <>
<p> <p>
This is an example site to demonstrate how to use{" "} This is an example site to demonstrate how to use{" "}
<a href="https://authjs.dev">Auth.js</a> for authentication. <a href="https://authjs.dev">Auth.js</a> for authentication.
@@ -18,15 +16,11 @@ export default function Page() {
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</p> </p>
<h2>Privacy Policy</h2> <h2>Privacy Policy</h2>
<p>
This site uses JSON Web Tokens and an in-memory database which resets
every ~2 hours.
</p>
<p> <p>
Data provided to this site is exclusively used to support signing in and 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 is not passed to any third party services, other than via SMTP or OAuth
for the purposes of authentication. for the purposes of authentication.
</p> </p>
</Layout> </>
) )
} }

View File

@@ -1,52 +0,0 @@
// This is an example of how to protect content using server rendering
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function Page({ content, session }) {
// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p>
<strong>{content}</strong>
</p>
</Layout>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
)
let content = null
if (session) {
const hostname = process.env.NEXTAUTH_URL || "http://localhost:3000"
const options = { headers: { cookie: context.req.headers.cookie } }
const res = await fetch(`${hostname}/api/examples/protected`, options)
const json = await res.json()
if (json.content) {
content = json.content
}
}
return {
props: {
session,
content,
},
}
}

View File

@@ -0,0 +1,39 @@
// This is an example of how to protect content using server rendering
import { auth } from "../auth"
import AccessDenied from "components/access-denied"
import { GetServerSideProps } from "next"
export default function Page({ content, session }) {
// If no session exists, display access denied message
if (!session) return <AccessDenied />
// If session exists, display content
return (
<>
<h1>Protected Page</h1>
<p>
<strong>{content}</strong>
</p>
</>
)
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await auth(context)
if (session) {
// Note usually you don't need to fetch from an API route in getServerSideProps
// This is done here to demonstrate how you can fetch from a third-party API
// with a valid session. Likely you would also not pass cookies but an `Authorization` header
const hostname =
process.env.NEXTAUTH_URL ??
(process.env.VERCEL
? "https://next-auth-example-v5.vercel.app"
: "http://localhost:3000")
const res = await fetch(`${hostname}/api/examples/protected`, {
headers: { cookie: context.req.headers.cookie ?? "" },
})
return { props: { session, content: await res.json() } }
}
return { props: {} }
}

View File

@@ -1,11 +1,8 @@
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import Layout from "../components/layout"
export default function Page() { export default function Page() {
const { status } = useSession({ const { status } = useSession({ required: true })
required: true,
})
const [content, setContent] = useState() const [content, setContent] = useState()
// Fetch content from protected route // Fetch content from protected route
@@ -21,15 +18,15 @@ export default function Page() {
fetchData() fetchData()
}, [status]) }, [status])
if (status === "loading") return <Layout>Loading...</Layout> if (status === "loading") return "Loading..."
// If session exists, display content // If session exists, display content
return ( return (
<Layout> <>
<h1>Protected Page</h1> <h1>Protected Page</h1>
<p> <p>
<strong>{content}</strong> <strong>{content}</strong>
</p> </p>
</Layout> </>
) )
} }

View File

@@ -1,50 +0,0 @@
import { unstable_getServerSession } from "next-auth/next"
import Layout from "../components/layout"
import { authOptions } from "./api/auth/[...nextauth]"
export default function Page() {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
// This is possible because of the shared context configured in `_app.js` that
// is used by `useSession()`.
return (
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the <strong>unstable_getServerSession()</strong> method
in <strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>unstable_getServerSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is currently the recommended
approach, although the API may still change, if you need to support
Server Side Rendering with authentication.
</p>
<p>
Using <strong>getSession()</strong> is still recommended on the client.
</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>
</Layout>
)
}
// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps(context) {
return {
props: {
session: await unstable_getServerSession(
context.req,
context.res,
authOptions
),
},
}
}

View File

@@ -1,48 +0,0 @@
import Layout from "../components/layout"
import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import { createClient } from "@supabase/supabase-js"
export default function Page() {
const { data: session } = useSession()
const [data, setData] = useState(null)
useEffect(() => {
if (session) {
// User is logged in, let's fetch their data.
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: { Authorization: `Bearer ${supabaseAccessToken}` },
},
}
)
// Fetch data with RLS enabled.
supabase
.from("users")
.select("*")
.then(({ data }) => setData(data))
}
}, [session])
return (
<Layout>
<h1>Fetch Data from Supabase with RLS</h1>
<h2>Client-side data fetching with RLS:</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
<h2>API Example</h2>
<p>
You can also use Supabase in API routes. See the code in the
`/pages/api/examples/supabase-rls.js` file.
</p>
<p>
<em>You must be signed in to see responses.</em>
</p>
<p>/api/examples/supabase-rls</p>
<iframe src="/api/examples/supabase-rls" />
</Layout>
)
}

View File

@@ -1,68 +0,0 @@
// This is an example of how to protect content using server rendering
// and fetching data from Supabase with RLS enabled.
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import { createClient } from "@supabase/supabase-js"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function Page({ data, session }) {
// If no session exists, display access denied message
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p>Data fetched during SSR from Supabase with RSL enabled:</p>
<pre>{JSON.stringify(data, null, 2)}</pre>
</Layout>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(
context.req,
context.res,
authOptions
)
if (!session)
return {
props: {
session,
data: null,
error: "No session",
},
}
const { supabaseAccessToken } = session
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
{
global: {
headers: {
Authorization: `Bearer ${supabaseAccessToken}`,
},
},
}
)
// Now you can query with RLS enabled.
const { data, error } = await supabase.from("users").select("*")
return {
props: {
session,
data,
error,
},
}
}

View File

@@ -0,0 +1,56 @@
-- CreateTable
CREATE TABLE "Account" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"providerAccountId" TEXT NOT NULL,
"refresh_token" TEXT,
"access_token" TEXT,
"expires_at" INTEGER,
"token_type" TEXT,
"scope" TEXT,
"id_token" TEXT,
"session_state" TEXT,
CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL PRIMARY KEY,
"sessionToken" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expires" DATETIME NOT NULL,
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT,
"email" TEXT,
"emailVerified" DATETIME,
"image" TEXT
);
-- CreateTable
CREATE TABLE "VerificationToken" (
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" DATETIME NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-- CreateIndex
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
-- CreateIndex
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

View File

@@ -8,24 +8,19 @@ generator client {
} }
model Account { model Account {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
type String type String
provider String provider String
providerAccountId String providerAccountId String
refresh_token String? refresh_token String?
access_token String? access_token String?
expires_at Int? expires_at Int?
token_type String? token_type String?
scope String? scope String?
id_token String? id_token String?
session_state String? session_state String?
oauth_token_secret String? user User @relation(fields: [userId], references: [id])
oauth_token String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@unique([provider, providerAccountId]) @@unique([provider, providerAccountId])
} }

View File

@@ -1,20 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import NextAuth from "next-auth"
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
// A JWT which can be used as Authorization header with supabase-js for RLS.
supabaseAccessToken?: string
user: {
/** The user's postal address. */
address: string
} & User
}
interface User {
foo: string
}
}

View File

@@ -1,22 +1,17 @@
NEXTAUTH_URL=http://localhost:3000 AUTH_SECRET= # Linux: `openssl rand -hex 32` or go to https://generate-secret.vercel.app/32
NEXTAUTH_SECRET= # Linux: `openssl rand -hex 32` or go to https://generate-secret.now.sh/32
AUTH_AUTH0_ID=
AUTH_AUTH0_SECRET=
AUTH_AUTH0_ISSUER=
AUTH0_ID= AUTH_FACEBOOK_ID=
AUTH0_SECRET= AUTH_FACEBOOK_SECRET=
AUTH0_ISSUER=
DESCOPE_ID= AUTH_GITHUB_ID=
DESCOPE_SECRET= AUTH_GITHUB_SECRET=
FACEBOOK_ID= AUTH_GOOGLE_ID=
FACEBOOK_SECRET= AUTH_GOOGLE_SECRET=
GITHUB_ID= AUTH_TWITTER_ID=
GITHUB_SECRET= AUTH_TWITTER_SECRET=
GOOGLE_ID=
GOOGLE_SECRET=
TWITTER_ID=
TWITTER_SECRET=

View File

@@ -2,41 +2,42 @@
<p align="center"> <p align="center">
<br/> <br/>
<a href="https://authjs.dev" target="_blank"> <a href="https://authjs.dev" target="_blank"><img width="150px" src="https://authjs.dev/img/logo/logo-sm.png" /></a>
<img height="64" src="https://authjs.dev/img/logo/logo-sm.png" /> <h3 align="center">NextAuth.js Example App</h3>
</a>
<a href="https://nextjs.org" target="_blank">
<img height="64" src="https://nextjs.org/static/favicon/android-chrome-192x192.png" />
</a>
<h3 align="center"><b>NextAuth.js</b> - Example App</h3>
<p align="center"> <p align="center">
Open Source. Full Stack. Own Your Data. Open Source. Full Stack. Own Your Data.
</p> </p>
<p align="center" style="align: center;"> <p align="center" style="align: center;">
<a href="https://npm.im/next-auth"> <a href="https://npm.im/next-auth">
<img alt="npm" src="https://img.shields.io/npm/v/next-auth?color=green&label=next-auth&style=flat-square"> <img alt="npm" src="https://img.shields.io/npm/v/next-auth?color=green&label=next-auth">
</a> </a>
<a href="https://bundlephobia.com/result?p=next-auth-example"> <a href="https://bundlephobia.com/result?p=next-auth-example">
<img src="https://img.shields.io/bundlephobia/minzip/next-auth?label=size&style=flat-square" alt="Bundle Size"/> <img src="https://img.shields.io/bundlephobia/minzip/next-auth?label=next-auth" alt="Bundle Size"/>
</a> </a>
<a href="https://www.npmtrends.com/next-auth"> <a href="https://www.npmtrends.com/next-auth">
<img src="https://img.shields.io/npm/dm/next-auth?label=downloads&style=flat-square" alt="Downloads" /> <img src="https://img.shields.io/npm/dm/next-auth?label=next-auth%20downloads" alt="Downloads" />
</a> </a>
<a href="https://npm.im/next-auth"> <a href="https://npm.im/next-auth">
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" /> <img src="https://img.shields.io/badge/npm-TypeScript-blue" alt="TypeScript" />
</a> </a>
</p> </p>
</p> </p>
## Overview ## Overview
NextAuth.js is a complete open-source authentication solution. NextAuth.js is a complete open source authentication solution.
This is an example application that shows how `next-auth` is applied to a basic Next.js app. This is an example application that shows how `next-auth` is applied to a basic Next.js app.
The deployed version can be found at [`next-auth-example.vercel.app`](https://next-auth-example.vercel.app) The deployed version can be found at [`next-auth-example.vercel.app`](https://next-auth-example.vercel.app)
Go to [next-auth.js.org](https://next-auth.js.org) for more information and documentation. ### About NextAuth.js
NextAuth.js is an easy to implement, full-stack (client/server) open source authentication library originally designed for [Next.js](https://nextjs.org) and [Serverless](https://vercel.com). Our goal is to [support even more frameworks](https://github.com/nextauthjs/next-auth/issues/2294) in the future.
Go to [next-auth.js.org](https://authjs.dev) for more information and documentation.
> _NextAuth.js is not officially associated with Vercel or Next.js._
## Getting Started ## Getting Started
@@ -66,19 +67,19 @@ You **can** skip configuring a database and come back to it later if you want.
For more information about setting up a database, please check out the following links: For more information about setting up a database, please check out the following links:
- Docs: [next-auth.js.org/adapters/overview](https://next-auth.js.org/adapters/overview) - Docs: [authjs.dev/reference/adapters](https://authjs.dev/reference/core/adapters)
### 3. Configure Authentication Providers ### 3. Configure Authentication Providers
1. Review and update options in `pages/api/auth/[...nextauth].js` as needed. 1. Review and update options in `auth.ts` as needed.
2. When setting up OAuth, in the developer admin page for each of your OAuth services, you should configure the callback URL to use a callback path of `{server}/api/auth/callback/{provider}`. 2. When setting up OAuth, in the developer admin page for each of your OAuth services, you should configure the callback URL to use a callback path of `{server}/api/auth/callback/{provider}`.
e.g. For Google OAuth you would use: `http://localhost:3000/api/auth/callback/google` e.g. For Google OAuth you would use: `http://localhost:3000/api/auth/callback/google`
A list of configured providers and their callback URLs is available from the endpoint `/api/auth/providers`. You can find more information at https://next-auth.js.org/configuration/providers/oauth A list of configured providers and their callback URLs is available from the endpoint `api/auth/providers`. You can find more information at https://authjs.dev/getting-started/oauth-tutorial
3. You can also choose to specify an SMTP server for passwordless sign in via email. 1. You can also choose to specify an SMTP server for passwordless sign in via email.
### 4. Start the application ### 4. Start the application
@@ -97,13 +98,15 @@ npm run start
### 5. Preparing for Production ### 5. Preparing for Production
Follow the [Deployment documentation](https://authjs.dev/guides/basics/deployment) or deploy the example instantly using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-auth-example) Follow the [Deployment documentation](https://authjs.dev/guides/basics/deployment)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/nextauthjs/next-auth-example&project-name=next-auth-example&repository-name=next-auth-example)
## Acknowledgements ## Acknowledgements
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss"> <a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/docs/static/img/powered-by-vercel.svg" alt="Powered By Vercel" /> <img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/docs/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
</a> </a>
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire Auth.js Team</p> <p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
## License
ISC

View File

@@ -0,0 +1,34 @@
"use client"
import CustomLink from "@/components/custom-link"
import { useEffect, useState } from "react"
export default function Page() {
const [data, setData] = useState()
useEffect(() => {
;(async () => {
const res = await fetch("/api/protected")
const json = await res.json()
setData(json)
})()
}, [])
return (
<div className="space-y-2">
<h1 className="text-3xl font-bold">Route Handler Usage</h1>
<p>
This page fetches data from an API{" "}
<CustomLink href="https://nextjs.org/docs/app/building-your-application/routing/route-handlers">
Route Handler
</CustomLink>
. The API is protected using the universal{" "}
<CustomLink href="https://nextjs.authjs.dev#auth">
<code>auth()</code>
</CustomLink>{" "}
method.
</p>
<h2 className="text-xl font-bold">Data from API Route:</h2>
<pre>
<code>{JSON.stringify(data, null, 2)}</code>
</pre>
</div>
)
}

View File

@@ -1,5 +1,2 @@
import NextAuth from "next-auth/next" import { handlers } from "auth"
import { config } from "auth" export const { GET, POST } = handlers
const handler = NextAuth(config)
export { handler as GET, handler as POST }

View File

@@ -0,0 +1,9 @@
import { auth } from "auth"
export const GET = auth((req) => {
if (req.auth) {
return Response.json({ data: "Protected data" })
}
return Response.json({ message: "Not authenticated" }, { status: 401 })
})

View File

@@ -0,0 +1,20 @@
import { auth } from "auth"
import ClientExample from "@/components/client-example"
import { SessionProvider } from "next-auth/react"
export default async function ClientPage() {
const session = await auth()
if (session?.user) {
session.user = {
name: session.user.name,
email: session.user.email,
picture: session.user.picture,
} // filter out sensitive data
}
return (
<SessionProvider session={session}>
<ClientExample />
</SessionProvider>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,29 @@
import "./globals.css"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import Footer from "@/components/footer"
import Header from "@/components/header"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "NextAuth.js Example",
description:
"This is an example site to demonstrate how to use NextAuth.js for authentication",
}
export default function RootLayout({ children }: React.PropsWithChildren) {
return (
<html lang="en">
<body className={inter.className}>
<div className="flex flex-col justify-between w-full h-full min-h-screen">
<Header />
<main className="flex-auto w-full max-w-3xl px-4 py-4 mx-auto sm:px-6 md:py-6">
{children}
</main>
<Footer />
</div>
</body>
</html>
)
}

View File

@@ -0,0 +1,20 @@
import CustomLink from "@/components/custom-link"
export default function Page() {
return (
<div className="space-y-2">
<h1 className="text-3xl font-bold">Middleware usage</h1>
<p>
This page is protected by using the universal{" "}
<CustomLink href="https://nextjs.authjs.dev#auth">
<code>auth()</code>
</CustomLink>{" "}
method in{" "}
<CustomLink href="https://nextjs.org/docs/app/building-your-application/routing/middleware">
Next.js Middleware
</CustomLink>
.
</p>
</div>
)
}

View File

@@ -0,0 +1,28 @@
import CustomLink from "@/components/custom-link"
import packageJSON from "../package.json"
export default function Index() {
return (
<div className="space-y-2">
<h1 className="text-3xl font-bold">NextAuth.js Example</h1>
<p>
This is an example site to demonstrate how to use{" "}
<CustomLink href="https://nextjs.authjs.dev">NextAuth.js</CustomLink>{" "}
for authentication. Check out the{" "}
<CustomLink href="/server-example" className="underline">
Server
</CustomLink>{" "}
and the{" "}
<CustomLink href="/client-example" className="underline">
Client
</CustomLink>{" "}
examples to see how to secure pages and get session data.
</p>
<p>
Current{" "}
<CustomLink href="https://nextjs.authjs.dev">NextAuth.js</CustomLink>{" "}
version: <em>next-auth@{packageJSON.dependencies["next-auth"]}</em>
</p>
</div>
)
}

View File

@@ -0,0 +1,30 @@
export default function PolicyPage() {
return (
<div className="space-y-2">
<section>
<h2 className="text-xl font-bold">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>
</section>
<section>
<h2 className="text-xl font-bold">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>
</section>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import CustomLink from "@/components/custom-link"
import SessionData from "@/components/session-data"
import { auth } from "auth"
export default async function Page() {
const session = await auth()
return (
<div className="space-y-2">
<h1 className="text-3xl font-bold">React Server Component Usage</h1>
<p>
This page is server-rendered as a{" "}
<CustomLink href="https://nextjs.org/docs/app/building-your-application/rendering/server-components">
React Server Component
</CustomLink>
. It gets the session data on the server using{" "}
<CustomLink href="https://nextjs.authjs.dev#auth">
<code>auth()</code>
</CustomLink>{" "}
method.
</p>
<SessionData session={session} />
</div>
)
}

View File

@@ -1,7 +1,4 @@
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next" import NextAuth from "next-auth"
import type { NextAuthOptions as NextAuthConfig } from "next-auth"
import { getServerSession } from "next-auth"
import Apple from "next-auth/providers/apple" import Apple from "next-auth/providers/apple"
import Atlassian from "next-auth/providers/atlassian" import Atlassian from "next-auth/providers/atlassian"
import Auth0 from "next-auth/providers/auth0" import Auth0 from "next-auth/providers/auth0"
@@ -65,233 +62,82 @@ import Zitadel from "next-auth/providers/zitadel"
import Zoho from "next-auth/providers/zoho" import Zoho from "next-auth/providers/zoho"
import Zoom from "next-auth/providers/zoom" import Zoom from "next-auth/providers/zoom"
// Read more at: https://next-auth.js.org/getting-started/typescript#module-augmentation import type { NextAuthConfig } from "next-auth"
declare module "next-auth/jwt" {
interface JWT {
/** The user's role. */
userRole?: "admin"
}
}
export const config = { export const config = {
theme: { theme: {
logo: "https://next-auth.js.org/img/logo/logo-sm.png", logo: "https://next-auth.js.org/img/logo/logo-sm.png",
}, },
providers: [ providers: [
Apple({ clientId: process.env.AUTH_APPLE_ID, clientSecret: process.env.AUTH_APPLE_SECRET }), Apple,
Atlassian({ clientId: process.env.AUTH_ATLASSIAN_ID, clientSecret: process.env.AUTH_ATLASSIAN_SECRET }), Atlassian,
Auth0({ clientId: process.env.AUTH_AUTH0_ID, clientSecret: process.env.AUTH_AUTH0_SECRET, issuer: process.env.AUTH_AUTH0_ISSUER }), Auth0,
Authentik({ clientId: process.env.AUTH_AUTHENTIK_ID, clientSecret: process.env.AUTH_AUTHENTIK_SECRET }), Authentik,
AzureAD({ clientId: process.env.AUTH_AZUREAD_ID, clientSecret: process.env.AUTH_AZUREAD_SECRET }), AzureAD,
AzureB2C({ clientId: process.env.AUTH_AZUREB2C_ID, clientSecret: process.env.AUTH_AZUREB2C_SECRET }), AzureB2C,
Battlenet({ clientId: process.env.AUTH_BN_ID, clientSecret: process.env.AUTH_BN_SECRET, issuer: process.env.AUTH_BN_ISSUER }), Battlenet,
Box({ clientId: process.env.AUTH_BOX_ID, clientSecret: process.env.AUTH_BOX_SECRET }), Box,
BoxyHQSAML({ clientId: process.env.AUTH_BOXYHQ_ID, clientSecret: process.env.AUTH_BOXYHQ_SECRET, issuer: process.env.AUTH_BOXYHQ_ISSUER }), BoxyHQSAML,
Bungie({ clientId: process.env.AUTH_BUNGIE_ID, clientSecret: process.env.AUTH_BUNGIE_SECRET }), Bungie,
Cognito({ clientId: process.env.AUTH_COGNITO_ID, clientSecret: process.env.AUTH_COGNITO_SECRET, issuer: process.env.AUTH_COGNITO_ISSUER }), Cognito,
Coinbase({ clientId: process.env.AUTH_COINBASE_ID, clientSecret: process.env.AUTH_COINBASE_SECRET }), Coinbase,
Discord({ clientId: process.env.AUTH_DISCORD_ID, clientSecret: process.env.AUTH_DISCORD_SECRET }), Discord,
Dropbox({ clientId: process.env.AUTH_DROPBOX_ID, clientSecret: process.env.AUTH_DROPBOX_SECRET }), Dropbox,
DuendeIDS6({ clientId: process.env.AUTH_DUENDEIDS6_ID, clientSecret: process.env.AUTH_DUENDEIDS6_SECRET }), DuendeIDS6,
Eveonline({ clientId: process.env.AUTH_EVEONLINE_ID, clientSecret: process.env.AUTH_EVEONLINE_SECRET }), Eveonline,
Facebook({ clientId: process.env.AUTH_FACEBOOK_ID, clientSecret: process.env.AUTH_FACEBOOK_SECRET }), Facebook,
Faceit({ clientId: process.env.AUTH_FACEIT_ID, clientSecret: process.env.AUTH_FACEIT_SECRET }), Faceit,
FortyTwoSchool({ clientId: process.env.AUTH_FORTYTWOSCHOOL_ID, clientSecret: process.env.AUTH_FORTYTWOSCHOOL_SECRET }), FortyTwoSchool,
Foursquare({ clientId: process.env.AUTH_FOURSQUARE_ID, clientSecret: process.env.AUTH_FOURSQUARE_SECRET }), Foursquare,
Freshbooks({ clientId: process.env.AUTH_FRESHBOOKS_ID, clientSecret: process.env.AUTH_FRESHBOOKS_SECRET }), Freshbooks,
Fusionauth({ clientId: process.env.AUTH_FUSIONAUTH_ID, clientSecret: process.env.AUTH_FUSIONAUTH_SECRET }), Fusionauth,
GitHub({ clientId: process.env.AUTH_GITHUB_ID, clientSecret: process.env.AUTH_GITHUB_SECRET }), GitHub,
Gitlab({ clientId: process.env.AUTH_GITLAB_ID, clientSecret: process.env.AUTH_GITLAB_SECRET }), Gitlab,
Google({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET }), Google,
Hubspot({ clientId: process.env.AUTH_HUBSPOT_ID, clientSecret: process.env.AUTH_HUBSPOT_SECRET }), Hubspot,
Instagram({ clientId: process.env.AUTH_INSTAGRAM_ID, clientSecret: process.env.AUTH_INSTAGRAM_SECRET }), Instagram,
Kakao({ clientId: process.env.AUTH_KAKAO_ID, clientSecret: process.env.AUTH_KAKAO_SECRET }), Kakao,
Keycloak({ clientId: process.env.AUTH_KEYCLOAK_ID, clientSecret: process.env.AUTH_KEYCLOAK_SECRET }), Keycloak,
Line({ clientId: process.env.AUTH_LINE_ID, clientSecret: process.env.AUTH_LINE_SECRET }), Line,
LinkedIn({ clientId: process.env.AUTH_LINKEDIN_ID, clientSecret: process.env.AUTH_LINKEDIN_SECRET }), LinkedIn,
Mailchimp({ clientId: process.env.AUTH_MAILCHIMP_ID, clientSecret: process.env.AUTH_MAILCHIMP_SECRET }), Mailchimp,
Mailru({ clientId: process.env.AUTH_MAILRU_ID, clientSecret: process.env.AUTH_MAILRU_SECRET }), Mailru,
Medium({ clientId: process.env.AUTH_MEDIUM_ID, clientSecret: process.env.AUTH_MEDIUM_SECRET }), Medium,
Naver({ clientId: process.env.AUTH_NAVER_ID, clientSecret: process.env.AUTH_NAVER_SECRET }), Naver,
Netlify({ clientId: process.env.AUTH_NETLIFY_ID, clientSecret: process.env.AUTH_NETLIFY_SECRET }), Netlify,
Okta({ clientId: process.env.AUTH_OKTA_ID, clientSecret: process.env.AUTH_OKTA_SECRET }), Okta,
Onelogin({ clientId: process.env.AUTH_ONELOGIN_ID, clientSecret: process.env.AUTH_ONELOGIN_SECRET }), Onelogin,
Osso({ clientId: process.env.AUTH_OSSO_ID, clientSecret: process.env.AUTH_OSSO_SECRET, issuer: process.env.AUTH_OSSO_ISSUER }), Osso,
Osu({ clientId: process.env.AUTH_OSU_ID, clientSecret: process.env.AUTH_OSU_SECRET }), Osu,
Passage({ clientId: process.env.AUTH_PASSAGE_ID, clientSecret: process.env.AUTH_PASSAGE_SECRET, issuer: process.env.AUTH_PASSAGE_ISSUER }), Passage,
Patreon({ clientId: process.env.AUTH_PATREON_ID, clientSecret: process.env.AUTH_PATREON_SECRET }), Patreon,
Pinterest({ clientId: process.env.AUTH_PINTEREST_ID, clientSecret: process.env.AUTH_PINTEREST_SECRET }), Pinterest,
Pipedrive({ clientId: process.env.AUTH_PIPEDRIVE_ID, clientSecret: process.env.AUTH_PIPEDRIVE_SECRET }), Pipedrive,
Reddit({ clientId: process.env.AUTH_REDDIT_ID, clientSecret: process.env.AUTH_REDDIT_SECRET }), Reddit,
Salesforce({ clientId: process.env.AUTH_SALESFORCE_ID, clientSecret: process.env.AUTH_SALESFORCE_SECRET }), Salesforce,
Slack({ clientId: process.env.AUTH_SLACK_ID, clientSecret: process.env.AUTH_SLACK_SECRET }), Slack,
Spotify({ clientId: process.env.AUTH_SPOTIFY_ID, clientSecret: process.env.AUTH_SPOTIFY_SECRET }), Spotify,
Strava({ clientId: process.env.AUTH_STRAVA_ID, clientSecret: process.env.AUTH_STRAVA_SECRET }), Strava,
Todoist({ clientId: process.env.AUTH_TODOIST_ID, clientSecret: process.env.AUTH_TODOIST_SECRET }), Todoist,
Trakt({ clientId: process.env.AUTH_TRAKT_ID, clientSecret: process.env.AUTH_TRAKT_SECRET }), Trakt,
Twitch({ clientId: process.env.AUTH_TWITCH_ID, clientSecret: process.env.AUTH_TWITCH_SECRET }), Twitch,
Twitter({ clientId: process.env.AUTH_TWITTER_ID, clientSecret: process.env.AUTH_TWITTER_SECRET, version: "2.0" }), Twitter,
UnitedEffects({ clientId: process.env.AUTH_UE_ID, clientSecret: process.env.AUTH_UE_SECRET, issuer: process.env.AUTH_UE_ISSUER }), UnitedEffects,
Vk({ clientId: process.env.AUTH_VK_ID, clientSecret: process.env.AUTH_VK_SECRET }), Vk,
Wikimedia({ clientId: process.env.AUTH_WIKIMEDIA_ID, clientSecret: process.env.AUTH_WIKIMEDIA_SECRET }), Wikimedia,
Wordpress({ clientId: process.env.AUTH_WORDPRESS_ID, clientSecret: process.env.AUTH_WORDPRESS_SECRET }), Wordpress,
WorkOS({ clientId: process.env.AUTH_WORKOS_ID, clientSecret: process.env.AUTH_WORKOS_SECRET }), WorkOS,
Yandex({ clientId: process.env.AUTH_YANDEX_ID, clientSecret: process.env.AUTH_YANDEX_SECRET }), Yandex,
Zitadel({ clientId: process.env.AUTH_ZITADEL_ID, clientSecret: process.env.AUTH_ZITADEL_SECRET }), Zitadel,
Zoho({ clientId: process.env.AUTH_ZOHO_ID, clientSecret: process.env.AUTH_ZOHO_SECRET }), Zoho,
Zoom({ clientId: process.env.AUTH_ZOOM_ID, clientSecret: process.env.AUTH_ZOOM_SECRET }), Zoom,
], ],
callbacks: { callbacks: {
async jwt({ token }) { authorized({ request, auth }) {
token.userRole = "admin" const { pathname } = request.nextUrl
return token return pathname === "/middleware-example" && !!auth
}, },
}, },
} satisfies NextAuthConfig } satisfies NextAuthConfig
// Helper function to get session without passing config every time export const { handlers, auth, signIn, signOut } = NextAuth(config)
// https://next-auth.js.org/configuration/nextjs#getserversession
export function auth(...args: [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]] | [NextApiRequest, NextApiResponse] | []) {
return getServerSession(...args, config)
}
// We recommend doing your own environment variable validation
declare global {
namespace NodeJS {
export interface ProcessEnv {
NEXTAUTH_SECRET: string
AUTH_APPLE_ID: string
AUTH_APPLE_SECRET: string
AUTH_ATLASSIAN_ID: string
AUTH_ATLASSIAN_SECRET: string
AUTH_AUTH0_ID: string
AUTH_AUTH0_ISSUER: string
AUTH_AUTH0_SECRET: string
AUTH_AUTHENTIK_ID: string
AUTH_AUTHENTIK_SECRET: string
AUTH_AZUREAD_ID: string
AUTH_AZUREAD_SECRET: string
AUTH_AZUREB2C_ID: string
AUTH_AZUREB2C_SECRET: string
AUTH_BN_ID: string
AUTH_BN_ISSUER: any
AUTH_BN_SECRET: string
AUTH_BOX_ID: string
AUTH_BOX_SECRET: string
AUTH_BOXYHQ_ID: string
AUTH_BOXYHQ_ISSUER: string
AUTH_BOXYHQ_SECRET: string
AUTH_BUNGIE_ID: string
AUTH_BUNGIE_SECRET: string
AUTH_COGNITO_ID: string
AUTH_COGNITO_ISSUER: string
AUTH_COGNITO_SECRET: string
AUTH_COINBASE_ID: string
AUTH_COINBASE_SECRET: string
AUTH_DISCORD_ID: string
AUTH_DISCORD_SECRET: string
AUTH_DROPBOX_ID: string
AUTH_DROPBOX_SECRET: string
AUTH_DUENDEIDS6_ID: string
AUTH_DUENDEIDS6_SECRET: string
AUTH_EVEONLINE_ID: string
AUTH_EVEONLINE_SECRET: string
AUTH_FACEBOOK_ID: string
AUTH_FACEBOOK_SECRET: string
AUTH_FACEIT_ID: string
AUTH_FACEIT_SECRET: string
AUTH_FORTYTWOSCHOOL_ID: string
AUTH_FORTYTWOSCHOOL_SECRET: string
AUTH_FOURSQUARE_ID: string
AUTH_FOURSQUARE_SECRET: string
AUTH_FRESHBOOKS_ID: string
AUTH_FRESHBOOKS_SECRET: string
AUTH_FUSIONAUTH_ID: string
AUTH_FUSIONAUTH_SECRET: string
AUTH_GITHUB_ID: string
AUTH_GITHUB_SECRET: string
AUTH_GITLAB_ID: string
AUTH_GITLAB_SECRET: string
AUTH_GOOGLE_ID: string
AUTH_GOOGLE_SECRET: string
AUTH_HUBSPOT_ID: string
AUTH_HUBSPOT_SECRET: string
AUTH_INSTAGRAM_ID: string
AUTH_INSTAGRAM_SECRET: string
AUTH_KAKAO_ID: string
AUTH_KAKAO_SECRET: string
AUTH_KEYCLOAK_ID: string
AUTH_KEYCLOAK_SECRET: string
AUTH_LINE_ID: string
AUTH_LINE_SECRET: string
AUTH_LINKEDIN_ID: string
AUTH_LINKEDIN_SECRET: string
AUTH_MAILCHIMP_ID: string
AUTH_MAILCHIMP_SECRET: string
AUTH_MAILRU_ID: string
AUTH_MAILRU_SECRET: string
AUTH_MEDIUM_ID: string
AUTH_MEDIUM_SECRET: string
AUTH_NAVER_ID: string
AUTH_NAVER_SECRET: string
AUTH_NETLIFY_ID: string
AUTH_NETLIFY_SECRET: string
AUTH_OKTA_ID: string
AUTH_OKTA_SECRET: string
AUTH_ONELOGIN_ID: string
AUTH_ONELOGIN_SECRET: string
AUTH_OSSO_ID: string
AUTH_OSSO_ISSUER: string
AUTH_OSSO_SECRET: string
AUTH_OSU_ID: string
AUTH_OSU_SECRET: string
AUTH_PASSAGE_ID: string
AUTH_PASSAGE_ISSUER: string
AUTH_PASSAGE_SECRET: string
AUTH_PATREON_ID: string
AUTH_PATREON_SECRET: string
AUTH_PINTEREST_ID: string
AUTH_PINTEREST_SECRET: string
AUTH_PIPEDRIVE_ID: string
AUTH_PIPEDRIVE_SECRET: string
AUTH_REDDIT_ID: string
AUTH_REDDIT_SECRET: string
AUTH_SALESFORCE_ID: string
AUTH_SALESFORCE_SECRET: string
AUTH_SLACK_ID: string
AUTH_SLACK_SECRET: string
AUTH_SPOTIFY_ID: string
AUTH_SPOTIFY_SECRET: string
AUTH_STRAVA_ID: string
AUTH_STRAVA_SECRET: string
AUTH_TODOIST_ID: string
AUTH_TODOIST_SECRET: string
AUTH_TRAKT_ID: string
AUTH_TRAKT_SECRET: string
AUTH_TWITCH_ID: string
AUTH_TWITCH_SECRET: string
AUTH_TWITTER_ID: string
AUTH_TWITTER_SECRET: string
AUTH_UE_ID: string
AUTH_UE_ISSUER: string
AUTH_UE_SECRET: string
AUTH_VK_ID: string
AUTH_VK_SECRET: string
AUTH_WIKIMEDIA_ID: string
AUTH_WIKIMEDIA_SECRET: string
AUTH_WORDPRESS_ID: string
AUTH_WORDPRESS_SECRET: string
AUTH_WORKOS_ID: string
AUTH_WORKOS_SECRET: string
AUTH_YANDEX_ID: string
AUTH_YANDEX_SECRET: string
AUTH_ZITADEL_ID: string
AUTH_ZITADEL_SECRET: string
AUTH_ZOHO_ID: string
AUTH_ZOHO_SECRET: string
AUTH_ZOOM_ID: string
AUTH_ZOOM_SECRET: string
}
}
}

View File

@@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

View File

@@ -0,0 +1,23 @@
import { signIn, signOut } from "auth"
import { Button } from "./ui/button"
export function SignIn({
provider,
...props
}: { provider?: string } & React.ComponentPropsWithRef<typeof Button>) {
return (
<form action={signIn(provider)}>
<Button {...props}>Sign In</Button>
</form>
)
}
export function SignOut(props: React.ComponentPropsWithRef<typeof Button>) {
return (
<form action={signOut()} className="w-full">
<Button variant="ghost" className="w-full p-0" {...props}>
Sign Out
</Button>
</form>
)
}

View File

@@ -0,0 +1,81 @@
"use client"
import { useSession } from "next-auth/react"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { useState } from "react"
import SessionData from "./session-data"
import CustomLink from "./custom-link"
const UpdateForm = () => {
const { data: session, update } = useSession()
const [name, setName] = useState(session?.user.name ?? "")
if (!session) return null
return (
<>
<h2 className="text-xl font-bold">Updating the session</h2>
<form
onSubmit={async () => {
if (session) {
const newSession = await update({
...session,
user: { ...session.user, name },
})
console.log({ newSession })
}
}}
className="flex items-center w-full max-w-sm space-x-2"
>
<Input
type="text"
placeholder={session.user.name ?? ""}
value={name}
onChange={(e) => {
setName(e.target.value)
}}
/>
<Button type="submit">Update</Button>
</form>
</>
)
}
export default function ClientExample() {
const { data: session, status } = useSession()
return (
<div className="space-y-2">
<h1 className="text-3xl font-bold">Client Side Rendering Usage</h1>
<p>
This page fetches session data client side using the{" "}
<CustomLink href="https://nextjs.authjs.dev/react#usesession">
<code>useSession</code>
</CustomLink>{" "}
React Hook.
</p>
<p>
It needs the{" "}
<CustomLink href="https://react.dev/reference/react/use-client">
<code>'use client'</code>
</CustomLink>{" "}
directive at the top of the file to enable client side rendering, and
the{" "}
<CustomLink href="https://nextjs.authjs.dev/react#sessionprovider">
<code>SessionProvider</code>
</CustomLink>{" "}
component in{" "}
<strong>
<code>client-example/page.tsx</code>
</strong>{" "}
to provide the session data.
</p>
{status === "loading" ? (
<div>Loading...</div>
) : (
<SessionData session={session} />
)}
<UpdateForm />
</div>
)
}

View File

@@ -0,0 +1,40 @@
import { cn } from "@/lib/utils"
import { ExternalLink } from "lucide-react"
import Link from "next/link"
interface CustomLinkProps extends React.LinkHTMLAttributes<HTMLAnchorElement> {
href: string
}
const CustomLink = ({
href,
children,
className,
...rest
}: CustomLinkProps) => {
const isInternalLink = href.startsWith("/")
const isAnchorLink = href.startsWith("#")
if (isInternalLink || isAnchorLink) {
return (
<Link href={href} className={className} {...rest}>
{children}
</Link>
)
}
return (
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
className={cn("items-center underline", className)}
{...rest}
>
{children}
<ExternalLink className=" ml-0.5 h-4 w-4 inline-block" />
</Link>
)
}
export default CustomLink

View File

@@ -1,14 +0,0 @@
.footer {
margin-top: 2rem;
}
.navItems {
margin-bottom: 1rem;
padding: 0;
list-style: none;
}
.navItem {
display: inline-block;
margin-right: 1rem;
}

View File

@@ -1,28 +1,16 @@
import Link from "next/link" import CustomLink from "./custom-link"
import styles from "./footer.module.css"
import packageJSON from "../package.json"
export default function Footer() { export default function Footer() {
return ( return (
<footer className={styles.footer}> <footer className="flex flex-col w-full px-4 mx-0 my-4 space-y-1 text-sm md:max-w-3xl md:my-12 md:mx-auto sm:px-6 md:h-5 md:items-center md:space-y-0 md:space-x-4 md:flex-row">
<hr /> <CustomLink href="https://nextjs.authjs.dev">Documentation</CustomLink>
<ul className={styles.navItems}> <CustomLink href="https://www.npmjs.com/package/next-auth">
<li className={styles.navItem}> NPM
<a href="https://next-auth.js.org">Documentation</a> </CustomLink>
</li> <CustomLink href="https://github.com/nextauthjs/next-auth/tree/main/apps/examples/nextjs">
<li className={styles.navItem}> Source on GitHub
<a href="https://www.npmjs.com/package/next-auth">NPM</a> </CustomLink>
</li> <CustomLink href="/policy">Policy</CustomLink>
<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>
) )
} }

View File

@@ -1,92 +0,0 @@
/* 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;
}

View File

@@ -1,94 +1,13 @@
import Link from "next/link" import { MainNav } from "./main-nav"
import { signIn, signOut, useSession } from "next-auth/react" import UserButton from "./user-button"
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() { export default function Header() {
const { data: session, status } = useSession()
const loading = status === "loading"
return ( return (
<header> <header className="sticky flex justify-center border-b">
<noscript> <div className="flex items-center justify-between w-full h-16 max-w-3xl px-4 mx-auto sm:px-6">
<style>{`.nojs-show { opacity: 1; top: 0; }`}</style> <MainNav />
</noscript> <UserButton />
<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> </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> </header>
) )
} }

View File

@@ -1,13 +0,0 @@
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 />
</>
)
}

View File

@@ -0,0 +1,83 @@
"use client"
import Image from "next/image"
import { cn } from "@/lib/utils"
import CustomLink from "./custom-link"
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "./ui/navigation-menu"
import React from "react"
import { Button } from "./ui/button"
export function MainNav() {
return (
<div className="flex items-center space-x-2 lg:space-x-6">
<CustomLink href="/">
<Button variant="ghost" className="p-0">
<Image src="/logo.png" alt="Home" width="32" height="32" />
</Button>
</CustomLink>
<NavigationMenu>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Server Side</NavigationMenuTrigger>
<NavigationMenuContent>
<ul className="grid gap-3 p-6 md:w-[400px] lg:w-[500px] lg:grid-cols-[.75fr_1fr]">
<ListItem href="/server-example" title="RSC Example">
Protecting React Server Component.
</ListItem>
<ListItem href="/middleware-example" title="Middleware Example">
Using Middleware to protect pages & APIs.
</ListItem>
<ListItem href="/api-example" title="Route Handler Example">
Getting the session inside an API Route.
</ListItem>
</ul>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
href="/client-example"
className={navigationMenuTriggerStyle()}
>
Client Side
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</div>
)
}
const ListItem = React.forwardRef<
React.ElementRef<"a">,
React.ComponentPropsWithoutRef<"a">
>(({ className, title, children, ...props }, ref) => {
return (
<li>
<NavigationMenuLink asChild>
<a
ref={ref}
className={cn(
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground",
className
)}
{...props}
>
<div className="text-sm font-medium leading-none">{title}</div>
<p className="text-sm leading-snug line-clamp-2 text-muted-foreground">
{children}
</p>
</a>
</NavigationMenuLink>
</li>
)
})
ListItem.displayName = "ListItem"

View File

@@ -0,0 +1,31 @@
import type { Session } from "next-auth"
export default function SessionData({ session }: { session: Session | null }) {
if (session) {
return (
<div className="w-full space-y-2 overflow-auto">
<h2 className="text-xl font-bold">Current Session Data</h2>
{Object.keys(session.user).length > 3 ? (
<p>
In this example, the whole session object is passed to the page,
including the raw user object. Our recommendation is to{" "}
<em>only pass the necessary fields</em> to the page, as the raw user
object may contain sensitive information.
</p>
) : (
<p>
In this example, only some fields in the user object is passed to
the page to avoid exposing sensitive information.
</p>
)}
<pre>{JSON.stringify(session, null, 2)}</pre>
</div>
)
}
return (
<p>
No session data, please <em>Sign In</em> first.
</p>
)
}

View File

@@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,128 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View File

@@ -0,0 +1,49 @@
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"
import { Button } from "./ui/button"
import { auth } from "auth"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "./ui/dropdown-menu"
import { SignIn, SignOut } from "./auth-components"
export default async function UserButton() {
const session = await auth()
return session ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative w-8 h-8 rounded-full">
<Avatar className="w-8 h-8">
{session.user?.picture && (
<AvatarImage
src={session.user?.picture}
alt={session.user?.name ?? ""}
/>
)}
<AvatarFallback>{session.user?.email}</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{session.user?.name}
</p>
<p className="text-xs leading-none text-muted-foreground">
{session.user?.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuItem>
<SignOut />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<SignIn />
)
}

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -1,17 +1 @@
import { withAuth } from "next-auth/middleware" export { auth as middleware } from "auth"
// 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"] }

View File

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

View File

@@ -15,17 +15,32 @@
"contributors": [ "contributors": [
"Balázs Orbán <info@balazsorban.com>", "Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>", "Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>" "Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <hi@thvu.dev>"
], ],
"dependencies": { "dependencies": {
"@radix-ui/react-avatar": "^1.0.3",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-navigation-menu": "^1.1.3",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.274.0",
"next": "latest", "next": "latest",
"next-auth": "latest", "next-auth": "experimental",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.16.2", "@types/node": "^18",
"@types/react": "^18.2.0", "@types/react": "^18.2.23",
"typescript": "5.2.2" "@types/react-dom": "^18.2.8",
"autoprefixer": "^10.4.15",
"postcss": "^8.4.29",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2"
} }
} }

View File

@@ -1,18 +0,0 @@
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>
)
}

View File

@@ -1,17 +0,0 @@
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>
)
}

View File

@@ -1,19 +0,0 @@
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>
)
}

View File

@@ -1,14 +0,0 @@
// 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))
}

View File

@@ -1,22 +0,0 @@
// This is an example of to protect an API route
import { auth } from "auth"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await auth(req, res)
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.",
})
}

View File

@@ -1,12 +0,0 @@
// This is an example of how to access a session from an API route
import { auth } from "auth"
import type { NextApiRequest, NextApiResponse } from "next"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const session = await auth(req, res)
res.send(JSON.stringify(session, null, 2))
}

View File

@@ -1,27 +0,0 @@
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>
)
}

View File

@@ -1,13 +0,0 @@
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>
)
}

View File

@@ -1,12 +0,0 @@
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>
)
}

View File

@@ -1,32 +0,0 @@
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>
)
}

View File

@@ -1,40 +0,0 @@
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>Protected Page</h1>
<p>
<strong>{content ?? "\u00a0"}</strong>
</p>
</Layout>
)
}

Some files were not shown because too many files have changed in this diff Show More