mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
11 Commits
patch-1
...
docs/api-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
354b03471c | ||
|
|
0a7286e857 | ||
|
|
cf544d6ec7 | ||
|
|
84e14d76b3 | ||
|
|
7794b6dfbb | ||
|
|
d195381224 | ||
|
|
b3d5ec596b | ||
|
|
34f8f36038 | ||
|
|
a79a5d6cbe | ||
|
|
cac71774a6 | ||
|
|
7376f10cac |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -12,7 +12,6 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
firebase-debug.log
|
||||
ui-debug.log
|
||||
.pnpm-debug.log
|
||||
|
||||
|
||||
@@ -79,6 +78,9 @@ docs/.docusaurus
|
||||
docs/providers.json
|
||||
|
||||
# Core
|
||||
packages/core/*.js
|
||||
packages/core/*.d.ts
|
||||
packages/core/*.d.ts.map
|
||||
packages/core/src/providers/oauth-types.ts
|
||||
packages/core/lib
|
||||
packages/core/providers
|
||||
@@ -94,7 +96,3 @@ packages/frameworks-sveltekit/.svelte-kit
|
||||
packages/frameworks-sveltekit/package
|
||||
packages/frameworks-sveltekit/vite.config.js.timestamp-*
|
||||
packages/frameworks-sveltekit/vite.config.ts.timestamp-*
|
||||
|
||||
# Adapters
|
||||
|
||||
docs/docs/reference/adapter
|
||||
@@ -3,7 +3,7 @@ import { Protected } from "~/components";
|
||||
export const { routeData, Page } = Protected((session) => {
|
||||
return (
|
||||
<main class="flex flex-col gap-2 items-center">
|
||||
<h1>This is a protected route</h1>
|
||||
<h1>This is a proteced route</h1>
|
||||
</main>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import startAppAndSignInImg from "./img/getting-started-app-start.png"
|
||||
import githubAuthCredentials from "./img/getting-started-github-auth.png"
|
||||
import nextAuthUserLoggedIn from "./img/getting-started-nextauth-success.png"
|
||||
|
||||
We know, authentication is hard. It's a rabbit hole and it's easy to get lost on it. The goal of making Auth.js is that you can add authentication easily to your project with just a few lines of code.
|
||||
We know, authentication is hard. Is a rabbit hole and it's easy to get lost on it. The goal of making Auth.js is that you can add authentication easily to your project with just a few lines of code.
|
||||
|
||||
The easiest way is to setup Auth.js with an [OAuth](https://en.wikipedia.org/wiki/OAuth) provider. In this tutorial we'll be setting Auth.js in a **Next.js app** to be able to login with **Github**.
|
||||
|
||||
@@ -214,7 +214,7 @@ Note that, for each provider, the configuration process will be similar to what
|
||||
2. Create create your OAuth application within it
|
||||
3. Set the callback URL
|
||||
4. Get the Client ID and Generate a Client Secret
|
||||
:::
|
||||
:::
|
||||
|
||||
## 3. Wiring all together
|
||||
|
||||
@@ -253,13 +253,11 @@ Once inserted and correct, Github will redirect the user to our app and Auth.js
|
||||
<img src={nextAuthUserLoggedIn} />
|
||||
|
||||
Great! We have completed the whole E2E authentication flow setup so that users can login in our application through Github!
|
||||
:::
|
||||
|
||||
:::info
|
||||
You can create your own Sign In page instead of using the default one from Auth.js. You can learn how to do so in our [dedicated guide for it](/guides/basics/pages).
|
||||
You can create your own Sign In page instead of using the default one from Auth.js. You can learn how to do so in our dedicated guide for it.
|
||||
:::
|
||||
|
||||
|
||||
## 4. Deploying to production
|
||||
|
||||
### Configuring different environments
|
||||
|
||||
@@ -72,11 +72,11 @@ export default NextAuth({
|
||||
providers: [
|
||||
Email({
|
||||
server: {
|
||||
host: process.env.SMTP_HOST,
|
||||
port: Number(process.env.SMTP_PORT),
|
||||
host: process.env.EMAIL_SERVER_HOST,
|
||||
port: Number(process.env.EMAIL_SERVER_PORT),
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASSWORD,
|
||||
user: process.env.EMAIL_SERVER_USER,
|
||||
pass: process.env.EMAIL_SERVER_PASSWORD,
|
||||
},
|
||||
},
|
||||
from: process.env.EMAIL_FROM,
|
||||
@@ -147,8 +147,8 @@ import EmailProvider from "next-auth/providers/email"
|
||||
|
||||
export default NextAuth({
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
+ adapter: MongoDBAdapter(clientPromise),
|
||||
providers: [
|
||||
+ adapter: MongoDBAdapter(clientPromise),
|
||||
EmailProvider({
|
||||
server: {
|
||||
host: process.env.EMAIL_SERVER_HOST,
|
||||
@@ -188,7 +188,7 @@ Let's now check our email, and look for one sent from NextAuth (check your spam
|
||||
|
||||
<img src={mailboxImg} alt="Screenshot of mailbox" />
|
||||
|
||||
Nice! We got one, coming from the sender specified in the `EMAIL_FROM` environment variable from our configuration above and that's is the sender we verified in Sendgrid.
|
||||
Nice! We got one, coming from the sender specified in the `EMAIL_FROM` environment variable from our configuration above and that's is the sender we verified in Sengrid.
|
||||
|
||||
Click on "Sign in" and a new browser tab will open, you should then land on your application as authenticated!
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Using a JWT to store the `refresh_token` is less secure than saving it in a data
|
||||
|
||||
#### JWT strategy
|
||||
|
||||
Using the [jwt](../../reference/core/types#jwt) and [session](../../reference/core/types#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
|
||||
Using the [jwt](../../reference/core/interfaces/types.CallbacksOptions.md#jwt) and [session](../../reference/core/interfaces/types.CallbacksOptions.md#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
|
||||
|
||||
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar.
|
||||
|
||||
@@ -45,10 +45,10 @@ export default Auth(new Request("https://example.com"), {
|
||||
// Save the access token and refresh token in the JWT on the initial login
|
||||
return {
|
||||
access_token: account.access_token,
|
||||
expires_at: Math.floor(Date.now() / 1000 + account.expires_in),
|
||||
expires_at: Date.now() + account.expires_in * 1000,
|
||||
refresh_token: account.refresh_token,
|
||||
}
|
||||
} else if (Date.now() < token.expires_at * 1000) {
|
||||
} else if (Date.now() < token.expires_at) {
|
||||
// If the access token has not expired yet, return it
|
||||
return token
|
||||
} else {
|
||||
@@ -74,7 +74,7 @@ export default Auth(new Request("https://example.com"), {
|
||||
return {
|
||||
...token, // Keep the previous token properties
|
||||
access_token: tokens.access_token,
|
||||
expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in),
|
||||
expires_at: Date.now() + tokens.expires_in * 1000,
|
||||
// Fall back to old refresh token, but note that
|
||||
// many providers may only allow using a refresh token once.
|
||||
refresh_token: tokens.refresh_token ?? token.refresh_token,
|
||||
@@ -136,7 +136,7 @@ export default Auth(new Request("https://example.com"), {
|
||||
const [google] = await prisma.account.findMany({
|
||||
where: { userId: user.id, provider: "google" },
|
||||
})
|
||||
if (google.expires_at * 1000 < Date.now()) {
|
||||
if (google.expires_at < Date.now()) {
|
||||
// If the access token has expired, try to refresh it
|
||||
try {
|
||||
// https://accounts.google.com/.well-known/openid-configuration
|
||||
@@ -159,7 +159,7 @@ export default Auth(new Request("https://example.com"), {
|
||||
await prisma.account.update({
|
||||
data: {
|
||||
access_token: tokens.access_token,
|
||||
expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in),
|
||||
expires_at: Date.now() + tokens.expires_in * 1000,
|
||||
refresh_token: tokens.refresh_token ?? google.refresh_token,
|
||||
},
|
||||
where: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Corporate proxy
|
||||
---
|
||||
|
||||
Using Auth.js behind a corporate proxy is not supported out of the box. This is due to the fact that the underlying library we use, [`openid-client`](https://npm.im/openid-client) which uses the built-in Node.js `http` / `https` libraries, and those do not support proxies by default:
|
||||
Using Auth.js behind a corporate proxy is not supported out of the box. This is due to the fact that the underlying library we use, [`openid-client`](https://npm.im/openid-client) which uses the built-in Node.js `http` / `https` libraries, and those do not support proxys by default:
|
||||
|
||||
- [`http` docs](https://nodejs.org/dist/latest-v18.x/docs/api/http.html)
|
||||
- [`https` docs](https://nodejs.org/dist/latest-v18.x/docs/api/https.html)
|
||||
|
||||
@@ -26,7 +26,7 @@ export default NextAuth({
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
// You might want to pull this call out so we're not making a new LDAP client on every login attempt
|
||||
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
|
||||
const client = ldap.createClient({
|
||||
url: process.env.LDAP_URI,
|
||||
})
|
||||
|
||||
@@ -23,8 +23,8 @@ AUTH_SECRET=your_auth_secret
|
||||
in this example we are using github so make sure to set the following environment variables:
|
||||
|
||||
```
|
||||
GITHUB_ID=your_github_oauth_id
|
||||
GITHUB_SECRET=your_github_oauth_secret
|
||||
GITHUB_ID=your_github_oatuh_id
|
||||
GITHUB_SECRET=your_github_oatuh_secret
|
||||
```
|
||||
|
||||
```ts
|
||||
|
||||
@@ -11,7 +11,7 @@ When using SSR, I recommend creating a `Protected` component that will trigger s
|
||||
|
||||
```tsx
|
||||
// components/Protected.tsx
|
||||
import { type Session } from "@auth/core/types";
|
||||
import { type Session } from "@auth/core";
|
||||
import { getSession } from "@auth/solid-start";
|
||||
import { Component, Show } from "solid-js";
|
||||
import { useRouteData } from "solid-start";
|
||||
@@ -60,7 +60,7 @@ import Protected from "~/components/Protected";
|
||||
export const { routeData, Page } = Protected((session) => {
|
||||
return (
|
||||
<main class="flex flex-col gap-2 items-center">
|
||||
<h1>This is a protected route</h1>
|
||||
<h1>This is a proteced route</h1>
|
||||
</main>
|
||||
);
|
||||
});
|
||||
@@ -110,7 +110,7 @@ And now you can easily create a protected route:
|
||||
export default () => {
|
||||
return (
|
||||
<main class="flex flex-col gap-2 items-center">
|
||||
<h1>This is a protected route</h1>
|
||||
<h1>This is a proteced route</h1>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ providers: [
|
||||
```
|
||||
|
||||
:::warning
|
||||
Trakt does not allow hotlinking images. Even the authenticated user's profile picture.
|
||||
Trakt does not allow hotlinking images. Even the authenticated user's profie picture.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
|
||||
@@ -91,7 +91,7 @@ type VerificationToken {
|
||||
## Securing your database
|
||||
|
||||
For production deployments you will want to restrict the access to the types used
|
||||
by next-auth. The main form of access control used in Dgraph is via `@auth` directive alongside types in the schema.
|
||||
by next-auth. The main form of access control used in Dgraph is via `@auth` directive alongide types in the schema.
|
||||
|
||||
#### Secure schema
|
||||
|
||||
|
||||
75
docs/docs/reference/06-adapters/firebase.md
Normal file
75
docs/docs/reference/06-adapters/firebase.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
id: firebase
|
||||
title: Firebase
|
||||
---
|
||||
|
||||
:::warning
|
||||
This adapter is still experimental and does not work with Auth.js 4 or newer. If you would like to help out upgrading it, please visit [this PR](https://github.com/nextauthjs/next-auth/pull/3873)
|
||||
:::
|
||||
|
||||
This is the Firebase Adapter for [`next-auth`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn
|
||||
npm install next-auth @next-auth/firebase-adapter@experimental
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
import { FirebaseAdapter } from "@next-auth/firebase-adapter"
|
||||
|
||||
import firebase from "firebase/app"
|
||||
import "firebase/firestore"
|
||||
|
||||
const firestore = (
|
||||
firebase.apps[0] ?? firebase.initializeApp(/* your config */)
|
||||
).firestore()
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://authjs.dev/reference/configuration/auth-options
|
||||
export default NextAuth({
|
||||
// https://authjs.dev/reference/providers/
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FirebaseAdapter(firestore),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
|
||||
|
||||
An example firebase config looks like this:
|
||||
|
||||
```js
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
|
||||
authDomain: "myapp-project-123.firebaseapp.com",
|
||||
databaseURL: "https://myapp-project-123.firebaseio.com",
|
||||
projectId: "myapp-project-123",
|
||||
storageBucket: "myapp-project-123.appspot.com",
|
||||
messagingSenderId: "65211879809",
|
||||
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
|
||||
measurementId: "G-8GSGZQ44ST",
|
||||
}
|
||||
```
|
||||
|
||||
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
|
||||
|
||||
:::tip **From Firebase**
|
||||
|
||||
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
|
||||
|
||||
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
|
||||
:::
|
||||
@@ -226,21 +226,6 @@ const docusaurusConfig = {
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"docusaurus-plugin-typedoc",
|
||||
{
|
||||
...typedocConfig,
|
||||
id: "firebase-adapter",
|
||||
plugin: [require.resolve("./typedoc-mdn-links")],
|
||||
watch: process.env.TYPEDOC_WATCH,
|
||||
entryPoints: ["../packages/adapter-firebase/src/index.ts"],
|
||||
tsconfig: "../packages/adapter-firebase/tsconfig.json",
|
||||
out: "reference/adapter/firebase",
|
||||
sidebar: {
|
||||
indexLabel: "Firebase",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ for (const file of files) {
|
||||
body.push(" */")
|
||||
const name = file.replace(/\.md$/, "")
|
||||
result[name] = {
|
||||
description: `Snippet generated from ${file} by pnpm \`generate-snippet\``,
|
||||
description: `Snippet genereated from ${file} by pnpm \`generate-snippet\``,
|
||||
scope: "typescript",
|
||||
prefix: name,
|
||||
body,
|
||||
|
||||
@@ -50,8 +50,12 @@ module.exports = {
|
||||
label: "Database Adapters",
|
||||
link: { type: "doc", id: "reference/adapters/overview" },
|
||||
items: [
|
||||
{ type: "doc", id: "reference/adapter/firebase/index" },
|
||||
{ type: "autogenerated", dirName: "reference/06-adapters" },
|
||||
{
|
||||
type: "autogenerated",
|
||||
dirName: "reference/06-adapters",
|
||||
// See: https://github.com/facebook/docusaurus/issues/5689
|
||||
// exclude: ["index"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Auth } from "@auth/core"
|
||||
import $1 from "@auth/core/providers/$2"
|
||||
|
||||
const request = new Request("https://example.com")
|
||||
const response = await AuthHandler(request, {
|
||||
const resposne = await AuthHandler(request, {
|
||||
providers: [$1({ clientId: "", clientSecret: "" })],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -9,7 +9,7 @@ import Auth from "@auth/core"
|
||||
import { $1 } from "@auth/core/providers/$2"
|
||||
|
||||
const request = new Request("https://example.com")
|
||||
const response = await AuthHandler(request, {
|
||||
const resposne = await AuthHandler(request, {
|
||||
providers: [$1({ clientId: "", clientSecret: "" })],
|
||||
})
|
||||
```
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"value": "sveltekit.authjs.dev"
|
||||
}
|
||||
],
|
||||
"destination": "https://authjs.dev/reference/sveltekit"
|
||||
"destination": "https://authjs.dev/reference/sveltekit/modules/main"
|
||||
},
|
||||
{
|
||||
"source": "/",
|
||||
@@ -93,7 +93,7 @@
|
||||
"value": "errors.authjs.dev"
|
||||
}
|
||||
],
|
||||
"destination": "https://authjs.dev/reference/core/errors/:path*"
|
||||
"destination": "https://authjs.dev/reference/core/modules/errors/:path*"
|
||||
},
|
||||
{
|
||||
"source": "/:path(.*)",
|
||||
@@ -123,7 +123,7 @@
|
||||
"value": "providers.authjs.dev"
|
||||
}
|
||||
],
|
||||
"destination": "https://authjs.dev/reference/core/providers_:path.default"
|
||||
"destination": "https://authjs.dev/reference/core/functions/providers_:path.default"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ The simplest way to use Dgraph is by copy pasting the unsecure schema into your
|
||||
|
||||
## Securing your database
|
||||
|
||||
Fore sake of security and mostly if your client directly communicate with the graphql server you obviously want to restrict the access to the types used by next-auth. That's why you see a lot of @auth directive alongside this types in the schema.
|
||||
Fore sake of security and mostly if your client directly communicate with the graphql server you obviously want to restrict the access to the types used by next-auth. That's why you see a lot of @auth directive alongide this types in the schema.
|
||||
|
||||
### Dgraph.Authorization
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export default NextAuth({
|
||||
The table respects the single table design pattern. This has many advantages:
|
||||
|
||||
- Only one table to manage, monitor and provision.
|
||||
- Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user).
|
||||
- Querying relations is faster than with multi-table schemas (for eg. retreiving all sessions for a user).
|
||||
- Only one table needs to be replicated, if you want to go multi-region.
|
||||
|
||||
Here is a schema of the table :
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@next-auth/dynamodb-adapter",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
"version": "3.0.0",
|
||||
"version": "1.2.0",
|
||||
"description": "AWS DynamoDB adapter for next-auth.",
|
||||
"keywords": [
|
||||
"next-auth",
|
||||
@@ -9,18 +9,11 @@
|
||||
"oauth",
|
||||
"dynamodb"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "./index.d.ts",
|
||||
"homepage": "https://authjs.dev",
|
||||
"bugs": {
|
||||
"url": "https://github.com/nextauthjs/next-auth/issues"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"import": "./index.js"
|
||||
}
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -33,10 +26,7 @@
|
||||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"index.d.ts.map",
|
||||
"src"
|
||||
"dist"
|
||||
],
|
||||
"author": "Pol Marnette",
|
||||
"license": "ISC",
|
||||
@@ -51,11 +41,7 @@
|
||||
"@next-auth/adapter-test": "workspace:*",
|
||||
"@next-auth/tsconfig": "workspace:*",
|
||||
"@shelf/jest-dynamodb": "^2.1.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"jest": "^27.4.3",
|
||||
"next-auth": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
import type {
|
||||
BatchWriteCommandInput,
|
||||
@@ -12,12 +12,16 @@ import type {
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
|
||||
import { format, generateUpdateExpression } from "./utils"
|
||||
|
||||
export { format, generateUpdateExpression }
|
||||
|
||||
export interface DynamoDBAdapterOptions {
|
||||
tableName?: string
|
||||
partitionKey?: string
|
||||
sortKey?: string
|
||||
indexName?: string
|
||||
indexPartitionKey?: string
|
||||
tableName?: string,
|
||||
partitionKey?: string,
|
||||
sortKey?: string,
|
||||
indexName?: string,
|
||||
indexPartitionKey?: string,
|
||||
indexSortKey?: string
|
||||
}
|
||||
|
||||
@@ -26,17 +30,17 @@ export function DynamoDBAdapter(
|
||||
options?: DynamoDBAdapterOptions
|
||||
): Adapter {
|
||||
const TableName = options?.tableName ?? "next-auth"
|
||||
const pk = options?.partitionKey ?? "pk"
|
||||
const sk = options?.sortKey ?? "sk"
|
||||
const IndexName = options?.indexName ?? "GSI1"
|
||||
const GSI1PK = options?.indexPartitionKey ?? "GSI1PK"
|
||||
const GSI1SK = options?.indexSortKey ?? "GSI1SK"
|
||||
const pk = options?.partitionKey ?? 'pk'
|
||||
const sk = options?.sortKey ?? 'sk'
|
||||
const IndexName = options?.indexName ?? 'GSI1'
|
||||
const GSI1PK = options?.indexPartitionKey ?? 'GSI1PK'
|
||||
const GSI1SK = options?.indexSortKey ?? 'GSI1SK'
|
||||
|
||||
return {
|
||||
async createUser(data) {
|
||||
const user: AdapterUser = {
|
||||
...(data as any),
|
||||
id: uuid(),
|
||||
id: randomBytes(16).toString("hex"),
|
||||
}
|
||||
|
||||
await client.put({
|
||||
@@ -46,8 +50,8 @@ export function DynamoDBAdapter(
|
||||
[pk]: `USER#${user.id}`,
|
||||
[sk]: `USER#${user.id}`,
|
||||
type: "USER",
|
||||
[GSI1PK]: `USER#${user.email}`,
|
||||
[GSI1SK]: `USER#${user.email}`,
|
||||
[GSI1PK]: `USER#${user.email as string}`,
|
||||
[GSI1SK]: `USER#${user.email as string}`,
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -161,7 +165,7 @@ export function DynamoDBAdapter(
|
||||
async linkAccount(data) {
|
||||
const item = {
|
||||
...data,
|
||||
id: uuid(),
|
||||
id: randomBytes(16).toString("hex"),
|
||||
[pk]: `USER#${data.userId}`,
|
||||
[sk]: `ACCOUNT#${data.provider}#${data.providerAccountId}`,
|
||||
[GSI1PK]: `ACCOUNT#${data.provider}`,
|
||||
@@ -225,7 +229,7 @@ export function DynamoDBAdapter(
|
||||
},
|
||||
async createSession(data) {
|
||||
const session = {
|
||||
id: uuid(),
|
||||
id: randomBytes(16).toString("hex"),
|
||||
...data,
|
||||
}
|
||||
await client.put({
|
||||
@@ -323,73 +327,3 @@ export function DynamoDBAdapter(
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/honeinc/is-iso-date/blob/master/index.js
|
||||
const isoDateRE =
|
||||
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
|
||||
function isDate(value: any) {
|
||||
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
|
||||
}
|
||||
|
||||
const format = {
|
||||
/** Takes a plain old JavaScript object and turns it into a Dynamodb object */
|
||||
to(object: Record<string, any>) {
|
||||
const newObject: Record<string, unknown> = {}
|
||||
for (const key in object) {
|
||||
const value = object[key]
|
||||
if (value instanceof Date) {
|
||||
// DynamoDB requires the TTL attribute be a UNIX timestamp (in secs).
|
||||
if (key === "expires") newObject[key] = value.getTime() / 1000
|
||||
else newObject[key] = value.toISOString()
|
||||
} else newObject[key] = value
|
||||
}
|
||||
return newObject
|
||||
},
|
||||
/** Takes a Dynamo object and returns a plain old JavaScript object */
|
||||
from<T = Record<string, unknown>>(object?: Record<string, any>): T | null {
|
||||
if (!object) return null
|
||||
const newObject: Record<string, unknown> = {}
|
||||
for (const key in object) {
|
||||
// Filter DynamoDB specific attributes so it doesn't get passed to core,
|
||||
// to avoid revealing the type of database
|
||||
if (["pk", "sk", "GSI1PK", "GSI1SK"].includes(key)) continue
|
||||
|
||||
const value = object[key]
|
||||
|
||||
if (isDate(value)) newObject[key] = new Date(value)
|
||||
// hack to keep type property in account
|
||||
else if (key === "type" && ["SESSION", "VT", "USER"].includes(value))
|
||||
continue
|
||||
// The expires property is stored as a UNIX timestamp in seconds, but
|
||||
// JavaScript needs it in milliseconds, so multiply by 1000.
|
||||
else if (key === "expires" && typeof value === "number")
|
||||
newObject[key] = new Date(value * 1000)
|
||||
else newObject[key] = value
|
||||
}
|
||||
return newObject as T
|
||||
},
|
||||
}
|
||||
|
||||
function generateUpdateExpression(object: Record<string, any>): {
|
||||
UpdateExpression: string
|
||||
ExpressionAttributeNames: Record<string, string>
|
||||
ExpressionAttributeValues: Record<string, unknown>
|
||||
} {
|
||||
const formattedSession = format.to(object)
|
||||
let UpdateExpression = "set"
|
||||
const ExpressionAttributeNames: Record<string, string> = {}
|
||||
const ExpressionAttributeValues: Record<string, unknown> = {}
|
||||
for (const property in formattedSession) {
|
||||
UpdateExpression += ` #${property} = :${property},`
|
||||
ExpressionAttributeNames["#" + property] = property
|
||||
ExpressionAttributeValues[":" + property] = formattedSession[property]
|
||||
}
|
||||
UpdateExpression = UpdateExpression.slice(0, -1)
|
||||
return {
|
||||
UpdateExpression,
|
||||
ExpressionAttributeNames,
|
||||
ExpressionAttributeValues,
|
||||
}
|
||||
}
|
||||
|
||||
export { format, generateUpdateExpression }
|
||||
|
||||
67
packages/adapter-dynamodb/src/utils.ts
Normal file
67
packages/adapter-dynamodb/src/utils.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
// https://github.com/honeinc/is-iso-date/blob/master/index.js
|
||||
const isoDateRE =
|
||||
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
|
||||
function isDate(value: any) {
|
||||
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
|
||||
}
|
||||
|
||||
export const format = {
|
||||
/** Takes a plain old JavaScript object and turns it into a Dynamodb object */
|
||||
to(object: Record<string, any>) {
|
||||
const newObject: Record<string, unknown> = {}
|
||||
for (const key in object) {
|
||||
const value = object[key]
|
||||
if (value instanceof Date) {
|
||||
// DynamoDB requires the TTL attribute be a UNIX timestamp (in secs).
|
||||
if (key === "expires") newObject[key] = value.getTime() / 1000
|
||||
else newObject[key] = value.toISOString()
|
||||
} else newObject[key] = value
|
||||
}
|
||||
return newObject
|
||||
},
|
||||
/** Takes a Dynamo object and returns a plain old JavaScript object */
|
||||
from<T = Record<string, unknown>>(object?: Record<string, any>): T | null {
|
||||
if (!object) return null
|
||||
const newObject: Record<string, unknown> = {}
|
||||
for (const key in object) {
|
||||
// Filter DynamoDB specific attributes so it doesn't get passed to core,
|
||||
// to avoid revealing the type of database
|
||||
if (["pk", "sk", "GSI1PK", "GSI1SK"].includes(key)) continue
|
||||
|
||||
const value = object[key]
|
||||
|
||||
if (isDate(value)) newObject[key] = new Date(value)
|
||||
// hack to keep type property in account
|
||||
else if (key === "type" && ["SESSION", "VT", "USER"].includes(value))
|
||||
continue
|
||||
// The expires property is stored as a UNIX timestamp in seconds, but
|
||||
// JavaScript needs it in milliseconds, so multiply by 1000.
|
||||
else if (key === "expires" && typeof value === "number")
|
||||
newObject[key] = new Date(value * 1000)
|
||||
else newObject[key] = value
|
||||
}
|
||||
return newObject as T
|
||||
},
|
||||
}
|
||||
|
||||
export function generateUpdateExpression(object: Record<string, any>): {
|
||||
UpdateExpression: string
|
||||
ExpressionAttributeNames: Record<string, string>
|
||||
ExpressionAttributeValues: Record<string, unknown>
|
||||
} {
|
||||
const formatedSession = format.to(object)
|
||||
let UpdateExpression = "set"
|
||||
const ExpressionAttributeNames: Record<string, string> = {}
|
||||
const ExpressionAttributeValues: Record<string, unknown> = {}
|
||||
for (const property in formatedSession) {
|
||||
UpdateExpression += ` #${property} = :${property},`
|
||||
ExpressionAttributeNames["#" + property] = property
|
||||
ExpressionAttributeValues[":" + property] = formatedSession[property]
|
||||
}
|
||||
UpdateExpression = UpdateExpression.slice(0, -1)
|
||||
return {
|
||||
UpdateExpression,
|
||||
ExpressionAttributeNames,
|
||||
ExpressionAttributeValues,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { format } from "../src/"
|
||||
import { format } from "../src/utils"
|
||||
|
||||
describe("dynamodb utils.format", () => {
|
||||
it("format.to() preserves non-Date non-expires properties", () => {
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": ".",
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"skipDefaultLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"stripInternal": true,
|
||||
"declarationMap": true,
|
||||
"declaration": true
|
||||
"outDir": "dist"
|
||||
},
|
||||
"exclude": ["tests", "dist", "jest.config.js", "jest-dynamodb-config.js"]
|
||||
}
|
||||
|
||||
1
packages/adapter-firebase/.firebaserc
Normal file
1
packages/adapter-firebase/.firebaserc
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
16
packages/adapter-firebase/CHANGELOG.md
Normal file
16
packages/adapter-firebase/CHANGELOG.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.1.3](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.2...@next-auth/firebase-adapter@0.1.3) (2021-08-17)
|
||||
|
||||
**Note:** Version bump only for package @next-auth/firebase-adapter
|
||||
|
||||
## [0.1.2](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.1...@next-auth/firebase-adapter@0.1.2) (2021-07-02)
|
||||
|
||||
**Note:** Version bump only for package @next-auth/firebase-adapter
|
||||
|
||||
## [0.1.1](https://github.com/nextauthjs/adapters/compare/@next-auth/firebase-adapter@0.1.0...@next-auth/firebase-adapter@0.1.1) (2021-06-30)
|
||||
|
||||
**Note:** Version bump only for package @next-auth/firebase-adapter
|
||||
@@ -1,8 +1,8 @@
|
||||
<p align="center">
|
||||
<br/>
|
||||
<a href="https://authjs.dev" target="_blank">
|
||||
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" /></a><img height="64px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/adapter-firebase/logo.svg" />
|
||||
<h3 align="center"><b>Firebase Adapter</b> - Auth.js</h3>
|
||||
<img height="64px" src="https://authjs.dev/img/logo/logo-sm.png" /></a><img height="64px" src="https://raw.githubusercontent.com/nextauthjs/adapters/main/packages/firebase/logo.svg" />
|
||||
<h3 align="center"><b>Firebase Adapter</b> - NextAuth.js</h3>
|
||||
<p align="center">
|
||||
Open Source. Full Stack. Own Your Data.
|
||||
</p>
|
||||
@@ -13,12 +13,72 @@
|
||||
</p>
|
||||
</p>
|
||||
|
||||
## Overview
|
||||
|
||||
This is the official Firebase Adapter for [Auth.js](https://authjs.dev) / [NextAuth.js](https://next-auth.js.org/), using the [Firebase Admin SDK](https://firebase.google.com/docs/admin/setup) and [Firestore](https://firebase.google.com/docs/firestore).
|
||||
This is the Firebase Adapter for [`auth.js`](https://authjs.dev). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
## Documentation
|
||||
You can find more Firebase information in the docs at [authjs.dev/reference/adapters/firebase](https://authjs.dev/reference/adapters/firebase).
|
||||
|
||||
Check out the [documentation](https://authjs.dev/reference/adapter/firebase) to learn how to use this adapter in your project.
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/firebase-adapter`.
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/firebase-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```js
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import { FirestoreAdapter } from "@next-auth/firebase-adapter"
|
||||
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getFirestore } from "firebase/firestore"
|
||||
|
||||
const app = initializeApp({ projectId: "next-auth-test" });
|
||||
const firestore = getFirestore(app);
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://authjs.dev/reference/configuration/auth-options
|
||||
export default NextAuth({
|
||||
// https://authjs.dev/reference/providers/oauth-builtin
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FirestoreAdapter(firestore),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
|
||||
|
||||
An example firebase config looks like this:
|
||||
|
||||
```js
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
|
||||
authDomain: "myapp-project-123.firebaseapp.com",
|
||||
databaseURL: "https://myapp-project-123.firebaseio.com",
|
||||
projectId: "myapp-project-123",
|
||||
storageBucket: "myapp-project-123.appspot.com",
|
||||
messagingSenderId: "65211879809",
|
||||
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
|
||||
measurementId: "G-8GSGZQ44ST",
|
||||
}
|
||||
```
|
||||
|
||||
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
|
||||
|
||||
> **From Firebase - Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
|
||||
>
|
||||
> For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"firestore": {
|
||||
"rules": "firestore.rules"
|
||||
},
|
||||
"emulator": {
|
||||
"emulators": {
|
||||
"firestore": {
|
||||
"port": 8080
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
rules_version = '2';
|
||||
|
||||
// Deny read/write access to all users under any conditions
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
match /{document=**} {
|
||||
allow read, write: if false;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
packages/adapter-firebase/jest.config.js
Normal file
1
packages/adapter-firebase/jest.config.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require("@next-auth/adapter-test/jest/jest-preset")
|
||||
@@ -33,4 +33,4 @@
|
||||
<circle cx="144" cy="144" r="40" fill="#757575"/>
|
||||
<path d="M144 146l-18 8v-8l18-8 18 8v7-1.5 2.5zm0-22l18 8v8l-18-8-18 8v-8zm6.75 29l9 4-15.75 7v-8z" fill="#fff" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/firebase-adapter",
|
||||
"version": "2.0.0",
|
||||
"version": "1.0.3",
|
||||
"description": "Firebase adapter for next-auth.",
|
||||
"homepage": "https://authjs.dev",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
@@ -12,44 +12,35 @@
|
||||
"Nico Domino <yo@ndo.dev>",
|
||||
"Alex Meuer <github@alexmeuer.com>"
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"import": "./index.js"
|
||||
}
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"src",
|
||||
"*.js",
|
||||
"*.d.ts*"
|
||||
"dist",
|
||||
"index.d.ts"
|
||||
],
|
||||
"license": "ISC",
|
||||
"keywords": [
|
||||
"next-auth",
|
||||
"next.js",
|
||||
"firebase",
|
||||
"firebase-admin"
|
||||
"firebase"
|
||||
],
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsc -w",
|
||||
"build": "tsc",
|
||||
"test": "firebase emulators:exec --only firestore --project next-auth-test 'jest -c tests/jest.config.js'"
|
||||
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase --token '$FIREBASE_TOKEN' emulators:exec --only firestore --project next-auth-test jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"firebase-admin": "^11.4.1",
|
||||
"firebase": "^9.7.0",
|
||||
"next-auth": "^4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next-auth/adapter-test": "workspace:*",
|
||||
"@next-auth/tsconfig": "workspace:*",
|
||||
"firebase-admin": "^11.4.1",
|
||||
"firebase": "^9.14.0",
|
||||
"firebase-tools": "^11.16.1",
|
||||
"jest": "^29.3.1",
|
||||
"jest": "^27.4.3",
|
||||
"next-auth": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
packages/adapter-firebase/src/converter.ts
Normal file
58
packages/adapter-firebase/src/converter.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Timestamp } from "firebase/firestore"
|
||||
import type {
|
||||
FirestoreDataConverter,
|
||||
QueryDocumentSnapshot,
|
||||
WithFieldValue,
|
||||
} from "firebase/firestore"
|
||||
|
||||
const isTimestamp = (value: unknown): value is Timestamp =>
|
||||
typeof value === "object" && value !== null && value instanceof Timestamp
|
||||
|
||||
interface GetConverterOptions {
|
||||
excludeId?: boolean
|
||||
}
|
||||
|
||||
export const getConverter = <Document extends Record<string, unknown>>(
|
||||
options?: GetConverterOptions
|
||||
): FirestoreDataConverter<Document> => ({
|
||||
// `PartialWithFieldValue` implicitly types `object` as `any`, so we want to explicitly type it
|
||||
toFirestore(object: WithFieldValue<Document>) {
|
||||
const document: Record<string, unknown> = {}
|
||||
|
||||
Object.keys(object).forEach((key) => {
|
||||
if (object[key] !== undefined) {
|
||||
document[key] = object[key]
|
||||
}
|
||||
})
|
||||
|
||||
return document
|
||||
},
|
||||
// We need to explicitly type `snapshot` since it uses `DocumentData` for generic type
|
||||
fromFirestore(snapshot: QueryDocumentSnapshot<Document>) {
|
||||
if (!snapshot.exists()) {
|
||||
return snapshot
|
||||
}
|
||||
|
||||
let document: Document = snapshot.data()
|
||||
|
||||
if (!options?.excludeId) {
|
||||
document = {
|
||||
...document,
|
||||
id: snapshot.id,
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in document) {
|
||||
const value = document[key]
|
||||
|
||||
if (isTimestamp(value)) {
|
||||
document = {
|
||||
...document,
|
||||
[key]: value.toDate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return document
|
||||
},
|
||||
})
|
||||
11
packages/adapter-firebase/src/getFirebase.ts
Normal file
11
packages/adapter-firebase/src/getFirebase.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { initializeApp, getApps, FirebaseOptions } from "firebase/app"
|
||||
|
||||
export default function getFirebase(firebaseOptions: FirebaseOptions) {
|
||||
const apps = getApps()
|
||||
const app = apps.find((app) => app.name === firebaseOptions.projectId)
|
||||
if (app) {
|
||||
return app
|
||||
} else {
|
||||
return initializeApp(firebaseOptions)
|
||||
}
|
||||
}
|
||||
@@ -1,302 +1,283 @@
|
||||
/**
|
||||
* <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: 16}}>
|
||||
* <span>
|
||||
* Official <b>Firebase</b> adapter for Auth.js / NextAuth.js,
|
||||
* using the <a href="https://firebase.google.com/docs/admin/setup">Firebase Admin SDK</a>
|
||||
* and <a href="https://firebase.google.com/docs/firestore">Firestore</a>.</span>
|
||||
* <a href="https://firebase.google.com/">
|
||||
* <img style={{display: "block"}} src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/adapter-firebase/logo.svg" height="48" width="48"/>
|
||||
* </a>
|
||||
* </div>
|
||||
*
|
||||
* ## Installation
|
||||
*
|
||||
* ```bash npm2yarn2pnpm
|
||||
* npm install next-auth @next-auth/firebase-admin-adapter firebase-admin
|
||||
* ```
|
||||
*
|
||||
* ## References
|
||||
* - [`GOOGLE_APPLICATION_CREDENTIALS` environment variable](https://cloud.google.com/docs/authentication/application-default-credentials#GAC)
|
||||
* - [Firebase Admin SDK setup](https://firebase.google.com/docs/admin/setup#initialize-sdk)
|
||||
*
|
||||
* @module @next-auth/firebase-adapter
|
||||
*/
|
||||
|
||||
import { type AppOptions } from "firebase-admin"
|
||||
import { Firestore } from "firebase-admin/firestore"
|
||||
|
||||
import type { Adapter, AdapterUser } from "next-auth/adapters"
|
||||
import { initializeApp } from "firebase/app"
|
||||
import type { FirebaseOptions } from "firebase/app"
|
||||
import {
|
||||
collestionsFactory,
|
||||
deleteDocs,
|
||||
initFirestore,
|
||||
addDoc,
|
||||
collection,
|
||||
deleteDoc,
|
||||
doc,
|
||||
getDoc,
|
||||
getOneDoc,
|
||||
mapFieldsFactory,
|
||||
} from "./utils"
|
||||
getDocs,
|
||||
getFirestore,
|
||||
limit,
|
||||
query,
|
||||
runTransaction,
|
||||
setDoc,
|
||||
where,
|
||||
connectFirestoreEmulator,
|
||||
} from "firebase/firestore"
|
||||
|
||||
export { initFirestore } from "./utils"
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
|
||||
/** Configure the Firebase Adapter. */
|
||||
export interface FirebaseAdapterConfig extends AppOptions {
|
||||
/**
|
||||
* The name of the app passed to {@link https://firebase.google.com/docs/reference/admin/node/firebase-admin.md#initializeapp `initializeApp()`}.
|
||||
*/
|
||||
name?: string
|
||||
firestore?: Firestore
|
||||
/**
|
||||
* Use this option if mixed `snake_case` and `camelCase` field names in the database is an issue for you.
|
||||
* Passing `snake_case` will convert all field and collection names to `snake_case`.
|
||||
* E.g. the collection `verificationTokens` will be `verification_tokens`,
|
||||
* and fields like `emailVerified` will be `email_verified` instead.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```ts title="pages/api/auth/[...nextauth].ts"
|
||||
* import NextAuth from "next-auth"
|
||||
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
|
||||
*
|
||||
* export default NextAuth({
|
||||
* adapter: FirestoreAdapter({ namingStrategy: "snake_case" })
|
||||
* // ...
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
namingStrategy?: "snake_case"
|
||||
import { getConverter } from "./converter"
|
||||
import getFirebase from "./getFirebase"
|
||||
|
||||
export type IndexableObject = Record<string, unknown>
|
||||
|
||||
export interface FirestoreAdapterOptions {
|
||||
emulator?: {
|
||||
host?: string
|
||||
port?: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #### Usage
|
||||
*
|
||||
* First, create a Firebase project and generate a service account key.
|
||||
* Visit: `https://console.firebase.google.com/u/0/project/{project-id}/settings/serviceaccounts/adminsdk` (replace `{project-id}` with your project's id)
|
||||
*
|
||||
* Now you have a few options to authenticate with the Firebase Admin SDK in your app:
|
||||
*
|
||||
* ##### 1. `GOOGLE_APPLICATION_CREDENTIALS` environment variable:
|
||||
* - Download the service account key and save it in your project. (Make sure to add the file to your `.gitignore`!)
|
||||
* - Add [`GOOGLE_APPLICATION_CREDENTIALS`](https://cloud.google.com/docs/authentication/application-default-credentials#GAC) to your environment variables and point it to the service account key file.
|
||||
* - The adapter will automatically pick up the environment variable and use it to authenticate with the Firebase Admin SDK.
|
||||
*
|
||||
* @example
|
||||
* ```ts title="pages/api/auth/[...nextauth].ts"
|
||||
* import NextAuth from "next-auth"
|
||||
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
|
||||
*
|
||||
* export default NextAuth({
|
||||
* adapter: FirestoreAdapter(),
|
||||
* // ...
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* ##### 2. Service account values as environment variables
|
||||
*
|
||||
* - Download the service account key to a temporary location. (Make sure to not commit this file to your repository!)
|
||||
* - Add the following environment variables to your project: `FIREBASE_PROJECT_ID`, `FIREBASE_CLIENT_EMAIL`, `FIREBASE_PRIVATE_KEY`.
|
||||
* - Pass the config to the adapter, using the environment variables as shown in the example below.
|
||||
*
|
||||
* @example
|
||||
* ```ts title="pages/api/auth/[...nextauth].ts"
|
||||
* import NextAuth from "next-auth"
|
||||
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
|
||||
* import { cert } from "firebase-admin/app"
|
||||
*
|
||||
* export default NextAuth({
|
||||
* adapter: FirestoreAdapter({
|
||||
* credential: cert({
|
||||
* projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
||||
* })
|
||||
* })
|
||||
* // ...
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* ##### 3. Use an existing Firestore instance
|
||||
*
|
||||
* If you already have a Firestore instance, you can pass that to the adapter directly instead.
|
||||
*
|
||||
* :::note
|
||||
* When passing an instance and in a serverless environment, remember to handle duplicate app initialization.
|
||||
* :::
|
||||
*
|
||||
* :::tip
|
||||
* You can use the {@link initFirestore} utility to initialize the app and get an instance safely.
|
||||
* :::
|
||||
*
|
||||
* @example
|
||||
* ```ts title="pages/api/auth/[...nextauth].ts"
|
||||
* import NextAuth from "next-auth"
|
||||
* import { FirestoreAdapter } from "@next-auth/firebase-adapter"
|
||||
* import { firestore } from "lib/firestore"
|
||||
*
|
||||
* export default NextAuth({
|
||||
* adapter: FirestoreAdapter(firestore),
|
||||
* // ...
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function FirestoreAdapter(
|
||||
config?: FirebaseAdapterConfig | Firestore
|
||||
): Adapter {
|
||||
const { db, namingStrategy = "default" } =
|
||||
config instanceof Firestore
|
||||
? { db: config }
|
||||
: { ...config, db: config?.firestore ?? initFirestore(config) }
|
||||
export function FirestoreAdapter({
|
||||
emulator,
|
||||
...firebaseOptions
|
||||
}: FirebaseOptions & FirestoreAdapterOptions): Adapter {
|
||||
const firebaseApp = getFirebase(firebaseOptions)
|
||||
const db = getFirestore(firebaseApp)
|
||||
|
||||
const preferSnakeCase = namingStrategy === "snake_case"
|
||||
const C = collestionsFactory(db, preferSnakeCase)
|
||||
const mapper = mapFieldsFactory(preferSnakeCase)
|
||||
if (emulator) {
|
||||
connectFirestoreEmulator(
|
||||
db,
|
||||
emulator?.host ?? "localhost",
|
||||
emulator?.port ?? 3001
|
||||
)
|
||||
}
|
||||
|
||||
const Users = collection(db, "users").withConverter(
|
||||
getConverter<AdapterUser & IndexableObject>()
|
||||
)
|
||||
const Sessions = collection(db, "sessions").withConverter(
|
||||
getConverter<AdapterSession & IndexableObject>()
|
||||
)
|
||||
const Accounts = collection(db, "accounts").withConverter(
|
||||
getConverter<AdapterAccount>()
|
||||
)
|
||||
const VerificationTokens = collection(db, "verificationTokens").withConverter(
|
||||
getConverter<VerificationToken & IndexableObject>({ excludeId: true })
|
||||
)
|
||||
|
||||
return {
|
||||
async createUser(userInit) {
|
||||
const { id: userId } = await C.users.add(userInit as AdapterUser)
|
||||
async createUser(newUser) {
|
||||
const userRef = await addDoc(Users, newUser)
|
||||
const userSnapshot = await getDoc(userRef)
|
||||
|
||||
const user = await getDoc(C.users.doc(userId))
|
||||
if (!user) throw new Error("[createUser] Failed to fetch created user")
|
||||
if (userSnapshot.exists() && Users.converter) {
|
||||
return Users.converter.fromFirestore(userSnapshot)
|
||||
}
|
||||
|
||||
return user
|
||||
throw new Error("[createUser] Failed to create user")
|
||||
},
|
||||
|
||||
async getUser(id) {
|
||||
return await getDoc(C.users.doc(id))
|
||||
},
|
||||
const userSnapshot = await getDoc(doc(Users, id))
|
||||
|
||||
if (userSnapshot.exists() && Users.converter) {
|
||||
return Users.converter.fromFirestore(userSnapshot)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
async getUserByEmail(email) {
|
||||
return await getOneDoc(C.users.where("email", "==", email))
|
||||
const userQuery = query(Users, where("email", "==", email), limit(1))
|
||||
const userSnapshots = await getDocs(userQuery)
|
||||
const userSnapshot = userSnapshots.docs[0]
|
||||
|
||||
if (userSnapshot?.exists() && Users.converter) {
|
||||
return Users.converter.fromFirestore(userSnapshot)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
async getUserByAccount({ provider, providerAccountId }) {
|
||||
const account = await getOneDoc(
|
||||
C.accounts
|
||||
.where("provider", "==", provider)
|
||||
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
|
||||
const accountQuery = query(
|
||||
Accounts,
|
||||
where("provider", "==", provider),
|
||||
where("providerAccountId", "==", providerAccountId),
|
||||
limit(1)
|
||||
)
|
||||
if (!account) return null
|
||||
const accountSnapshots = await getDocs(accountQuery)
|
||||
const accountSnapshot = accountSnapshots.docs[0]
|
||||
|
||||
return await getDoc(C.users.doc(account.userId))
|
||||
if (accountSnapshot?.exists()) {
|
||||
const { userId } = accountSnapshot.data()
|
||||
const userDoc = await getDoc(doc(Users, userId))
|
||||
|
||||
if (userDoc.exists() && Users.converter) {
|
||||
return Users.converter.fromFirestore(userDoc)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
async updateUser(partialUser) {
|
||||
if (!partialUser.id) throw new Error("[updateUser] Missing id")
|
||||
const userRef = doc(Users, partialUser.id)
|
||||
|
||||
const userRef = C.users.doc(partialUser.id)
|
||||
await setDoc(userRef, partialUser, { merge: true })
|
||||
|
||||
await userRef.set(partialUser, { merge: true })
|
||||
const userSnapshot = await getDoc(userRef)
|
||||
|
||||
const user = await getDoc(userRef)
|
||||
if (!user) throw new Error("[updateUser] Failed to fetch updated user")
|
||||
if (userSnapshot.exists() && Users.converter) {
|
||||
return Users.converter.fromFirestore(userSnapshot)
|
||||
}
|
||||
|
||||
return user
|
||||
throw new Error("[updateUser] Failed to update user")
|
||||
},
|
||||
|
||||
async deleteUser(userId) {
|
||||
await db.runTransaction(async (transaction) => {
|
||||
const accounts = await C.accounts
|
||||
.where(mapper.toDb("userId"), "==", userId)
|
||||
.get()
|
||||
const sessions = await C.sessions
|
||||
.where(mapper.toDb("userId"), "==", userId)
|
||||
.get()
|
||||
const userRef = doc(Users, userId)
|
||||
const accountsQuery = query(Accounts, where("userId", "==", userId))
|
||||
const sessionsQuery = query(Sessions, where("userId", "==", userId))
|
||||
|
||||
transaction.delete(C.users.doc(userId))
|
||||
// TODO: May be better to use events instead of transactions?
|
||||
await runTransaction(db, async (transaction) => {
|
||||
const accounts = await getDocs(accountsQuery)
|
||||
const sessions = await getDocs(sessionsQuery)
|
||||
|
||||
transaction.delete(userRef)
|
||||
accounts.forEach((account) => transaction.delete(account.ref))
|
||||
sessions.forEach((session) => transaction.delete(session.ref))
|
||||
})
|
||||
},
|
||||
|
||||
async linkAccount(accountInit) {
|
||||
const ref = await C.accounts.add(accountInit)
|
||||
const account = await ref.get().then((doc) => doc.data())
|
||||
return account ?? null
|
||||
async linkAccount(account) {
|
||||
const accountRef = await addDoc(Accounts, account)
|
||||
const accountSnapshot = await getDoc(accountRef)
|
||||
|
||||
if (accountSnapshot.exists() && Accounts.converter) {
|
||||
return Accounts.converter.fromFirestore(accountSnapshot)
|
||||
}
|
||||
},
|
||||
|
||||
async unlinkAccount({ provider, providerAccountId }) {
|
||||
await deleteDocs(
|
||||
C.accounts
|
||||
.where("provider", "==", provider)
|
||||
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
|
||||
.limit(1)
|
||||
const accountQuery = query(
|
||||
Accounts,
|
||||
where("provider", "==", provider),
|
||||
where("providerAccountId", "==", providerAccountId),
|
||||
limit(1)
|
||||
)
|
||||
const accountSnapshots = await getDocs(accountQuery)
|
||||
const accountSnapshot = accountSnapshots.docs[0]
|
||||
|
||||
if (accountSnapshot?.exists()) {
|
||||
await deleteDoc(accountSnapshot.ref)
|
||||
}
|
||||
},
|
||||
|
||||
async createSession(sessionInit) {
|
||||
const ref = await C.sessions.add(sessionInit)
|
||||
const session = await ref.get().then((doc) => doc.data())
|
||||
async createSession(session) {
|
||||
const sessionRef = await addDoc(Sessions, session)
|
||||
const sessionSnapshot = await getDoc(sessionRef)
|
||||
|
||||
if (session) return session ?? null
|
||||
if (sessionSnapshot.exists() && Sessions.converter) {
|
||||
return Sessions.converter.fromFirestore(sessionSnapshot)
|
||||
}
|
||||
|
||||
throw new Error("[createSession] Failed to fetch created session")
|
||||
throw new Error("[createSession] Failed to create session")
|
||||
},
|
||||
|
||||
async getSessionAndUser(sessionToken) {
|
||||
const session = await getOneDoc(
|
||||
C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken)
|
||||
const sessionQuery = query(
|
||||
Sessions,
|
||||
where("sessionToken", "==", sessionToken),
|
||||
limit(1)
|
||||
)
|
||||
if (!session) return null
|
||||
const sessionSnapshots = await getDocs(sessionQuery)
|
||||
const sessionSnapshot = sessionSnapshots.docs[0]
|
||||
|
||||
const user = await getDoc(C.users.doc(session.userId))
|
||||
if (!user) return null
|
||||
if (sessionSnapshot?.exists() && Sessions.converter) {
|
||||
const session = Sessions.converter.fromFirestore(sessionSnapshot)
|
||||
const userDoc = await getDoc(doc(Users, session.userId))
|
||||
|
||||
return { session, user }
|
||||
if (userDoc.exists() && Users.converter) {
|
||||
const user = Users.converter.fromFirestore(userDoc)
|
||||
|
||||
return { session, user }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
async updateSession(partialSession) {
|
||||
const sessionId = await db.runTransaction(async (transaction) => {
|
||||
const sessionSnapshot = (
|
||||
await transaction.get(
|
||||
C.sessions
|
||||
.where(
|
||||
mapper.toDb("sessionToken"),
|
||||
"==",
|
||||
partialSession.sessionToken
|
||||
)
|
||||
.limit(1)
|
||||
)
|
||||
).docs[0]
|
||||
if (!sessionSnapshot?.exists) return null
|
||||
const sessionQuery = query(
|
||||
Sessions,
|
||||
where("sessionToken", "==", partialSession.sessionToken),
|
||||
limit(1)
|
||||
)
|
||||
const sessionSnapshots = await getDocs(sessionQuery)
|
||||
const sessionSnapshot = sessionSnapshots.docs[0]
|
||||
|
||||
transaction.set(sessionSnapshot.ref, partialSession, { merge: true })
|
||||
if (sessionSnapshot?.exists()) {
|
||||
await setDoc(sessionSnapshot.ref, partialSession, { merge: true })
|
||||
|
||||
return sessionSnapshot.id
|
||||
})
|
||||
const sessionDoc = await getDoc(sessionSnapshot.ref)
|
||||
|
||||
if (!sessionId) return null
|
||||
if (sessionDoc?.exists() && Sessions.converter) {
|
||||
const session = Sessions.converter.fromFirestore(sessionDoc)
|
||||
|
||||
const session = await getDoc(C.sessions.doc(sessionId))
|
||||
if (session) return session
|
||||
throw new Error("[updateSession] Failed to fetch updated session")
|
||||
return session
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
async deleteSession(sessionToken) {
|
||||
await deleteDocs(
|
||||
C.sessions
|
||||
.where(mapper.toDb("sessionToken"), "==", sessionToken)
|
||||
.limit(1)
|
||||
const sessionQuery = query(
|
||||
Sessions,
|
||||
where("sessionToken", "==", sessionToken),
|
||||
limit(1)
|
||||
)
|
||||
const sessionSnapshots = await getDocs(sessionQuery)
|
||||
const sessionSnapshot = sessionSnapshots.docs[0]
|
||||
|
||||
if (sessionSnapshot?.exists()) {
|
||||
await deleteDoc(sessionSnapshot.ref)
|
||||
}
|
||||
},
|
||||
|
||||
async createVerificationToken(verificationToken) {
|
||||
await C.verification_tokens.add(verificationToken)
|
||||
return verificationToken
|
||||
const verificationTokenRef = await addDoc(
|
||||
VerificationTokens,
|
||||
verificationToken
|
||||
)
|
||||
const verificationTokenSnapshot = await getDoc(verificationTokenRef)
|
||||
|
||||
if (verificationTokenSnapshot.exists() && VerificationTokens.converter) {
|
||||
const { id, ...verificationToken } =
|
||||
VerificationTokens.converter.fromFirestore(verificationTokenSnapshot)
|
||||
|
||||
return verificationToken
|
||||
}
|
||||
},
|
||||
|
||||
async useVerificationToken({ identifier, token }) {
|
||||
const verificationTokenSnapshot = (
|
||||
await C.verification_tokens
|
||||
.where("identifier", "==", identifier)
|
||||
.where("token", "==", token)
|
||||
.limit(1)
|
||||
.get()
|
||||
).docs[0]
|
||||
const verificationTokensQuery = query(
|
||||
VerificationTokens,
|
||||
where("identifier", "==", identifier),
|
||||
where("token", "==", token),
|
||||
limit(1)
|
||||
)
|
||||
const verificationTokenSnapshots = await getDocs(verificationTokensQuery)
|
||||
const verificationTokenSnapshot = verificationTokenSnapshots.docs[0]
|
||||
|
||||
if (!verificationTokenSnapshot) return null
|
||||
if (verificationTokenSnapshot?.exists() && VerificationTokens.converter) {
|
||||
await deleteDoc(verificationTokenSnapshot.ref)
|
||||
|
||||
const data = verificationTokenSnapshot.data()
|
||||
await verificationTokenSnapshot.ref.delete()
|
||||
return data
|
||||
const { id, ...verificationToken } =
|
||||
VerificationTokens.converter.fromFirestore(verificationTokenSnapshot)
|
||||
|
||||
return verificationToken
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { AppOptions, getApps, initializeApp } from "firebase-admin/app"
|
||||
|
||||
import {
|
||||
getFirestore,
|
||||
initializeFirestore,
|
||||
Timestamp,
|
||||
} from "firebase-admin/firestore"
|
||||
|
||||
import type {
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
import { FirebaseAdapterConfig } from "."
|
||||
|
||||
// for consistency, store all fields as snake_case in the database
|
||||
const MAP_TO_FIRESTORE: Record<string, string | undefined> = {
|
||||
userId: "user_id",
|
||||
sessionToken: "session_token",
|
||||
providerAccountId: "provider_account_id",
|
||||
emailVerified: "email_verified",
|
||||
}
|
||||
const MAP_FROM_FIRESTORE: Record<string, string | undefined> = {}
|
||||
|
||||
for (const key in MAP_TO_FIRESTORE) {
|
||||
MAP_FROM_FIRESTORE[MAP_TO_FIRESTORE[key]!] = key
|
||||
}
|
||||
|
||||
const identity = <T>(x: T) => x
|
||||
|
||||
/** @internal */
|
||||
export function mapFieldsFactory(preferSnakeCase?: boolean) {
|
||||
if (preferSnakeCase) {
|
||||
return {
|
||||
toDb: (field: string) => MAP_TO_FIRESTORE[field] ?? field,
|
||||
fromDb: (field: string) => MAP_FROM_FIRESTORE[field] ?? field,
|
||||
}
|
||||
}
|
||||
return { toDb: identity, fromDb: identity }
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function getConverter<Document extends Record<string, any>>(options: {
|
||||
excludeId?: boolean
|
||||
preferSnakeCase?: boolean
|
||||
}): FirebaseFirestore.FirestoreDataConverter<Document> {
|
||||
const mapper = mapFieldsFactory(options?.preferSnakeCase ?? false)
|
||||
|
||||
return {
|
||||
toFirestore(object) {
|
||||
const document: Record<string, unknown> = {}
|
||||
|
||||
for (const key in object) {
|
||||
if (key === "id") continue
|
||||
const value = object[key]
|
||||
if (value !== undefined) {
|
||||
document[mapper.toDb(key)] = value
|
||||
} else {
|
||||
console.warn(`FirebaseAdapter: value for key "${key}" is undefined`)
|
||||
}
|
||||
}
|
||||
|
||||
return document
|
||||
},
|
||||
|
||||
fromFirestore(
|
||||
snapshot: FirebaseFirestore.QueryDocumentSnapshot<Document>
|
||||
): Document {
|
||||
const document = snapshot.data()! // we can guarantee it exists
|
||||
|
||||
const object: Record<string, unknown> = {}
|
||||
|
||||
if (!options?.excludeId) {
|
||||
object.id = snapshot.id
|
||||
}
|
||||
|
||||
for (const key in document) {
|
||||
let value: any = document[key]
|
||||
if (value instanceof Timestamp) value = value.toDate()
|
||||
|
||||
object[mapper.fromDb(key)] = value
|
||||
}
|
||||
|
||||
return object as Document
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getOneDoc<T>(
|
||||
querySnapshot: FirebaseFirestore.Query<T>
|
||||
): Promise<T | null> {
|
||||
const querySnap = await querySnapshot.limit(1).get()
|
||||
return querySnap.docs[0]?.data() ?? null
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function deleteDocs<T>(
|
||||
querySnapshot: FirebaseFirestore.Query<T>
|
||||
): Promise<void> {
|
||||
const querySnap = await querySnapshot.get()
|
||||
for (const doc of querySnap.docs) {
|
||||
await doc.ref.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getDoc<T>(
|
||||
docRef: FirebaseFirestore.DocumentReference<T>
|
||||
): Promise<T | null> {
|
||||
const docSnap = await docRef.get()
|
||||
return docSnap.data() ?? null
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function collestionsFactory(
|
||||
db: FirebaseFirestore.Firestore,
|
||||
preferSnakeCase = false
|
||||
) {
|
||||
return {
|
||||
users: db
|
||||
.collection("users")
|
||||
.withConverter(getConverter<AdapterUser>({ preferSnakeCase })),
|
||||
sessions: db
|
||||
.collection("sessions")
|
||||
.withConverter(getConverter<AdapterSession>({ preferSnakeCase })),
|
||||
accounts: db
|
||||
.collection("accounts")
|
||||
.withConverter(getConverter<AdapterAccount>({ preferSnakeCase })),
|
||||
verification_tokens: db
|
||||
.collection(
|
||||
preferSnakeCase ? "verification_tokens" : "verificationTokens"
|
||||
)
|
||||
.withConverter(
|
||||
getConverter<VerificationToken>({ preferSnakeCase, excludeId: true })
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function that helps making sure that there is no duplicate app initialization issues in serverless environments.
|
||||
* If no parameter is passed, it will use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to initialize a Firestore instance.
|
||||
*
|
||||
* @example
|
||||
* ```ts title="lib/firestore.ts"
|
||||
* import { initFirestore } from "@next-auth/firebase-adapter"
|
||||
* import { cert } from "firebase-admin/app"
|
||||
*
|
||||
* export const firestore = initFirestore({
|
||||
* credential: cert({
|
||||
* projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
* clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
* privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function initFirestore(
|
||||
options: AppOptions & { name?: FirebaseAdapterConfig["name"] } = {}
|
||||
) {
|
||||
const apps = getApps()
|
||||
const app = options.name ? apps.find((a) => a.name === options.name) : apps[0]
|
||||
|
||||
if (app) return getFirestore(app)
|
||||
|
||||
return initializeFirestore(initializeApp(options, options.name))
|
||||
}
|
||||
@@ -1,57 +1,118 @@
|
||||
import { runBasicTests } from "@next-auth/adapter-test"
|
||||
import { FirestoreAdapter } from "../src"
|
||||
|
||||
import { FirestoreAdapter, type FirebaseAdapterConfig } from "../src"
|
||||
import {
|
||||
collestionsFactory,
|
||||
initFirestore,
|
||||
getFirestore,
|
||||
connectFirestoreEmulator,
|
||||
terminate,
|
||||
collection,
|
||||
query,
|
||||
where,
|
||||
limit,
|
||||
getDocs,
|
||||
getDoc,
|
||||
getOneDoc,
|
||||
mapFieldsFactory,
|
||||
} from "../src/utils"
|
||||
doc,
|
||||
} from "firebase/firestore"
|
||||
import { initializeApp } from "firebase/app"
|
||||
import { getConverter } from "../src/converter"
|
||||
import type {
|
||||
AdapterSession,
|
||||
AdapterUser,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
import type { Account } from "next-auth"
|
||||
|
||||
describe.each([
|
||||
{ namingStrategy: "snake_case" },
|
||||
{ namingStrategy: "default" },
|
||||
] as Partial<FirebaseAdapterConfig>[])(
|
||||
"FirebaseAdapter with config: %s",
|
||||
(config) => {
|
||||
config.name = `next-auth-test-${config.namingStrategy}`
|
||||
config.projectId = "next-auth-test"
|
||||
config.databaseURL = "http://localhost:8080"
|
||||
const app = initializeApp({ projectId: "next-auth-test" })
|
||||
const firestore = getFirestore(app)
|
||||
|
||||
const db = initFirestore(config)
|
||||
const preferSnakeCase = config.namingStrategy === "snake_case"
|
||||
const mapper = mapFieldsFactory(preferSnakeCase)
|
||||
const C = collestionsFactory(db, preferSnakeCase)
|
||||
connectFirestoreEmulator(firestore, "localhost", 8080)
|
||||
|
||||
for (const [name, collection] of Object.entries(C)) {
|
||||
test(`collection "${name}" should be empty`, async () => {
|
||||
expect((await collection.count().get()).data().count).toBe(0)
|
||||
})
|
||||
}
|
||||
type IndexableObject = Record<string, unknown>
|
||||
|
||||
runBasicTests({
|
||||
adapter: FirestoreAdapter(config),
|
||||
db: {
|
||||
disconnect: async () => await db.terminate(),
|
||||
session: (sessionToken) =>
|
||||
getOneDoc(
|
||||
C.sessions.where(mapper.toDb("sessionToken"), "==", sessionToken)
|
||||
),
|
||||
user: (userId) => getDoc(C.users.doc(userId)),
|
||||
account: ({ provider, providerAccountId }) =>
|
||||
getOneDoc(
|
||||
C.accounts
|
||||
.where("provider", "==", provider)
|
||||
.where(mapper.toDb("providerAccountId"), "==", providerAccountId)
|
||||
),
|
||||
verificationToken: ({ identifier, token }) =>
|
||||
getOneDoc(
|
||||
C.verification_tokens
|
||||
.where("identifier", "==", identifier)
|
||||
.where("token", "==", token)
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
const Users = collection(firestore, "users").withConverter(
|
||||
getConverter<AdapterUser & IndexableObject>()
|
||||
)
|
||||
const Sessions = collection(firestore, "sessions").withConverter(
|
||||
getConverter<AdapterSession & IndexableObject>()
|
||||
)
|
||||
const Accounts = collection(firestore, "accounts").withConverter(
|
||||
getConverter<Account>()
|
||||
)
|
||||
const VerificationTokens = collection(
|
||||
firestore,
|
||||
"verificationTokens"
|
||||
).withConverter(
|
||||
getConverter<VerificationToken & IndexableObject>({ excludeId: true })
|
||||
)
|
||||
|
||||
runBasicTests({
|
||||
adapter: FirestoreAdapter({ projectId: "next-auth-test" }),
|
||||
db: {
|
||||
async disconnect() {
|
||||
await terminate(firestore)
|
||||
},
|
||||
async session(sessionToken) {
|
||||
const snapshotQuery = query(
|
||||
Sessions,
|
||||
where("sessionToken", "==", sessionToken),
|
||||
limit(1)
|
||||
)
|
||||
const snapshots = await getDocs(snapshotQuery)
|
||||
const snapshot = snapshots.docs[0]
|
||||
|
||||
if (snapshot?.exists() && Sessions.converter) {
|
||||
const session = Sessions.converter.fromFirestore(snapshot)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
async user(id) {
|
||||
const snapshot = await getDoc(doc(Users, id))
|
||||
|
||||
if (snapshot?.exists() && Users.converter) {
|
||||
const user = Users.converter.fromFirestore(snapshot)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
async account({ provider, providerAccountId }) {
|
||||
const snapshotQuery = query(
|
||||
Accounts,
|
||||
where("provider", "==", provider),
|
||||
where("providerAccountId", "==", providerAccountId),
|
||||
limit(1)
|
||||
)
|
||||
const snapshots = await getDocs(snapshotQuery)
|
||||
const snapshot = snapshots.docs[0]
|
||||
|
||||
if (snapshot?.exists() && Accounts.converter) {
|
||||
const account = Accounts.converter.fromFirestore(snapshot)
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
async verificationToken({ identifier, token }) {
|
||||
const snapshotQuery = query(
|
||||
VerificationTokens,
|
||||
where("identifier", "==", identifier),
|
||||
where("token", "==", token),
|
||||
limit(1)
|
||||
)
|
||||
const snapshots = await getDocs(snapshotQuery)
|
||||
const snapshot = snapshots.docs[0]
|
||||
|
||||
if (snapshot?.exists() && VerificationTokens.converter) {
|
||||
const verificationToken =
|
||||
VerificationTokens.converter.fromFirestore(snapshot)
|
||||
|
||||
return verificationToken
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import config from "@next-auth/adapter-test/jest/jest-preset.js"
|
||||
|
||||
//TODO: update rest of the packages to Jest 29+
|
||||
const {testURL, ...rest} = config
|
||||
export default {
|
||||
...rest,
|
||||
testEnvironmentOptions: {
|
||||
url: testURL
|
||||
},
|
||||
rootDir: ".."
|
||||
}
|
||||
@@ -1,23 +1,11 @@
|
||||
{
|
||||
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"outDir": ".",
|
||||
"rootDir": "src",
|
||||
"skipDefaultLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"stripInternal": true,
|
||||
"declarationMap": true,
|
||||
"declaration": true
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"tests"
|
||||
]
|
||||
}
|
||||
"exclude": ["tests", "dist", "jest.config.js"]
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ Depending on your architecture you can use PouchDB's http adapter to reach any d
|
||||
|
||||
1. Install `next-auth` and `@next-auth/pouchdb-adapter`, as well as `pouchdb`.
|
||||
|
||||
> **Prerequisite**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
|
||||
> **Prerequesite**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/pouchdb-adapter pouchdb
|
||||
|
||||
@@ -41,7 +41,7 @@ export const PouchDBAdapter: Adapter<
|
||||
> = (pouchdb) => {
|
||||
return {
|
||||
async getAdapter({ session, secret, ...appOptions }) {
|
||||
// create PouchDB indexes if they don't exist
|
||||
// create PoucDB indexes if they don't exist
|
||||
const res = await pouchdb.getIndexes()
|
||||
const indexes = res.indexes.map((index) => index.name, [])
|
||||
if (!indexes.includes("nextAuthUserByEmail")) {
|
||||
|
||||
@@ -24,7 +24,7 @@ This is the Upstash Redis adapter for [`next-auth`](https://authjs.dev). This pa
|
||||
npm install next-auth @next-auth/upstash-redis-adapter @upstash/redis
|
||||
```
|
||||
|
||||
2. Add the following code to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
2. Add the follwing code to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```js
|
||||
import NextAuth from "next-auth"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@auth/core",
|
||||
"version": "0.4.0",
|
||||
"version": "0.3.0",
|
||||
"description": "Authentication for the Web.",
|
||||
"keywords": [
|
||||
"authentication",
|
||||
@@ -27,7 +27,7 @@
|
||||
"types": "./index.d.ts",
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.d.ts*",
|
||||
"*.d.ts",
|
||||
"lib",
|
||||
"providers",
|
||||
"src"
|
||||
@@ -61,7 +61,7 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@panva/hkdf": "^1.0.4",
|
||||
"@panva/hkdf": "^1.0.2",
|
||||
"cookie": "0.5.0",
|
||||
"jose": "^4.11.1",
|
||||
"oauth4webapi": "^2.0.6",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* A database adapter provides a common interface for Auth.js so that it can work with
|
||||
* _any_ database/ORM adapter without concerning itself with the implementation details of the database/ORM.
|
||||
*
|
||||
* Auth.js supports 2 session strategies to persist the login state of a user.
|
||||
* Auth.js supports 2 session strtategies to persist the login state of a user.
|
||||
* The default is to use a cookie + {@link https://authjs.dev/concepts/session-strategies#jwt JWT}
|
||||
* based session store (`strategy: "jwt"`),
|
||||
* but you can also use a database adapter to store the session in a database.
|
||||
@@ -26,7 +26,7 @@
|
||||
*
|
||||
* ## Usage
|
||||
*
|
||||
* {@link https://authjs.dev/reference/adapters/overview Built-in adapters} already implement this interface, so you likely won't need to
|
||||
* {@link https://authjs.dev/reference/adapters/overview Built-in adapters} already implement this interfac, so you likely won't need to
|
||||
* implement it yourself. If you do, you can use the following example as a
|
||||
* starting point.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
/**
|
||||
*
|
||||
* :::warning Experimental
|
||||
* `@auth/core` is under active development.
|
||||
* :::
|
||||
*
|
||||
* This is the main entry point to the Auth.js library.
|
||||
*
|
||||
@@ -22,7 +18,7 @@
|
||||
* ```ts
|
||||
* import { Auth } from "@auth/core"
|
||||
*
|
||||
* const request = new Request("https://example.com")
|
||||
* const request = new Request("https://example.com"
|
||||
* const response = await Auth(request, {...})
|
||||
*
|
||||
* console.log(response instanceof Response) // true
|
||||
@@ -166,7 +162,7 @@ export async function Auth(
|
||||
* const response = await AuthHandler(request, authConfig)
|
||||
* ```
|
||||
*
|
||||
* @see [Initialization](https://authjs.dev/reference/configuration/auth-options)
|
||||
* @see [Initiailzation](https://authjs.dev/reference/configuration/auth-options)
|
||||
*/
|
||||
export interface AuthConfig {
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* issued and used by Auth.js.
|
||||
*
|
||||
* The JWT issued by Auth.js is _encrypted by default_, using the _A256GCM_ algorithm ({@link https://www.rfc-editor.org/rfc/rfc7516 JWE}).
|
||||
* It uses the `AUTH_SECRET` environment variable to derive a sufficient encryption key.
|
||||
* It uses the `AUTH_SECRET` environment variable to dervice a sufficient encryption key.
|
||||
*
|
||||
* :::info Note
|
||||
* Auth.js JWTs are meant to be used by the same app that issued them.
|
||||
@@ -203,7 +203,7 @@ export interface JWTOptions {
|
||||
/**
|
||||
* The secret used to encode/decode the Auth.js issued JWT.
|
||||
*
|
||||
* @deprecated Set the `AUTH_SECRET` environment variable or
|
||||
* @deprecated Set the `AUTH_SECRET` environment vairable or
|
||||
* use the top-level `secret` option instead
|
||||
*/
|
||||
secret: string
|
||||
|
||||
@@ -15,8 +15,8 @@ import type { SessionToken } from "./cookie.js"
|
||||
* It prevents insecure behaviour, such as linking OAuth accounts unless a user is
|
||||
* signed in and authenticated with an existing valid account.
|
||||
*
|
||||
* All verification (e.g. OAuth flows or email address verification flows) are
|
||||
* done prior to this handler being called to avoid additional complexity in this
|
||||
* All verification (e.g. OAuth flows or email address verificaiton flows) are
|
||||
* done prior to this handler being called to avoid additonal complexity in this
|
||||
* handler.
|
||||
*/
|
||||
export async function handleLogin(
|
||||
@@ -203,7 +203,7 @@ export async function handleLogin(
|
||||
// accounts (by email or provider account id)...
|
||||
//
|
||||
// If no account matching the same [provider].id or .email exists, we can
|
||||
// create a new account for the user, link it to the OAuth account and
|
||||
// create a new account for the user, link it to the OAuth acccount and
|
||||
// create a new session for them so they are signed in with it.
|
||||
const { id: _, ...newUser } = { ...profile, emailVerified: null }
|
||||
user = await createUser(newUser)
|
||||
|
||||
@@ -16,7 +16,7 @@ interface CreateCSRFTokenParams {
|
||||
* where 'token' is the CSRF token and 'hash' is a hash made of the token and
|
||||
* the secret, and the two values are joined by a pipe '|'. By storing the
|
||||
* value and the hash of the value (with the secret used as a salt) we can
|
||||
* verify the cookie was set by the server and not by a malicious attacker.
|
||||
* verify the cookie was set by the server and not by a malicous attacker.
|
||||
*
|
||||
* For more details, see the following OWASP links:
|
||||
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function init({
|
||||
|
||||
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default
|
||||
|
||||
// User provided options are overridden by other options,
|
||||
// User provided options are overriden by other options,
|
||||
// except for the options with special handling above
|
||||
const options: InternalOptions = {
|
||||
debug: false,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
--color-control-border: #bbb;
|
||||
--color-button-active-background: #f9f9f9;
|
||||
--color-button-active-border: #aaa;
|
||||
--color-separator: #ccc;
|
||||
--color-seperator: #ccc;
|
||||
}
|
||||
|
||||
.__next-auth-theme-dark {
|
||||
@@ -26,7 +26,7 @@
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
--color-separator: #444;
|
||||
--color-seperator: #444;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -38,7 +38,7 @@
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
--color-separator: #444;
|
||||
--color-seperator: #444;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ a.site {
|
||||
hr {
|
||||
display: block;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--color-separator);
|
||||
border-top: 1px solid var(--color-seperator);
|
||||
margin: 2rem auto 1rem auto;
|
||||
overflow: visible;
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ export async function callback(params: {
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
return { redirect: callbackUrl, cookies }
|
||||
} else if (provider.type === "credentials" && method === "POST") {
|
||||
const credentials = body ?? {}
|
||||
const credentials = body
|
||||
|
||||
// TODO: Forward the original request as is, instead of reconstructing it
|
||||
Object.entries(query ?? {}).forEach(([k, v]) =>
|
||||
|
||||
@@ -84,7 +84,7 @@ export interface Auth0Profile {
|
||||
* import Auth0 from "@auth/core/providers/auth0"
|
||||
*
|
||||
* const request = new Request("https://example.com")
|
||||
* const response = await Auth(request, {
|
||||
* const resposne = await Auth(request, {
|
||||
* providers: [Auth0({ clientId: "", clientSecret: "", issuer: "" })],
|
||||
* })
|
||||
* ```
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Awaitable, User } from "../types.js"
|
||||
import type { JSXInternal } from "preact/src/jsx.js"
|
||||
|
||||
/**
|
||||
* Besides providing type safety inside {@link CredentialsConfig.authorize}
|
||||
* Besieds providing type safety inside {@link CredentialsConfig.authorize}
|
||||
* it also determines how the credentials input fields will be rendered
|
||||
* on the default sign in page.
|
||||
*/
|
||||
@@ -40,16 +40,8 @@ export interface CredentialsConfig<
|
||||
* //...
|
||||
*/
|
||||
authorize: (
|
||||
/**
|
||||
* The available keys are determined by {@link CredentialInput}.
|
||||
*
|
||||
* @note The existence/correctness of a field cannot be guaranteed at compile time,
|
||||
* so you should always validate the input before using it.
|
||||
*
|
||||
* You can add basic validation depending on your use case,
|
||||
* or you can use a popular library like [Zod](https://zod.dev) for example.
|
||||
*/
|
||||
credentials: Partial<Record<keyof CredentialsInputs, unknown>>,
|
||||
/** See {@link CredentialInput} */
|
||||
credentials: Record<keyof CredentialsInputs, string> | undefined,
|
||||
/** The original request is forward for convenience */
|
||||
request: Request
|
||||
) => Awaitable<User | null>
|
||||
@@ -79,10 +71,10 @@ export type CredentialsProviderType = "Credentials"
|
||||
* @example
|
||||
* ```js
|
||||
* import Auth from "@auth/core"
|
||||
* import Credentials from "@auth/core/providers/credentials"
|
||||
* import { Credentials } from "@auth/core/providers/credentials"
|
||||
*
|
||||
* const request = new Request("https://example.com")
|
||||
* const response = await AuthHandler(request, {
|
||||
* const resposne = await AuthHandler(request, {
|
||||
* providers: [
|
||||
* Credentials({
|
||||
* credentials: {
|
||||
|
||||
@@ -82,7 +82,7 @@ export interface EmailConfig extends CommonProviderOptions {
|
||||
export type EmailProviderType = "email"
|
||||
|
||||
/** TODO: */
|
||||
export default function Email(config: EmailConfig): EmailConfig {
|
||||
export function Email(config: EmailConfig): EmailConfig {
|
||||
return {
|
||||
id: "email",
|
||||
type: "email",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Foursquare(options) {
|
||||
const { apiVersion = "20230131" } = options
|
||||
const { apiVersion = "20210801" } = options
|
||||
return {
|
||||
id: "foursquare",
|
||||
name: "Foursquare",
|
||||
@@ -15,7 +15,7 @@ export default function Foursquare(options) {
|
||||
return fetch(url).then((res) => res.json())
|
||||
},
|
||||
},
|
||||
profile({ response: { user: profile } }) {
|
||||
profile({ response: { profile } }) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: `${profile.firstName} ${profile.lastName}`,
|
||||
|
||||
@@ -78,7 +78,7 @@ export interface GitHubProfile {
|
||||
* import GitHub from "@auth/core/providers/github"
|
||||
*
|
||||
* const request = new Request("https://example.com")
|
||||
* const response = await Auth(request, {
|
||||
* const resposne = await Auth(request, {
|
||||
* providers: [GitHub({ clientId: "", clientSecret: "" })],
|
||||
* })
|
||||
* ```
|
||||
|
||||
@@ -54,10 +54,10 @@ export interface GitLabProfile extends Record<string, any> {
|
||||
*
|
||||
* ```js
|
||||
* import Auth from "@auth/core"
|
||||
* import GitLab from "@auth/core/providers/gitlab"
|
||||
* import { GitLab } from "@auth/core/providers/gitlab"
|
||||
*
|
||||
* const request = new Request("https://example.com")
|
||||
* const response = await AuthHandler(request, {
|
||||
* const resposne = await AuthHandler(request, {
|
||||
* providers: [
|
||||
* GitLab({clientId: "", clientSecret: ""})
|
||||
* ]
|
||||
|
||||
@@ -4,8 +4,11 @@ import type {
|
||||
CredentialsConfig,
|
||||
CredentialsProviderType,
|
||||
} from "./credentials.js"
|
||||
import type EmailProvider from "./email.js"
|
||||
import type { EmailConfig, EmailProviderType } from "./email.js"
|
||||
import type {
|
||||
Email as EmailProvider,
|
||||
EmailConfig,
|
||||
EmailProviderType,
|
||||
} from "./email.js"
|
||||
import type {
|
||||
OAuth2Config,
|
||||
OAuthConfig,
|
||||
|
||||
@@ -20,10 +20,10 @@ export interface SpotifyProfile extends Record<string, any> {
|
||||
*
|
||||
* ```ts
|
||||
* import Auth from "@auth/core"
|
||||
* import Spotify from "@auth/core/providers/spotify"
|
||||
* import { Spotify } from "@auth/core/providers/spotify"
|
||||
*
|
||||
* const request = new Request("https://example.com")
|
||||
* const response = await AuthHandler(request, {
|
||||
* const resposne = await AuthHandler(request, {
|
||||
* providers: [
|
||||
* Spotify({clientId: "", clientSecret: ""})
|
||||
* ]
|
||||
|
||||
@@ -27,8 +27,8 @@ AUTH_TRUST_HOST=true
|
||||
in this example we are using github so make sure to set the following environment variables:
|
||||
|
||||
```
|
||||
GITHUB_ID=your_github_oauth_id
|
||||
GITHUB_SECRET=your_github_oauth_secret
|
||||
GITHUB_ID=your_github_oatuh_id
|
||||
GITHUB_SECRET=your_github_oatuh_secret
|
||||
```
|
||||
|
||||
```ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@auth/sveltekit",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.0",
|
||||
"description": "Authentication for SvelteKit.",
|
||||
"keywords": [
|
||||
"authentication",
|
||||
@@ -20,7 +20,7 @@
|
||||
"Balázs Orbán <info@balazsorban.com>",
|
||||
"Nico Domino <yo@ndo.dev>",
|
||||
"Lluis Agusti <hi@llu.lu>",
|
||||
"Iain Collins <me@iaincollins.com>"
|
||||
"Iain Collins <me@iaincollins.com"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "svelte-package -w",
|
||||
@@ -69,4 +69,4 @@
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@
|
||||
* This code sample already implements the correct method by using `const { session } = await parent();`
|
||||
* :::
|
||||
*
|
||||
* You should NOT put authorization logic in a `+layout.server.ts` as the logic is not guaranteed to propagate to leafs in the tree.
|
||||
* You should NOT put authorization logic in a `+layout.server.ts` as the logic is not guaranteed to propragate to leafs in the tree.
|
||||
* Prefer to manually protect each route through the `+page.server.ts` file to avoid mistakes.
|
||||
* It is possible to force the layout file to run the load function on all routes, however that relies certain behaviours that can change and are not easily checked.
|
||||
* For more information about these caveats make sure to read this issue in the SvelteKit repository: https://github.com/sveltejs/kit/issues/6315
|
||||
|
||||
2945
pnpm-lock.yaml
generated
2945
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -47,15 +47,6 @@
|
||||
"docs#dev": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false
|
||||
},
|
||||
"docs#build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": [
|
||||
"build",
|
||||
"docs/reference/core",
|
||||
"docs/reference/sveltekit",
|
||||
"docs/reference/adapter/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user