@auth/unstorage-adapter
Version:
Unstorage adapter for Auth.js.
375 lines (346 loc) • 11.5 kB
text/typescript
/**
* <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
},
}
}