mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
fix(organization): refresh active organization after email sign-in (#11375)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Refresh the active organization after sign-in when a session hook selects the initial organization.
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user