mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-25 08:31:37 -05:00
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:
committed by
GitHub
parent
6181119e2b
commit
bea45db85f
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user