diff --git a/lib/Auth.ts b/lib/Auth.ts index d7df0093..f42bfb0c 100644 --- a/lib/Auth.ts +++ b/lib/Auth.ts @@ -13,10 +13,10 @@ import ResendUtils from "./ResendUtils"; import CollectionId from "./client/CollectionId"; import { AdapterUser } from "next-auth/adapters"; import { wait } from "./client/ClientUtils"; - -const adapter = MongoDBAdapter(clientPromise, { databaseName: process.env.DB }); +import MongoAuthAdapter from "./DbInterfaceAuthAdapter"; const cachedDb = getDatabase(); +const adapter = MongoAuthAdapter(cachedDb); export const AuthenticationOptions: AuthOptions = { secret: process.env.NEXTAUTH_SECRET, @@ -83,9 +83,7 @@ export const AuthenticationOptions: AuthOptions = { ], callbacks: { async session({ session, user }) { - session.user = await ( - await cachedDb - ).findObjectById(CollectionId.Users, new ObjectId(user.id)); + session.user = user; return session; }, diff --git a/lib/DbInterfaceAuthAdapter.ts b/lib/DbInterfaceAuthAdapter.ts new file mode 100644 index 00000000..56372868 --- /dev/null +++ b/lib/DbInterfaceAuthAdapter.ts @@ -0,0 +1,268 @@ +import { format, MongoDBAdapter } from "@next-auth/mongodb-adapter"; +import { + Adapter, + AdapterAccount, + AdapterSession, + AdapterUser, + VerificationToken, +} from "next-auth/adapters"; +import DbInterface from "./client/dbinterfaces/DbInterface"; +import CollectionId from "./client/CollectionId"; +import { User, Session } from "./Types"; +import { GenerateSlug } from "./Utils"; +import { ObjectId } from "bson"; + +export default function DbInterfaceAuthAdapter( + dbPromise: Promise, +): Adapter { + const adapter: Adapter = { + createUser: async (data: Record) => { + const db = await dbPromise; + + const adapterUser = format.to(data); + + const user = new User( + adapterUser.name ?? "Unknown", + adapterUser.email, + adapterUser.image ?? process.env.DEFAULT_IMAGE, + false, + await GenerateSlug( + db, + CollectionId.Users, + adapterUser.name ?? "Unknown", + ), + [], + [], + undefined, + 0, + 1, + ); + + user._id = new ObjectId(adapterUser._id) as any; + + await db.addObject(CollectionId.Users, user); + return format.from(adapterUser); + }, + getUser: async (id: string) => { + const db = await dbPromise; + + if (id.length !== 24) return null; + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + + if (!user) return null; + return format.from(user); + }, + getUserByEmail: async (email: string) => { + const db = await dbPromise; + + const account = await db.findObject(CollectionId.Users, { email }); + + if (!account) return null; + return format.from(account); + }, + getUserByAccount: async ( + providerAccountId: Pick, + ) => { + const db = await dbPromise; + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) return null; + + const user = await db.findObjectById( + CollectionId.Users, + account.userId as any as ObjectId, + ); + + if (!user) return null; + return format.from(user); + }, + updateUser: async ( + data: Partial & Pick, + ) => { + const db = await dbPromise; + const { _id, ...user } = format.to(data); + + const existing = await db.findObjectById( + CollectionId.Users, + new ObjectId(_id), + ); + + const result = await db.updateObjectById( + CollectionId.Users, + new ObjectId(_id), + user as Partial, + ); + + return format.from({ ...existing, ...user, _id: _id }); + }, + deleteUser: async (id: string) => { + const db = await dbPromise; + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(id), + ); + if (!user) return null; + + const account = await db.findObject(CollectionId.Accounts, { + userId: user._id, + }); + + const session = await db.findObject(CollectionId.Sessions, { + userId: user._id, + }); + + const promises = [ + db.deleteObjectById(CollectionId.Users, new ObjectId(id)), + ]; + + if (account) { + promises.push( + db.deleteObjectById(CollectionId.Accounts, new ObjectId(account._id)), + ); + } + + if (session) { + promises.push( + db.deleteObjectById(CollectionId.Sessions, new ObjectId(session._id)), + ); + } + + await Promise.all(promises); + + return format.from(user); + }, + linkAccount: async (data: Record) => { + const db = await dbPromise; + const account = format.to(data); + + await db.addObject(CollectionId.Accounts, account); + + return account; + }, + unlinkAccount: async ( + providerAccountId: Pick, + ) => { + const db = await dbPromise; + + const account = await db.findObject(CollectionId.Accounts, { + providerAccountId: providerAccountId.providerAccountId, + }); + + if (!account) return null; + + await db.deleteObjectById( + CollectionId.Accounts, + new ObjectId(account._id), + ); + + return format.from(account); + }, + getSessionAndUser: async (sessionToken: string) => { + const db = await dbPromise; + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + + if (!session) return null; + + const user = await db.findObjectById( + CollectionId.Users, + new ObjectId(session.userId), + ); + + if (!user) return null; + return { + session: format.from(session), + user: format.from(user), + }; + }, + createSession: async (data: Record) => { + const db = await dbPromise; + + const session = format.to(data); + session.userId = new ObjectId(session.userId) as any; + + await db.addObject(CollectionId.Sessions, session as unknown as Session); + + return format.from(session); + }, + updateSession: async ( + data: Partial & Pick, + ) => { + const db = await dbPromise; + const { _id, ...session } = format.to(data); + + const existing = await db.findObject(CollectionId.Sessions, { + sessionToken: session.sessionToken, + }); + + if (!existing) return null; + + if (session.userId) { + session.userId = new ObjectId(session.userId) as any; + } + + await db.updateObjectById( + CollectionId.Sessions, + new ObjectId(existing._id), + session as unknown as Partial, + ); + + return format.from({ ...existing, ...data }); + }, + deleteSession: async (sessionToken: string) => { + const db = await dbPromise; + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + + if (!session) return null; + + await db.deleteObjectById( + CollectionId.Sessions, + new ObjectId(session._id), + ); + + return format.from(session); + }, + createVerificationToken: async (token: VerificationToken) => { + const db = await dbPromise; + await db.addObject( + CollectionId.VerificationTokens, + format.to(token) as VerificationToken, + ); + return token; + }, + useVerificationToken: async (token: { + identifier: string; + token: string; + }) => { + const db = await dbPromise; + + const existing = await db.findObject(CollectionId.VerificationTokens, { + token: token.token, + }); + + if (!existing) return null; + + await db.deleteObjectById( + CollectionId.VerificationTokens, + new ObjectId(existing._id), + ); + + return format.from(existing); + }, + }; + + return adapter; +} diff --git a/lib/Types.ts b/lib/Types.ts index 52539d0a..d006e7f0 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -30,6 +30,9 @@ export interface Account extends NextAuthAccount { export interface Session extends NextAuthSession { _id: string; + sessionToken: string; + userId: ObjectId; + expires: string; } export class User implements NextAuthUser { diff --git a/lib/client/CollectionId.ts b/lib/client/CollectionId.ts index 5eb209a3..00c0c646 100644 --- a/lib/client/CollectionId.ts +++ b/lib/client/CollectionId.ts @@ -1,3 +1,4 @@ +import { VerificationToken } from "next-auth/adapters"; import { Season, Competition, @@ -12,6 +13,7 @@ import { CompPicklistGroup, WebhookHolder, } from "../Types"; +import { ObjectId } from "bson"; enum CollectionId { Seasons = "Seasons", @@ -22,6 +24,7 @@ enum CollectionId { Users = "users", Accounts = "accounts", Sessions = "sessions", + VerificationTokens = "verification_tokens", Forms = "Forms", PitReports = "Pitreports", Picklists = "Picklists", @@ -51,14 +54,16 @@ export type CollectionIdToType = ? Account : Id extends CollectionId.Sessions ? Session - : Id extends CollectionId.PitReports - ? Pitreport - : Id extends CollectionId.Picklists - ? CompPicklistGroup - : Id extends CollectionId.SubjectiveReports - ? SubjectiveReport - : Id extends CollectionId.Webhooks - ? WebhookHolder - : Id extends CollectionId.Misc - ? any - : any; + : Id extends CollectionId.VerificationTokens + ? VerificationToken & { _id: ObjectId } + : Id extends CollectionId.PitReports + ? Pitreport + : Id extends CollectionId.Picklists + ? CompPicklistGroup + : Id extends CollectionId.SubjectiveReports + ? SubjectiveReport + : Id extends CollectionId.Webhooks + ? WebhookHolder + : Id extends CollectionId.Misc + ? any + : any; diff --git a/package-lock.json b/package-lock.json index 677158b3..daa00cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", diff --git a/package.json b/package.json index 558cd6bb..308a4c55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.3", + "version": "1.2.4", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/lib/DbInterfaceAuthAdapter.test.ts new file mode 100644 index 00000000..4e2e0262 --- /dev/null +++ b/tests/lib/DbInterfaceAuthAdapter.test.ts @@ -0,0 +1,38 @@ +import CollectionId from "@/lib/client/CollectionId"; +import InMemoryDbInterface from "@/lib/client/dbinterfaces/InMemoryDbInterface"; +import DbInterfaceAuthAdapter from "@/lib/DbInterfaceAuthAdapter"; +import { get } from "http"; + +const prototype = DbInterfaceAuthAdapter(undefined as any); + +async function getDatabase() {} + +async function getAdapterAndDb() { + const db = new InMemoryDbInterface(); + await db.init(); + + return { + adapter: DbInterfaceAuthAdapter(Promise.resolve(db)), + db, + }; +} + +describe(prototype.createUser.name, () => { + test("Adds a user to the database", async () => { + const { db, adapter } = await getAdapterAndDb(); + + const user = { + name: "Test User", + email: "test@gmail.com", + image: "test.png", + }; + + await adapter.createUser(user); + + const foundUser = await db.findObject(CollectionId.Users, { + email: user.email, + }); + + expect(foundUser).toMatchObject(user); + }); +});