fix: sync updateSession changes to secondary storage and active-sessions list (#6988)

Co-authored-by: Taesu <166604494+bytaesu@users.noreply.github.com>
Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
Ridhim Singh Raizada
2025-12-30 15:53:26 +05:30
committed by GitHub
parent 6181119e2b
commit bea45db85f
2 changed files with 187 additions and 13 deletions

View File

@@ -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<string, string>();
const testExpirationMap = new Map<string, number>();
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();
});
});

View File

@@ -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,
}