fix(organization): refresh active organization after email sign-in (#11375)

This commit is contained in:
Taesu
2026-09-30 19:29:52 +00:00
committed by GitHub
parent 55cb92e940
commit 69defbcaba
5 changed files with 132 additions and 20 deletions
@@ -0,0 +1,5 @@
---
"better-auth": patch
---
Refresh the active organization after sign-in when a session hook selects the initial organization.
+15 -17
View File
@@ -39,6 +39,20 @@ const resolvePublicAuthUrl = (basePath?: string) => {
return undefined;
};
export const matchesSessionSignal = (path: string) =>
path === "/sign-out" ||
path === "/update-user" ||
path === "/update-session" ||
path === "/sign-up/email" ||
path === "/sign-in/email" ||
path === "/delete-user" ||
path === "/verify-email" ||
path === "/revoke-sessions" ||
path === "/revoke-session" ||
path === "/revoke-other-sessions" ||
path === "/change-email" ||
path === "/change-password";
export const getClientConfig = (
options?: BetterAuthClientOptions | undefined,
loadEnv?: boolean | undefined,
@@ -118,23 +132,7 @@ export const getClientConfig = (
const atomListeners: ClientAtomListener[] = [
{
signal: "$sessionSignal",
matcher(path) {
const matchesCommonPaths =
path === "/sign-out" ||
path === "/update-user" ||
path === "/update-session" ||
path === "/sign-up/email" ||
path === "/sign-in/email" ||
path === "/delete-user" ||
path === "/verify-email" ||
path === "/revoke-sessions" ||
path === "/revoke-session" ||
path === "/revoke-other-sessions" ||
path === "/change-email" ||
path === "/change-password";
return matchesCommonPaths;
},
matcher: matchesSessionSignal,
callback(path) {
if (path === "/sign-out") {
broadcastSessionUpdate("signout");
+42 -1
View File
@@ -1,6 +1,7 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, onTestFinished, vi } from "vitest";
import { organizationClient } from "../plugins/organization/client";
import { getClientConfig } from "./config";
import { createDynamicPathProxy } from "./proxy";
@@ -136,4 +137,44 @@ describe("createDynamicPathProxy", () => {
vi.useRealTimers();
});
/**
* @see https://github.com/better-auth/better-auth/issues/9710
*/
it("refreshes the organization signal once for auth and organization paths", async () => {
const { $fetch, pluginPathMethods, pluginsAtoms, atomListeners } =
getClientConfig({
baseURL: "http://localhost:3000",
fetchOptions: {
customFetchImpl: async () => Response.json({}),
},
plugins: [organizationClient()],
});
const activeOrgSignal = pluginsAtoms.$activeOrgSignal;
if (!activeOrgSignal) throw new Error("Active org signal missing");
const updates = vi.fn();
onTestFinished(activeOrgSignal.listen(updates));
const proxy = createDynamicPathProxy(
{} as {
signIn: { email: () => Promise<unknown> };
signUp: { email: () => Promise<unknown> };
organization: { setActive: () => Promise<unknown> };
},
$fetch,
pluginPathMethods,
pluginsAtoms,
atomListeners,
);
await proxy.signIn.email();
await vi.waitFor(() => expect(updates).toHaveBeenCalledTimes(1));
await proxy.signUp.email();
await vi.waitFor(() => expect(updates).toHaveBeenCalledTimes(2));
await proxy.organization.setActive();
await vi.waitFor(() => expect(updates).toHaveBeenCalledTimes(3));
});
});
@@ -1,6 +1,7 @@
import { describe, it } from "vitest";
import { describe, expect, it, onTestFinished, vi } from "vitest";
import { betterAuth } from "../../auth/full";
import { createAuthClient } from "../../client";
import { getTestInstance } from "../../test-utils/test-instance";
import { inferOrgAdditionalFields, organizationClient } from "./client";
import { organization } from "./organization";
@@ -70,3 +71,69 @@ describe("organization", () => {
});
});
});
/**
* @see https://github.com/better-auth/better-auth/issues/9710
*/
it("refreshes the active organization after sign-in sets it in a session hook", async () => {
let initialOrganizationId: string | undefined;
const { auth, client, runWithUser, testUser, sessionSetter } =
await getTestInstance(
{
plugins: [organization()],
databaseHooks: {
session: {
create: {
before: async (session) => ({
data: {
...session,
activeOrganizationId: initialOrganizationId,
},
}),
},
},
},
},
{ clientOptions: { plugins: [organizationClient()] } },
);
await runWithUser(testUser.email, testUser.password, async (headers) => {
const organization = await auth.api.createOrganization({
body: { name: "Test Workspace", slug: "test-workspace" },
headers,
});
if (!organization) throw new Error("Organization was not created");
initialOrganizationId = organization.id;
headers.delete("cookie");
const previousWindow = global.window;
global.window = {} as Window & typeof globalThis;
onTestFinished(() => {
global.window = previousWindow;
});
const activeOrganization = client.$store.atoms.activeOrganization;
if (!activeOrganization)
throw new Error("Active organization atom missing");
const unsubscribe = activeOrganization.subscribe(() => {});
onTestFinished(unsubscribe);
await vi.waitFor(() => {
expect(activeOrganization.get().isPending).toBe(false);
});
expect(activeOrganization.get().data).toBeNull();
const result = await client.signIn.email({
email: testUser.email,
password: testUser.password,
fetchOptions: { onSuccess: sessionSetter(headers) },
});
expect(result.error).toBeNull();
const fullOrganization = await auth.api.getFullOrganization({ headers });
expect(fullOrganization?.id).toBe(initialOrganizationId);
await vi.waitFor(() => {
expect(activeOrganization.get().data?.id).toBe(initialOrganizationId);
});
});
});
@@ -3,6 +3,7 @@ import type { DBFieldAttribute } from "@better-auth/core/db";
import type { BetterFetch } from "@better-fetch/fetch";
import { atom } from "nanostores";
import { useAuthQuery } from "../../client";
import { matchesSessionSignal } from "../../client/config";
import type {
InferInvitation,
InferMember,
@@ -249,7 +250,7 @@ export const organizationClient = <CO extends OrganizationClientOptions>(
},
{
matcher(path) {
return path === "/sign-out" || path.startsWith("/organization");
return matchesSessionSignal(path) || path.startsWith("/organization");
},
signal: "$activeOrgSignal",
},