commit 284a04ba2ab4c81c2e8b8a4c517bd2f996db3753 Author: Skye Date: Wed Feb 22 00:03:16 2023 +0900 init: from https://github.com/nextauthjs/next-auth/tree/main/packages/adapter-mongodb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0a20d83 --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0455e62 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +

+
+      +

MongoDB Adapter - NextAuth.js

+

+ Open Source. Full Stack. Own Your Data. +

+

+ CI Test + Bundle Size + @next-auth/mongodb-adapter Version +

+

+ +## 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 diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..bed4f09 --- /dev/null +++ b/logo.svg @@ -0,0 +1 @@ +MongoDB_Logo_FullColorBlack_RGB \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ba8ea7f --- /dev/null +++ b/package.json @@ -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 ", + "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" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a8f8af0 --- /dev/null +++ b/src/index.ts @@ -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["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>(object: Record): T { + const newObject: Record = {} + 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>(object: Record) { + const newObject: Record = { + _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, + 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(c.Users), + A: _db.collection(c.Accounts), + S: _db.collection(c.Sessions), + V: _db.collection(c?.VerificationTokens), + } + })() + + return { + async createUser(data) { + const user = to(data) + await (await db).U.insertOne(user) + return from(user) + }, + async getUser(id) { + const user = await (await db).U.findOne({ _id: _id(id) }) + if (!user) return null + return from(user) + }, + async getUserByEmail(email) { + const user = await (await db).U.findOne({ email }) + if (!user) return null + return from(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(user) + }, + async updateUser(data) { + const { _id, ...user } = to(data) + + const result = await ( + await db + ).U.findOneAndUpdate({ _id }, { $set: user }, { returnDocument: "after" }) + + return from(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(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(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(user), + session: from(session), + } + }, + async createSession(data) { + const session = to(data) + await (await db).S.insertOne(session) + return from(session) + }, + async updateSession(data) { + const { _id, ...session } = to(data) + + const result = await ( + await db + ).S.findOneAndUpdate( + { sessionToken: session.sessionToken }, + { $set: session }, + { returnDocument: "after" } + ) + return from(result.value!) + }, + async deleteSession(sessionToken) { + const { value: session } = await ( + await db + ).S.findOneAndDelete({ + sessionToken, + }) + return from(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 + }, + } +} diff --git a/tests/custom.test.ts b/tests/custom.test.ts new file mode 100644 index 0000000..c4269a1 --- /dev/null +++ b/tests/custom.test.ts @@ -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 + }, + }, +}) diff --git a/tests/index.test.ts b/tests/index.test.ts new file mode 100644 index 0000000..aeabccf --- /dev/null +++ b/tests/index.test.ts @@ -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 + }, + }, +}) diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..6ca0a6d --- /dev/null +++ b/tests/test.sh @@ -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 \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0b19a11 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@next-auth/tsconfig/tsconfig.adapters.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "exclude": ["tests", "dist", "jest.config.js"] +}