UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

548 lines (546 loc) • 17.2 kB
import { getDate } from "../utils/date.mjs"; import { getIp } from "../utils/get-request-ip.mjs"; import { parseSessionInput, parseSessionOutput, parseUserOutput } from "./schema.mjs"; import { getWithHooks } from "./with-hooks.mjs"; import { getCurrentAdapter, getCurrentAuthContext, runWithTransaction } from "@better-auth/core/context"; import { generateId, safeJSONParse } from "@better-auth/core/utils"; //#region src/db/internal-adapter.ts const createInternalAdapter = (adapter, ctx) => { const logger = ctx.logger; const options = ctx.options; const secondaryStorage = options.secondaryStorage; const sessionExpiration = options.session?.expiresIn || 3600 * 24 * 7; const { createWithHooks, updateWithHooks, updateManyWithHooks, deleteWithHooks, deleteManyWithHooks } = getWithHooks(adapter, ctx); async function refreshUserSessions(user) { if (!secondaryStorage) return; const listRaw = await secondaryStorage.get(`active-sessions-${user.id}`); if (!listRaw) return; const now = Date.now(); const validSessions = (safeJSONParse(listRaw) || []).filter((s) => s.expiresAt > now); await Promise.all(validSessions.map(async ({ token }) => { const cached = await secondaryStorage.get(token); if (!cached) return; const parsed = safeJSONParse(cached); if (!parsed) return; const sessionTTL = Math.max(Math.floor(new Date(parsed.session.expiresAt).getTime() - now) / 1e3, 0); await secondaryStorage.set(token, JSON.stringify({ session: parsed.session, user }), Math.floor(sessionTTL)); })); } return { createOAuthUser: async (user, account) => { return runWithTransaction(adapter, async () => { const createdUser = await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...user }, "user", void 0); return { user: createdUser, account: await createWithHooks({ ...account, userId: createdUser.id, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }, "account", void 0) }; }); }, createUser: async (user) => { return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...user, email: user.email?.toLowerCase() }, "user", void 0); }, createAccount: async (account) => { return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...account }, "account", void 0); }, listSessions: async (userId) => { if (secondaryStorage) { const currentList = await secondaryStorage.get(`active-sessions-${userId}`); if (!currentList) return []; const list = safeJSONParse(currentList) || []; const now = Date.now(); const validSessions = list.filter((s) => s.expiresAt > now); const sessions = []; for (const session of validSessions) { const sessionStringified = await secondaryStorage.get(session.token); if (sessionStringified) { const s = safeJSONParse(sessionStringified); if (!s) return []; const parsedSession = parseSessionOutput(ctx.options, { ...s.session, expiresAt: new Date(s.session.expiresAt) }); sessions.push(parsedSession); } } return sessions; } return await (await getCurrentAdapter(adapter)).findMany({ model: "session", where: [{ field: "userId", value: userId }] }); }, listUsers: async (limit, offset, sortBy, where) => { return await (await getCurrentAdapter(adapter)).findMany({ model: "user", limit, offset, sortBy, where }); }, countTotalUsers: async (where) => { const total = await (await getCurrentAdapter(adapter)).count({ model: "user", where }); if (typeof total === "string") return parseInt(total); return total; }, deleteUser: async (userId) => { if (!secondaryStorage || options.session?.storeSessionInDatabase) await deleteManyWithHooks([{ field: "userId", value: userId }], "session", void 0); await deleteManyWithHooks([{ field: "userId", value: userId }], "account", void 0); await deleteWithHooks([{ field: "id", value: userId }], "user", void 0); }, createSession: async (userId, dontRememberMe, override, overrideAll) => { const ctx$1 = await getCurrentAuthContext().catch(() => null); const headers = ctx$1?.headers || ctx$1?.request?.headers; const { id: _, ...rest } = override || {}; const defaultAdditionalFields = parseSessionInput(ctx$1?.context.options ?? options, {}); const data = { ipAddress: ctx$1?.request || ctx$1?.headers ? getIp(ctx$1?.request || ctx$1?.headers, ctx$1?.context.options) || "" : "", userAgent: headers?.get("user-agent") || "", ...rest, expiresAt: dontRememberMe ? getDate(3600 * 24, "sec") : getDate(sessionExpiration, "sec"), userId, token: generateId(32), createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...defaultAdditionalFields, ...overrideAll ? rest : {} }; return await createWithHooks(data, "session", secondaryStorage ? { fn: async (sessionData) => { /** * store the session token for the user * so we can retrieve it later for listing sessions */ const currentList = await secondaryStorage.get(`active-sessions-${userId}`); let list = []; const now = Date.now(); if (currentList) { list = safeJSONParse(currentList) || []; list = list.filter((session) => session.expiresAt > now); } const sorted = list.sort((a, b) => a.expiresAt - b.expiresAt); let furthestSessionExp = sorted.at(-1)?.expiresAt; sorted.push({ token: data.token, expiresAt: data.expiresAt.getTime() }); if (!furthestSessionExp || furthestSessionExp < data.expiresAt.getTime()) furthestSessionExp = data.expiresAt.getTime(); const furthestSessionTTL = Math.max(Math.floor((furthestSessionExp - now) / 1e3), 0); if (furthestSessionTTL > 0) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(sorted), furthestSessionTTL); const user = await adapter.findOne({ model: "user", where: [{ field: "id", value: userId }] }); const sessionTTL = Math.max(Math.floor((data.expiresAt.getTime() - now) / 1e3), 0); if (sessionTTL > 0) await secondaryStorage.set(data.token, JSON.stringify({ session: sessionData, user }), sessionTTL); return sessionData; }, executeMainFn: options.session?.storeSessionInDatabase } : void 0); }, findSession: async (token) => { if (secondaryStorage) { const sessionStringified = await secondaryStorage.get(token); if (!sessionStringified && !options.session?.storeSessionInDatabase) return null; if (sessionStringified) { const s = safeJSONParse(sessionStringified); if (!s) return null; return { session: parseSessionOutput(ctx.options, { ...s.session, expiresAt: new Date(s.session.expiresAt), createdAt: new Date(s.session.createdAt), updatedAt: new Date(s.session.updatedAt) }), user: parseUserOutput(ctx.options, { ...s.user, createdAt: new Date(s.user.createdAt), updatedAt: new Date(s.user.updatedAt) }) }; } } const result = await (await getCurrentAdapter(adapter)).findOne({ model: "session", where: [{ value: token, field: "token" }], join: { user: true } }); if (!result) return null; const { user, ...session } = result; if (!user) return null; return { session: parseSessionOutput(ctx.options, session), user: parseUserOutput(ctx.options, user) }; }, findSessions: async (sessionTokens) => { if (secondaryStorage) { const sessions$1 = []; for (const sessionToken of sessionTokens) { const sessionStringified = await secondaryStorage.get(sessionToken); if (sessionStringified) { const s = safeJSONParse(sessionStringified); if (!s) return []; const session = { session: { ...s.session, expiresAt: new Date(s.session.expiresAt) }, user: { ...s.user, createdAt: new Date(s.user.createdAt), updatedAt: new Date(s.user.updatedAt) } }; sessions$1.push(session); } } return sessions$1; } const sessions = await (await getCurrentAdapter(adapter)).findMany({ model: "session", where: [{ field: "token", value: sessionTokens, operator: "in" }], join: { user: true } }); if (!sessions.length) return []; if (sessions.some((session) => !session.user)) return []; return sessions.map((_session) => { const { user, ...session } = _session; return { session, user }; }); }, updateSession: async (sessionToken, session) => { return await updateWithHooks(session, [{ field: "token", value: sessionToken }], "session", secondaryStorage ? { async fn(data) { const currentSession = await secondaryStorage.get(sessionToken); let updatedSession = null; if (currentSession) { const parsedSession = safeJSONParse(currentSession); if (!parsedSession) return null; updatedSession = { ...parsedSession.session, ...data }; return updatedSession; } else return null; }, executeMainFn: options.session?.storeSessionInDatabase } : void 0); }, deleteSession: async (token) => { if (secondaryStorage) { const data = await secondaryStorage.get(token); if (data) { const { session } = safeJSONParse(data) ?? {}; if (!session) { logger.error("Session not found in secondary storage"); return; } const userId = session.userId; const currentList = await secondaryStorage.get(`active-sessions-${userId}`); if (currentList) { let list = safeJSONParse(currentList) || []; const now = Date.now(); const filtered = list.filter((session$1) => session$1.expiresAt > now && session$1.token !== token); const furthestSessionExp = filtered.sort((a, b) => a.expiresAt - b.expiresAt).at(-1)?.expiresAt; if (filtered.length > 0 && furthestSessionExp && furthestSessionExp > Date.now()) await secondaryStorage.set(`active-sessions-${userId}`, JSON.stringify(filtered), Math.floor((furthestSessionExp - now) / 1e3)); else await secondaryStorage.delete(`active-sessions-${userId}`); } else logger.error("Active sessions list not found in secondary storage"); } await secondaryStorage.delete(token); if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return; } await deleteWithHooks([{ field: "token", value: token }], "session", void 0); }, deleteAccounts: async (userId) => { await deleteManyWithHooks([{ field: "userId", value: userId }], "account", void 0); }, deleteAccount: async (accountId) => { await deleteWithHooks([{ field: "id", value: accountId }], "account", void 0); }, deleteSessions: async (userIdOrSessionTokens) => { if (secondaryStorage) { if (typeof userIdOrSessionTokens === "string") { const activeSession = await secondaryStorage.get(`active-sessions-${userIdOrSessionTokens}`); const sessions = activeSession ? safeJSONParse(activeSession) : []; if (!sessions) return; for (const session of sessions) await secondaryStorage.delete(session.token); await secondaryStorage.delete(`active-sessions-${userIdOrSessionTokens}`); } else for (const sessionToken of userIdOrSessionTokens) if (await secondaryStorage.get(sessionToken)) await secondaryStorage.delete(sessionToken); if (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase) return; } await deleteManyWithHooks([{ field: Array.isArray(userIdOrSessionTokens) ? "token" : "userId", value: userIdOrSessionTokens, operator: Array.isArray(userIdOrSessionTokens) ? "in" : void 0 }], "session", void 0); }, findOAuthUser: async (email, accountId, providerId) => { const account = await (await getCurrentAdapter(adapter)).findMany({ model: "account", where: [{ value: accountId, field: "accountId" }], join: { user: true } }).then((accounts) => { return accounts.find((a) => a.providerId === providerId); }); if (account) if (account.user) return { user: account.user, accounts: [account] }; else { const user = await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ value: email.toLowerCase(), field: "email" }] }); if (user) return { user, accounts: [account] }; return null; } else { const user = await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ value: email.toLowerCase(), field: "email" }] }); if (user) return { user, accounts: await (await getCurrentAdapter(adapter)).findMany({ model: "account", where: [{ value: user.id, field: "userId" }] }) || [] }; else return null; } }, findUserByEmail: async (email, options$1) => { const result = await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ value: email.toLowerCase(), field: "email" }], join: { ...options$1?.includeAccounts ? { account: true } : {} } }); if (!result) return null; const { account: accounts, ...user } = result; return { user, accounts: accounts ?? [] }; }, findUserById: async (userId) => { if (!userId) return null; return await (await getCurrentAdapter(adapter)).findOne({ model: "user", where: [{ field: "id", value: userId }] }); }, linkAccount: async (account) => { return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...account }, "account", void 0); }, updateUser: async (userId, data) => { const user = await updateWithHooks(data, [{ field: "id", value: userId }], "user", void 0); await refreshUserSessions(user); await refreshUserSessions(user); return user; }, updateUserByEmail: async (email, data) => { const user = await updateWithHooks(data, [{ field: "email", value: email.toLowerCase() }], "user", void 0); await refreshUserSessions(user); await refreshUserSessions(user); return user; }, updatePassword: async (userId, password) => { await updateManyWithHooks({ password }, [{ field: "userId", value: userId }, { field: "providerId", value: "credential" }], "account", void 0); }, findAccounts: async (userId) => { return await (await getCurrentAdapter(adapter)).findMany({ model: "account", where: [{ field: "userId", value: userId }] }); }, findAccount: async (accountId) => { return await (await getCurrentAdapter(adapter)).findOne({ model: "account", where: [{ field: "accountId", value: accountId }] }); }, findAccountByProviderId: async (accountId, providerId) => { return await (await getCurrentAdapter(adapter)).findOne({ model: "account", where: [{ field: "accountId", value: accountId }, { field: "providerId", value: providerId }] }); }, findAccountByUserId: async (userId) => { return await (await getCurrentAdapter(adapter)).findMany({ model: "account", where: [{ field: "userId", value: userId }] }); }, updateAccount: async (id, data) => { return await updateWithHooks(data, [{ field: "id", value: id }], "account", void 0); }, createVerificationValue: async (data) => { return await createWithHooks({ createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date(), ...data }, "verification", void 0); }, findVerificationValue: async (identifier) => { const verification = await (await getCurrentAdapter(adapter)).findMany({ model: "verification", where: [{ field: "identifier", value: identifier }], sortBy: { field: "createdAt", direction: "desc" }, limit: 1 }); if (!options.verification?.disableCleanup) await deleteManyWithHooks([{ field: "expiresAt", value: /* @__PURE__ */ new Date(), operator: "lt" }], "verification", void 0); return verification[0]; }, deleteVerificationValue: async (id) => { await deleteWithHooks([{ field: "id", value: id }], "verification", void 0); }, deleteVerificationByIdentifier: async (identifier) => { await deleteWithHooks([{ field: "identifier", value: identifier }], "verification", void 0); }, updateVerificationValue: async (id, data) => { return await updateWithHooks(data, [{ field: "id", value: id }], "verification", void 0); } }; }; //#endregion export { createInternalAdapter }; //# sourceMappingURL=internal-adapter.mjs.map