UNPKG

@auth/unstorage-adapter

Version:

Unstorage adapter for Auth.js.

375 lines (346 loc) 11.5 kB
/** * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px"}}> * <p>Official <a href="https://unstorage.unjs.io/">Unstorage</a> adapter for Auth.js / NextAuth.js.</p> * <a href="https://unstorage.unjs.io/"> * <img style={{display: "block"}} src="https://authjs.dev/img/adapters/unstorage.svg" width="60"/> * </a> * </div> * * ## Installation * * ```bash npm2yarn * npm install unstorage @auth/unstorage-adapter * ``` * * @module @auth/unstorage-adapter */ import type { Adapter, AdapterUser, AdapterAccount, AdapterSession, AdapterAuthenticator, VerificationToken, } from "@auth/core/adapters" import { isDate } from "@auth/core/adapters" import type { Storage, StorageValue } from "unstorage" /** This is the interface of the Unstorage adapter options. */ export interface UnstorageAdapterOptions { /** * The base prefix for your keys */ baseKeyPrefix?: string /** * The prefix for the `account` key */ accountKeyPrefix?: string /** * The prefix for the `accountByUserId` key */ accountByUserIdPrefix?: string /** * The prefix for the `emailKey` key */ emailKeyPrefix?: string /** * The prefix for the `sessionKey` key */ sessionKeyPrefix?: string /** * The prefix for the `sessionByUserId` key */ sessionByUserIdKeyPrefix?: string /** * The prefix for the `user` key */ userKeyPrefix?: string /** * The prefix for the `verificationToken` key */ verificationTokenKeyPrefix?: string /** * The prefix for the `authenticator` key */ authenticatorKeyPrefix?: string /** * The prefix for the `authenticator-by-user-id` key */ authenticatorUserKeyPrefix?: string /** * Use `getItemRaw/setItemRaw` instead of `getItem/setItem`. * * This is an experimental feature. Please check [unjs/unstorage#142](https://github.com/unjs/unstorage/issues/142) for more information. */ useItemRaw?: boolean } export const defaultOptions = { baseKeyPrefix: "", accountKeyPrefix: "user:account:", accountByUserIdPrefix: "user:account:by-user-id:", emailKeyPrefix: "user:email:", sessionKeyPrefix: "user:session:", sessionByUserIdKeyPrefix: "user:session:by-user-id:", userKeyPrefix: "user:", verificationTokenKeyPrefix: "user:token:", authenticatorKeyPrefix: "authenticator:", authenticatorUserKeyPrefix: "authenticator:by-user-id:", useItemRaw: false, } export function hydrateDates(json: Record<string, any>) { return Object.entries(json).reduce((acc, [key, val]) => { acc[key] = isDate(val) ? new Date(val as string) : val return acc }, {} as any) } export function UnstorageAdapter( storage: Storage, options: UnstorageAdapterOptions = {} ): Adapter { const mergedOptions = { ...defaultOptions, ...options, } const { baseKeyPrefix } = mergedOptions const accountKeyPrefix = baseKeyPrefix + mergedOptions.accountKeyPrefix const accountByUserIdPrefix = baseKeyPrefix + mergedOptions.accountByUserIdPrefix const emailKeyPrefix = baseKeyPrefix + mergedOptions.emailKeyPrefix const sessionKeyPrefix = baseKeyPrefix + mergedOptions.sessionKeyPrefix const sessionByUserIdKeyPrefix = baseKeyPrefix + mergedOptions.sessionByUserIdKeyPrefix const userKeyPrefix = baseKeyPrefix + mergedOptions.userKeyPrefix const verificationTokenKeyPrefix = baseKeyPrefix + mergedOptions.verificationTokenKeyPrefix const authenticatorKeyPrefix = baseKeyPrefix + mergedOptions.authenticatorKeyPrefix const authenticatorUserKeyPrefix = baseKeyPrefix + mergedOptions.authenticatorUserKeyPrefix async function getItem<T extends StorageValue>(key: string) { if (mergedOptions.useItemRaw) { return await storage.getItemRaw<T>(key) } else { return await storage.getItem<T>(key) } } async function setItem(key: string, value: string) { if (mergedOptions.useItemRaw) { return await storage.setItemRaw(key, value) } else { return await storage.setItem(key, value) } } const setObjectAsJson = async (key: string, obj: Record<string, any>) => { if (mergedOptions.useItemRaw) { await storage.setItemRaw(key, obj) } else { await storage.setItem(key, JSON.stringify(obj)) } } const setAccount = async (id: string, account: AdapterAccount) => { const accountKey = accountKeyPrefix + id await Promise.all([ setObjectAsJson(accountKey, account), setItem(accountByUserIdPrefix + account.userId, accountKey), ]) return account } const getAccount = async (id: string) => { const account = await getItem<AdapterAccount>(accountKeyPrefix + id) if (!account) return null return hydrateDates(account) } const setSession = async ( id: string, session: AdapterSession ): Promise<AdapterSession> => { const sessionKey = sessionKeyPrefix + id await Promise.all([ setObjectAsJson(sessionKey, session), setItem(sessionByUserIdKeyPrefix + session.userId, sessionKey), ]) return session } const getSession = async (id: string) => { const session = await getItem<AdapterSession>(sessionKeyPrefix + id) if (!session) return null return hydrateDates(session) } const setUser = async ( id: string, user: AdapterUser ): Promise<AdapterUser> => { await Promise.all([ setObjectAsJson(userKeyPrefix + id, user), setItem(`${emailKeyPrefix}${user.email as string}`, id), ]) return user } const getUser = async (id: string) => { const user = await getItem<AdapterUser>(userKeyPrefix + id) if (!user) return null return hydrateDates(user) } const setAuthenticator = async ( credentialId: string, authenticator: AdapterAuthenticator ): Promise<AdapterAuthenticator> => { const newCredsToSet = [credentialId] const getItemReturn = await getItem<string[]>( `${authenticatorUserKeyPrefix}${authenticator.userId}` ) if (getItemReturn && getItemReturn[0] !== newCredsToSet[0]) { newCredsToSet.push(...getItemReturn) } await Promise.all([ setObjectAsJson(authenticatorKeyPrefix + credentialId, authenticator), setItem( `${authenticatorUserKeyPrefix}${authenticator.userId}`, JSON.stringify(newCredsToSet) ), ]) return authenticator } const getAuthenticator = async (credentialId: string) => { const authenticator = await getItem<AdapterAuthenticator>( authenticatorKeyPrefix + credentialId ) if (!authenticator) return null return hydrateDates(authenticator) } const getAuthenticatorByUserId = async ( userId: string ): Promise<AdapterAuthenticator[] | []> => { const credentialIds = await getItem<string[]>( `${authenticatorUserKeyPrefix}${userId}` ) if (!credentialIds) return [] const authenticators: AdapterAuthenticator[] = [] for (const credentialId of credentialIds) { const authenticator = await getAuthenticator(credentialId) if (authenticator) { hydrateDates(authenticator) authenticators.push(authenticator) } } return authenticators } return { async getAccount(providerAccountId: string, provider: string) { const accountId = `${provider}:${providerAccountId}` const account = await getAccount(accountId) if (!account) return null return account }, async createUser(user) { const id = crypto.randomUUID() return await setUser(id, { ...user, id }) }, getUser, async getUserByEmail(email) { const userId = await getItem<string>(emailKeyPrefix + email) if (!userId) { return null } return await getUser(userId) }, async getUserByAccount(account) { const dbAccount = await getAccount( `${account.provider}:${account.providerAccountId}` ) if (!dbAccount) return null return await getUser(dbAccount.userId) }, async updateUser(updates) { const userId = updates.id as string const user = await getUser(userId) return await setUser(userId, { ...(user as AdapterUser), ...updates }) }, async linkAccount(account) { const id = `${account.provider}:${account.providerAccountId}` return await setAccount(id, { ...account, id }) }, createSession: (session) => setSession(session.sessionToken, session), async getSessionAndUser(sessionToken) { const session = await getSession(sessionToken) if (!session) return null const user = await getUser(session.userId) if (!user) return null return { session, user } }, async updateSession(updates) { const session = await getSession(updates.sessionToken) if (!session) return null return await setSession(updates.sessionToken, { ...session, ...updates }) }, async deleteSession(sessionToken) { await storage.removeItem(sessionKeyPrefix + sessionToken) }, async createVerificationToken(verificationToken) { await setObjectAsJson( verificationTokenKeyPrefix + verificationToken.identifier + ":" + verificationToken.token, verificationToken ) return verificationToken }, async useVerificationToken(verificationToken) { const tokenKey = verificationTokenKeyPrefix + verificationToken.identifier + ":" + verificationToken.token const token = await getItem<VerificationToken>(tokenKey) if (!token) return null await storage.removeItem(tokenKey) return hydrateDates(token) }, async unlinkAccount(account) { const id = `${account.provider}:${account.providerAccountId}` const dbAccount = await getAccount(id) if (!dbAccount) return const accountKey = `${accountKeyPrefix}${id}` await Promise.all([ storage.removeItem(accountKey), storage.removeItem( `${accountByUserIdPrefix} + ${dbAccount.userId as string}` ), ]) }, async deleteUser(userId) { const user = await getUser(userId) if (!user) return const accountByUserKey = accountByUserIdPrefix + userId const accountKey = await getItem<string>(accountByUserKey) const sessionByUserIdKey = sessionByUserIdKeyPrefix + userId const sessionKey = await getItem<string>(sessionByUserIdKey) await Promise.all([ storage.removeItem(userKeyPrefix + userId), storage.removeItem(`${emailKeyPrefix}${user.email as string}`), storage.removeItem(accountKey as string), storage.removeItem(accountByUserKey), storage.removeItem(sessionKey as string), storage.removeItem(sessionByUserIdKey), ]) }, async createAuthenticator(authenticator) { await setAuthenticator(authenticator.credentialID, authenticator) return authenticator }, async getAuthenticator(credentialID) { return getAuthenticator(credentialID) }, async listAuthenticatorsByUserId(userId) { const user = await getUser(userId) if (!user) return [] return getAuthenticatorByUserId(user.id) }, async updateAuthenticatorCounter(credentialID, counter) { const authenticator = await getAuthenticator(credentialID) authenticator.counter = Number(counter) await setAuthenticator(credentialID, authenticator) return authenticator }, } }