mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-24 16:11:53 -05:00
feat(multi-session): add support for custom session ID logic
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { BetterAuthPlugin } from "@better-auth/core";
|
||||
import type { Session, User } from "@better-auth/core/db";
|
||||
import {
|
||||
createAuthEndpoint,
|
||||
createAuthMiddleware,
|
||||
@@ -30,6 +31,13 @@ export interface MultiSessionConfig {
|
||||
* @default 5
|
||||
*/
|
||||
maximumSessions?: number | undefined;
|
||||
|
||||
/**
|
||||
* Determines the unique identifier for a session to prevent duplicates in the session list.
|
||||
* By default, it groups sessions by User ID.
|
||||
* @default (data) => data.user.id
|
||||
*/
|
||||
getUniqSessionId?: (data: { session: Session; user: User }) => string;
|
||||
}
|
||||
|
||||
import { MULTI_SESSION_ERROR_CODES as ERROR_CODES } from "./error-codes";
|
||||
@@ -51,8 +59,9 @@ const revokeDeviceSessionBodySchema = z.object({
|
||||
export const multiSession = (options?: MultiSessionConfig | undefined) => {
|
||||
const opts = {
|
||||
maximumSessions: 5,
|
||||
getUniqSessionId: (data) => data.user.id,
|
||||
...options,
|
||||
};
|
||||
} satisfies Required<MultiSessionConfig>;
|
||||
|
||||
const isMultiSessionCookie = (key: string) => key.includes("_multi-");
|
||||
|
||||
@@ -102,16 +111,13 @@ export const multiSession = (options?: MultiSessionConfig | undefined) => {
|
||||
const validSessions = sessions.filter(
|
||||
(session) => session && session.session.expiresAt > new Date(),
|
||||
);
|
||||
const uniqueUserSessions = validSessions.reduce(
|
||||
(acc, session) => {
|
||||
if (!acc.find((s) => s.user.id === session.user.id)) {
|
||||
acc.push(session);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as typeof validSessions,
|
||||
const uniqueSessionsMap = new Map(
|
||||
validSessions.map((session) => [
|
||||
opts.getUniqSessionId(session),
|
||||
session,
|
||||
]),
|
||||
);
|
||||
return ctx.json(uniqueUserSessions);
|
||||
return ctx.json([...uniqueSessionsMap.values()]);
|
||||
},
|
||||
),
|
||||
/**
|
||||
|
||||
@@ -213,4 +213,83 @@ describe("multi-session", async () => {
|
||||
});
|
||||
expect(attackerSessionAfter.data).toBeNull();
|
||||
});
|
||||
|
||||
it("should deduplicate sessions based on getUniqSessionId", async () => {
|
||||
const dedupeHeaders = new Headers();
|
||||
const userCredentials = {
|
||||
email: "dedupe@test.com",
|
||||
password: "password",
|
||||
name: "Dedupe User",
|
||||
};
|
||||
|
||||
await client.signUp.email(userCredentials, {
|
||||
onSuccess: cookieSetter(dedupeHeaders),
|
||||
});
|
||||
|
||||
await client.signIn.email(userCredentials, {
|
||||
onSuccess: cookieSetter(dedupeHeaders),
|
||||
});
|
||||
|
||||
const res = await client.multiSession.listDeviceSessions({
|
||||
fetchOptions: {
|
||||
headers: dedupeHeaders,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.data).toHaveLength(1);
|
||||
expect(res.data!.at(0)!.user.email).toBe(userCredentials.email);
|
||||
});
|
||||
|
||||
describe("multi-session with custom unique logic", async () => {
|
||||
const { client, testUser, cookieSetter } = await getTestInstance(
|
||||
{
|
||||
plugins: [
|
||||
multiSession({
|
||||
getUniqSessionId: (data) => data.session.token,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
clientOptions: {
|
||||
plugins: [multiSessionClient()],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
it("should show multiple sessions for the same user with custom getUniqSessionId", async () => {
|
||||
const customHeaders = new Headers();
|
||||
|
||||
await client.signIn.email(
|
||||
{
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
{
|
||||
onSuccess: cookieSetter(customHeaders),
|
||||
},
|
||||
);
|
||||
|
||||
await client.signIn.email(
|
||||
{
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
{
|
||||
onSuccess: cookieSetter(customHeaders),
|
||||
},
|
||||
);
|
||||
|
||||
const res = await client.multiSession.listDeviceSessions({
|
||||
fetchOptions: {
|
||||
headers: customHeaders,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.data).toHaveLength(2);
|
||||
expect(res.data!.at(0)!.session.token).not.toBe(
|
||||
res.data!.at(1)!.session.token,
|
||||
);
|
||||
expect(res.data!.at(0)!.user.id).toBe(res.data!.at(1)!.user.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user