UNPKG

@auth/d1-adapter

Version:

A Cloudflare D1 adapter for Auth.js

241 lines (240 loc) 9.49 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 { 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"; // format is borrowed from the supabase adapter, graciously function format(obj) { for (const [key, value] of Object.entries(obj)) { if (value === null) { delete obj[key]; } if (isDate(value)) { obj[key] = new Date(value); } } return obj; } // D1 doesnt like undefined, it wants null when calling bind function cleanBindings(bindings) { return bindings.map((e) => (e === undefined ? null : e)); } export async function createRecord(db, CREATE_SQL, bindings, GET_SQL, getBindings) { try { bindings = cleanBindings(bindings); await db .prepare(CREATE_SQL) .bind(...bindings) .run(); return await getRecord(db, GET_SQL, getBindings); } catch (e) { console.error(e.message, e.cause?.message); throw e; } } export async function getRecord(db, SQL, bindings) { try { bindings = cleanBindings(bindings); const res = await db .prepare(SQL) .bind(...bindings) .first(); if (res) { return format(res); } else { return null; } } catch (e) { console.error(e.message, e.cause?.message); throw e; } } export async function updateRecord(db, SQL, bindings) { try { bindings = cleanBindings(bindings); return await db .prepare(SQL) .bind(...bindings) .run(); } catch (e) { console.error(e.message, e.cause?.message); throw e; } } export async function deleteRecord(db, SQL, bindings) { try { bindings = cleanBindings(bindings); await db .prepare(SQL) .bind(...bindings) .run(); } catch (e) { console.error(e.message, e.cause?.message); throw e; } } export function D1Adapter(db) { // we need to run migrations if we dont have the right tables return { async createUser(user) { const id = crypto.randomUUID(); const createBindings = [ id, user.name, user.email, user.emailVerified?.toISOString(), user.image, ]; const getBindings = [id]; const newUser = await createRecord(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(db, GET_USER_BY_ID_SQL, [id]); }, async getUserByEmail(email) { return await getRecord(db, GET_USER_BY_EMAIL_SQL, [email]); }, async getUserByAccount({ providerAccountId, provider }) { return await getRecord(db, GET_USER_BY_ACCOUNTL_SQL, [ providerAccountId, provider, ]); }, async updateUser(user) { const params = await getRecord(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(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(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(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 = await getRecord(db, GET_SESSION_BY_TOKEN_SQL, [sessionToken]); if (session === null) return null; const user = await getRecord(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(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(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; }, }; }