This commit is contained in:
commit
284a04ba2a
9 changed files with 484 additions and 0 deletions
8
CHANGELOG.md
Normal file
8
CHANGELOG.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/nextauthjs/adapters/compare/@next-auth/mongodb-adapter@1.0.0...@next-auth/mongodb-adapter@1.0.1) (2021-12-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @next-auth/mongodb-adapter
|
88
README.md
Normal file
88
README.md
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<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="./logo.svg" />
|
||||||
|
<h3 align="center"><b>MongoDB 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="CI Test" />
|
||||||
|
<a href="https://www.npmjs.com/package/@next-auth/mongodb-adapter" target="_blank"><img src="https://img.shields.io/bundlephobia/minzip/@next-auth/mongodb-adapter" alt="Bundle Size"/></a>
|
||||||
|
<a href="https://www.npmjs.com/package/@next-auth/mongodb-adapter" target="_blank"><img src="https://img.shields.io/npm/v/@next-auth/mongodb-adapter" alt="@next-auth/mongodb-adapter Version" /></a>
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This is the MongoDB Adapter for [`auth.js`](https://authjs.dev). This package can only be used in conjunction with the primary `auth.js` package. It is not a standalone package.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Install `mongodb`, `next-auth` and `@next-auth/mongodb-adapter`
|
||||||
|
|
||||||
|
```js
|
||||||
|
npm install mongodb next-auth @next-auth/mongodb-adapter@next
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add `lib/mongodb.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
|
||||||
|
import { MongoClient } from "mongodb"
|
||||||
|
|
||||||
|
const uri = process.env.MONGODB_URI
|
||||||
|
const options = {
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
useNewUrlParser: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
let client
|
||||||
|
let clientPromise
|
||||||
|
|
||||||
|
if (!process.env.MONGODB_URI) {
|
||||||
|
throw new Error("Please add your Mongo URI to .env.local")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
// In development mode, use a global variable so that the value
|
||||||
|
// is preserved across module reloads caused by HMR (Hot Module Replacement).
|
||||||
|
if (!global._mongoClientPromise) {
|
||||||
|
client = new MongoClient(uri, options)
|
||||||
|
global._mongoClientPromise = client.connect()
|
||||||
|
}
|
||||||
|
clientPromise = global._mongoClientPromise
|
||||||
|
} else {
|
||||||
|
// In production mode, it's best to not use a global variable.
|
||||||
|
client = new MongoClient(uri, options)
|
||||||
|
clientPromise = client.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a module-scoped MongoClient promise. By doing this in a
|
||||||
|
// separate module, the client can be shared across functions.
|
||||||
|
export default clientPromise
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import NextAuth from "next-auth"
|
||||||
|
import { MongoDBAdapter } from "@next-auth/mongodb-adapter"
|
||||||
|
import clientPromise from "lib/mongodb"
|
||||||
|
|
||||||
|
// For more information on each option (and a full list of options) go to
|
||||||
|
// https://authjs.dev/reference/configuration/auth-options
|
||||||
|
export default NextAuth({
|
||||||
|
adapter: MongoDBAdapter(clientPromise, {
|
||||||
|
databaseName: 'my-data-base-name'
|
||||||
|
}),
|
||||||
|
...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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/.github/blob/main/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ISC
|
1
logo.svg
Normal file
1
logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.6 KiB |
47
package.json
Normal file
47
package.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "@next-auth/mongodb-adapter",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"description": "mongoDB adapter for next-auth.",
|
||||||
|
"homepage": "https://authjs.dev",
|
||||||
|
"repository": "https://github.com/nextauthjs/next-auth",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/nextauthjs/next-auth/issues"
|
||||||
|
},
|
||||||
|
"author": "Balázs Orbán <info@balazsorban.com>",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"license": "ISC",
|
||||||
|
"keywords": [
|
||||||
|
"next-auth",
|
||||||
|
"next.js",
|
||||||
|
"oauth",
|
||||||
|
"mongodb",
|
||||||
|
"adapter"
|
||||||
|
],
|
||||||
|
"private": false,
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "./tests/test.sh",
|
||||||
|
"test:watch": "./tests/test.sh -w",
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"README.md",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"mongodb": "^4.1.1",
|
||||||
|
"next-auth": "^4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@next-auth/adapter-test": "workspace:*",
|
||||||
|
"@next-auth/tsconfig": "workspace:*",
|
||||||
|
"jest": "^27.4.3",
|
||||||
|
"mongodb": "^4.4.0",
|
||||||
|
"next-auth": "workspace:*"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "@next-auth/adapter-test/jest"
|
||||||
|
}
|
||||||
|
}
|
193
src/index.ts
Normal file
193
src/index.ts
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
|
import { ObjectId } from "mongodb"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Adapter,
|
||||||
|
AdapterUser,
|
||||||
|
AdapterAccount,
|
||||||
|
AdapterSession,
|
||||||
|
VerificationToken,
|
||||||
|
} from "next-auth/adapters"
|
||||||
|
import type { MongoClient } from "mongodb"
|
||||||
|
|
||||||
|
export interface MongoDBAdapterOptions {
|
||||||
|
collections?: {
|
||||||
|
Users?: string
|
||||||
|
Accounts?: string
|
||||||
|
Sessions?: string
|
||||||
|
VerificationTokens?: string
|
||||||
|
}
|
||||||
|
databaseName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultCollections: Required<
|
||||||
|
Required<MongoDBAdapterOptions>["collections"]
|
||||||
|
> = {
|
||||||
|
Users: "users",
|
||||||
|
Accounts: "accounts",
|
||||||
|
Sessions: "sessions",
|
||||||
|
VerificationTokens: "verification_tokens",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const format = {
|
||||||
|
/** Takes a mongoDB object and returns a plain old JavaScript object */
|
||||||
|
from<T = Record<string, unknown>>(object: Record<string, any>): T {
|
||||||
|
const newObject: Record<string, unknown> = {}
|
||||||
|
for (const key in object) {
|
||||||
|
const value = object[key]
|
||||||
|
if (key === "_id") {
|
||||||
|
newObject.id = value.toHexString()
|
||||||
|
} else if (key === "userId") {
|
||||||
|
newObject[key] = value.toHexString()
|
||||||
|
} else {
|
||||||
|
newObject[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newObject as T
|
||||||
|
},
|
||||||
|
/** Takes a plain old JavaScript object and turns it into a mongoDB object */
|
||||||
|
to<T = Record<string, unknown>>(object: Record<string, any>) {
|
||||||
|
const newObject: Record<string, unknown> = {
|
||||||
|
_id: _id(object.id),
|
||||||
|
}
|
||||||
|
for (const key in object) {
|
||||||
|
const value = object[key]
|
||||||
|
if (key === "userId") newObject[key] = _id(value)
|
||||||
|
else if (key === "id") continue
|
||||||
|
else newObject[key] = value
|
||||||
|
}
|
||||||
|
return newObject as T & { _id: ObjectId }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Converts from string to ObjectId */
|
||||||
|
export function _id(hex?: string) {
|
||||||
|
if (hex?.length !== 24) return new ObjectId()
|
||||||
|
return new ObjectId(hex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MongoDBAdapter(
|
||||||
|
client: Promise<MongoClient>,
|
||||||
|
options: MongoDBAdapterOptions = {}
|
||||||
|
): Adapter {
|
||||||
|
const { collections } = options
|
||||||
|
const { from, to } = format
|
||||||
|
|
||||||
|
const db = (async () => {
|
||||||
|
const _db = (await client).db(options.databaseName)
|
||||||
|
const c = { ...defaultCollections, ...collections }
|
||||||
|
return {
|
||||||
|
U: _db.collection<AdapterUser>(c.Users),
|
||||||
|
A: _db.collection<AdapterAccount>(c.Accounts),
|
||||||
|
S: _db.collection<AdapterSession>(c.Sessions),
|
||||||
|
V: _db.collection<VerificationToken>(c?.VerificationTokens),
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
return {
|
||||||
|
async createUser(data) {
|
||||||
|
const user = to<AdapterUser>(data)
|
||||||
|
await (await db).U.insertOne(user)
|
||||||
|
return from<AdapterUser>(user)
|
||||||
|
},
|
||||||
|
async getUser(id) {
|
||||||
|
const user = await (await db).U.findOne({ _id: _id(id) })
|
||||||
|
if (!user) return null
|
||||||
|
return from<AdapterUser>(user)
|
||||||
|
},
|
||||||
|
async getUserByEmail(email) {
|
||||||
|
const user = await (await db).U.findOne({ email })
|
||||||
|
if (!user) return null
|
||||||
|
return from<AdapterUser>(user)
|
||||||
|
},
|
||||||
|
async getUserByAccount(provider_providerAccountId) {
|
||||||
|
const account = await (await db).A.findOne(provider_providerAccountId)
|
||||||
|
if (!account) return null
|
||||||
|
const user = await (
|
||||||
|
await db
|
||||||
|
).U.findOne({ _id: new ObjectId(account.userId) })
|
||||||
|
if (!user) return null
|
||||||
|
return from<AdapterUser>(user)
|
||||||
|
},
|
||||||
|
async updateUser(data) {
|
||||||
|
const { _id, ...user } = to<AdapterUser>(data)
|
||||||
|
|
||||||
|
const result = await (
|
||||||
|
await db
|
||||||
|
).U.findOneAndUpdate({ _id }, { $set: user }, { returnDocument: "after" })
|
||||||
|
|
||||||
|
return from<AdapterUser>(result.value!)
|
||||||
|
},
|
||||||
|
async deleteUser(id) {
|
||||||
|
const userId = _id(id)
|
||||||
|
const m = await db
|
||||||
|
await Promise.all([
|
||||||
|
m.A.deleteMany({ userId }),
|
||||||
|
m.S.deleteMany({ userId: userId as any }),
|
||||||
|
m.U.deleteOne({ _id: userId }),
|
||||||
|
])
|
||||||
|
},
|
||||||
|
linkAccount: async (data) => {
|
||||||
|
const account = to<AdapterAccount>(data)
|
||||||
|
await (await db).A.insertOne(account)
|
||||||
|
return account
|
||||||
|
},
|
||||||
|
async unlinkAccount(provider_providerAccountId) {
|
||||||
|
const { value: account } = await (
|
||||||
|
await db
|
||||||
|
).A.findOneAndDelete(provider_providerAccountId)
|
||||||
|
return from<AdapterAccount>(account!)
|
||||||
|
},
|
||||||
|
async getSessionAndUser(sessionToken) {
|
||||||
|
const session = await (await db).S.findOne({ sessionToken })
|
||||||
|
if (!session) return null
|
||||||
|
const user = await (
|
||||||
|
await db
|
||||||
|
).U.findOne({ _id: new ObjectId(session.userId) })
|
||||||
|
if (!user) return null
|
||||||
|
return {
|
||||||
|
user: from<AdapterUser>(user),
|
||||||
|
session: from<AdapterSession>(session),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async createSession(data) {
|
||||||
|
const session = to<AdapterSession>(data)
|
||||||
|
await (await db).S.insertOne(session)
|
||||||
|
return from<AdapterSession>(session)
|
||||||
|
},
|
||||||
|
async updateSession(data) {
|
||||||
|
const { _id, ...session } = to<AdapterSession>(data)
|
||||||
|
|
||||||
|
const result = await (
|
||||||
|
await db
|
||||||
|
).S.findOneAndUpdate(
|
||||||
|
{ sessionToken: session.sessionToken },
|
||||||
|
{ $set: session },
|
||||||
|
{ returnDocument: "after" }
|
||||||
|
)
|
||||||
|
return from<AdapterSession>(result.value!)
|
||||||
|
},
|
||||||
|
async deleteSession(sessionToken) {
|
||||||
|
const { value: session } = await (
|
||||||
|
await db
|
||||||
|
).S.findOneAndDelete({
|
||||||
|
sessionToken,
|
||||||
|
})
|
||||||
|
return from<AdapterSession>(session!)
|
||||||
|
},
|
||||||
|
async createVerificationToken(data) {
|
||||||
|
await (await db).V.insertOne(to(data))
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
async useVerificationToken(identifier_token) {
|
||||||
|
const { value: verificationToken } = await (
|
||||||
|
await db
|
||||||
|
).V.findOneAndDelete(identifier_token)
|
||||||
|
|
||||||
|
if (!verificationToken) return null
|
||||||
|
// @ts-expect-error
|
||||||
|
delete verificationToken._id
|
||||||
|
return verificationToken
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
54
tests/custom.test.ts
Normal file
54
tests/custom.test.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { runBasicTests } from "@next-auth/adapter-test"
|
||||||
|
import { defaultCollections, format, MongoDBAdapter, _id } from "../src"
|
||||||
|
import { MongoClient } from "mongodb"
|
||||||
|
const name = "custom-test"
|
||||||
|
const client = new MongoClient(`mongodb://localhost:27017/${name}`)
|
||||||
|
const clientPromise = client.connect()
|
||||||
|
|
||||||
|
const collections = { ...defaultCollections, Users: "some_userz" }
|
||||||
|
|
||||||
|
runBasicTests({
|
||||||
|
adapter: MongoDBAdapter(clientPromise, {
|
||||||
|
collections,
|
||||||
|
}),
|
||||||
|
db: {
|
||||||
|
async disconnect() {
|
||||||
|
await client.db().dropDatabase()
|
||||||
|
await client.close()
|
||||||
|
},
|
||||||
|
async user(id) {
|
||||||
|
const user = await client
|
||||||
|
.db()
|
||||||
|
.collection(collections.Users)
|
||||||
|
.findOne({ _id: _id(id) })
|
||||||
|
|
||||||
|
if (!user) return null
|
||||||
|
return format.from(user)
|
||||||
|
},
|
||||||
|
async account(provider_providerAccountId) {
|
||||||
|
const account = await client
|
||||||
|
.db()
|
||||||
|
.collection(collections.Accounts)
|
||||||
|
.findOne(provider_providerAccountId)
|
||||||
|
if (!account) return null
|
||||||
|
return format.from(account)
|
||||||
|
},
|
||||||
|
async session(sessionToken) {
|
||||||
|
const session = await client
|
||||||
|
.db()
|
||||||
|
.collection(collections.Sessions)
|
||||||
|
.findOne({ sessionToken })
|
||||||
|
if (!session) return null
|
||||||
|
return format.from(session)
|
||||||
|
},
|
||||||
|
async verificationToken(identifier_token) {
|
||||||
|
const token = await client
|
||||||
|
.db()
|
||||||
|
.collection(collections.VerificationTokens)
|
||||||
|
.findOne(identifier_token)
|
||||||
|
if (!token) return null
|
||||||
|
const { _id, ...rest } = token
|
||||||
|
return rest
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
51
tests/index.test.ts
Normal file
51
tests/index.test.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { runBasicTests } from "@next-auth/adapter-test"
|
||||||
|
import { defaultCollections, format, MongoDBAdapter, _id } from "../src"
|
||||||
|
import { MongoClient } from "mongodb"
|
||||||
|
|
||||||
|
const name = "test"
|
||||||
|
const client = new MongoClient(`mongodb://localhost:27017/${name}`)
|
||||||
|
const clientPromise = client.connect()
|
||||||
|
|
||||||
|
runBasicTests({
|
||||||
|
adapter: MongoDBAdapter(clientPromise),
|
||||||
|
db: {
|
||||||
|
async disconnect() {
|
||||||
|
await client.db().dropDatabase()
|
||||||
|
await client.close()
|
||||||
|
},
|
||||||
|
async user(id) {
|
||||||
|
const user = await client
|
||||||
|
.db()
|
||||||
|
.collection(defaultCollections.Users)
|
||||||
|
.findOne({ _id: _id(id) })
|
||||||
|
|
||||||
|
if (!user) return null
|
||||||
|
return format.from(user)
|
||||||
|
},
|
||||||
|
async account(provider_providerAccountId) {
|
||||||
|
const account = await client
|
||||||
|
.db()
|
||||||
|
.collection(defaultCollections.Accounts)
|
||||||
|
.findOne(provider_providerAccountId)
|
||||||
|
if (!account) return null
|
||||||
|
return format.from(account)
|
||||||
|
},
|
||||||
|
async session(sessionToken) {
|
||||||
|
const session = await client
|
||||||
|
.db()
|
||||||
|
.collection(defaultCollections.Sessions)
|
||||||
|
.findOne({ sessionToken })
|
||||||
|
if (!session) return null
|
||||||
|
return format.from(session)
|
||||||
|
},
|
||||||
|
async verificationToken(identifier_token) {
|
||||||
|
const token = await client
|
||||||
|
.db()
|
||||||
|
.collection(defaultCollections.VerificationTokens)
|
||||||
|
.findOne(identifier_token)
|
||||||
|
if (!token) return null
|
||||||
|
const { _id, ...rest } = token
|
||||||
|
return rest
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
34
tests/test.sh
Executable file
34
tests/test.sh
Executable file
|
@ -0,0 +1,34 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
CONTAINER_NAME=next-auth-mongodb-test
|
||||||
|
|
||||||
|
JEST_WATCH=false
|
||||||
|
|
||||||
|
# Is the watch flag passed to the script?
|
||||||
|
while getopts w flag
|
||||||
|
do
|
||||||
|
case "${flag}" in
|
||||||
|
w) JEST_WATCH=true;;
|
||||||
|
*) continue;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Start db
|
||||||
|
docker run -d --rm -p 27017:27017 --name ${CONTAINER_NAME} mongo
|
||||||
|
|
||||||
|
echo "Waiting 3 sec for db to start..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
if $JEST_WATCH; then
|
||||||
|
# Run jest in watch mode
|
||||||
|
npx jest tests --watch
|
||||||
|
# Only stop the container after jest has been quit
|
||||||
|
docker stop "${CONTAINER_NAME}"
|
||||||
|
else
|
||||||
|
# Always stop container, but exit with 1 when tests are failing
|
||||||
|
if npx jest;then
|
||||||
|
docker stop ${CONTAINER_NAME}
|
||||||
|
else
|
||||||
|
docker stop ${CONTAINER_NAME} && exit 1
|
||||||
|
fi
|
||||||
|
fi
|
8
tsconfig.json
Normal file
8
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"]
|
||||||
|
}
|
Loading…
Reference in a new issue