better-auth
Version:
The most comprehensive authentication framework for TypeScript.
548 lines (546 loc) • 17.2 kB
JavaScript
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