Compare commits
30 Commits
next-auth@
...
@next-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a07932b9 | ||
|
|
25c7ce1d2b | ||
|
|
227a233bd8 | ||
|
|
cf9f133aa3 | ||
|
|
2301c1be44 | ||
|
|
6e408e24bf | ||
|
|
f277989c69 | ||
|
|
6146e93288 | ||
|
|
1ff565da6c | ||
|
|
41f75cf870 | ||
|
|
dd591ed8d0 | ||
|
|
297bc2317f | ||
|
|
b170138e70 | ||
|
|
a307079e0f | ||
|
|
d52b7a6b7d | ||
|
|
30b69a07eb | ||
|
|
0d1757814f | ||
|
|
068f9b50b8 | ||
|
|
dac490b7a1 | ||
|
|
c0f51669e2 | ||
|
|
cbf8ce3510 | ||
|
|
1c19cc71df | ||
|
|
9d962a0056 | ||
|
|
8387c78e3f | ||
|
|
58e30a6af6 | ||
|
|
b7ff987baf | ||
|
|
782812a52b | ||
|
|
32f2a0cea3 | ||
|
|
3343ef18b2 | ||
|
|
6280fe9e10 |
1
.github/ISSUE_TEMPLATE/3_bug_adapter.yml
vendored
@@ -31,6 +31,7 @@ body:
|
|||||||
- "@next-auth/pouchdb-adapter"
|
- "@next-auth/pouchdb-adapter"
|
||||||
- "@next-auth/prisma-adapter"
|
- "@next-auth/prisma-adapter"
|
||||||
- "@next-auth/sequelize-adapter"
|
- "@next-auth/sequelize-adapter"
|
||||||
|
- "@next-auth/supabase-adapter"
|
||||||
- "@next-auth/typeorm-legacy-adapter"
|
- "@next-auth/typeorm-legacy-adapter"
|
||||||
- "@next-auth/upstash-redis-adapter"
|
- "@next-auth/upstash-redis-adapter"
|
||||||
- "@next-auth/xata-adapter"
|
- "@next-auth/xata-adapter"
|
||||||
|
|||||||
3
.github/issue-labeler.yml
vendored
@@ -30,6 +30,9 @@ prisma:
|
|||||||
sequelize:
|
sequelize:
|
||||||
- "@next-auth/sequelize-adapter"
|
- "@next-auth/sequelize-adapter"
|
||||||
|
|
||||||
|
supabase:
|
||||||
|
- "@next-auth/supabase-adapter"
|
||||||
|
|
||||||
typeorm-legacy:
|
typeorm-legacy:
|
||||||
- "@next-auth/typeorm-legacy-adapter"
|
- "@next-auth/typeorm-legacy-adapter"
|
||||||
|
|
||||||
|
|||||||
3
.github/pr-labeler.yml
vendored
@@ -42,6 +42,9 @@ prisma:
|
|||||||
sequelize:
|
sequelize:
|
||||||
- packages/adapter-sequelize/**
|
- packages/adapter-sequelize/**
|
||||||
|
|
||||||
|
supabase:
|
||||||
|
- packages/adapter-supabase/**
|
||||||
|
|
||||||
typeorm-legacy:
|
typeorm-legacy:
|
||||||
- packages/adapter-typeorm-legacy/**
|
- packages/adapter-typeorm-legacy/**
|
||||||
|
|
||||||
|
|||||||
2
.github/version-pr/action.yml
vendored
@@ -4,5 +4,5 @@ outputs:
|
|||||||
version:
|
version:
|
||||||
description: "npm package version"
|
description: "npm package version"
|
||||||
runs:
|
runs:
|
||||||
using: "node18"
|
using: "node16"
|
||||||
main: "index.js"
|
main: "index.js"
|
||||||
|
|||||||
6
.github/workflows/codeql-analysis.yml
vendored
@@ -18,10 +18,10 @@ jobs:
|
|||||||
language: ["javascript"]
|
language: ["javascript"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
2
.github/workflows/label-issue.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
name: Triage
|
name: Triage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: github/issue-labeler@v2.4.1
|
- uses: github/issue-labeler@v2.5
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
configuration-path: ".github/issue-labeler.yml"
|
configuration-path: ".github/issue-labeler.yml"
|
||||||
|
|||||||
2
.github/workflows/label-pr.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
name: Triage
|
name: Triage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v3
|
- uses: actions/labeler@v4
|
||||||
with:
|
with:
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
configuration-path: ".github/pr-labeler.yml"
|
configuration-path: ".github/pr-labeler.yml"
|
||||||
|
|||||||
14
.github/workflows/release.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Init
|
- name: Init
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2.2.1
|
uses: pnpm/action-setup@v2.2.4
|
||||||
with:
|
with:
|
||||||
version: 7.5.1
|
version: 7.5.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
@@ -49,11 +49,11 @@ jobs:
|
|||||||
environment: Production
|
environment: Production
|
||||||
steps:
|
steps:
|
||||||
- name: Init
|
- name: Init
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2.2.1
|
uses: pnpm/action-setup@v2.2.4
|
||||||
with:
|
with:
|
||||||
version: 7.5.1
|
version: 7.5.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
@@ -81,9 +81,9 @@ jobs:
|
|||||||
environment: Preview
|
environment: Preview
|
||||||
steps:
|
steps:
|
||||||
- name: Init
|
- name: Init
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v2.2.1
|
uses: pnpm/action-setup@v2.2.4
|
||||||
with:
|
with:
|
||||||
version: 7.5.1
|
version: 7.5.1
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN_PKG }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN_PKG }}
|
||||||
- name: Comment version on PR
|
- name: Comment version on PR
|
||||||
uses: NejcZdovc/comment-pr@v1
|
uses: NejcZdovc/comment-pr@v2
|
||||||
with:
|
with:
|
||||||
message:
|
message:
|
||||||
"🎉 Experimental release [published 📦️ on npm](https://npmjs.com/package/next-auth/v/${{ env.VERSION }})!\n \
|
"🎉 Experimental release [published 📦️ on npm](https://npmjs.com/package/next-auth/v/${{ env.VERSION }})!\n \
|
||||||
|
|||||||
2
.github/workflows/sync-examples.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Run GitHub File Sync
|
- name: Run GitHub File Sync
|
||||||
# Can update to v1 when https://github.com/BetaHuhn/repo-file-sync-action/issues/168 is resolved
|
# Can update to v1 when https://github.com/BetaHuhn/repo-file-sync-action/issues/168 is resolved
|
||||||
uses: BetaHuhn/repo-file-sync-action@v1.16.5
|
uses: BetaHuhn/repo-file-sync-action@v1.16.5
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -65,6 +65,7 @@ dev.db*
|
|||||||
packages/adapter-prisma/prisma/dev.db
|
packages/adapter-prisma/prisma/dev.db
|
||||||
packages/adapter-prisma/prisma/migrations
|
packages/adapter-prisma/prisma/migrations
|
||||||
db.sqlite
|
db.sqlite
|
||||||
|
packages/adapter-supabase/supabase/.branches
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
coverage
|
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.
|
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
|
### Pull Requests
|
||||||
|
|
||||||
- The latest changes are always in `main`, so please make your Pull Request against that branch.
|
- The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||||
@@ -85,6 +87,7 @@ If you think your custom provider might be useful to others, we encourage you to
|
|||||||
|
|
||||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
|
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
|
||||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||||
|
3. Add provider logo svgs, like `google-dark.svg` (dark mode) and `google.svg` (light mode) to the `/packages/next-auth/provider-logos/` directory. Don't forget to set the provider's styling options in the `provider.style` config object.
|
||||||
|
|
||||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||||
|
|
||||||
@@ -116,4 +119,4 @@ When accepting Pull Requests, make sure the following:
|
|||||||
|
|
||||||
### Skipping a release
|
### 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=
|
DATABASE_URL=
|
||||||
|
|
||||||
WIKIMEDIA_ID=
|
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
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { unstable_getServerSession } from "next-auth/next"
|
import { unstable_getServerSession } from "next-auth/next"
|
||||||
import { authOptions } from "pages/api/auth/[...nextauth]"
|
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const session = await unstable_getServerSession(authOptions)
|
const session = await unstable_getServerSession()
|
||||||
return <pre>{JSON.stringify(session, null, 2)}</pre>
|
return <pre>{JSON.stringify(session, null, 2)}</pre>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ export default function Header() {
|
|||||||
<li className={styles.navItem}>
|
<li className={styles.navItem}>
|
||||||
<Link href="/middleware-protected">Middleware protected</Link>
|
<Link href="/middleware-protected">Middleware protected</Link>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -16,9 +16,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next-auth/fauna-adapter": "workspace:*",
|
"@next-auth/fauna-adapter": "workspace:*",
|
||||||
"@next-auth/prisma-adapter": "workspace:*",
|
"@next-auth/prisma-adapter": "workspace:*",
|
||||||
|
"@next-auth/supabase-adapter": "workspace:*",
|
||||||
"@next-auth/typeorm-legacy-adapter": "workspace:*",
|
"@next-auth/typeorm-legacy-adapter": "workspace:*",
|
||||||
"@prisma/client": "^3",
|
"@prisma/client": "^3",
|
||||||
|
"@supabase/supabase-js": "^2.0.5",
|
||||||
"faunadb": "^4",
|
"faunadb": "^4",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
"next": "13.0.2",
|
"next": "13.0.2",
|
||||||
"next-auth": "workspace:*",
|
"next-auth": "workspace:*",
|
||||||
"nodemailer": "^6",
|
"nodemailer": "^6",
|
||||||
@@ -26,6 +29,7 @@
|
|||||||
"react-dom": "^18"
|
"react-dom": "^18"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jsonwebtoken": "^8.5.5",
|
||||||
"@types/react": "^18.0.15",
|
"@types/react": "^18.0.15",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"fake-smtp-server": "^0.8.0",
|
"fake-smtp-server": "^0.8.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import NextAuth from "next-auth"
|
import NextAuth from "next-auth"
|
||||||
import type { NextAuthOptions } from "next-auth"
|
import type { NextAuthOptions } from "next-auth"
|
||||||
|
import jwt from "jsonwebtoken"
|
||||||
|
|
||||||
// Providers
|
// Providers
|
||||||
import Apple from "next-auth/providers/apple"
|
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 GitHub from "next-auth/providers/github"
|
||||||
import Gitlab from "next-auth/providers/gitlab"
|
import Gitlab from "next-auth/providers/gitlab"
|
||||||
import Google from "next-auth/providers/google"
|
import Google from "next-auth/providers/google"
|
||||||
import Hubspot from "next-auth/providers/hubspot"
|
|
||||||
import IDS4 from "next-auth/providers/identity-server4"
|
import IDS4 from "next-auth/providers/identity-server4"
|
||||||
import Instagram from "next-auth/providers/instagram"
|
import Instagram from "next-auth/providers/instagram"
|
||||||
import Keycloak from "next-auth/providers/keycloak"
|
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 Patreon from "next-auth/providers/patreon"
|
||||||
import Slack from "next-auth/providers/slack"
|
import Slack from "next-auth/providers/slack"
|
||||||
import Spotify from "next-auth/providers/spotify"
|
import Spotify from "next-auth/providers/spotify"
|
||||||
import Todoist from "next-auth/providers/todoist"
|
|
||||||
import Trakt from "next-auth/providers/trakt"
|
import Trakt from "next-auth/providers/trakt"
|
||||||
import Twitch from "next-auth/providers/twitch"
|
import Twitch from "next-auth/providers/twitch"
|
||||||
import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
|
import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
|
||||||
import Vk from "next-auth/providers/vk"
|
import Vk from "next-auth/providers/vk"
|
||||||
import Wikimedia from "next-auth/providers/wikimedia"
|
import Wikimedia from "next-auth/providers/wikimedia"
|
||||||
import WorkOS from "next-auth/providers/workos"
|
import WorkOS from "next-auth/providers/workos"
|
||||||
import Zitadel from "next-auth/providers/zitadel"
|
|
||||||
|
|
||||||
// Adapters
|
// 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
|
// Add an adapter you want to test here.
|
||||||
// import { PrismaClient } from "@prisma/client"
|
const adapters = {
|
||||||
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
prisma() {
|
||||||
// const client = globalThis.prisma || new PrismaClient()
|
const client = globalThis.prisma || new PrismaClient()
|
||||||
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
||||||
// const adapter = PrismaAdapter(client)
|
return PrismaAdapter(client)
|
||||||
|
},
|
||||||
// // Fauna
|
typeorm() {
|
||||||
// import { Client as FaunaClient } from "faunadb"
|
return TypeORMLegacyAdapter({
|
||||||
// import { FaunaAdapter } from "@next-auth/fauna-adapter"
|
type: "sqlite",
|
||||||
// const opts = { secret: process.env.FAUNA_SECRET, domain: process.env.FAUNA_DOMAIN }
|
name: "next-auth-test-memory",
|
||||||
// const client = globalThis.fauna || new FaunaClient(opts)
|
database: "./typeorm/dev.db",
|
||||||
// if (process.env.NODE_ENV !== "production") globalThis.fauna = client
|
synchronize: true,
|
||||||
// const adapter = FaunaAdapter(client)
|
})
|
||||||
|
},
|
||||||
// // TypeORM
|
fauna() {
|
||||||
// import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
|
const client =
|
||||||
// const adapter = TypeORMLegacyAdapter({
|
globalThis.fauna ||
|
||||||
// type: "sqlite",
|
new FaunaClient({
|
||||||
// name: "next-auth-test-memory",
|
secret: process.env.FAUNA_SECRET,
|
||||||
// database: "./typeorm/dev.db",
|
domain: process.env.FAUNA_DOMAIN,
|
||||||
// synchronize: true,
|
})
|
||||||
// })
|
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 = {
|
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",
|
debug: process.env.NODE_ENV !== "production",
|
||||||
theme: {
|
theme: {
|
||||||
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
|
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 }),
|
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
|
||||||
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
|
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
|
||||||
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_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 }),
|
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 }),
|
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 }),
|
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 }),
|
Patreon({ clientId: process.env.PATREON_ID, clientSecret: process.env.PATREON_SECRET }),
|
||||||
Slack({ clientId: process.env.SLACK_ID, clientSecret: process.env.SLACK_SECRET }),
|
Slack({ clientId: process.env.SLACK_ID, clientSecret: process.env.SLACK_SECRET }),
|
||||||
Spotify({ clientId: process.env.SPOTIFY_ID, clientSecret: process.env.SPOTIFY_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 }),
|
Trakt({ clientId: process.env.TRAKT_ID, clientSecret: process.env.TRAKT_SECRET }),
|
||||||
Twitch({ clientId: process.env.TWITCH_ID, clientSecret: process.env.TWITCH_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 }),
|
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 }),
|
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
|
||||||
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
|
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
|
||||||
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_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 }),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ import { unstable_getServerSession } from "next-auth/next"
|
|||||||
import { authOptions } from "../auth/[...nextauth]"
|
import { authOptions } from "../auth/[...nextauth]"
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const session = await unstable_getServerSession(authOptions)
|
const session = await unstable_getServerSession(req, res, authOptions)
|
||||||
res.json(session)
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||||
*/
|
*/
|
||||||
interface Session {
|
interface Session {
|
||||||
|
// A JWT which can be used as Authorization header with supabase-js for RLS.
|
||||||
|
supabaseAccessToken?: string
|
||||||
user: {
|
user: {
|
||||||
/** The user's postal address. */
|
/** The user's postal address. */
|
||||||
address: string
|
address: string
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
@@ -15,26 +15,23 @@ The MongoDB adapter does not handle connections automatically, so you will have
|
|||||||
npm install next-auth @next-auth/mongodb-adapter mongodb
|
npm install next-auth @next-auth/mongodb-adapter mongodb
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Add `lib/mongodb.js`
|
2. Add `lib/mongodb.ts`
|
||||||
|
|
||||||
```js
|
```ts
|
||||||
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
|
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
|
||||||
import { MongoClient } from "mongodb"
|
import { MongoClient } from 'mongodb'
|
||||||
|
|
||||||
const uri = process.env.MONGODB_URI
|
|
||||||
const options = {
|
|
||||||
useUnifiedTopology: true,
|
|
||||||
useNewUrlParser: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
let client
|
|
||||||
let clientPromise
|
|
||||||
|
|
||||||
if (!process.env.MONGODB_URI) {
|
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
|
// In development mode, use a global variable so that the value
|
||||||
// is preserved across module reloads caused by HMR (Hot Module Replacement).
|
// is preserved across module reloads caused by HMR (Hot Module Replacement).
|
||||||
if (!global._mongoClientPromise) {
|
if (!global._mongoClientPromise) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ We have a list of official adapters that are distributed as their own packages u
|
|||||||
- [`neo4j`](./neo4j)
|
- [`neo4j`](./neo4j)
|
||||||
- [`typeorm-legacy`](./typeorm)
|
- [`typeorm-legacy`](./typeorm)
|
||||||
- [`sequelize`](./sequelize)
|
- [`sequelize`](./sequelize)
|
||||||
|
- [`supabase`](./supabase)
|
||||||
- [`dgraph`](./dgraph)
|
- [`dgraph`](./dgraph)
|
||||||
- [`upstash-redis`](./upstash-redis)
|
- [`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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -75,10 +75,9 @@ You can also use `unstable_getServerSession` in Next.js' server components:
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { unstable_getServerSession } from "next-auth/next"
|
import { unstable_getServerSession } from "next-auth/next"
|
||||||
import { authOptions } from "pages/api/auth/[...nextauth]"
|
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const session = await unstable_getServerSession(authOptions)
|
const session = await unstable_getServerSession()
|
||||||
return <pre>{JSON.stringify(session, null, 2)}</pre>
|
return <pre>{JSON.stringify(session, null, 2)}</pre>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -472,7 +472,8 @@ cookies: {
|
|||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
path: '/',
|
path: '/',
|
||||||
secure: useSecureCookies
|
secure: useSecureCookies,
|
||||||
|
maxAge: 900
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
@@ -482,6 +483,7 @@ cookies: {
|
|||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
path: "/",
|
path: "/",
|
||||||
secure: useSecureCookies,
|
secure: useSecureCookies,
|
||||||
|
maxAge: 900
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
nonce: {
|
nonce: {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Adding support for signing in via email in addition to one or more OAuth service
|
|||||||
Configuration is similar to other providers, but the options are different:
|
Configuration is similar to other providers, but the options are different:
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
import EmailProvider from `next-auth/providers/email`
|
import EmailProvider from "next-auth/providers/email"
|
||||||
...
|
...
|
||||||
providers: [
|
providers: [
|
||||||
EmailProvider({
|
EmailProvider({
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ interface OAuthConfig {
|
|||||||
issuer?: string
|
issuer?: string
|
||||||
client?: Partial<ClientMetadata>
|
client?: Partial<ClientMetadata>
|
||||||
allowDangerousEmailAccountLinking?: boolean
|
allowDangerousEmailAccountLinking?: boolean
|
||||||
|
/**
|
||||||
|
* Object containing the settings for the styling of the providers sign-in buttons
|
||||||
|
*/
|
||||||
|
style: ProviderStyleType
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -428,7 +432,8 @@ If you think your custom provider might be useful to others, we encourage you to
|
|||||||
You only need to add three changes:
|
You only need to add three changes:
|
||||||
|
|
||||||
1. Add your config: [`src/providers/{provider}.ts`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers)<br />
|
1. Add your config: [`src/providers/{provider}.ts`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers)<br />
|
||||||
• make sure you use a named default export, like this: `export default function YourProvider`
|
- Make sure you use a named default export, like this: `export default function YourProvider`
|
||||||
|
- Add two SVG's of the provider logo, like `google-dark.svg` (dark mode) and `google.svg` (light mode), to the `/packages/next-auth/provider-logos/` directory as well as the styling config to the provider config object. See existing provider for example
|
||||||
2. Add provider documentation: [`/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/docs/docs/providers)
|
2. Add provider documentation: [`/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/docs/docs/providers)
|
||||||
3. Add the new provider name to the `Provider type` dropdown options in [`the provider issue template`](<[http](https://github.com/nextauthjs/next-auth/edit/main/.github/ISSUE_TEMPLATE/2_bug_provider.yml)>)
|
3. Add the new provider name to the `Provider type` dropdown options in [`the provider issue template`](<[http](https://github.com/nextauthjs/next-auth/edit/main/.github/ISSUE_TEMPLATE/2_bug_provider.yml)>)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ http://developers.strava.com/docs/reference/
|
|||||||
|
|
||||||
The **Strava Provider** comes with a set of default options:
|
The **Strava Provider** comes with a set of default options:
|
||||||
|
|
||||||
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.js)
|
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.ts)
|
||||||
|
|
||||||
You can override any of the options to suit your own use case. Ensure the redirect_uri configuration fits your needs accordingly.
|
You can override any of the options to suit your own use case. Ensure the redirect_uri configuration fits your needs accordingly.
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ For the time being, the `withAuth` middleware only supports `"jwt"` as [session
|
|||||||
More details can be found [here](https://next-auth.js.org/configuration/nextjs#middleware).
|
More details can be found [here](https://next-auth.js.org/configuration/nextjs#middleware).
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
To inclue all `dashboard` nested routes (sub pages like `/dashboard/settings`, `/dashboard/profile`) you can pass `matcher: "/dashboard/:path*"` to `config`.
|
To include all `dashboard` nested routes (sub pages like `/dashboard/settings`, `/dashboard/profile`) you can pass `matcher: "/dashboard/:path*"` to `config`.
|
||||||
|
|
||||||
For other patterns check out the [Next.js Middleware documentation](https://nextjs.org/docs/advanced-features/middleware#matcher).
|
For other patterns check out the [Next.js Middleware documentation](https://nextjs.org/docs/advanced-features/middleware#matcher).
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ module.exports = {
|
|||||||
"adapters/neo4j",
|
"adapters/neo4j",
|
||||||
"adapters/typeorm",
|
"adapters/typeorm",
|
||||||
"adapters/sequelize",
|
"adapters/sequelize",
|
||||||
|
"adapters/supabase",
|
||||||
"adapters/mikro-orm",
|
"adapters/mikro-orm",
|
||||||
"adapters/dgraph",
|
"adapters/dgraph",
|
||||||
"adapters/upstash-redis",
|
"adapters/upstash-redis",
|
||||||
|
|||||||
@@ -75,10 +75,9 @@ You can also use `unstable_getServerSession` in Next.js' server components:
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { unstable_getServerSession } from "next-auth/next"
|
import { unstable_getServerSession } from "next-auth/next"
|
||||||
import { authOptions } from "pages/api/auth/[...nextauth]"
|
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const session = await unstable_getServerSession(authOptions)
|
const session = await unstable_getServerSession()
|
||||||
return <pre>{JSON.stringify(session, null, 2)}</pre>
|
return <pre>{JSON.stringify(session, null, 2)}</pre>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-auth/firebase-adapter",
|
"name": "@next-auth/firebase-adapter",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"description": "Firebase adapter for next-auth.",
|
"description": "Firebase adapter for next-auth.",
|
||||||
"homepage": "https://next-auth.js.org",
|
"homepage": "https://next-auth.js.org",
|
||||||
"repository": "https://github.com/nextauthjs/next-auth",
|
"repository": "https://github.com/nextauthjs/next-auth",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase emulators:exec --only firestore --project next-auth-test jest"
|
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase --token '$FIREBASE_TOKEN' emulators:exec --only firestore --project next-auth-test jest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"firebase": "^9.7.0",
|
"firebase": "^9.7.0",
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next-auth/adapter-test": "workspace:*",
|
"@next-auth/adapter-test": "workspace:*",
|
||||||
"@next-auth/tsconfig": "workspace:*",
|
"@next-auth/tsconfig": "workspace:*",
|
||||||
"firebase": "^9.7.0",
|
"firebase": "^9.14.0",
|
||||||
"firebase-tools": "^10.7.2",
|
"firebase-tools": "^11.16.1",
|
||||||
"jest": "^27.4.3",
|
"jest": "^27.4.3",
|
||||||
"next-auth": "workspace:*"
|
"next-auth": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,10 +86,10 @@ export function FirestoreAdapter({
|
|||||||
async getUserByEmail(email) {
|
async getUserByEmail(email) {
|
||||||
const userQuery = query(Users, where("email", "==", email), limit(1))
|
const userQuery = query(Users, where("email", "==", email), limit(1))
|
||||||
const userSnapshots = await getDocs(userQuery)
|
const userSnapshots = await getDocs(userQuery)
|
||||||
const userSnpashot = userSnapshots.docs[0]
|
const userSnapshot = userSnapshots.docs[0]
|
||||||
|
|
||||||
if (userSnpashot?.exists() && Users.converter) {
|
if (userSnapshot?.exists() && Users.converter) {
|
||||||
return Users.converter.fromFirestore(userSnpashot)
|
return Users.converter.fromFirestore(userSnapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@next-auth/tsconfig/tsconfig.base.json",
|
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@next-auth/pouchdb-adapter",
|
"name": "@next-auth/pouchdb-adapter",
|
||||||
"version": "0.1.4",
|
"version": "0.1.5",
|
||||||
"description": "PouchDB adapter for next-auth.",
|
"description": "PouchDB adapter for next-auth.",
|
||||||
"homepage": "https://next-auth.js.org",
|
"homepage": "https://next-auth.js.org",
|
||||||
"repository": "https://github.com/nextauthjs/next-auth",
|
"repository": "https://github.com/nextauthjs/next-auth",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@next-auth/tsconfig/tsconfig.base.json",
|
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
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",
|
"name": "next-auth",
|
||||||
"version": "4.16.2",
|
"version": "4.18.0",
|
||||||
"description": "Authentication for Next.js",
|
"description": "Authentication for Next.js",
|
||||||
"homepage": "https://next-auth.js.org",
|
"homepage": "https://next-auth.js.org",
|
||||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
"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 |