Compare commits
22 Commits
next-auth@
...
next-auth@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068f9b50b8 | ||
|
|
dac490b7a1 | ||
|
|
c0f51669e2 | ||
|
|
cbf8ce3510 | ||
|
|
1c19cc71df | ||
|
|
9d962a0056 | ||
|
|
8387c78e3f | ||
|
|
58e30a6af6 | ||
|
|
b7ff987baf | ||
|
|
782812a52b | ||
|
|
32f2a0cea3 | ||
|
|
3343ef18b2 | ||
|
|
6280fe9e10 | ||
|
|
52a2bf3e28 | ||
|
|
180c6252d9 | ||
|
|
362e981e6d | ||
|
|
5198eb19f7 | ||
|
|
0210cfccf3 | ||
|
|
e90925bea0 | ||
|
|
27a0b70d87 | ||
|
|
c676e93d8a | ||
|
|
f498e9cd0a |
1
.github/ISSUE_TEMPLATE/3_bug_adapter.yml
vendored
@@ -31,6 +31,7 @@ body:
|
||||
- "@next-auth/pouchdb-adapter"
|
||||
- "@next-auth/prisma-adapter"
|
||||
- "@next-auth/sequelize-adapter"
|
||||
- "@next-auth/supabase-adapter"
|
||||
- "@next-auth/typeorm-legacy-adapter"
|
||||
- "@next-auth/upstash-redis-adapter"
|
||||
- "@next-auth/xata-adapter"
|
||||
|
||||
3
.github/issue-labeler.yml
vendored
@@ -30,6 +30,9 @@ prisma:
|
||||
sequelize:
|
||||
- "@next-auth/sequelize-adapter"
|
||||
|
||||
supabase:
|
||||
- "@next-auth/supabase-adapter"
|
||||
|
||||
typeorm-legacy:
|
||||
- "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
|
||||
3
.github/pr-labeler.yml
vendored
@@ -42,6 +42,9 @@ prisma:
|
||||
sequelize:
|
||||
- packages/adapter-sequelize/**
|
||||
|
||||
supabase:
|
||||
- packages/adapter-supabase/**
|
||||
|
||||
typeorm-legacy:
|
||||
- packages/adapter-typeorm-legacy/**
|
||||
|
||||
|
||||
2
.github/version-pr/action.yml
vendored
@@ -4,5 +4,5 @@ outputs:
|
||||
version:
|
||||
description: "npm package version"
|
||||
runs:
|
||||
using: "node18"
|
||||
using: "node16"
|
||||
main: "index.js"
|
||||
|
||||
1
.gitignore
vendored
@@ -65,6 +65,7 @@ dev.db*
|
||||
packages/adapter-prisma/prisma/dev.db
|
||||
packages/adapter-prisma/prisma/migrations
|
||||
db.sqlite
|
||||
packages/adapter-supabase/supabase/.branches
|
||||
|
||||
# Tests
|
||||
coverage
|
||||
|
||||
@@ -12,6 +12,8 @@ Please raise any significant new functionality or breaking change an issue for d
|
||||
|
||||
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
|
||||
|
||||
Before contributing, we recommend you read the [Tour de Source: NextAuth.js](https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTc2MQ==) post to become more familiar with the libraries inner workings.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
- The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
@@ -116,4 +118,4 @@ When accepting Pull Requests, make sure the following:
|
||||
|
||||
### Skipping a release
|
||||
|
||||
If a commit contains `[skip release]` in their message, it will be excluded from the commit analysis and won't participate in the release type determination. This is useful, if the PR being merged should not trigger a new `npm` release.
|
||||
If a commit contains `[skip release]` in their message, it will be excluded from the commit analysis and won't participate in the release type determination. This is useful, if the PR being merged should not trigger a new `npm` release.
|
||||
|
||||
@@ -48,4 +48,11 @@ EMAIL_FROM=user@gmail.com
|
||||
DATABASE_URL=
|
||||
|
||||
WIKIMEDIA_ID=
|
||||
WIKIMEDIA_SECRET=
|
||||
WIKIMEDIA_SECRET=
|
||||
|
||||
# Supabase Example Configuration
|
||||
# Supabase Example Configuration
|
||||
# NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
|
||||
# SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU
|
||||
# SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
|
||||
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs
|
||||
12
apps/dev/app/layout.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<head></head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
6
apps/dev/app/server-component/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
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>
|
||||
}
|
||||
@@ -90,6 +90,12 @@ export default function Header() {
|
||||
<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>
|
||||
|
||||
@@ -4,5 +4,6 @@ module.exports = {
|
||||
config.experiments = { ...config.experiments, topLevelAwait: true }
|
||||
return config
|
||||
},
|
||||
experimental: { appDir: true },
|
||||
typescript: { ignoreBuildErrors: true },
|
||||
}
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
"dependencies": {
|
||||
"@next-auth/fauna-adapter": "workspace:*",
|
||||
"@next-auth/prisma-adapter": "workspace:*",
|
||||
"@next-auth/supabase-adapter": "workspace:*",
|
||||
"@next-auth/typeorm-legacy-adapter": "workspace:*",
|
||||
"@prisma/client": "^3",
|
||||
"@supabase/supabase-js": "^2.0.5",
|
||||
"faunadb": "^4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"next": "13.0.2",
|
||||
"next-auth": "workspace:*",
|
||||
"nodemailer": "^6",
|
||||
@@ -26,6 +29,7 @@
|
||||
"react-dom": "^18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"fake-smtp-server": "^0.8.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import NextAuth from "next-auth"
|
||||
import type { NextAuthOptions } from "next-auth"
|
||||
import jwt from "jsonwebtoken"
|
||||
|
||||
// Providers
|
||||
import Apple from "next-auth/providers/apple"
|
||||
@@ -18,7 +19,6 @@ import Freshbooks from "next-auth/providers/freshbooks"
|
||||
import GitHub from "next-auth/providers/github"
|
||||
import Gitlab from "next-auth/providers/gitlab"
|
||||
import Google from "next-auth/providers/google"
|
||||
import Hubspot from "next-auth/providers/hubspot"
|
||||
import IDS4 from "next-auth/providers/identity-server4"
|
||||
import Instagram from "next-auth/providers/instagram"
|
||||
import Keycloak from "next-auth/providers/keycloak"
|
||||
@@ -30,43 +30,76 @@ import Osu from "next-auth/providers/osu"
|
||||
import Patreon from "next-auth/providers/patreon"
|
||||
import Slack from "next-auth/providers/slack"
|
||||
import Spotify from "next-auth/providers/spotify"
|
||||
import Todoist from "next-auth/providers/todoist"
|
||||
import Trakt from "next-auth/providers/trakt"
|
||||
import Twitch from "next-auth/providers/twitch"
|
||||
import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
|
||||
import Vk from "next-auth/providers/vk"
|
||||
import Wikimedia from "next-auth/providers/wikimedia"
|
||||
import WorkOS from "next-auth/providers/workos"
|
||||
import Zitadel from "next-auth/providers/zitadel"
|
||||
|
||||
// Adapters
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
import { Client as FaunaClient } from "faunadb"
|
||||
import { FaunaAdapter } from "@next-auth/fauna-adapter"
|
||||
import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
|
||||
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
|
||||
|
||||
// // Prisma
|
||||
// import { PrismaClient } from "@prisma/client"
|
||||
// import { PrismaAdapter } from "@next-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 "@next-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 { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
|
||||
// const adapter = TypeORMLegacyAdapter({
|
||||
// type: "sqlite",
|
||||
// name: "next-auth-test-memory",
|
||||
// database: "./typeorm/dev.db",
|
||||
// synchronize: true,
|
||||
// })
|
||||
// Add an adapter you want to test here.
|
||||
const adapters = {
|
||||
prisma() {
|
||||
const client = globalThis.prisma || new PrismaClient()
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
||||
return PrismaAdapter(client)
|
||||
},
|
||||
typeorm() {
|
||||
return TypeORMLegacyAdapter({
|
||||
type: "sqlite",
|
||||
name: "next-auth-test-memory",
|
||||
database: "./typeorm/dev.db",
|
||||
synchronize: true,
|
||||
})
|
||||
},
|
||||
fauna() {
|
||||
const client =
|
||||
globalThis.fauna ||
|
||||
new FaunaClient({
|
||||
secret: process.env.FAUNA_SECRET,
|
||||
domain: process.env.FAUNA_DOMAIN,
|
||||
})
|
||||
if (process.env.NODE_ENV !== "production") global.fauna = client
|
||||
return FaunaAdapter(client)
|
||||
},
|
||||
supabase() {
|
||||
return SupabaseAdapter({
|
||||
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
})
|
||||
},
|
||||
noop() {
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
// adapter,
|
||||
adapter: adapters.noop(),
|
||||
callbacks: {
|
||||
async session({ session, user }) {
|
||||
// NOTE: this is needed when using Supabase with RLS. Otherwise this callback can be removed.
|
||||
const signingSecret = process.env.SUPABASE_JWT_SECRET
|
||||
if (signingSecret) {
|
||||
const payload = {
|
||||
aud: "authenticated",
|
||||
exp: Math.floor(new Date(session.expires).getTime() / 1000),
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
role: "authenticated",
|
||||
}
|
||||
session.supabaseAccessToken = jwt.sign(payload, signingSecret)
|
||||
}
|
||||
return session
|
||||
},
|
||||
},
|
||||
debug: process.env.NODE_ENV !== "production",
|
||||
theme: {
|
||||
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
|
||||
@@ -94,7 +127,6 @@ export const authOptions: NextAuthOptions = {
|
||||
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
|
||||
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
|
||||
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
|
||||
Hubspot({ clientId: process.env.HUBSPOT_ID, clientSecret: process.env.HUBSPOT_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 }),
|
||||
@@ -106,7 +138,6 @@ export const authOptions: NextAuthOptions = {
|
||||
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 }),
|
||||
Todoist({ clientId: process.env.TODOIST_ID, clientSecret: process.env.TODOIST_SECRET }),
|
||||
Trakt({ clientId: process.env.TRAKT_ID, clientSecret: process.env.TRAKT_SECRET }),
|
||||
Twitch({ clientId: process.env.TWITCH_ID, clientSecret: process.env.TWITCH_SECRET }),
|
||||
Twitter({ version: "2.0", clientId: process.env.TWITTER_ID, clientSecret: process.env.TWITTER_SECRET }),
|
||||
@@ -114,7 +145,6 @@ export const authOptions: NextAuthOptions = {
|
||||
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 }),
|
||||
Zitadel({ issuer: process.env.ZITADEL_ISSUER, clientId: process.env.ZITADEL_CLIENT_ID, clientSecret: process.env.ZITADEL_CLIENT_SECRET }),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// 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]';
|
||||
import { authOptions } from "../auth/[...nextauth]"
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await unstable_getServerSession(req, res, authOptions)
|
||||
res.send(JSON.stringify(session, null, 2))
|
||||
res.json(session)
|
||||
}
|
||||
|
||||
30
apps/dev/pages/api/examples/supabase-rls.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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))
|
||||
}
|
||||
49
apps/dev/pages/supabase-client-rls.js
Normal file
@@ -0,0 +1,49 @@
|
||||
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) {
|
||||
console.log(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>
|
||||
)
|
||||
}
|
||||
68
apps/dev/pages/supabase-ssr.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
@@ -15,7 +19,20 @@
|
||||
"incremental": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", "jest.config.js"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"jest.config.js"
|
||||
]
|
||||
}
|
||||
|
||||
2
apps/dev/types/nextauth.d.ts
vendored
@@ -6,6 +6,8 @@ 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
|
||||
|
||||
@@ -13,13 +13,12 @@ export default function ServerSidePage({ session }: { session: Session }) {
|
||||
<h1>Server Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the <strong>unstable_getServerSession()</strong> method
|
||||
in <strong>unstable_getServerSideProps()</strong>.
|
||||
in <strong>getServerSideProps()</strong>.
|
||||
</p>
|
||||
<p>
|
||||
Using <strong>unstable_getServerSession()</strong> in{" "}
|
||||
<strong>unstable_getServerSideProps()</strong> is the recommended
|
||||
approach if you need to support Server Side Rendering with
|
||||
authentication.
|
||||
<strong>getServerSideProps()</strong> is the recommended approach if you
|
||||
need to support Server Side Rendering with authentication.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of Server Side Rendering is this page does not require
|
||||
|
||||
12
apps/playground-nuxt/.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
4
apps/playground-nuxt/.eslintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
dist
|
||||
node_modules
|
||||
tsconfig.json
|
||||
package.json
|
||||
10
apps/playground-nuxt/.eslintrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": [
|
||||
"@nuxtjs/eslint-config-typescript"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"off"
|
||||
]
|
||||
}
|
||||
}
|
||||
52
apps/playground-nuxt/.gitignore
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
*.log*
|
||||
|
||||
# Temp directories
|
||||
.temp
|
||||
.tmp
|
||||
.cache
|
||||
|
||||
# Yarn
|
||||
**/.yarn/cache
|
||||
**/.yarn/*state*
|
||||
|
||||
# Generated dirs
|
||||
dist
|
||||
|
||||
# Nuxt
|
||||
.nuxt
|
||||
.output
|
||||
.vercel_build_output
|
||||
.build-*
|
||||
.env
|
||||
.netlify
|
||||
|
||||
# Env
|
||||
.env
|
||||
|
||||
# Testing
|
||||
reports
|
||||
coverage
|
||||
*.lcov
|
||||
.nyc_output
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# Intellij idea
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
.vercel
|
||||
1
apps/playground-nuxt/.nuxtrc
Normal file
@@ -0,0 +1 @@
|
||||
imports.autoImport=false
|
||||
108
apps/playground-nuxt/README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# NextAuth + Nuxt 3 Playground
|
||||
|
||||
NextAuth.js is committed to bringing easy authentication to other frameworks. [#2294](https://github.com/nextauthjs/next-auth/issues/2294)
|
||||
|
||||
Nuxt 3 support with NextAuth.js is currently experimental. This directory contains a minimal, proof-of-concept application. Parts of this is expected to be abstracted away into a package like` @next-auth/nuxt.`
|
||||
|
||||
This package uses Nuxt's [module starter](https://github.com/nuxt/starter/tree/module).
|
||||
|
||||
Demo: https://next-auth-nuxt-demo.vercel.app
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Add the module to the modules section of `nuxt.config.ts`:
|
||||
|
||||
```ts
|
||||
export default defineNuxtConfig({
|
||||
// temporary module name.
|
||||
modules: ['next-auth-nuxt'],
|
||||
// https://v3.nuxtjs.org/migration/runtime-config#runtime-config
|
||||
runtimeConfig: {
|
||||
secret: process.env.NEXTAUTH_SECRET
|
||||
github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET
|
||||
}
|
||||
},
|
||||
// https://v3.nuxtjs.org/guide/concepts/esm#aliasing-libraries
|
||||
// Fix for GithubProvider (or whichever provider you choose) is not a function error in Vite
|
||||
alias: {
|
||||
'next-auth/providers/github': 'node_modules/next-auth/providers/github.js'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Add API route
|
||||
|
||||
To add `NextAuth.js` to a project create a file called `[...].ts` in `server/api/auth`. This contains the dynamic route handler for NextAuth.js which will also contain all of your global NextAuth.js configurations.
|
||||
|
||||
```ts
|
||||
// ~/server/api/auth/[...].ts
|
||||
import { NextAuthNuxtHandler } from 'next-auth-nuxt/handler'
|
||||
import GithubProvider from 'next-auth/providers/github'
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
export const authOptions = {
|
||||
secret: runtimeConfig.secret,
|
||||
providers: [
|
||||
GithubProvider({
|
||||
clientId: runtimeConfig.github.clientId,
|
||||
clientSecret: runtimeConfig.github.clientSecret
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
export default NextAuthNuxtHandler(authOptions)
|
||||
```
|
||||
|
||||
All requests to `/api/auth/*` (`signIn`, `callback`, `signOut`, etc.) will automatically be handled by NextAuth.js.
|
||||
|
||||
### Frontend - Add Vue Composable
|
||||
|
||||
The `useSession()` Vue Composable is the easiest way to check if someone is signed in.
|
||||
|
||||
```html
|
||||
<script setup lang="ts">
|
||||
const { data: session } = useSession()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="session">
|
||||
Signed in as {{ session.user.email }} <br />
|
||||
<button @click="signOut">Sign out</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
Not signed in <br />
|
||||
<button @click="signIn">Sign in</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Backend - API Route
|
||||
|
||||
To protect an API Route, you can use the `getServerSession()` method.
|
||||
|
||||
```ts
|
||||
import { getServerSession } from 'next-auth-nuxt/handler'
|
||||
import { authOptions } from '~/server/api/auth/[...]'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getServerSession(event, authOptions)
|
||||
|
||||
if (session) {
|
||||
return {
|
||||
content: 'This is protected content. You can access this content because you are signed in.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: 'You must be signed in to view the protected content on this page.'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
- Run `pnpm dev:generate` to generate type stubs.
|
||||
- Use `pnpm dev` to start `playground` in development mode.
|
||||
1
apps/playground-nuxt/client.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dist/runtime/client'
|
||||
1
apps/playground-nuxt/handler.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dist/runtime/server/handler'
|
||||
49
apps/playground-nuxt/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "next-auth-nuxt",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"packageManager": "pnpm@7.1.1",
|
||||
"license": "MIT",
|
||||
"main": "./dist/module.cjs",
|
||||
"types": "./dist/types.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/module.mjs",
|
||||
"require": "./dist/module.cjs"
|
||||
},
|
||||
"./handler": {
|
||||
"import": "./dist/runtime/server/handler.mjs",
|
||||
"types": "./dist/runtime/server/handler.d.ts"
|
||||
},
|
||||
"./client": {
|
||||
"import": "./dist/runtime/client/index.mjs",
|
||||
"types": "./dist/runtime/client/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"handler.d.ts",
|
||||
"client.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"prepack": "nuxt-module-build",
|
||||
"dev": "pnpm prepack && nuxi dev playground",
|
||||
"dev:build": "nuxi build playground",
|
||||
"dev:build:vercel": "NITRO_PRESET=vercel nuxi build playground",
|
||||
"dev:prepare": "nuxt-module-build --stub && nuxi prepare playground"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^3.0.0-rc.13",
|
||||
"h3": "^0.8.6",
|
||||
"next-auth": "^4.16.2",
|
||||
"pathe": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/module-builder": "^0.2.0",
|
||||
"@nuxt/schema": "^3.0.0-rc.12",
|
||||
"@nuxtjs/eslint-config-typescript": "^11.0.0",
|
||||
"eslint": "^8.26.0",
|
||||
"nuxt": "^3.0.0-rc.13",
|
||||
"next-auth-nuxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
4
apps/playground-nuxt/playground/.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
NEXTAUTH_URL=
|
||||
NEXTAUTH_SECRET=
|
||||
40
apps/playground-nuxt/playground/app.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<Header />
|
||||
<NuxtPage />
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
li,
|
||||
p {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
iframe {
|
||||
background: #ccc;
|
||||
border: 1px solid #ccc;
|
||||
height: 10rem;
|
||||
width: 100%;
|
||||
border-radius: .5rem;
|
||||
filter: invert(1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Access Denied</h1>
|
||||
<p>
|
||||
<a href="/api/auth/signin">You must be signed in to view this page</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
30
apps/playground-nuxt/playground/components/Footer.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<footer class="fotter">
|
||||
<hr>
|
||||
<ul class="navItems">
|
||||
<li class="navItem">
|
||||
<a href="https://github.com/nextauthjs/next-auth/tree/main/apps/playground-nuxt">Demo GitHub</a>
|
||||
</li>
|
||||
<li class="navItem">
|
||||
<a href="https://next-auth.js.org">Next.js Documentation</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.navItems {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
155
apps/playground-nuxt/playground/components/Header.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { useSession, signIn, signOut, computed } from '#imports'
|
||||
|
||||
const { data: session, status } = useSession()
|
||||
const loading = computed(() => status.value === 'loading')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="signedInStatus">
|
||||
<p :class="['nojs-show', !session && loading ? 'loading' : 'loaded']">
|
||||
<template v-if="session">
|
||||
<span v-if="session.user?.image" :style="{ backgroundImage: `url(${session.user.image})` }" class="avatar" />
|
||||
<span class="signedInText">
|
||||
<small>Signed in as</small><br>
|
||||
<strong>{{ session.user?.email || session.user?.name }}</strong>
|
||||
</span>
|
||||
<a href="/api/auth/signout" class="button" @click.prevent="signOut">Sign out</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="notSignedInText">You are not signed in</span>
|
||||
<a href="/api/auth/signin" class="buttonPrimary" @click.prevent="signIn">Sign in</a>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="navItems">
|
||||
<li class="navItem">
|
||||
<NuxtLink to="/">
|
||||
Home
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="navItem">
|
||||
<NuxtLink to="/client">
|
||||
Client
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="navItem">
|
||||
<NuxtLink to="/server">
|
||||
Server
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="navItem">
|
||||
<NuxtLink to="/protected">
|
||||
Protected
|
||||
</NuxtLink>
|
||||
</li>
|
||||
<li class="navItem">
|
||||
<NuxtLink to="/api-example">
|
||||
API
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.nojs-show {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.signedInStatus {
|
||||
display: block;
|
||||
min-height: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.loaded {
|
||||
position: relative;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 .6rem .6rem;
|
||||
padding: .6rem 1rem;
|
||||
margin: 0;
|
||||
background-color: rgba(0,0,0,.05);
|
||||
transition: all 0.2s ease-in;
|
||||
}
|
||||
|
||||
.loading {
|
||||
top: -2rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.signedInText,
|
||||
.notSignedInText {
|
||||
position: absolute;
|
||||
padding-top: .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: -.4rem;
|
||||
font-weight: 500;
|
||||
border-radius: .3rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
padding: .7rem .8rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background-color: transparent;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.buttonPrimary {
|
||||
background-color: #346df1;
|
||||
border-color: #346df1;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: .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;
|
||||
}
|
||||
</style>
|
||||
20
apps/playground-nuxt/playground/nuxt.config.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import MyModule from '../src/module'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
modules: [
|
||||
MyModule
|
||||
],
|
||||
// https://v3.nuxtjs.org/migration/runtime-config#runtime-config
|
||||
runtimeConfig: {
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
github: {
|
||||
clientId: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET
|
||||
}
|
||||
},
|
||||
// https://v3.nuxtjs.org/guide/concepts/esm#aliasing-libraries
|
||||
// Fix for GithubProvider is not a function error in Vite
|
||||
alias: {
|
||||
'next-auth/providers/github': 'node_modules/next-auth/providers/github.js'
|
||||
}
|
||||
})
|
||||
4
apps/playground-nuxt/playground/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "playground",
|
||||
"private": true
|
||||
}
|
||||
15
apps/playground-nuxt/playground/pages/api-example.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<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" />
|
||||
</div>
|
||||
</template>
|
||||
18
apps/playground-nuxt/playground/pages/client.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Client Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the <strong>useSession()</strong> Vue Composable in the <strong><Header/></strong> component.
|
||||
</p>
|
||||
<p>
|
||||
The <strong>useSession()</strong> Vue Composable 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 a provided session via <strong>Vue Plugin</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>
|
||||
</div>
|
||||
</template>
|
||||
8
apps/playground-nuxt/playground/pages/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Nuxt 3 + NextAuth.js Example</h1>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use <a href="https://v3.nuxtjs.org/">Nuxt 3</a> with <a href="https://next-auth.js.org">NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
19
apps/playground-nuxt/playground/pages/protected.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { useSession, useFetch, useLazyFetch } from '#imports'
|
||||
import AccessDenied from '~/components/AccessDenied.vue'
|
||||
|
||||
const { data: session } = useSession()
|
||||
const { data } = await useLazyFetch('/api/examples/protected', {
|
||||
server: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AccessDenied v-if="!session" />
|
||||
<template v-else>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{{ data?.content || "\u00a0" }}</strong></p>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
24
apps/playground-nuxt/playground/pages/server.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { useFetch } from '#imports'
|
||||
|
||||
await useFetch('/api/examples/session')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1>Server Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the <strong>getServerSession()</strong> method inside an api route and is fetched using the <strong>useFetch()</strong> composable.
|
||||
</p>
|
||||
<p>
|
||||
Using <strong>getServerSession()</strong> is the recommended approach if you need to
|
||||
support Server Side Rendering with authentication.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of Server Side Rendering is this page does not require client side JavaScript.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of Server Side Rendering is that this page is slower to render.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
17
apps/playground-nuxt/playground/server/api/auth/[...].ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextAuthNuxtHandler } from 'next-auth-nuxt/handler'
|
||||
import GithubProvider from 'next-auth/providers/github'
|
||||
import type { NextAuthOptions } from 'next-auth'
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
secret: runtimeConfig.secret,
|
||||
providers: [
|
||||
GithubProvider({
|
||||
clientId: runtimeConfig.github.clientId,
|
||||
clientSecret: runtimeConfig.github.clientSecret
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default NextAuthNuxtHandler(authOptions)
|
||||
10
apps/playground-nuxt/playground/server/api/examples/jwt.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { getToken } from 'next-auth/jwt'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// @ts-expect-error: cookies property is not present in h3
|
||||
event.req.cookies = parseCookies(event)
|
||||
const token = await getToken({
|
||||
req: event.req
|
||||
})
|
||||
return token
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { getServerSession } from 'next-auth-nuxt/handler'
|
||||
import { authOptions } from '../auth/[...]'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getServerSession(event, authOptions)
|
||||
|
||||
if (session) {
|
||||
return {
|
||||
content: 'This is protected content. You can access this content because you are signed in.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: 'You must be signed in to view the protected content on this page.'
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { getServerSession } from 'next-auth-nuxt/handler'
|
||||
import { authOptions } from '../auth/[...]'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getServerSession(event, authOptions)
|
||||
return session
|
||||
})
|
||||
6386
apps/playground-nuxt/pnpm-lock.yaml
generated
Normal file
2
apps/playground-nuxt/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- playground
|
||||
40
apps/playground-nuxt/src/module.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { fileURLToPath } from 'url'
|
||||
import { addImports, addPlugin, defineNuxtModule, extendViteConfig } from '@nuxt/kit'
|
||||
import { resolve } from 'pathe'
|
||||
|
||||
export interface ModuleOptions {
|
||||
}
|
||||
|
||||
export default defineNuxtModule<ModuleOptions>({
|
||||
meta: {
|
||||
name: 'next-auth-nuxt',
|
||||
configKey: 'auth'
|
||||
},
|
||||
defaults: {
|
||||
},
|
||||
async setup (_options, nuxt) {
|
||||
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
|
||||
nuxt.options.build.transpile.push(runtimeDir)
|
||||
|
||||
addPlugin(resolve(runtimeDir, 'plugin.client'))
|
||||
|
||||
// Composables are auto-imported in client.
|
||||
const client = resolve(runtimeDir, 'client')
|
||||
await addImports([
|
||||
{ name: 'getSession', from: client },
|
||||
{ name: 'getCsrfToken', from: client },
|
||||
{ name: 'getProviders', from: client },
|
||||
{ name: 'signIn', from: client },
|
||||
{ name: 'signOut', from: client },
|
||||
{ name: 'useSession', from: client }
|
||||
])
|
||||
|
||||
// We can safely expose this to client.
|
||||
extendViteConfig((config) => {
|
||||
config.define = config.define || {}
|
||||
config.define['process.env.NEXTAUTH_URL'] = JSON.stringify(process.env.NEXTAUTH_URL)
|
||||
config.define['process.env.NEXTAUTH_URL_INTERNAL'] = JSON.stringify(process.env.NEXTAUTH_URL_INTERNAL)
|
||||
config.define['process.env.VERCEL_URL'] = JSON.stringify(process.env.VERCEL_URL)
|
||||
})
|
||||
}
|
||||
})
|
||||
369
apps/playground-nuxt/src/runtime/client/index.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import type { NextAuthClientConfig } from 'next-auth/client/_utils'
|
||||
import type { Plugin, Ref } from 'vue'
|
||||
import { ref, reactive, computed, inject, toRefs } from 'vue'
|
||||
import { BroadcastChannel, apiBaseUrl, fetchData, now } from 'next-auth/client/_utils'
|
||||
import type { Session } from 'next-auth'
|
||||
import type {
|
||||
BuiltInProviderType,
|
||||
RedirectableProviderType
|
||||
} from 'next-auth/providers'
|
||||
import type { H3EventContext } from 'h3'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import _logger, { proxyLogger } from '../lib/logger'
|
||||
import type {
|
||||
ClientSafeProvider,
|
||||
LiteralUnion,
|
||||
SessionProviderProps,
|
||||
SignInAuthorizationParams,
|
||||
SignInOptions,
|
||||
SignInResponse,
|
||||
SignOutParams,
|
||||
SignOutResponse
|
||||
} from '../types'
|
||||
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
// happens server side in server/index.js
|
||||
// 1. An empty value is legitimate when the code is being invoked client side as
|
||||
// relative URLs are valid in that context and so defaults to empty.
|
||||
// 2. When invoked server side the value is picked up from an environment
|
||||
// variable and defaults to 'http://localhost:3000'.
|
||||
const __NEXTAUTH: NextAuthClientConfig = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL).origin,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).path,
|
||||
baseUrlServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL ??
|
||||
process.env.NEXTAUTH_URL ??
|
||||
process.env.VERCEL_URL
|
||||
).origin,
|
||||
basePathServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL ?? process.env.NEXTAUTH_URL
|
||||
).path,
|
||||
_lastSync: 0,
|
||||
_session: undefined,
|
||||
_getSession: () => {}
|
||||
}
|
||||
|
||||
export interface CtxOrReq {
|
||||
req?: H3EventContext['req']
|
||||
event?: { req: H3EventContext['req'] }
|
||||
}
|
||||
|
||||
export type GetSessionParams = CtxOrReq & {
|
||||
event?: 'storage' | 'timer' | 'hidden' | string
|
||||
triggerEvent?: boolean
|
||||
broadcast?: boolean
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
function isServer () {
|
||||
return (process as any).server
|
||||
}
|
||||
|
||||
export async function getSession (params?: GetSessionParams) {
|
||||
const session = await fetchData<Session>(
|
||||
'session',
|
||||
__NEXTAUTH,
|
||||
logger,
|
||||
params
|
||||
)
|
||||
if (params?.broadcast ?? true) { broadcast.post({ event: 'session', data: { trigger: 'getSession' } }) }
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Cross Site Request Forgery Token (CSRF Token)
|
||||
* required to make POST requests (e.g. for signing in and signing out).
|
||||
* You likely only need to use this if you are not using the built-in
|
||||
* `signIn()` and `signOut()` methods.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken)
|
||||
*/
|
||||
export async function getCsrfToken (params?: CtxOrReq) {
|
||||
const response = await fetchData<{ csrfToken: string }>(
|
||||
'csrf',
|
||||
__NEXTAUTH,
|
||||
logger,
|
||||
params
|
||||
)
|
||||
return response?.csrfToken
|
||||
}
|
||||
|
||||
/**
|
||||
* It calls `/api/auth/providers` and returns
|
||||
* a list of the currently configured authentication providers.
|
||||
* It can be useful if you are creating a dynamic custom sign in page.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getproviders)
|
||||
*/
|
||||
export async function getProviders () {
|
||||
return await fetchData<
|
||||
Record<LiteralUnion<BuiltInProviderType>, ClientSafeProvider>
|
||||
>('providers', __NEXTAUTH, logger)
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side method to initiate a signin flow
|
||||
* or send the user to the signin page listing all possible providers.
|
||||
* Automatically adds the CSRF token to the request.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
|
||||
*/
|
||||
export async function signIn<
|
||||
P extends RedirectableProviderType | undefined = undefined,
|
||||
> (
|
||||
provider?: LiteralUnion<BuiltInProviderType>,
|
||||
options?: SignInOptions,
|
||||
authorizationParams?: SignInAuthorizationParams
|
||||
): Promise<
|
||||
P extends RedirectableProviderType ? SignInResponse | undefined : undefined
|
||||
> {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options ?? {}
|
||||
|
||||
const baseUrl = apiBaseUrl(__NEXTAUTH)
|
||||
const providers = await getProviders()
|
||||
|
||||
if (!providers) {
|
||||
window.location.href = `${baseUrl}/error`
|
||||
return
|
||||
}
|
||||
|
||||
if (!provider || !(provider in providers)) {
|
||||
window.location.href = `${baseUrl}/signin?${new URLSearchParams({
|
||||
callbackUrl
|
||||
})}`
|
||||
return
|
||||
}
|
||||
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const isSupportingReturn = isCredentials || isEmail
|
||||
|
||||
const signInUrl = `${baseUrl}/${
|
||||
isCredentials ? 'callback' : 'signin'
|
||||
}/${provider}`
|
||||
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
|
||||
const res = await fetch(_signInUrl, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
// @ts-expect-error: Internal
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (redirect || !isSupportingReturn) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location.href = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) { window.location.reload() }
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get('error')
|
||||
|
||||
if (res.ok) { await __NEXTAUTH._getSession({ event: 'storage' }) }
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
} as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the user out, by removing the session cookie.
|
||||
* Automatically adds the CSRF token to the request.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
|
||||
*/
|
||||
export async function signOut<R extends boolean = true> (
|
||||
options?: SignOutParams<R>
|
||||
): Promise<R extends true ? undefined : SignOutResponse> {
|
||||
const { callbackUrl = window.location.href } = options ?? {}
|
||||
const baseUrl = apiBaseUrl(__NEXTAUTH)
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
// @ts-expect-error: Internal
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
|
||||
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
|
||||
|
||||
if (options?.redirect ?? true) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location.href = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) { window.location.reload() }
|
||||
// @ts-expect-error: Internal
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export function SessionProviderPlugin (options: SessionProviderProps): Plugin {
|
||||
return {
|
||||
install (app) {
|
||||
const { basePath } = options
|
||||
|
||||
if (basePath) { __NEXTAUTH.basePath = basePath }
|
||||
|
||||
/**
|
||||
* If session was `null`, there was an attempt to fetch it,
|
||||
* but it failed, but we still treat it as a valid initial value.
|
||||
*/
|
||||
const hasInitialSession = options.session !== undefined
|
||||
|
||||
/** If session was passed, initialize as already synced */
|
||||
__NEXTAUTH._lastSync = hasInitialSession ? now() : 0
|
||||
|
||||
if (hasInitialSession) { __NEXTAUTH._session = options.session }
|
||||
|
||||
const session = ref(options.session)
|
||||
|
||||
/** If session was passed, initialize as not loading */
|
||||
const loading = ref(!hasInitialSession)
|
||||
|
||||
__NEXTAUTH._getSession = async ({ event } = {}) => {
|
||||
try {
|
||||
const storageEvent = event === 'storage'
|
||||
// We should always update if we don't have a client session yet
|
||||
// or if there are events from other tabs/windows
|
||||
if (storageEvent || __NEXTAUTH._session === undefined) {
|
||||
__NEXTAUTH._lastSync = now()
|
||||
__NEXTAUTH._session = await getSession({
|
||||
broadcast: !storageEvent
|
||||
})
|
||||
session.value = __NEXTAUTH._session
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
// If there is no time defined for when a session should be considered
|
||||
// stale, then it's okay to use the value we have until an event is
|
||||
// triggered which updates it
|
||||
!event ||
|
||||
// If the client doesn't have a session then we don't need to call
|
||||
// the server to check if it does (if they have signed in via another
|
||||
// tab or window that will come through as a "stroage" event
|
||||
// event anyway)
|
||||
__NEXTAUTH._session === null ||
|
||||
// Bail out early if the client session is not stale yet
|
||||
now() < __NEXTAUTH._lastSync
|
||||
) { return }
|
||||
|
||||
// An event or session staleness occurred, update the client session.
|
||||
__NEXTAUTH._lastSync = now()
|
||||
__NEXTAUTH._session = await getSession()
|
||||
session.value = __NEXTAUTH._session
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_SESSION_ERROR', error as Error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
__NEXTAUTH._getSession()
|
||||
|
||||
const { refetchOnWindowFocus = true } = options
|
||||
|
||||
// Listen for when the page is visible, if the user switches tabs
|
||||
// and makes our tab visible again, re-fetch the session, but only if
|
||||
// this feature is not disabled.
|
||||
const visibilityHandler = () => {
|
||||
if (refetchOnWindowFocus && document.visibilityState === 'visible') { __NEXTAUTH._getSession({ event: 'visibilitychange' }) }
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', visibilityHandler, false)
|
||||
|
||||
const unsubscribeFromBroadcast = broadcast.receive(() =>
|
||||
__NEXTAUTH._getSession({ event: 'storage' })
|
||||
)
|
||||
|
||||
const { refetchInterval } = options
|
||||
let refetchIntervalTimer: NodeJS.Timer
|
||||
|
||||
if (refetchInterval) {
|
||||
refetchIntervalTimer = setInterval(() => {
|
||||
if (__NEXTAUTH._session) { __NEXTAUTH._getSession({ event: 'poll' }) }
|
||||
}, refetchInterval * 1000)
|
||||
}
|
||||
|
||||
const originalUnmount = app.unmount
|
||||
app.unmount = function nextAuthUnmount () {
|
||||
document.removeEventListener('visibilitychange', visibilityHandler, false)
|
||||
unsubscribeFromBroadcast?.()
|
||||
clearInterval(refetchIntervalTimer)
|
||||
__NEXTAUTH._lastSync = 0
|
||||
__NEXTAUTH._session = undefined
|
||||
__NEXTAUTH._getSession = () => {}
|
||||
originalUnmount()
|
||||
}
|
||||
|
||||
const status = computed(() => loading.value ? 'loading' : session.value ? 'authenticated' : 'unauthenticated')
|
||||
const value = reactive({
|
||||
data: session,
|
||||
status
|
||||
})
|
||||
|
||||
app.provide('SessionKey', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue Composable that gives you access
|
||||
* to the logged in user's session data.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
|
||||
*/
|
||||
export function useSession (): {
|
||||
data: Ref<SessionProviderProps['session']>;
|
||||
status: Ref<string>;
|
||||
} {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
data: ref(null),
|
||||
status: ref('loading')
|
||||
}
|
||||
}
|
||||
|
||||
const value = inject<{
|
||||
data: SessionProviderProps['session']
|
||||
status: string
|
||||
}>('SessionKey')
|
||||
if (!value) {
|
||||
throw new Error('Could not resolve provided session value')
|
||||
}
|
||||
const { data, status } = toRefs(value)
|
||||
|
||||
return {
|
||||
data,
|
||||
status
|
||||
}
|
||||
}
|
||||
115
apps/playground-nuxt/src/runtime/lib/errors.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { Adapter } from 'next-auth/adapters'
|
||||
import type { EventCallbacks, LoggerInstance } from 'next-auth'
|
||||
|
||||
/**
|
||||
* Same as the default `Error`, but it is JSON serializable.
|
||||
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
*/
|
||||
export class UnknownError extends Error {
|
||||
code: string
|
||||
constructor (error: Error | string) {
|
||||
super((error as Error)?.message ?? error)
|
||||
this.name = 'UnknownError'
|
||||
this.code = (error as any).code
|
||||
if (error instanceof Error) { this.stack = error.stack }
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
return {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
stack: this.stack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OAuthCallbackError extends UnknownError {
|
||||
name = 'OAuthCallbackError'
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when an Email address is already associated with an account
|
||||
* but the user is trying an OAuth account that is not linked to it.
|
||||
*/
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
name = 'AccountNotLinkedError'
|
||||
}
|
||||
|
||||
export class MissingAPIRoute extends UnknownError {
|
||||
name = 'MissingAPIRouteError'
|
||||
code = 'MISSING_NEXTAUTH_API_ROUTE_ERROR'
|
||||
}
|
||||
|
||||
export class MissingSecret extends UnknownError {
|
||||
name = 'MissingSecretError'
|
||||
code = 'NO_SECRET'
|
||||
}
|
||||
|
||||
export class MissingAuthorize extends UnknownError {
|
||||
name = 'MissingAuthorizeError'
|
||||
code = 'CALLBACK_CREDENTIALS_HANDLER_ERROR'
|
||||
}
|
||||
|
||||
export class MissingAdapter extends UnknownError {
|
||||
name = 'MissingAdapterError'
|
||||
code = 'EMAIL_REQUIRES_ADAPTER_ERROR'
|
||||
}
|
||||
|
||||
export class UnsupportedStrategy extends UnknownError {
|
||||
name = 'UnsupportedStrategyError'
|
||||
code = 'CALLBACK_CREDENTIALS_JWT_ERROR'
|
||||
}
|
||||
|
||||
type Method = (...args: any[]) => Promise<any>
|
||||
|
||||
export function upperSnake (s: string) {
|
||||
return s.replace(/([A-Z])/g, '_$1').toUpperCase()
|
||||
}
|
||||
|
||||
export function capitalize (s: string) {
|
||||
return `${s[0].toUpperCase()}${s.slice(1)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an object of methods and adds error handling.
|
||||
*/
|
||||
export function eventsErrorHandler (
|
||||
methods: Partial<EventCallbacks>,
|
||||
logger: LoggerInstance
|
||||
): Partial<EventCallbacks> {
|
||||
return Object.keys(methods).reduce<any>((acc, name) => {
|
||||
acc[name] = async (...args: any[]) => {
|
||||
try {
|
||||
const method: Method = methods[name as keyof Method]
|
||||
return await method(...args)
|
||||
} catch (e) {
|
||||
logger.error(`${upperSnake(name)}_EVENT_ERROR`, e as Error)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
/** Handles adapter induced errors. */
|
||||
export function adapterErrorHandler (
|
||||
adapter: Adapter | undefined,
|
||||
logger: LoggerInstance
|
||||
): Adapter | undefined {
|
||||
if (!adapter) { return }
|
||||
|
||||
return Object.keys(adapter).reduce<any>((acc, name) => {
|
||||
acc[name] = async (...args: any[]) => {
|
||||
try {
|
||||
logger.debug(`adapter_${name}`, { args })
|
||||
const method: Method = adapter[name as keyof Method]
|
||||
return await method(...args)
|
||||
} catch (error) {
|
||||
logger.error(`adapter_error_${name}`, error as Error)
|
||||
const e = new UnknownError(error as Error)
|
||||
e.name = `${capitalize(name)}Error`
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
113
apps/playground-nuxt/src/runtime/lib/logger.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { UnknownError } from './errors'
|
||||
|
||||
// TODO: better typing
|
||||
/** Makes sure that error is always serializable */
|
||||
function formatError (o: unknown): unknown {
|
||||
if (o instanceof Error && !(o instanceof UnknownError)) { return { message: o.message, stack: o.stack, name: o.name } }
|
||||
|
||||
if (hasErrorProperty(o)) {
|
||||
o.error = formatError(o.error) as Error
|
||||
o.message = o.message ?? o.error.message
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
function hasErrorProperty (
|
||||
x: unknown
|
||||
): x is { error: Error; [key: string]: unknown } {
|
||||
return !!(x as any)?.error
|
||||
}
|
||||
|
||||
export type WarningCode =
|
||||
| 'NEXTAUTH_URL'
|
||||
| 'NO_SECRET'
|
||||
| 'TWITTER_OAUTH_2_BETA'
|
||||
| 'DEBUG_ENABLED'
|
||||
|
||||
/**
|
||||
* Override any of the methods, and the rest will use the default logger.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#logger)
|
||||
*/
|
||||
export interface LoggerInstance extends Record<string, Function> {
|
||||
warn: (code: WarningCode) => void
|
||||
error: (
|
||||
code: string,
|
||||
/**
|
||||
* Either an instance of (JSON serializable) Error
|
||||
* or an object that contains some debug information.
|
||||
* (Error is still available through `metadata.error`)
|
||||
*/
|
||||
metadata: Error | { error: Error; [key: string]: unknown }
|
||||
) => void
|
||||
debug: (code: string, metadata: unknown) => void
|
||||
}
|
||||
|
||||
const _logger: LoggerInstance = {
|
||||
error (code, metadata) {
|
||||
metadata = formatError(metadata) as Error
|
||||
console.error(
|
||||
`[next-auth][error][${code}]`,
|
||||
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
|
||||
metadata.message,
|
||||
metadata
|
||||
)
|
||||
},
|
||||
warn (code) {
|
||||
console.warn(
|
||||
`[next-auth][warn][${code}]`,
|
||||
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`
|
||||
)
|
||||
},
|
||||
debug (code, metadata) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[next-auth][debug][${code}]`, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the built-in logger with user's implementation.
|
||||
* Any `undefined` level will use the default logger.
|
||||
*/
|
||||
export function setLogger (
|
||||
newLogger: Partial<LoggerInstance> = {},
|
||||
debug?: boolean
|
||||
) {
|
||||
// Turn off debug logging if `debug` isn't set to `true`
|
||||
if (!debug) { _logger.debug = () => {} }
|
||||
|
||||
if (newLogger.error) { _logger.error = newLogger.error }
|
||||
if (newLogger.warn) { _logger.warn = newLogger.warn }
|
||||
if (newLogger.debug) { _logger.debug = newLogger.debug }
|
||||
}
|
||||
|
||||
export default _logger
|
||||
|
||||
/** Serializes client-side log messages and sends them to the server */
|
||||
export function proxyLogger (
|
||||
logger: LoggerInstance = _logger,
|
||||
basePath?: string
|
||||
): LoggerInstance {
|
||||
try {
|
||||
if (typeof window === 'undefined') { return logger }
|
||||
|
||||
const clientLogger: Record<string, unknown> = {}
|
||||
for (const level in logger) {
|
||||
clientLogger[level] = (code: string, metadata: Error) => {
|
||||
_logger[level](code, metadata) // Logs to console
|
||||
|
||||
if (level === 'error') {
|
||||
metadata = formatError(metadata) as Error
|
||||
}(metadata as any).client = true
|
||||
const url = `${basePath}/_log`
|
||||
const body = new URLSearchParams({ level, code, ...(metadata as any) })
|
||||
if (navigator.sendBeacon) { return navigator.sendBeacon(url, body) }
|
||||
|
||||
return fetch(url, { method: 'POST', body, keepalive: true })
|
||||
}
|
||||
}
|
||||
return clientLogger as unknown as LoggerInstance
|
||||
} catch {
|
||||
return _logger
|
||||
}
|
||||
}
|
||||
34
apps/playground-nuxt/src/runtime/lib/parse-url.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export interface InternalUrl {
|
||||
/** @default "http://localhost:3000" */
|
||||
origin: string
|
||||
/** @default "localhost:3000" */
|
||||
host: string
|
||||
/** @default "/api/auth" */
|
||||
path: string
|
||||
/** @default "http://localhost:3000/api/auth" */
|
||||
base: string
|
||||
/** @default "http://localhost:3000/api/auth" */
|
||||
toString: () => string
|
||||
}
|
||||
|
||||
/** Returns an `URL` like object to make requests/redirects from server-side */
|
||||
export default function parseUrl (url?: string): InternalUrl {
|
||||
const defaultUrl = new URL('http://localhost:3000/api/auth')
|
||||
|
||||
if (url && !url.startsWith('http')) { url = `https://${url}` }
|
||||
|
||||
const _url = new URL(url ?? defaultUrl)
|
||||
const path = (_url.pathname === '/' ? defaultUrl.pathname : _url.pathname)
|
||||
// Remove trailing slash
|
||||
.replace(/\/$/, '')
|
||||
|
||||
const base = `${_url.origin}${path}`
|
||||
|
||||
return {
|
||||
origin: _url.origin,
|
||||
host: _url.host,
|
||||
path,
|
||||
base,
|
||||
toString: () => base
|
||||
}
|
||||
}
|
||||
7
apps/playground-nuxt/src/runtime/plugin.client.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// @ts-expect-error: Nuxt auto-import
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
import { SessionProviderPlugin } from './client'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(SessionProviderPlugin({}))
|
||||
})
|
||||
93
apps/playground-nuxt/src/runtime/server/handler.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { NextAuthAction, NextAuthOptions, Session } from 'next-auth'
|
||||
import type { RequestInternal } from 'next-auth/core'
|
||||
import { NextAuthHandler } from 'next-auth/core'
|
||||
import {
|
||||
appendHeader,
|
||||
defineEventHandler,
|
||||
isMethod,
|
||||
sendRedirect,
|
||||
setCookie,
|
||||
readBody,
|
||||
parseCookies,
|
||||
getQuery
|
||||
} from 'h3'
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
export function NextAuthNuxtHandler (options: NextAuthOptions) {
|
||||
return defineEventHandler(async (event) => {
|
||||
// Catch-all route params in Nuxt goes to the underscore property
|
||||
const nextauth = event.context.params._.split('/')
|
||||
|
||||
const req: RequestInternal | Request = {
|
||||
host: process.env.NEXTAUTH_URL,
|
||||
body: undefined,
|
||||
query: getQuery(event),
|
||||
headers: event.req.headers,
|
||||
method: event.req.method,
|
||||
cookies: parseCookies(event),
|
||||
action: nextauth[0] as NextAuthAction,
|
||||
providerId: nextauth[1],
|
||||
error: nextauth[1]
|
||||
}
|
||||
|
||||
if (isMethod(event, 'POST')) {
|
||||
req.body = await readBody(event)
|
||||
}
|
||||
|
||||
const response = await NextAuthHandler({
|
||||
req,
|
||||
options
|
||||
})
|
||||
|
||||
const { headers, cookies, body, redirect, status = 200 } = response
|
||||
event.res.statusCode = status
|
||||
|
||||
headers?.forEach((header) => {
|
||||
appendHeader(event, header.key, header.value)
|
||||
})
|
||||
|
||||
cookies?.forEach((cookie) => {
|
||||
setCookie(event, cookie.name, cookie.value, cookie.options)
|
||||
})
|
||||
|
||||
if (redirect) {
|
||||
if (isMethod(event, 'POST')) {
|
||||
const body = await readBody(event)
|
||||
if (body?.json !== 'true') { await sendRedirect(event, redirect, 302) }
|
||||
|
||||
return {
|
||||
url: redirect
|
||||
}
|
||||
} else {
|
||||
await sendRedirect(event, redirect, 302)
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
})
|
||||
}
|
||||
|
||||
export async function getServerSession (
|
||||
event: H3Event,
|
||||
options: NextAuthOptions
|
||||
): Promise<Session | null> {
|
||||
options.secret = process.env.NEXTAUTH_SECRET
|
||||
|
||||
const session = await NextAuthHandler<Session>({
|
||||
req: {
|
||||
host: process.env.NEXTAUTH_URL,
|
||||
action: 'session',
|
||||
method: 'GET',
|
||||
cookies: parseCookies(event),
|
||||
headers: event.req.headers
|
||||
},
|
||||
options
|
||||
})
|
||||
|
||||
const { body } = session
|
||||
|
||||
if (body && Object.keys(body).length) {
|
||||
return body
|
||||
}
|
||||
return null
|
||||
}
|
||||
78
apps/playground-nuxt/src/runtime/types.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Session } from 'next-auth'
|
||||
import type { BuiltInProviderType, ProviderType } from 'next-auth/providers'
|
||||
|
||||
export interface UseSessionOptions<R extends boolean> {
|
||||
required: R
|
||||
/** Defaults to `signIn` */
|
||||
onUnauthenticated?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Util type that matches some strings literally, but allows any other string as well.
|
||||
* @source https://github.com/microsoft/TypeScript/issues/29729#issuecomment-832522611
|
||||
*/
|
||||
export type LiteralUnion<T extends U, U = string> =
|
||||
| T
|
||||
| (U & Record<never, never>)
|
||||
|
||||
export interface ClientSafeProvider {
|
||||
id: LiteralUnion<BuiltInProviderType>
|
||||
name: string
|
||||
type: ProviderType
|
||||
signinUrl: string
|
||||
callbackUrl: string
|
||||
}
|
||||
|
||||
export interface SignInOptions extends Record<string, unknown> {
|
||||
/**
|
||||
* Defaults to the current URL.
|
||||
* @docs https://next-auth.js.org/getting-started/client#specifying-a-callbackurl
|
||||
*/
|
||||
callbackUrl?: string
|
||||
/** @docs https://next-auth.js.org/getting-started/client#using-the-redirect-false-option */
|
||||
redirect?: boolean
|
||||
}
|
||||
|
||||
export interface SignInResponse {
|
||||
error: string | undefined
|
||||
status: number
|
||||
ok: boolean
|
||||
url: string | null
|
||||
}
|
||||
|
||||
/** Match `inputType` of `new URLSearchParams(inputType)` */
|
||||
export type SignInAuthorizationParams =
|
||||
| string
|
||||
| string[][]
|
||||
| Record<string, string>
|
||||
| URLSearchParams
|
||||
|
||||
/** @docs https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */
|
||||
export interface SignOutResponse {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface SignOutParams<R extends boolean = true> {
|
||||
/** @docs https://next-auth.js.org/getting-started/client#specifying-a-callbackurl-1 */
|
||||
callbackUrl?: string
|
||||
/** @docs https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */
|
||||
redirect?: R
|
||||
}
|
||||
|
||||
/** @docs: https://next-auth.js.org/getting-started/client#options */
|
||||
export interface SessionProviderProps {
|
||||
// children: React.ReactNode
|
||||
session?: Session | null
|
||||
baseUrl?: string
|
||||
basePath?: string
|
||||
/**
|
||||
* A time interval (in seconds) after which the session will be re-fetched.
|
||||
* If set to `0` (default), the session is not polled.
|
||||
*/
|
||||
refetchInterval?: number
|
||||
/**
|
||||
* `SessionProvider` automatically refetches the session when the user switches between windows.
|
||||
* This option activates this behaviour if set to `true` (default).
|
||||
*/
|
||||
refetchOnWindowFocus?: boolean
|
||||
}
|
||||
4
apps/playground-nuxt/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://v3.nuxtjs.org/concepts/typescript
|
||||
"extends": "./playground/.nuxt/tsconfig.json"
|
||||
}
|
||||
@@ -19,22 +19,19 @@ npm install next-auth @next-auth/mongodb-adapter mongodb
|
||||
|
||||
```js
|
||||
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
|
||||
import { MongoClient } from "mongodb"
|
||||
|
||||
const uri = process.env.MONGODB_URI
|
||||
const options = {
|
||||
useUnifiedTopology: true,
|
||||
useNewUrlParser: true,
|
||||
}
|
||||
|
||||
let client
|
||||
let clientPromise
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error("Please add your Mongo URI to .env.local")
|
||||
throw new Error('Invalid/Missing environment variable: "MONGODB_URI"')
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const uri = process.env.MONGODB_URI
|
||||
const options = {}
|
||||
|
||||
let client
|
||||
let clientPromise: Promise<MongoClient>
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// In development mode, use a global variable so that the value
|
||||
// is preserved across module reloads caused by HMR (Hot Module Replacement).
|
||||
if (!global._mongoClientPromise) {
|
||||
|
||||
@@ -21,6 +21,7 @@ We have a list of official adapters that are distributed as their own packages u
|
||||
- [`neo4j`](./neo4j)
|
||||
- [`typeorm-legacy`](./typeorm)
|
||||
- [`sequelize`](./sequelize)
|
||||
- [`supabase`](./supabase)
|
||||
- [`dgraph`](./dgraph)
|
||||
- [`upstash-redis`](./upstash-redis)
|
||||
|
||||
|
||||
309
docs/docs/adapters/supabase.md
Normal file
@@ -0,0 +1,309 @@
|
||||
---
|
||||
id: supabase
|
||||
title: Supabase
|
||||
---
|
||||
|
||||
# Supabase
|
||||
|
||||
This is the Supabase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
:::note
|
||||
This adapter is developed by the community and not officially maintained or supported by Supabase. It uses the Supabase Database to store user and session data in a separate `next_auth` schema. It is a standalone Auth server that does not interface with Supabase Auth and therefore provides a different feature set.
|
||||
|
||||
If you’re looking for an officially maintained Auth server with additional features like [built-in email server](https://supabase.com/docs/guides/auth/auth-email#configure-email-settings?utm_source=next-auth-docs&medium=referral&campaign=next-auth), [phone auth](https://supabase.com/docs/guides/auth/auth-twilio?utm_source=next-auth-docs&medium=referral&campaign=next-auth), and [Multi Factor Authentication (MFA / 2FA)](https://supabase.com/contact/mfa?utm_source=next-auth-docs&medium=referral&campaign=next-auth), please use [Supabase Auth](https://supabase.com/auth) with the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
|
||||
:::
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `@supabase/supabase-js`, `next-auth` and `@next-auth/supabase-adapter`.
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install @supabase/supabase-js next-auth @next-auth/supabase-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [...],
|
||||
adapter: SupabaseAdapter({
|
||||
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
}),
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### Create the `next_auth` schema in Supabase
|
||||
|
||||
Setup your database as described in our main [schema](/adapters/models), by copying the SQL schema below in the Supabase [SQL Editor](https://app.supabase.com/project/_/sql).
|
||||
|
||||
Alternatively you can select the NextAuth Quickstart card on the [SQL Editor page](https://app.supabase.com/project/_/sql), or [create a migration with the Supabase CLI](https://supabase.com/docs/guides/cli/local-development#database-migrations?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
|
||||
|
||||
```sql
|
||||
--
|
||||
-- Name: next_auth; Type: SCHEMA;
|
||||
--
|
||||
CREATE SCHEMA next_auth;
|
||||
|
||||
GRANT USAGE ON SCHEMA next_auth TO service_role;
|
||||
GRANT ALL ON SCHEMA next_auth TO postgres;
|
||||
|
||||
--
|
||||
-- Create users table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.users
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name text,
|
||||
email text,
|
||||
"emailVerified" timestamp with time zone,
|
||||
image text,
|
||||
CONSTRAINT users_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT email_unique UNIQUE (email)
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.users TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.users TO service_role;
|
||||
|
||||
--- uid() function to be used in RLS policies
|
||||
CREATE FUNCTION next_auth.uid() RETURNS uuid
|
||||
LANGUAGE sql STABLE
|
||||
AS $$
|
||||
select
|
||||
coalesce(
|
||||
nullif(current_setting('request.jwt.claim.sub', true), ''),
|
||||
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
|
||||
)::uuid
|
||||
$$;
|
||||
|
||||
--
|
||||
-- Create sessions table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.sessions
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
expires timestamp with time zone NOT NULL,
|
||||
"sessionToken" text NOT NULL,
|
||||
"userId" uuid,
|
||||
CONSTRAINT sessions_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT sessionToken_unique UNIQUE ("sessionToken"),
|
||||
CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId")
|
||||
REFERENCES next_auth.users (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.sessions TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.sessions TO service_role;
|
||||
|
||||
--
|
||||
-- Create accounts table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.accounts
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
type text NOT NULL,
|
||||
provider text NOT NULL,
|
||||
"providerAccountId" text NOT NULL,
|
||||
refresh_token text,
|
||||
access_token text,
|
||||
expires_at bigint,
|
||||
token_type text,
|
||||
scope text,
|
||||
id_token text,
|
||||
session_state text,
|
||||
oauth_token_secret text,
|
||||
oauth_token text,
|
||||
"userId" uuid,
|
||||
CONSTRAINT accounts_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT provider_unique UNIQUE (provider, "providerAccountId"),
|
||||
CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId")
|
||||
REFERENCES next_auth.users (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.accounts TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.accounts TO service_role;
|
||||
|
||||
--
|
||||
-- Create verification_tokens table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.verification_tokens
|
||||
(
|
||||
identifier text,
|
||||
token text,
|
||||
expires timestamp with time zone NOT NULL,
|
||||
CONSTRAINT verification_tokens_pkey PRIMARY KEY (token),
|
||||
CONSTRAINT token_unique UNIQUE (token),
|
||||
CONSTRAINT token_identifier_unique UNIQUE (token, identifier)
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.verification_tokens TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.verification_tokens TO service_role;
|
||||
```
|
||||
|
||||
### Expose the `next_auth` schema in Supabase
|
||||
|
||||
Expose the `next_auth` schema via the Serverless API in the [API settings](https://app.supabase.com/project/_/settings/api) by adding `next_auth` to the "Exposed schemas" list.
|
||||
|
||||
When developing locally add `next_auth` to the `schemas` array in the `config.toml` file in the `supabase` folder that was generated by the [Supabase CLI](https://supabase.com/docs/guides/cli/local-development#initialize-your-project?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
|
||||
|
||||
## Enabling Row Level Security (RLS)
|
||||
|
||||
Postgres provides a powerful feature called [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security?utm_source=next-auth-docs&medium=referral&campaign=next-auth) to limit access to data.
|
||||
|
||||
This works by sending a signed JWT to your [Supabase Serverless API](https://supabase.com/docs/guides/api?utm_source=next-auth-docs&medium=referral&campaign=next-auth). There is two steps to make this work with NextAuth:
|
||||
|
||||
### 1. Generate the Supabase `access_token` JWT in the session callback
|
||||
|
||||
To sign the JWT use the `jsonwebtoken` package:
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install jsonwebtoken
|
||||
```
|
||||
|
||||
Using the [NexthAuth Session callback](https://next-auth.js.org/configuration/callbacks#session-callback) create the Supabase `access_token` and append it to the `session` object.
|
||||
|
||||
To sign the JWT use the Supabase JWT secret which can be found in the [API settings](https://app.supabase.com/project/_/settings/api)
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
|
||||
import jwt from "jsonwebtoken"
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [...],
|
||||
adapter: SupabaseAdapter({
|
||||
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
}),
|
||||
callbacks: {
|
||||
async session({ session, user }) {
|
||||
const signingSecret = process.env.SUPABASE_JWT_SECRET
|
||||
if (signingSecret) {
|
||||
const payload = {
|
||||
aud: "authenticated",
|
||||
exp: Math.floor(new Date(session.expires).getTime() / 1000),
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
role: "authenticated",
|
||||
}
|
||||
session.supabaseAccessToken = jwt.sign(payload, signingSecret)
|
||||
}
|
||||
return session
|
||||
},
|
||||
},
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Inject the Supabase `access_token` JWT into the Supabase Client
|
||||
|
||||
For example, given the following public schema:
|
||||
|
||||
```sql
|
||||
/**
|
||||
* USERS
|
||||
* Note: This table contains user data. Users should only be able to view and update their own data.
|
||||
*/
|
||||
create table users (
|
||||
-- UUID from next_auth.users
|
||||
id uuid not null primary key,
|
||||
name text,
|
||||
email text,
|
||||
image text,
|
||||
constraint "users_id_fkey" foreign key ("id")
|
||||
references next_auth.users (id) match simple
|
||||
on update no action
|
||||
on delete cascade -- if user is deleted in NextAuth they will also be deleted in our public table.
|
||||
);
|
||||
alter table users enable row level security;
|
||||
create policy "Can view own user data." on users for select using (next_auth.uid() = id);
|
||||
create policy "Can update own user data." on users for update using (next_auth.uid() = id);
|
||||
|
||||
/**
|
||||
* This trigger automatically creates a user entry when a new user signs up via NextAuth.
|
||||
*/
|
||||
create function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.users (id, name, email, image)
|
||||
values (new.id, new.name, new.email, new.image);
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
create trigger on_auth_user_created
|
||||
after insert on next_auth.users
|
||||
for each row execute procedure public.handle_new_user();
|
||||
```
|
||||
|
||||
The `supabaseAccessToken` is now available on the `session` object and can be passed to the supabase-js client. This works in any environment: client-side, server-side (API routes, SSR), as well as in middleware edge functions!
|
||||
|
||||
```js
|
||||
// ...
|
||||
// Use `useSession()` or `unstable_getServerSession()` to get the NextAuth 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("*")
|
||||
```
|
||||
|
||||
## Usage with TypeScript
|
||||
|
||||
You can pass types that were [generated with the Supabase CLI](/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion.
|
||||
|
||||
Creating a new supabase client object:
|
||||
|
||||
```tsx
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
import { Database } from "../database.types"
|
||||
|
||||
const supabase = createClient<Database>()
|
||||
```
|
||||
|
||||
### Extend the session type with the `supabaseAccessToken`
|
||||
|
||||
In order to extend the `session` object with the `supabaseAccessToken` we need to extend the `session` interface in a `types/next-auth.d.ts` file:
|
||||
|
||||
```ts title="types/next-auth.d.ts"
|
||||
import NextAuth, { DefaultSession } 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
|
||||
} & DefaultSession["user"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -24,7 +24,7 @@ export const authOptions: NextAuthOptions = {
|
||||
export default NextAuth(authOptions);
|
||||
```
|
||||
|
||||
In `getServerSideProps`:
|
||||
### In `getServerSideProps`:
|
||||
```js
|
||||
import { authOptions } from 'pages/api/auth/[...nextauth]'
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
@@ -48,7 +48,8 @@ export async function getServerSideProps(context) {
|
||||
}
|
||||
}
|
||||
```
|
||||
In API routes:
|
||||
|
||||
### In API Routes:
|
||||
```js
|
||||
import { authOptions } from 'pages/api/auth/[...nextauth]'
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
@@ -68,6 +69,23 @@ export async function handler(req, res) {
|
||||
}
|
||||
```
|
||||
|
||||
### In `app/` directory:
|
||||
|
||||
You can also use `unstable_getServerSession` in Next.js' server components:
|
||||
|
||||
```tsx
|
||||
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>
|
||||
}
|
||||
```
|
||||
|
||||
:::warning
|
||||
Currently, the underlying Next.js `cookies()` method does [only provides read access](https://beta.nextjs.org/docs/api-reference/cookies) to the request cookies. This means that the `expires` value is stripped away from `session` in Server Components. Furthermore, there is a hard expiry on sessions, after which the user will be required to sign in again. (The default expiry is 30 days).
|
||||
:::
|
||||
|
||||
## Middleware
|
||||
|
||||
You can use a Next.js Middleware with NextAuth.js to protect your site.
|
||||
|
||||
@@ -83,7 +83,7 @@ TWITTER_SECRET=YOUR_TWITTER_CLIENT_SECRET
|
||||
4. Now you can add the provider settings to the NextAuth.js options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import TwitterProvider from "next-auth/providers/"
|
||||
import TwitterProvider from "next-auth/providers/twitter"
|
||||
...
|
||||
providers: [
|
||||
TwitterProvider({
|
||||
|
||||
@@ -65,6 +65,7 @@ module.exports = {
|
||||
"adapters/neo4j",
|
||||
"adapters/typeorm",
|
||||
"adapters/sequelize",
|
||||
"adapters/supabase",
|
||||
"adapters/mikro-orm",
|
||||
"adapters/dgraph",
|
||||
"adapters/upstash-redis",
|
||||
|
||||
@@ -24,7 +24,7 @@ export const authOptions: NextAuthOptions = {
|
||||
export default NextAuth(authOptions);
|
||||
```
|
||||
|
||||
In `getServerSideProps`:
|
||||
### In `getServerSideProps`:
|
||||
```js
|
||||
import { authOptions } from 'pages/api/[...nextauth]'
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
@@ -48,7 +48,8 @@ export async function getServerSideProps(context) {
|
||||
}
|
||||
}
|
||||
```
|
||||
In API routes:
|
||||
|
||||
### In API routes:
|
||||
```js
|
||||
import { authOptions } from 'pages/api/[...nextauth]'
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
@@ -68,6 +69,23 @@ export async function handler(req, res) {
|
||||
}
|
||||
```
|
||||
|
||||
### In `app/` directory:
|
||||
|
||||
You can also use `unstable_getServerSession` in Next.js' server components:
|
||||
|
||||
```tsx
|
||||
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>
|
||||
}
|
||||
```
|
||||
|
||||
:::warning
|
||||
Currently, the underlying Next.js `cookies()` method does [only provides read access](https://beta.nextjs.org/docs/api-reference/cookies) to the request cookies. This means that the `expires` value is stripped away from `session` in Server Components. Furthermore, there is a hard expiry on sessions, after which the user will be required to sign in again. (The default expiry is 30 days).
|
||||
:::
|
||||
|
||||
## Middleware
|
||||
|
||||
You can use a Next.js Middleware with NextAuth.js to protect your site.
|
||||
|
||||
3
packages/adapter-supabase/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# Run `supabase start` in this directory to get the Supabase API URL and service role key!
|
||||
SUPABASE_URL=http://localhost:54321
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU
|
||||
57
packages/adapter-supabase/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
<p align="center">
|
||||
<br/>
|
||||
<a href="https://next-auth.js.org" target="_blank">
|
||||
<img height="64px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a><img height="64px" src="./logo.svg" />
|
||||
<h3 align="center"><b>Supabase Adapter</b> - NextAuth.js</h3>
|
||||
<p align="center">
|
||||
Open Source. Full Stack. Own Your Data.
|
||||
</p>
|
||||
<p align="center" style="align: center;">
|
||||
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg?branch=main" alt="Build Test" />
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/@next-auth/supabase-adapter/latest" alt="Bundle Size"/>
|
||||
<img src="https://img.shields.io/npm/v/@next-auth/supabase-adapter" alt="@next-auth/supabase-adapter Version" />
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## Overview
|
||||
|
||||
This is the Supabase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
You can find more Supabase information in the docs at [next-auth.js.org/adapters/supabase](https://next-auth.js.org/adapters/supabase).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `@supabase/supabase-js`, `next-auth` and `@next-auth/supabase-adapter`.
|
||||
|
||||
```js
|
||||
npm install @supabase/supabase-js next-auth @next-auth/supabase-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```js
|
||||
import NextAuth from "next-auth"
|
||||
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
// ...
|
||||
],
|
||||
adapter: SupabaseAdapter({
|
||||
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
}),
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
ISC
|
||||
15
packages/adapter-supabase/logo.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="109" height="113" viewBox="0 0 109 113" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
|
||||
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#249361"/>
|
||||
<stop offset="1" stop-color="#3ECF8E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
40
packages/adapter-supabase/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@next-auth/supabase-adapter",
|
||||
"version": "0.2.0",
|
||||
"description": "Supabase adapter for next-auth.",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
"bugs": {
|
||||
"url": "https://github.com/nextauthjs/next-auth/issues"
|
||||
},
|
||||
"author": "Martin Sonnberger <martin.sonnberger@icloud.com>",
|
||||
"main": "dist/index.js",
|
||||
"keywords": [
|
||||
"next-auth",
|
||||
"next.js",
|
||||
"supabase"
|
||||
],
|
||||
"license": "ISC",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "./tests/test.sh"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.0.5",
|
||||
"next-auth": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next-auth/adapter-test": "workspace:^0.0.0",
|
||||
"@next-auth/tsconfig": "workspace:^0.0.0",
|
||||
"@supabase/supabase-js": "^2.0.5",
|
||||
"jest": "^27.4.3",
|
||||
"next-auth": "workspace:*"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "@next-auth/adapter-test/jest"
|
||||
}
|
||||
}
|
||||
140
packages/adapter-supabase/src/database.types.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json }
|
||||
| Json[]
|
||||
|
||||
export interface Database {
|
||||
next_auth: {
|
||||
Tables: {
|
||||
accounts: {
|
||||
Row: {
|
||||
id: string
|
||||
type: string | null
|
||||
provider: string | null
|
||||
providerAccountId: string | null
|
||||
refresh_token: string | null
|
||||
access_token: string | null
|
||||
expires_at: number | null
|
||||
token_type: string | null
|
||||
scope: string | null
|
||||
id_token: string | null
|
||||
session_state: string | null
|
||||
oauth_token_secret: string | null
|
||||
oauth_token: string | null
|
||||
userId: string | null
|
||||
}
|
||||
Insert: {
|
||||
id?: string
|
||||
type?: string | null
|
||||
provider?: string | null
|
||||
providerAccountId?: string | null
|
||||
refresh_token?: string | null
|
||||
access_token?: string | null
|
||||
expires_at?: number | null
|
||||
token_type?: string | null
|
||||
scope?: string | null
|
||||
id_token?: string | null
|
||||
session_state?: string | null
|
||||
oauth_token_secret?: string | null
|
||||
oauth_token?: string | null
|
||||
userId?: string | null
|
||||
}
|
||||
Update: {
|
||||
id?: string
|
||||
type?: string | null
|
||||
provider?: string | null
|
||||
providerAccountId?: string | null
|
||||
refresh_token?: string | null
|
||||
access_token?: string | null
|
||||
expires_at?: number | null
|
||||
token_type?: string | null
|
||||
scope?: string | null
|
||||
id_token?: string | null
|
||||
session_state?: string | null
|
||||
oauth_token_secret?: string | null
|
||||
oauth_token?: string | null
|
||||
userId?: string | null
|
||||
}
|
||||
}
|
||||
sessions: {
|
||||
Row: {
|
||||
expires: string | null
|
||||
sessionToken: string | null
|
||||
userId: string | null
|
||||
id: string
|
||||
}
|
||||
Insert: {
|
||||
expires?: string | null
|
||||
sessionToken?: string | null
|
||||
userId?: string | null
|
||||
id?: string
|
||||
}
|
||||
Update: {
|
||||
expires?: string | null
|
||||
sessionToken?: string | null
|
||||
userId?: string | null
|
||||
id?: string
|
||||
}
|
||||
}
|
||||
users: {
|
||||
Row: {
|
||||
name: string | null
|
||||
email: string | null
|
||||
emailVerified: string | null
|
||||
image: string | null
|
||||
id: string
|
||||
}
|
||||
Insert: {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
emailVerified?: string | null
|
||||
image?: string | null
|
||||
id?: string
|
||||
}
|
||||
Update: {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
emailVerified?: string | null
|
||||
image?: string | null
|
||||
id?: string
|
||||
}
|
||||
}
|
||||
verification_tokens: {
|
||||
Row: {
|
||||
id: number
|
||||
identifier: string | null
|
||||
token: string | null
|
||||
expires: string | null
|
||||
}
|
||||
Insert: {
|
||||
id?: number
|
||||
identifier?: string | null
|
||||
token?: string | null
|
||||
expires?: string | null
|
||||
}
|
||||
Update: {
|
||||
id?: number
|
||||
identifier?: string | null
|
||||
token?: string | null
|
||||
expires?: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
uid: {
|
||||
Args: Record<PropertyKey, never>
|
||||
Returns: string
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
212
packages/adapter-supabase/src/index.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
import { Database } from "./database.types"
|
||||
import {
|
||||
Adapter,
|
||||
AdapterSession,
|
||||
AdapterUser,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
|
||||
function isDate(date: any) {
|
||||
return (
|
||||
new Date(date).toString() !== "Invalid Date" && !isNaN(Date.parse(date))
|
||||
)
|
||||
}
|
||||
|
||||
export function format<T>(obj: Record<string, any>): T {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (value === null) {
|
||||
delete obj[key]
|
||||
}
|
||||
|
||||
if (isDate(value)) {
|
||||
obj[key] = new Date(value)
|
||||
}
|
||||
}
|
||||
|
||||
return obj as T
|
||||
}
|
||||
|
||||
export const SupabaseAdapter = ({
|
||||
url,
|
||||
secret,
|
||||
}: {
|
||||
url: string
|
||||
secret: string
|
||||
}): Adapter => {
|
||||
const supabase = createClient<Database, "next_auth">(url, secret, {
|
||||
db: { schema: "next_auth" },
|
||||
global: {
|
||||
headers: { "X-Client-Info": "@next-auth/supabase-adapter@0.1.0" },
|
||||
},
|
||||
})
|
||||
return {
|
||||
async createUser(user) {
|
||||
const { data, error } = await supabase
|
||||
.from("users")
|
||||
.insert({
|
||||
...user,
|
||||
emailVerified: user.emailVerified?.toISOString(),
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return format<AdapterUser>(data)
|
||||
},
|
||||
async getUser(id) {
|
||||
const { data, error } = await supabase
|
||||
.from("users")
|
||||
.select()
|
||||
.eq("id", id)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data) return null
|
||||
|
||||
return format<AdapterUser>(data)
|
||||
},
|
||||
async getUserByEmail(email) {
|
||||
const { data, error } = await supabase
|
||||
.from("users")
|
||||
.select()
|
||||
.eq("email", email)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data) return null
|
||||
|
||||
return format<AdapterUser>(data)
|
||||
},
|
||||
async getUserByAccount({ providerAccountId, provider }) {
|
||||
const { data, error } = await supabase
|
||||
.from("accounts")
|
||||
.select("users (*)")
|
||||
.match({ provider, providerAccountId })
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data || !data.users) return null
|
||||
|
||||
return format<AdapterUser>(data.users)
|
||||
},
|
||||
async updateUser(user) {
|
||||
const { data, error } = await supabase
|
||||
.from("users")
|
||||
.update({
|
||||
...user,
|
||||
emailVerified: user.emailVerified?.toISOString(),
|
||||
})
|
||||
.eq("id", user.id)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return format<AdapterUser>(data)
|
||||
},
|
||||
async deleteUser(userId) {
|
||||
const { error } = await supabase.from("users").delete().eq("id", userId)
|
||||
|
||||
if (error) throw error
|
||||
},
|
||||
async linkAccount(account) {
|
||||
const { error } = await supabase.from("accounts").insert(account)
|
||||
|
||||
if (error) throw error
|
||||
},
|
||||
async unlinkAccount({ providerAccountId, provider }) {
|
||||
const { error } = await supabase
|
||||
.from("accounts")
|
||||
.delete()
|
||||
.match({ provider, providerAccountId })
|
||||
|
||||
if (error) throw error
|
||||
},
|
||||
async createSession({ sessionToken, userId, expires }) {
|
||||
const { data, error } = await supabase
|
||||
.from("sessions")
|
||||
.insert({ sessionToken, userId, expires: expires.toISOString() })
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return format<AdapterSession>(data)
|
||||
},
|
||||
async getSessionAndUser(sessionToken) {
|
||||
const { data, error } = await supabase
|
||||
.from("sessions")
|
||||
.select("*, users(*)")
|
||||
.eq("sessionToken", sessionToken)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data) return null
|
||||
|
||||
const { users: user, ...session } = data
|
||||
|
||||
return {
|
||||
user: format<AdapterUser>(
|
||||
user as Database["next_auth"]["Tables"]["users"]["Row"]
|
||||
),
|
||||
session: format<AdapterSession>(session),
|
||||
}
|
||||
},
|
||||
async updateSession(session) {
|
||||
const { data, error } = await supabase
|
||||
.from("sessions")
|
||||
.update({
|
||||
...session,
|
||||
expires: session.expires?.toISOString(),
|
||||
})
|
||||
.eq("sessionToken", session.sessionToken)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
return format<AdapterSession>(data)
|
||||
},
|
||||
async deleteSession(sessionToken) {
|
||||
const { error } = await supabase
|
||||
.from("sessions")
|
||||
.delete()
|
||||
.eq("sessionToken", sessionToken)
|
||||
|
||||
if (error) throw error
|
||||
},
|
||||
async createVerificationToken(token) {
|
||||
const { data, error } = await supabase
|
||||
.from("verification_tokens")
|
||||
.insert({
|
||||
...token,
|
||||
expires: token.expires.toISOString(),
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
const { id, ...verificationToken } = data
|
||||
|
||||
return format<VerificationToken>(verificationToken)
|
||||
},
|
||||
async useVerificationToken({ identifier, token }) {
|
||||
const { data, error } = await supabase
|
||||
.from("verification_tokens")
|
||||
.delete()
|
||||
.match({ identifier, token })
|
||||
.select()
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data) return null
|
||||
|
||||
const { id, ...verificationToken } = data
|
||||
|
||||
return format<VerificationToken>(verificationToken)
|
||||
},
|
||||
}
|
||||
}
|
||||
44
packages/adapter-supabase/supabase/config.toml
Normal file
@@ -0,0 +1,44 @@
|
||||
# A string used to distinguish different Supabase projects on the same host. Defaults to the working
|
||||
# directory name when running `supabase init`.
|
||||
project_id = "nextauth"
|
||||
|
||||
[api]
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. public and storage are always included.
|
||||
schemas = ["next_auth"]
|
||||
# Extra schemas to add to the search_path of every request.
|
||||
extra_search_path = ["extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 14
|
||||
|
||||
[studio]
|
||||
# Port to use for Supabase Studio.
|
||||
port = 54323
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
|
||||
[auth]
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://localhost:3000"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://localhost:3000"]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
|
||||
# week).
|
||||
jwt_expiry = 3600
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
@@ -0,0 +1,101 @@
|
||||
--
|
||||
-- Name: next_auth; Type: SCHEMA;
|
||||
--
|
||||
CREATE SCHEMA next_auth;
|
||||
|
||||
GRANT USAGE ON SCHEMA next_auth TO service_role;
|
||||
GRANT ALL ON SCHEMA next_auth TO postgres;
|
||||
|
||||
--
|
||||
-- Create users table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.users
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name text,
|
||||
email text,
|
||||
"emailVerified" timestamp with time zone,
|
||||
image text,
|
||||
CONSTRAINT users_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT email_unique UNIQUE (email)
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.users TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.users TO service_role;
|
||||
|
||||
--- uid() function to be used in RLS policies
|
||||
CREATE FUNCTION next_auth.uid() RETURNS uuid
|
||||
LANGUAGE sql STABLE
|
||||
AS $$
|
||||
select
|
||||
coalesce(
|
||||
nullif(current_setting('request.jwt.claim.sub', true), ''),
|
||||
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
|
||||
)::uuid
|
||||
$$;
|
||||
|
||||
--
|
||||
-- Create sessions table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.sessions
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
expires timestamp with time zone NOT NULL,
|
||||
"sessionToken" text NOT NULL,
|
||||
"userId" uuid,
|
||||
CONSTRAINT sessions_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT sessionToken_unique UNIQUE ("sessionToken"),
|
||||
CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId")
|
||||
REFERENCES next_auth.users (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.sessions TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.sessions TO service_role;
|
||||
|
||||
--
|
||||
-- Create accounts table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.accounts
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
type text NOT NULL,
|
||||
provider text NOT NULL,
|
||||
"providerAccountId" text NOT NULL,
|
||||
refresh_token text,
|
||||
access_token text,
|
||||
expires_at bigint,
|
||||
token_type text,
|
||||
scope text,
|
||||
id_token text,
|
||||
session_state text,
|
||||
oauth_token_secret text,
|
||||
oauth_token text,
|
||||
"userId" uuid,
|
||||
CONSTRAINT accounts_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT provider_unique UNIQUE (provider, "providerAccountId"),
|
||||
CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId")
|
||||
REFERENCES next_auth.users (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.accounts TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.accounts TO service_role;
|
||||
|
||||
--
|
||||
-- Create verification_tokens table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.verification_tokens
|
||||
(
|
||||
identifier text,
|
||||
token text,
|
||||
expires timestamp with time zone NOT NULL,
|
||||
CONSTRAINT verification_tokens_pkey PRIMARY KEY (token),
|
||||
CONSTRAINT token_unique UNIQUE (token),
|
||||
CONSTRAINT token_identifier_unique UNIQUE (token, identifier)
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.verification_tokens TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.verification_tokens TO service_role;
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* USERS
|
||||
* Note: This table contains user data. Users should only be able to view and update their own data.
|
||||
*/
|
||||
create table users (
|
||||
-- UUID from next_auth.users
|
||||
id uuid not null primary key,
|
||||
name text,
|
||||
email text,
|
||||
image text,
|
||||
constraint "users_id_fkey" foreign key ("id")
|
||||
references next_auth.users (id) match simple
|
||||
on update no action
|
||||
on delete cascade -- if user is deleted in NextAuth they will also be deleted in our public table.
|
||||
);
|
||||
alter table users enable row level security;
|
||||
create policy "Can view own user data." on users for select using (next_auth.uid() = id);
|
||||
create policy "Can update own user data." on users for update using (next_auth.uid() = id);
|
||||
|
||||
/**
|
||||
* This trigger automatically creates a user entry when a new user signs up via NextAuth.
|
||||
*/
|
||||
create function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.users (id, name, email, image)
|
||||
values (new.id, new.name, new.email, new.image);
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
create trigger on_auth_user_created
|
||||
after insert on next_auth.users
|
||||
for each row execute procedure public.handle_new_user();
|
||||
76
packages/adapter-supabase/tests/index.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { runBasicTests } from "@next-auth/adapter-test"
|
||||
import { format, SupabaseAdapter } from "../src"
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
import type {
|
||||
AdapterSession,
|
||||
AdapterUser,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
import type { Account } from "next-auth"
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.SUPABASE_URL ?? "http://localhost:54321",
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY ??
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU",
|
||||
{ db: { schema: "next_auth" } }
|
||||
)
|
||||
|
||||
runBasicTests({
|
||||
adapter: SupabaseAdapter({
|
||||
url: process.env.SUPABASE_URL ?? "http://localhost:54321",
|
||||
secret:
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY ??
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSJ9.vI9obAHOGyVVKa3pD--kJlyxp-Z2zV9UUMAhKpNLAcU",
|
||||
}),
|
||||
db: {
|
||||
async session(sessionToken) {
|
||||
const { data, error } = await supabase
|
||||
.from("sessions")
|
||||
.select()
|
||||
.eq("sessionToken", sessionToken)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data) return null
|
||||
|
||||
return format<AdapterSession>(data)
|
||||
},
|
||||
async user(id) {
|
||||
const { data, error } = await supabase
|
||||
.from("users")
|
||||
.select()
|
||||
.eq("id", id)
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data) return null
|
||||
|
||||
return format<AdapterUser>(data)
|
||||
},
|
||||
async account({ provider, providerAccountId }) {
|
||||
const { data, error } = await supabase
|
||||
.from("accounts")
|
||||
.select()
|
||||
.match({ provider, providerAccountId })
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!data) return null
|
||||
|
||||
return format<Account>(data)
|
||||
},
|
||||
async verificationToken({ identifier, token }) {
|
||||
const { data, error } = await supabase
|
||||
.from("verification_tokens")
|
||||
.select()
|
||||
.match({ identifier, token })
|
||||
.single()
|
||||
|
||||
if (error) throw error
|
||||
|
||||
const { id, ...verificationToken } = data
|
||||
|
||||
return format<VerificationToken>(verificationToken)
|
||||
},
|
||||
},
|
||||
})
|
||||
19
packages/adapter-supabase/tests/test.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# install Supabase CLI when run on CI
|
||||
if [ "$CI" = true ]; then
|
||||
wget -O supabase.deb https://github.com/supabase/cli/releases/download/v0.29.0/supabase_0.29.0_linux_amd64.deb
|
||||
sudo dpkg -i supabase.deb
|
||||
fi
|
||||
|
||||
# Start Supabase, grep key and set it as SUPABASE_SERVICE_ROLE_KEY environment variable
|
||||
line=$(supabase start | grep 'service_role key')
|
||||
IFS=':'; arr=($line); unset IFS;
|
||||
export SUPABASE_SERVICE_ROLE_KEY=${arr[1]}
|
||||
|
||||
# Always stop Supabase, but exit with 1 when tests are failing
|
||||
if npx jest; then
|
||||
supabase stop
|
||||
else
|
||||
supabase stop && exit 1
|
||||
fi
|
||||
8
packages/adapter-supabase/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"exclude": ["tests", "dist", "jest.config.js"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "4.15.1",
|
||||
"version": "4.17.0",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
|
||||
4
packages/next-auth/provider-logos/apple-dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 32 376.4 449.4" width="32" height="32">
|
||||
<title>Apple icon</title>
|
||||
<path fill="#fff" d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 588 B |
4
packages/next-auth/provider-logos/apple.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 32 376.4 449.4" width="32" height="32">
|
||||
<title>Apple icon</title>
|
||||
<path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 576 B |
8
packages/next-auth/provider-logos/atlassian-dark.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg viewBox="0.29136862699701993 -41.138268758326056 145.22149045698177 186.73799623391153" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
|
||||
<linearGradient id="a" gradientTransform="matrix(1 0 0 -1 0 228)" gradientUnits="userSpaceOnUse" x1="62.57" x2="25.03" y1="150.13" y2="85.11">
|
||||
<stop offset="0" stop-color="#0052cc"/>
|
||||
<stop offset=".92" stop-color="#2684ff"/>
|
||||
</linearGradient>
|
||||
<path d="M43 67a4.14 4.14 0 0 0-5.79-.78A4.29 4.29 0 0 0 36 67.73L.45 138.85a4.25 4.25 0 0 0 1.9 5.7 4.18 4.18 0 0 0 1.9.45h49.53a4.08 4.08 0 0 0 3.8-2.35C68.27 120.57 61.79 87 43 67z" fill="url(#a)"/>
|
||||
<path d="M69.13 2.28a93.82 93.82 0 0 0-5.48 92.61l23.88 47.76a4.25 4.25 0 0 0 3.8 2.35h49.52a4.24 4.24 0 0 0 4.25-4.25 4.31 4.31 0 0 0-.44-1.9L76.36 2.26a4 4 0 0 0-7.23 0z" fill="#2684ff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 810 B |
4
packages/next-auth/provider-logos/atlassian.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg viewBox="0.29136862699701993 -41.138268758326056 145.22149045698177 186.73799623391153" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
|
||||
<path d="M43 67a4.14 4.14 0 0 0-5.79-.78A4.29 4.29 0 0 0 36 67.73L.45 138.85a4.25 4.25 0 0 0 1.9 5.7 4.18 4.18 0 0 0 1.9.45h49.53a4.08 4.08 0 0 0 3.8-2.35C68.27 120.57 61.79 87 43 67z" fill="#fff"/>
|
||||
<path d="M69.13 2.28a93.82 93.82 0 0 0-5.48 92.61l23.88 47.76a4.25 4.25 0 0 0 3.8 2.35h49.52a4.24 4.24 0 0 0 4.25-4.25 4.31 4.31 0 0 0-.44-1.9L76.36 2.26a4 4 0 0 0-7.23 0z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 549 B |
12
packages/next-auth/provider-logos/auth0-dark.svg
Normal file
@@ -0,0 +1,12 @@
|
||||
<svg width="32" height="32" viewBox="0 0 41 45" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M35.3018 0H20.5L25.0737 14.076H39.8755L27.9009 22.4701L32.4746 36.6253C40.1827 31.081 42.7027 22.6883 39.8755 14.076L35.3018 0Z" fill="white"/>
|
||||
<path d="M1.12504 14.076H15.9268L20.5005 0H5.69875L1.12504 14.076C-1.70213 22.6898 0.8178 31.081 8.52592 36.6253L13.0996 22.4701L1.12504 14.076Z" fill="white"/>
|
||||
<path d="M8.52539 36.6251L20.5 44.9998L32.4746 36.6251L20.5 28.1084L8.52539 36.6251Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="41" height="45" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 677 B |
3
packages/next-auth/provider-logos/auth0.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 256 287" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet">
|
||||
<path d="M203.24 231.531l-28.73-88.434 75.208-54.64h-92.966L128.019.025l-.009-.024h92.98l28.74 88.446.002-.002.024-.013c16.69 51.31-.5 109.67-46.516 143.098zm-150.45 0l-.023.017 75.228 54.655 75.245-54.67-75.221-54.656-75.228 54.654zM6.295 88.434c-17.57 54.088 2.825 111.4 46.481 143.108l.007-.028 28.735-88.429-75.192-54.63h92.944L128.004.024 128.01 0H35.025L6.294 88.434z" fill="#EB5424"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 523 B |
3
packages/next-auth/provider-logos/azure-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 59.242 47.271" width="32" height="32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m32.368 0-17.468 15.145-14.9 26.75h13.437zm2.323 3.543-7.454 21.008 14.291 17.956-27.728 4.764h45.442z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
3
packages/next-auth/provider-logos/azure.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 59.242 47.271" width="32" height="32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m32.368 0-17.468 15.145-14.9 26.75h13.437zm2.323 3.543-7.454 21.008 14.291 17.956-27.728 4.764h45.442z" fill="#0072c6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 231 B |
3
packages/next-auth/provider-logos/battlenet-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 580.44" width="32" height="32">
|
||||
<path d="M473.49,197.63c-75.94-35.11-185.08-57.42-287.78-49.11,5.15-34,17.85-57.69,38.7-62.68,28.69-6.88,60,12,89.84,46.35,19.55,2.53,42.73,7,58.86,10.71C318.7,40.56,245.72-16.8,190.21,4.36,148,20.47,126.39,78.59,129,156.69c-55,11.7-97.87,32.49-125.31,62.39-1.39,1.61-4.53,5.67-3.41,7.61.85,1.47,3.65-.18,4.85-1,31.83-22.26,72.58-34.31,125.66-41.89,7.56,83.32,42.81,189,101.36,273.78-32,12.56-58.89,13.39-73.64-2.17-20.29-21.41-19.61-57.95-4.77-101-7.58-18.2-15.31-40.51-20.15-56.34C72.12,396.41,58.93,488.29,105,525.78c35.07,28.52,96.18,18.15,162.54-23.12,37.64,41.79,77.07,68.51,116.69,77.33,2.09.39,7.17,1.09,8.29-.85.85-1.47-2-3.08-3.28-3.71-35.19-16.44-66-45.71-99.1-87.88C358.53,439.34,432.42,356,476.57,262.88c26.9,21.47,41,44.3,34.94,64.85-8.4,28.29-40.38,46-85.06,54.63C414.48,398,399,415.86,387.74,428c115.84,4,202-30.47,211.43-89.12,7.17-44.64-32.37-92.38-101.3-129.21,17.38-53.49,20.8-101,8.63-139.72-.71-2-2.64-6.76-4.88-6.76-1.7,0-1.68,3.26-1.58,4.7C503.41,106.55,493.47,147.88,473.49,197.63ZM260.21,444.33c-49-78.61-77.24-171.21-77.06-264.84h0C275.71,176.39,370,198.2,451,245.17h0c-43.59,81.71-109.64,152.49-190.82,199.15Z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
packages/next-auth/provider-logos/battlenet.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 580.44" width="32" height="32">
|
||||
<path d="M473.49,197.63c-75.94-35.11-185.08-57.42-287.78-49.11,5.15-34,17.85-57.69,38.7-62.68,28.69-6.88,60,12,89.84,46.35,19.55,2.53,42.73,7,58.86,10.71C318.7,40.56,245.72-16.8,190.21,4.36,148,20.47,126.39,78.59,129,156.69c-55,11.7-97.87,32.49-125.31,62.39-1.39,1.61-4.53,5.67-3.41,7.61.85,1.47,3.65-.18,4.85-1,31.83-22.26,72.58-34.31,125.66-41.89,7.56,83.32,42.81,189,101.36,273.78-32,12.56-58.89,13.39-73.64-2.17-20.29-21.41-19.61-57.95-4.77-101-7.58-18.2-15.31-40.51-20.15-56.34C72.12,396.41,58.93,488.29,105,525.78c35.07,28.52,96.18,18.15,162.54-23.12,37.64,41.79,77.07,68.51,116.69,77.33,2.09.39,7.17,1.09,8.29-.85.85-1.47-2-3.08-3.28-3.71-35.19-16.44-66-45.71-99.1-87.88C358.53,439.34,432.42,356,476.57,262.88c26.9,21.47,41,44.3,34.94,64.85-8.4,28.29-40.38,46-85.06,54.63C414.48,398,399,415.86,387.74,428c115.84,4,202-30.47,211.43-89.12,7.17-44.64-32.37-92.38-101.3-129.21,17.38-53.49,20.8-101,8.63-139.72-.71-2-2.64-6.76-4.88-6.76-1.7,0-1.68,3.26-1.58,4.7C503.41,106.55,493.47,147.88,473.49,197.63ZM260.21,444.33c-49-78.61-77.24-171.21-77.06-264.84h0C275.71,176.39,370,198.2,451,245.17h0c-43.59,81.71-109.64,152.49-190.82,199.15Z" fill="#148eff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
packages/next-auth/provider-logos/box-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 444.893 245.414">
|
||||
<g fill="#fff">
|
||||
<path d="M239.038 72.43c-33.081 0-61.806 18.6-76.322 45.904-14.516-27.305-43.24-45.902-76.32-45.902-19.443 0-37.385 6.424-51.821 17.266V16.925h-.008C34.365 7.547 26.713 0 17.286 0 7.858 0 .208 7.547.008 16.925H0v143.333h.036c.768 47.051 39.125 84.967 86.359 84.967 33.08 0 61.805-18.603 76.32-45.908 14.517 27.307 43.241 45.906 76.321 45.906 47.715 0 86.396-38.684 86.396-86.396.001-47.718-38.682-86.397-86.394-86.397zM86.395 210.648c-28.621 0-51.821-23.201-51.821-51.82 0-28.623 23.201-51.823 51.821-51.823 28.621 0 51.822 23.2 51.822 51.823 0 28.619-23.201 51.82-51.822 51.82zm152.643 0c-28.622 0-51.821-23.201-51.821-51.822 0-28.623 23.2-51.821 51.821-51.821 28.619 0 51.822 23.198 51.822 51.821-.001 28.621-23.203 51.822-51.822 51.822z"/>
|
||||
<path d="M441.651 218.033l-44.246-59.143 44.246-59.144-.008-.007c5.473-7.62 3.887-18.249-3.652-23.913-7.537-5.658-18.187-4.221-23.98 3.157l-.004-.002-38.188 51.047-38.188-51.047-.006.009c-5.793-7.385-16.441-8.822-23.981-3.16-7.539 5.664-9.125 16.293-3.649 23.911l-.008.005 44.245 59.144-44.245 59.143.008.005c-5.477 7.62-3.89 18.247 3.649 23.909 7.54 5.664 18.188 4.225 23.981-3.155l.006.007 38.188-51.049 38.188 51.049.004-.002c5.794 7.377 16.443 8.814 23.98 3.154 7.539-5.662 9.125-16.291 3.652-23.91l.008-.008z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
6
packages/next-auth/provider-logos/box.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 444.893 245.414">
|
||||
<g fill="#0075C9">
|
||||
<path d="M239.038 72.43c-33.081 0-61.806 18.6-76.322 45.904-14.516-27.305-43.24-45.902-76.32-45.902-19.443 0-37.385 6.424-51.821 17.266V16.925h-.008C34.365 7.547 26.713 0 17.286 0 7.858 0 .208 7.547.008 16.925H0v143.333h.036c.768 47.051 39.125 84.967 86.359 84.967 33.08 0 61.805-18.603 76.32-45.908 14.517 27.307 43.241 45.906 76.321 45.906 47.715 0 86.396-38.684 86.396-86.396.001-47.718-38.682-86.397-86.394-86.397zM86.395 210.648c-28.621 0-51.821-23.201-51.821-51.82 0-28.623 23.201-51.823 51.821-51.823 28.621 0 51.822 23.2 51.822 51.823 0 28.619-23.201 51.82-51.822 51.82zm152.643 0c-28.622 0-51.821-23.201-51.821-51.822 0-28.623 23.2-51.821 51.821-51.821 28.619 0 51.822 23.198 51.822 51.821-.001 28.621-23.203 51.822-51.822 51.822z"/>
|
||||
<path d="M441.651 218.033l-44.246-59.143 44.246-59.144-.008-.007c5.473-7.62 3.887-18.249-3.652-23.913-7.537-5.658-18.187-4.221-23.98 3.157l-.004-.002-38.188 51.047-38.188-51.047-.006.009c-5.793-7.385-16.441-8.822-23.981-3.16-7.539 5.664-9.125 16.293-3.649 23.911l-.008.005 44.245 59.144-44.245 59.143.008.005c-5.477 7.62-3.89 18.247 3.649 23.909 7.54 5.664 18.188 4.225 23.981-3.155l.006.007 38.188-51.049 38.188 51.049.004-.002c5.794 7.377 16.443 8.814 23.98 3.154 7.539-5.662 9.125-16.291 3.652-23.91l.008-.008z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
10
packages/next-auth/provider-logos/cognito.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="32" height="32" viewBox="0 0 256 299" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
|
||||
<path d="M208.752 58.061l25.771-6.636.192.283.651 155.607-.843.846-5.31.227-20.159-3.138-.302-.794V58.061M59.705 218.971l.095.007 68.027 19.767.173.133.296.236-.096 59.232-.2.252-68.295-33.178v-46.449" fill="#7A3E65"/>
|
||||
<path d="M208.752 204.456l-80.64 19.312-40.488-9.773-27.919 4.976L128 238.878l105.405-28.537 1.118-2.18-25.771-3.705" fill="#CFB2C1"/>
|
||||
<path d="M196.295 79.626l-.657-.749-66.904-19.44-.734.283-.672-.343L22.052 89.734l-.575.703.845.463 24.075 3.53.851-.289 80.64-19.311 40.488 9.773 27.919-4.977" fill="#512843"/>
|
||||
<path d="M47.248 240.537l-25.771 6.221-.045-.149-1.015-155.026 1.06-1.146 25.771 3.704v146.396" fill="#C17B9E"/>
|
||||
<path d="M82.04 180.403l45.96 5.391.345-.515.187-71.887-.532-.589-45.96 5.392v62.208" fill="#7A3E65"/>
|
||||
<path d="M173.96 180.403L128 185.794v-72.991l45.96 5.392v62.208M196.295 79.626L128 59.72V0l68.295 33.177v46.449" fill="#C17B9E"/>
|
||||
<path d="M128 0L0 61.793v175.011l21.477 9.954V90.437L128 59.72V0" fill="#7A3E65"/>
|
||||
<path d="M234.523 51.425v156.736L128 238.878v59.72l128-61.794V61.793l-21.477-10.368" fill="#C17B9E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
packages/next-auth/provider-logos/discord-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 256 293" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
|
||||
<path d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0zm-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732-.292-.146-.439-.292-.585-.438-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.776 123.776 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238 0-8.923-6.582-16.237-14.92-16.237" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
3
packages/next-auth/provider-logos/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 256 293" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
|
||||
<path d="M226.011 0H29.99C13.459 0 0 13.458 0 30.135v197.778c0 16.677 13.458 30.135 29.989 30.135h165.888l-7.754-27.063 18.725 17.408 17.7 16.384L256 292.571V30.135C256 13.458 242.542 0 226.011 0zm-56.466 191.05s-5.266-6.291-9.655-11.85c19.164-5.413 26.478-17.408 26.478-17.408-5.998 3.95-11.703 6.73-16.823 8.63-7.314 3.073-14.336 5.12-21.211 6.291-14.044 2.633-26.917 1.902-37.888-.146-8.339-1.61-15.507-3.95-21.504-6.29-3.365-1.317-7.022-2.926-10.68-4.974-.438-.293-.877-.439-1.316-.732-.292-.146-.439-.292-.585-.438-2.633-1.463-4.096-2.487-4.096-2.487s7.022 11.703 25.6 17.261c-4.388 5.56-9.801 12.142-9.801 12.142-32.33-1.024-44.617-22.235-44.617-22.235 0-47.104 21.065-85.285 21.065-85.285 21.065-15.799 41.106-15.36 41.106-15.36l1.463 1.756C80.75 77.53 68.608 89.088 68.608 89.088s3.218-1.755 8.63-4.242c15.653-6.876 28.088-8.777 33.208-9.216.877-.147 1.609-.293 2.487-.293a123.776 123.776 0 0 1 29.55-.292c13.896 1.609 28.818 5.705 44.031 14.043 0 0-11.556-10.971-36.425-18.578l2.048-2.34s20.041-.44 41.106 15.36c0 0 21.066 38.18 21.066 85.284 0 0-12.435 21.211-44.764 22.235zm-68.023-68.316c-8.338 0-14.92 7.314-14.92 16.237 0 8.924 6.728 16.238 14.92 16.238 8.339 0 14.921-7.314 14.921-16.238.147-8.923-6.582-16.237-14.92-16.237m53.394 0c-8.339 0-14.922 7.314-14.922 16.237 0 8.924 6.73 16.238 14.922 16.238 8.338 0 14.92-7.314 14.92-16.238 0-8.923-6.582-16.237-14.92-16.237" fill="#7289DA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4
packages/next-auth/provider-logos/facebook-dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" width="32" height="32" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="6702.77 18309.17 6561.66 6561.660000000007" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9983.6 18309.17c1811.95 0 3280.83 1468.88 3280.83 3280.83s-1468.88 3280.83-3280.83 3280.83S6702.77 23401.95 6702.77 21590s1468.88-3280.83 3280.83-3280.83z" fill="#fff"/>
|
||||
<path d="M10409.89 24843.29v-2534.17h714.43l94.7-891.91h-809.13l1.2-446.44c0-232.63 22.1-357.22 356.24-357.22h446.68v-892.06h-714.59c-858.35 0-1160.42 432.65-1160.42 1160.34v535.45h-535.07v891.99H9339v2498.09c208.45 41.53 423.95 63.47 644.6 63.47a3310.9 3310.9 0 0 0 426.29-27.54z" fill="#006aff" fill-rule="nonzero"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 774 B |
8
packages/next-auth/provider-logos/facebook.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg clip-rule="evenodd" fill-rule="evenodd" width="32" height="32" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="6702.77 18309.17 6561.66 6561.660000000007" xmlns="http://www.w3.org/2000/svg">
|
||||
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="9983.6" x2="9983.6" y1="18249.39" y2="25150.62">
|
||||
<stop offset="0" stop-color="#00b2ff"/>
|
||||
<stop offset="1" stop-color="#006aff"/>
|
||||
</linearGradient>
|
||||
<path d="M9983.6 18309.17c1811.95 0 3280.83 1468.88 3280.83 3280.83s-1468.88 3280.83-3280.83 3280.83S6702.77 23401.95 6702.77 21590s1468.88-3280.83 3280.83-3280.83z" fill="url(#a)"/>
|
||||
<path d="M10409.89 24843.29v-2534.17h714.43l94.7-891.91h-809.13l1.2-446.44c0-232.63 22.1-357.22 356.24-357.22h446.68v-892.06h-714.59c-858.35 0-1160.42 432.65-1160.42 1160.34v535.45h-535.07v891.99H9339v2498.09c208.45 41.53 423.95 63.47 644.6 63.47a3310.9 3310.9 0 0 0 426.29-27.54z" fill="#fff" fill-rule="nonzero"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 991 B |
18
packages/next-auth/provider-logos/foursquare-dark.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 275.9 275.9" style="enable-background:new 0 0 275.9 275.9;">
|
||||
<style type="text/css">
|
||||
.st0{enable-background:new ;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<rect x="0.1" y="0.3" width="275.4" height="275.6"/>
|
||||
<g class="st0">
|
||||
<path class="st1" d="M69.2,50.1H121v8.5H77.8v23.8h38.4v8.5H77.8v34.4h-8.6C69.2,125.2,69.2,50.1,69.2,50.1z"/>
|
||||
<path class="st1" d="M67.7,198.8l8.4-1.9c1.5,10.4,8.7,16.6,20.4,16.6c10.5,0,18.8-4.9,18.8-12.9c0-5.7-4.2-10.5-20.6-15.4
|
||||
c-18.6-5.3-24.8-12.1-24.8-22.1c0-12.9,10.4-19.8,25.5-19.8c16.9,0,24.4,8.6,27,20.4l-8.4,1.9c-2.1-9.7-8.7-13.8-18.9-13.8
|
||||
c-9.6,0-16.4,3.6-16.4,10.5c0,5.6,4.4,9.9,19.6,14.7c18.1,5.6,25.9,11.7,25.9,23.3c0,14.4-12.1,21.9-27.5,21.9
|
||||
C80.6,222.1,69.6,213.8,67.7,198.8z"/>
|
||||
<path class="st1" d="M134.5,182.9c0-22.3,14.6-39.7,37-39.7c22.3,0,36.7,17.5,36.7,39.7c0,11.1-3.7,20.7-9.9,27.7
|
||||
c3,3,5.9,6,8.8,9.2l-6.2,6c-3-3.2-6.1-6.4-9.2-9.5c-5.7,3.7-12.5,5.8-20.2,5.8C149.9,222.1,134.5,205.6,134.5,182.9z M185.2,209.9
|
||||
c-2.9-2.8-5.8-5.5-8.8-8.1l6-6.1c3.2,2.8,6.4,5.7,9.4,8.6c4.5-5.4,7.2-12.8,7.2-21.5c0-17.6-10.7-31-27.5-31s-27.6,13.4-27.6,31
|
||||
c0,18.1,11.7,30.8,27.6,30.8C176.6,213.6,181.2,212.4,185.2,209.9z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
18
packages/next-auth/provider-logos/foursquare.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 275.9 275.9" style="enable-background:new 0 0 275.9 275.9;">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{enable-background:new ;}
|
||||
</style>
|
||||
<rect x="0.1" y="0.3" class="st0" width="275.4" height="275.6"/>
|
||||
<g class="st1">
|
||||
<path d="M69.2,50.1H121v8.5H77.8v23.8h38.4v8.5H77.8v34.4h-8.6C69.2,125.2,69.2,50.1,69.2,50.1z"/>
|
||||
<path d="M67.7,198.8l8.4-1.9c1.5,10.4,8.7,16.6,20.4,16.6c10.5,0,18.8-4.9,18.8-12.9c0-5.7-4.2-10.5-20.6-15.4
|
||||
c-18.6-5.3-24.8-12.1-24.8-22.1c0-12.9,10.4-19.8,25.5-19.8c16.9,0,24.4,8.6,27,20.4l-8.4,1.9c-2.1-9.7-8.7-13.8-18.9-13.8
|
||||
c-9.6,0-16.4,3.6-16.4,10.5c0,5.6,4.4,9.9,19.6,14.7c18.1,5.6,25.9,11.7,25.9,23.3c0,14.4-12.1,21.9-27.5,21.9
|
||||
C80.6,222.1,69.6,213.8,67.7,198.8z"/>
|
||||
<path d="M134.5,182.9c0-22.3,14.6-39.7,37-39.7c22.3,0,36.7,17.5,36.7,39.7c0,11.1-3.7,20.7-9.9,27.7c3,3,5.9,6,8.8,9.2l-6.2,6
|
||||
c-3-3.2-6.1-6.4-9.2-9.5c-5.7,3.7-12.5,5.8-20.2,5.8C149.9,222.1,134.5,205.6,134.5,182.9z M185.2,209.9c-2.9-2.8-5.8-5.5-8.8-8.1
|
||||
l6-6.1c3.2,2.8,6.4,5.7,9.4,8.6c4.5-5.4,7.2-12.8,7.2-21.5c0-17.6-10.7-31-27.5-31s-27.6,13.4-27.6,31c0,18.1,11.7,30.8,27.6,30.8
|
||||
C176.6,213.6,181.2,212.4,185.2,209.9z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
packages/next-auth/provider-logos/freshbooks-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox=".99522558 .9999996 253.69877442 253.6940004" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m107.948 1a106.948 106.948 0 0 0 -106.948 106.966v146.728h146.727c59.067 0 106.955-47.88 106.967-106.948v-146.746zm86.724 43.635a34.6 34.6 0 0 1 -10.164 24.51 34.768 34.768 0 0 1 -24.562 10.152h-37.242v29.73h50.314v34.796h-50.065v71.663h-41.233v-179.891h40.983v32.625c1.072-18.32 16.275-32.625 34.668-32.625h37.358z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 467 B |
3
packages/next-auth/provider-logos/freshbooks.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox=".99522558 .9999996 253.69877442 253.6940004" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m107.948 1a106.948 106.948 0 0 0 -106.948 106.966v146.728h146.727c59.067 0 106.955-47.88 106.967-106.948v-146.746zm86.724 43.635a34.6 34.6 0 0 1 -10.164 24.51 34.768 34.768 0 0 1 -24.562 10.152h-37.242v29.73h50.314v34.796h-50.065v71.663h-41.233v-179.891h40.983v32.625c1.072-18.32 16.275-32.625 34.668-32.625h37.358z" fill="#0075dd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
4
packages/next-auth/provider-logos/github-dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>GitHub icon</title>
|
||||
<path fill="#fff" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 859 B |
4
packages/next-auth/provider-logos/github.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>GitHub dark icon</title>
|
||||
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 852 B |
8
packages/next-auth/provider-logos/gitlab-dark.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="32" height="32" xmlns="http://www.w3.org/2000/svg" viewBox="93.97 97.52 192.05 184.95">
|
||||
<defs>
|
||||
<style>.cls-1{fill:#fff;}</style>
|
||||
</defs>
|
||||
<g id="LOGO">
|
||||
<path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 591 B |