/* eslint-disable unicorn/no-null,max-params */ import { createHash, randomBytes } from "crypto"; import type { AdapterInstance } from "next-auth/adapters"; import omit from "lodash/omit"; import type { IMetamigoRepositories } from "../records"; import type { UnsavedAccount, SavedAccount } from "../records/account"; import type { UserId, UnsavedUser, SavedUser } from "../records/user"; import type { UnsavedSession, SavedSession } from "../records/session"; // Sessions expire after 30 days of being idle export const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000; // Sessions updated only if session is greater than this value (0 = always) export const defaulteSessionUpdateAge = 24 * 60 * 60 * 1000; const getCompoundId = (providerId: any, providerAccountId: any) => createHash("sha256") .update(`${providerId}:${providerAccountId}`) .digest("hex"); const randomToken = () => randomBytes(32).toString("hex"); export class NextAuthAdapter implements AdapterInstance { constructor( private repos: TRepositories, private readonly sessionMaxAge = defaultSessionMaxAge, private readonly sessionUpdateAge = defaulteSessionUpdateAge ) { } async createUser(profile: UnsavedUser): Promise { // @ts-expect-error return this.repos.users.upsert(omit(profile, ["isActive", "id"])); } async getUser(id: UserId): Promise { const user = await this.repos.users.findById({ id }); if (!user) return null; // if a user has no linked accounts, then we do not return it // see: https://github.com/nextauthjs/next-auth/issues/876 const accounts = await this.repos.accounts.findAllBy({ userId: user.id, }); if (!accounts || accounts.length === 0) return null; return user; } async getUserByEmail(email: string): Promise { const user = await this.repos.users.findBy({ email }); if (!user) return null; // if a user has no linked accounts, then we do not return it // see: https://github.com/nextauthjs/next-auth/issues/876 const accounts = await this.repos.accounts.findAllBy({ userId: user.id, }); if (!accounts || accounts.length === 0) return null; return user; } async getUserByProviderAccountId( providerId: string, providerAccountId: string ): Promise { const account = await this.repos.accounts.findBy({ compoundId: getCompoundId(providerId, providerAccountId), }); if (!account) return null; return this.repos.users.findById({ id: account.userId }); } async updateUser(user: SavedUser): Promise { return this.repos.users.update(user); } // @ts-expect-error async linkAccount( userId: string, providerId: string, providerType: string, providerAccountId: string, refreshToken: string, accessToken: string, accessTokenExpires: number ): Promise { const exists = await this.repos.users.existsById({ id: userId }); if (!exists) return; const account: UnsavedAccount = { accessToken, refreshToken, compoundId: getCompoundId(providerId, providerAccountId), providerAccountId, providerId, providerType, accessTokenExpires: accessTokenExpires ? new Date(accessTokenExpires) : new Date(), userId, }; await this.repos.accounts.insert(account); } async unlinkAccount( userId: string, providerId: string, providerAccountId: string ): Promise { await this.repos.accounts.removeBy({ userId, compoundId: getCompoundId(providerId, providerAccountId), }); } createSession(user: SavedUser): Promise { let expires; if (this.sessionMaxAge) { const dateExpires = new Date(Date.now() + this.sessionMaxAge); expires = dateExpires.toISOString(); } const session: UnsavedSession = { // @ts-expect-error expires, userId: user.id, sessionToken: randomToken(), accessToken: randomToken(), }; return this.repos.sessions.insert(session); } async getSession(sessionToken: string): Promise { const session = await this.repos.sessions.findBy({ sessionToken }); if (session && session.expires && new Date() > session.expires) { this.repos.sessions.remove(session); return null; } return session; } async updateSession( session: SavedSession, force?: boolean ): Promise { if ( this.sessionMaxAge && (this.sessionUpdateAge || this.sessionUpdateAge === 0) && session.expires ) { // Calculate last updated date, to throttle write updates to database // Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge // e.g. ({expiry date} - 30 days) + 1 hour // // Default for sessionMaxAge is 30 days. // Default for sessionUpdateAge is 1 hour. const dateSessionIsDueToBeUpdated = new Date( session.expires.getTime() - this.sessionMaxAge + this.sessionUpdateAge ); // Trigger update of session expiry date and write to database, only // if the session was last updated more than {sessionUpdateAge} ago if (new Date() > dateSessionIsDueToBeUpdated) { const newExpiryDate = new Date(); newExpiryDate.setTime(newExpiryDate.getTime() + this.sessionMaxAge); session.expires = newExpiryDate; } else if (!force) { return null; } } else if (!force) { // If session MaxAge, session UpdateAge or session.expires are // missing then don't even try to save changes, unless force is set. return null; } const { expires } = session; return this.repos.sessions.update({ ...session, expires }); } async deleteSession(sessionToken: string): Promise { await this.repos.sessions.removeBy({ sessionToken }); } }