UNPKG

@auth/d1-adapter

Version:

A Cloudflare D1 adapter for Auth.js

337 lines (320 loc) 9.54 kB
/** * <div style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "16px"}}> * <p>An official <a href="https://developers.cloudflare.com/d1/">Cloudflare D1</a> adapter for Auth.js / NextAuth.js.</p> * <a href="https://developers.cloudflare.com/d1/"> * <img style={{display: "block"}} src="/img/adapters/d1.svg" width="48" /> * </a> * </div> * * ## Warning * This adapter is not developed or maintained by Cloudflare and they haven't declared the D1 api stable. The author will make an effort to keep this adapter up to date. * The adapter is compatible with the D1 api as of March 22, 2023. * * ## Installation * * ```bash npm2yarn * npm install next-auth @auth/d1-adapter * ``` * * @module @auth/d1-adapter */ import type { D1Database as WorkerDatabase } from "@cloudflare/workers-types" import type { D1Database as MiniflareD1Database } from "@miniflare/d1" import { type Adapter, type AdapterSession, type AdapterUser, type AdapterAccount, type VerificationToken as AdapterVerificationToken, isDate, } from "@auth/core/adapters" import { CREATE_ACCOUNT_SQL, CREATE_SESSION_SQL, CREATE_USER_SQL, CREATE_VERIFICATION_TOKEN_SQL, DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL, DELETE_ACCOUNT_BY_USER_ID_SQL, DELETE_SESSION_BY_USER_ID_SQL, DELETE_SESSION_SQL, DELETE_USER_SQL, DELETE_VERIFICATION_TOKEN_SQL, GET_ACCOUNT_BY_ID_SQL, GET_SESSION_BY_TOKEN_SQL, GET_USER_BY_ACCOUNTL_SQL, GET_USER_BY_EMAIL_SQL, GET_USER_BY_ID_SQL, GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL, UPDATE_SESSION_BY_SESSION_TOKEN_SQL, UPDATE_USER_BY_ID_SQL, } from "./queries.js" export { up } from "./migrations.js" /** * @type @cloudflare/workers-types.D1Database | @miniflare/d1.D1Database */ export type D1Database = WorkerDatabase | MiniflareD1Database // format is borrowed from the supabase adapter, graciously function format<T>(obj: Record<string, any>): T { for (const [key, value] of Object.entries(obj)) { if (value === null) { delete obj[key] } if (isDate(value)) { obj[key] = new Date(value) } } return obj as T } // D1 doesnt like undefined, it wants null when calling bind function cleanBindings(bindings: any[]) { return bindings.map((e) => (e === undefined ? null : e)) } export async function createRecord<RecordType>( db: D1Database, CREATE_SQL: string, bindings: any[], GET_SQL: string, getBindings: any[] ) { try { bindings = cleanBindings(bindings) await db .prepare(CREATE_SQL) .bind(...bindings) .run() return await getRecord<RecordType>(db, GET_SQL, getBindings) } catch (e: any) { console.error(e.message, e.cause?.message) throw e } } export async function getRecord<RecordType>( db: D1Database, SQL: string, bindings: any[] ): Promise<RecordType | null> { try { bindings = cleanBindings(bindings) const res: any = await db .prepare(SQL) .bind(...bindings) .first() if (res) { return format<RecordType>(res) } else { return null } } catch (e: any) { console.error(e.message, e.cause?.message) throw e } } export async function updateRecord( db: D1Database, SQL: string, bindings: any[] ) { try { bindings = cleanBindings(bindings) return await db .prepare(SQL) .bind(...bindings) .run() } catch (e: any) { console.error(e.message, e.cause?.message) throw e } } export async function deleteRecord( db: D1Database, SQL: string, bindings: any[] ) { try { bindings = cleanBindings(bindings) await db .prepare(SQL) .bind(...bindings) .run() } catch (e: any) { console.error(e.message, e.cause?.message) throw e } } export function D1Adapter(db: D1Database): Adapter { // we need to run migrations if we dont have the right tables return { async createUser(user) { const id: string = crypto.randomUUID() const createBindings = [ id, user.name, user.email, user.emailVerified?.toISOString(), user.image, ] const getBindings = [id] const newUser = await createRecord<AdapterUser>( db, CREATE_USER_SQL, createBindings, GET_USER_BY_ID_SQL, getBindings ) if (newUser) return newUser throw new Error("Error creating user: Cannot get user after creation.") }, async getUser(id) { return await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [id]) }, async getUserByEmail(email) { return await getRecord<AdapterUser>(db, GET_USER_BY_EMAIL_SQL, [email]) }, async getUserByAccount({ providerAccountId, provider }) { return await getRecord<AdapterUser>(db, GET_USER_BY_ACCOUNTL_SQL, [ providerAccountId, provider, ]) }, async updateUser(user) { const params = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [ user.id, ]) if (params) { // copy any properties not in the update into the existing one and use that for bind params // covers the scenario where the user arg doesnt have all of the current users properties Object.assign(params, user) const res = await updateRecord(db, UPDATE_USER_BY_ID_SQL, [ params.name, params.email, params.emailVerified?.toISOString(), params.image, params.id, ]) if (res.success) { const user = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [ params.id, ]) if (user) return user throw new Error( "Error updating user: Cannot get user after updating." ) } } throw new Error("Error updating user: Failed to run the update SQL.") }, async deleteUser(userId) { // miniflare doesn't support batch operations or multiline sql statements await deleteRecord(db, DELETE_ACCOUNT_BY_USER_ID_SQL, [userId]) await deleteRecord(db, DELETE_SESSION_BY_USER_ID_SQL, [userId]) await deleteRecord(db, DELETE_USER_SQL, [userId]) return null }, async linkAccount(a) { // convert user_id to userId and provider_account_id to providerAccountId const id = crypto.randomUUID() const createBindings = [ id, a.userId, a.type, a.provider, a.providerAccountId, a.refresh_token, a.access_token, a.expires_at, a.token_type, a.scope, a.id_token, a.session_state, a.oauth_token ?? null, a.oauth_token_secret ?? null, ] const getBindings = [id] return await createRecord<AdapterAccount>( db, CREATE_ACCOUNT_SQL, createBindings, GET_ACCOUNT_BY_ID_SQL, getBindings ) }, async unlinkAccount({ providerAccountId, provider }) { await deleteRecord( db, DELETE_ACCOUNT_BY_PROVIDER_AND_PROVIDER_ACCOUNT_ID_SQL, [provider, providerAccountId] ) }, async createSession({ sessionToken, userId, expires }) { const id = crypto.randomUUID() const createBindings = [id, sessionToken, userId, expires.toISOString()] const getBindings = [sessionToken] const session = await createRecord<AdapterSession>( db, CREATE_SESSION_SQL, createBindings, GET_SESSION_BY_TOKEN_SQL, getBindings ) if (session) return session throw new Error(`Couldn't create session`) }, async getSessionAndUser(sessionToken) { const session: any = await getRecord<AdapterSession>( db, GET_SESSION_BY_TOKEN_SQL, [sessionToken] ) if (session === null) return null const user = await getRecord<AdapterUser>(db, GET_USER_BY_ID_SQL, [ session.userId, ]) if (user === null) return null return { session, user } }, async updateSession({ sessionToken, expires }) { if (expires === undefined) { await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken]) return null } const session = await getRecord<AdapterSession>( db, GET_SESSION_BY_TOKEN_SQL, [sessionToken] ) if (!session) return null session.expires = expires await updateRecord(db, UPDATE_SESSION_BY_SESSION_TOKEN_SQL, [ expires?.toISOString(), sessionToken, ]) return await db .prepare(UPDATE_SESSION_BY_SESSION_TOKEN_SQL) .bind(expires?.toISOString(), sessionToken) .first() }, async deleteSession(sessionToken) { await deleteRecord(db, DELETE_SESSION_SQL, [sessionToken]) return null }, async createVerificationToken({ identifier, expires, token }) { return await createRecord( db, CREATE_VERIFICATION_TOKEN_SQL, [identifier, expires.toISOString(), token], GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL, [identifier, token] ) }, async useVerificationToken({ identifier, token }) { const verificationToken = await getRecord<AdapterVerificationToken>( db, GET_VERIFICATION_TOKEN_BY_IDENTIFIER_AND_TOKEN_SQL, [identifier, token] ) if (!verificationToken) return null await deleteRecord(db, DELETE_VERIFICATION_TOKEN_SQL, [identifier, token]) return verificationToken }, } }