diff --git a/packages/better-auth/src/db/internal-adapter.test.ts b/packages/better-auth/src/db/internal-adapter.test.ts index 5f216d443b..9d5ffd2051 100644 --- a/packages/better-auth/src/db/internal-adapter.test.ts +++ b/packages/better-auth/src/db/internal-adapter.test.ts @@ -635,4 +635,120 @@ describe("internal adapter test", async () => { accounts = await internalAdapter.findAccounts(user.id); expect(accounts.length).toBe(0); }); + + it("should update session and active-sessions list in secondary storage", async () => { + const testMap = new Map(); + const testExpirationMap = new Map(); + + const testDb = new Database(":memory:"); + const testSqliteDialect = new SqliteDialect({ + database: testDb, + }); + + const testOpts = { + database: { + dialect: testSqliteDialect, + type: "sqlite", + }, + secondaryStorage: { + set(key: string, value: string, ttl?: number) { + testMap.set(key, value); + if (ttl !== undefined) { + testExpirationMap.set(key, ttl); + } + }, + get(key: string) { + return testMap.get(key) || null; + }, + delete(key: string) { + testMap.delete(key); + testExpirationMap.delete(key); + }, + }, + } satisfies BetterAuthOptions; + + // Run migrations for the new database + (await getMigrations(testOpts)).runMigrations(); + + const testCtx = await init(testOpts); + const testInternalAdapter = testCtx.internalAdapter; + + // Create a user first + const user = await testInternalAdapter.createUser({ + name: "test-user-update", + email: "test-update@email.com", + }); + + // Create a session + const session = await testInternalAdapter.createSession(user.id); + + // Verify session is in secondary storage + const storedSessionStr = testMap.get(session.token); + expect(storedSessionStr).toBeDefined(); + + const storedSession = safeJSONParse<{ + session: Session; + user: User; + }>(storedSessionStr!); + + expect(storedSession?.session.ipAddress).toBe(""); + + // Get initial active-sessions list + const initialListStr = testMap.get(`active-sessions-${user.id}`); + expect(initialListStr).toBeDefined(); + const initialList = safeJSONParse<{ token: string; expiresAt: number }[]>( + initialListStr!, + ); + expect(initialList).toBeDefined(); + expect(initialList!.length).toBe(1); + const initialExpiresAt = initialList![0]!.expiresAt; + + // Update the session with new ipAddress and expiresAt + const updatedIpAddress = "192.168.1.1"; + const newExpiresAt = new Date(initialExpiresAt + 60 * 60 * 1000); + await testInternalAdapter.updateSession(session.token, { + ipAddress: updatedIpAddress, + expiresAt: newExpiresAt, + }); + + // Get the session from secondary storage again + const updatedStoredSessionStr = testMap.get(session.token); + expect(updatedStoredSessionStr).toBeDefined(); + + const updatedStoredSession = safeJSONParse<{ + session: Session; + user: User; + }>(updatedStoredSessionStr!); + + // The session in secondary storage MUST have the updated data + expect(updatedStoredSession?.session.ipAddress).toBe(updatedIpAddress); + + // User should still be intact + expect(updatedStoredSession?.user.id).toBe(user.id); + + // Get updated active-sessions list + const updatedListStr = testMap.get(`active-sessions-${user.id}`); + expect(updatedListStr).toBeDefined(); + const updatedList = safeJSONParse<{ token: string; expiresAt: number }[]>( + updatedListStr!, + ); + expect(updatedList).toBeDefined(); + + // The expiresAt in active-sessions list should be updated + expect(updatedList!.length).toBe(1); + expect(updatedList![0]!.token).toBe(session.token); + expect(updatedList![0]!.expiresAt).toBe(newExpiresAt.getTime()); + + // TTL should also be updated + const updatedTTL = testExpirationMap.get(`active-sessions-${user.id}`); + const expectedTTL = Math.floor( + (newExpiresAt.getTime() - Date.now()) / 1000, + ); + expect(updatedTTL).toBeDefined(); + expect(updatedTTL! - expectedTTL).toBeLessThanOrEqual(1); + expect(updatedTTL! - expectedTTL).toBeGreaterThanOrEqual(0); + + // Clean up DB + testDb.close(); + }); }); diff --git a/packages/better-auth/src/db/internal-adapter.ts b/packages/better-auth/src/db/internal-adapter.ts index 4e2a34444e..5295fe5825 100644 --- a/packages/better-auth/src/db/internal-adapter.ts +++ b/packages/better-auth/src/db/internal-adapter.ts @@ -507,21 +507,79 @@ export const createInternalAdapter = ( ? { async fn(data) { const currentSession = await secondaryStorage.get(sessionToken); - let updatedSession: Session | null = null; - if (currentSession) { - const parsedSession = safeJSONParse<{ - session: Session; - user: User; - }>(currentSession); - if (!parsedSession) return null; - updatedSession = { - ...parsedSession.session, - ...data, - }; - return updatedSession; - } else { + if (!currentSession) { return null; } + + const parsedSession = safeJSONParse<{ + session: Session; + user: User; + }>(currentSession); + if (!parsedSession) return null; + + const mergedSession = { + ...parsedSession.session, + ...data, + expiresAt: new Date( + data.expiresAt ?? parsedSession.session.expiresAt, + ), + createdAt: new Date(parsedSession.session.createdAt), + updatedAt: new Date( + data.updatedAt ?? parsedSession.session.updatedAt, + ), + }; + + const updatedSession = parseSessionOutput( + ctx.options, + mergedSession, + ); + + const now = Date.now(); + const expiresMs = new Date(updatedSession.expiresAt).getTime(); + const sessionTTL = Math.max( + Math.floor((expiresMs - now) / 1000), + 0, + ); + + if (sessionTTL > 0) { + await secondaryStorage.set( + sessionToken, + JSON.stringify({ + session: updatedSession, + user: parsedSession.user, + }), + sessionTTL, + ); + + const listKey = `active-sessions-${updatedSession.userId}`; + const listRaw = await secondaryStorage.get(listKey); + const list: { token: string; expiresAt: number }[] = listRaw + ? safeJSONParse(listRaw) || [] + : []; + + const filtered = list + .filter( + (s) => s.token !== sessionToken && s.expiresAt > now, + ) + .concat([{ token: sessionToken, expiresAt: expiresMs }]); + + const sorted = filtered.sort( + (a, b) => a.expiresAt - b.expiresAt, + ); + const furthestSessionExp = sorted.at(-1)?.expiresAt; + + if (furthestSessionExp && furthestSessionExp > now) { + await secondaryStorage.set( + listKey, + JSON.stringify(sorted), + Math.floor((furthestSessionExp - now) / 1000), + ); + } else { + await secondaryStorage.delete(listKey); + } + } + + return updatedSession; }, executeMainFn: options.session?.storeSessionInDatabase, }