@cedarjs/tenancy 8.0.0-canary.3541 → 8.0.0-canary.3545

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -130,17 +130,51 @@ Roles map to permissions in one place; fields ask for a permission with
130
130
  A role only gets what its list says, so a new role is one entry. Permission
131
131
  names are typed from the definitions. `withPermissions` in `getCurrentUser` gives each
132
132
  membership its permissions, which `useAuth().hasPermission()` checks inside
133
- `OrgScope`. Every permission is about an organization: fields about the user
133
+ an organization route. Every permission is about an organization: fields about the user
134
134
  rather than an organization use `@requireUserAuth`.
135
135
 
136
136
  ## Web
137
137
 
138
- `@cedarjs/tenancy/web` exports `OrgScope`, `useCurrentOrg` and
139
- `getMemberships`/`hasOrgRole` for the client side: a per-organization Apollo
140
- client provided under organization routes, keyed by the immutable organization
141
- id so switching organizations never mixes caches. `useCurrentOrg().hasOrgRole`
142
- checks the user's role in the organization, and `OrgScope`'s `roles` and
143
- `unauthorized` props guard routes by it. `useAuth().hasUserRole` checks
138
+ `@cedarjs/tenancy/web` exports `createTenancy`, a ready-made `tenancy`,
139
+ `useCurrentOrg`, `useOrgBySlug` and `getMemberships`/`hasOrgRole` for the client
140
+ side. Pass the tenancy to the router and put organization routes in a
141
+ `PrivateSet`: the router renders a per-organization Apollo client, keyed by the
142
+ immutable organization id so switching organizations never mixes caches, around
143
+ the set's layouts and page. `useCurrentOrg().hasOrgRole` checks the user's role
144
+ in the organization. To guard routes by it, give a `PrivateSet` `orgRoles` or
145
+ `orgPermission`, and `unauthorized`:
146
+
147
+ <!-- prettier-ignore -->
148
+ ```tsx
149
+ import { tenancy } from 'src/tenancy'
150
+
151
+ <Router useAuth={useAuth} tenancy={tenancy}>
152
+ <PrivateSet wrap={OrgLayout} unauthenticated="login">
153
+ <Route path="/org/{orgSlug}/projects" page={ProjectsPage} name="projects" />
154
+ </PrivateSet>
155
+ <PrivateSet wrap={OrgLayout} unauthenticated="login" unauthorized="projects" orgRoles="owner">
156
+ <Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
157
+ </PrivateSet>
158
+ <PrivateSet wrap={OrgLayout} unauthenticated="login" unauthorized="projects" orgPermission="members.manage">
159
+ <Route path="/org/{orgSlug}/members" page={MembersPage} name="members" />
160
+ </PrivateSet>
161
+ </Router>
162
+ ```
163
+
164
+ ```tsx title="web/src/tenancy.tsx"
165
+ import { createTenancy } from '@cedarjs/tenancy/web'
166
+
167
+ export const tenancy = createTenancy({
168
+ notAMember: <p>You don't have access to this organization.</p>,
169
+ })
170
+ ```
171
+
172
+ The router checks the role and permission in the organization of the route's
173
+ `orgSlug` param. A `PrivateSet` with `orgRoles` or `orgPermission` throws when
174
+ the router has no `tenancy` or its `useOrg` finds no organization for the
175
+ route; with the default lookup, that's a route without `{orgSlug}`. A route
176
+ with `{orgSlug}` outside a `PrivateSet` is a public page with no organization
177
+ context. `useAuth().hasUserRole` and `<PrivateSet userRoles>` check
144
178
  `currentUser.roles`; there's no `useAuth().hasRole` or `<PrivateSet roles>` in
145
- an app with tenancy. See the how-to for the full
146
- `<Set wrap={OrgScope}>` setup `yarn cedar setup tenancy` generates.
179
+ an app with tenancy. See the
180
+ how-to for the full setup `yarn cedar setup tenancy` generates.
@@ -135,7 +135,7 @@ function tenantScopeError(model) {
135
135
  );
136
136
  }
137
137
  return new TenantScopeError(
138
- `"${model}" is tenant-owned, and the signed-in user has no organization in scope for this request. Either the request carried no \`cedar-org\` header, which queries made outside \`OrgScope\` and functions not wrapped in \`withTenancy\` do not, or the user has no membership yet and needs one before they can read tenant-owned data.`
138
+ `"${model}" is tenant-owned, and the signed-in user has no organization in scope for this request. Either the request carried no \`cedar-org\` header, which queries made outside an organization route and functions not wrapped in \`withTenancy\` do not, or the user has no membership yet and needs one before they can read tenant-owned data.`
139
139
  );
140
140
  }
141
141
  function requireTenantId(ctx, model) {
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
2
  import type { OrgContextValue } from './types.js';
3
3
  /**
4
- * Carries the organization `OrgScope` resolved for the current route.
5
- * Read through `useCurrentOrg()`; only defined under `OrgScope`.
4
+ * Carries the organization the router resolved for the current route.
5
+ * Read through `useCurrentOrg()`; only defined under an organization route.
6
6
  */
7
7
  export declare const OrgContext: React.Context<OrgContextValue | undefined>;
8
8
  //# sourceMappingURL=OrgContext.d.ts.map
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import type { Tenancy, UseOrg } from '@cedarjs/router';
3
+ import type { OrgBySlug } from './useOrgBySlug.js';
4
+ /**
5
+ * A `useOrg` for `createTenancy`: the router's `UseOrg`, but returning full
6
+ * memberships (see `OrgBySlug`)
7
+ */
8
+ export type UseTenancyOrg = (useAuth: Parameters<UseOrg>[0], params: Record<string, string>) => OrgBySlug | null;
9
+ export interface CreateTenancyOptions {
10
+ /**
11
+ * Finds the organization of a route, given the router's `useAuth` and the
12
+ * route's path params: `null` when the route isn't in an organization.
13
+ * By default it is the one with the route's `orgSlug` param (see
14
+ * `useOrgBySlug`). Use it for an organization that doesn't come from the
15
+ * path, such as from the host:
16
+ *
17
+ * createTenancy({
18
+ * useOrg: (useAuth) => useOrgBySlug(useAuth, subdomain()),
19
+ * })
20
+ *
21
+ * It's a hook: the router calls it on every render of a private set. It
22
+ * returns the full memberships from `currentUser.memberships`, as
23
+ * `useOrgBySlug` does, since the provider needs the organization's id,
24
+ * slug and name.
25
+ */
26
+ useOrg?: UseTenancyOrg;
27
+ /** Rendered for a signed-in user who isn't a member of the organization */
28
+ notAMember?: React.ReactNode;
29
+ /**
30
+ * Called by `useCurrentOrg().setOrg` instead of navigating, for apps that
31
+ * select the organization from state rather than a URL segment.
32
+ */
33
+ onSetOrg?: (idOrSlug: string) => void;
34
+ }
35
+ /**
36
+ * Makes the `tenancy` to pass to the `Router`. The router then renders the
37
+ * organization's provider itself, around the layouts and page of every
38
+ * `PrivateSet` whose route is in an organization (by default, one with
39
+ * `{orgSlug}` in its path), and checks `orgRoles` and `orgPermission` on
40
+ * those sets.
41
+ *
42
+ * Call it once, at the top level of a module, like `web/src/tenancy.tsx`:
43
+ * a new tenancy on each render would remount the provider.
44
+ *
45
+ * export const tenancy = createTenancy({
46
+ * notAMember: <p>You don't have access to this organization.</p>,
47
+ * })
48
+ *
49
+ * <Router useAuth={useAuth} tenancy={tenancy}>
50
+ */
51
+ export declare function createTenancy(options?: CreateTenancyOptions): Tenancy;
52
+ /**
53
+ * The tenancy of an app that finds the organization with the route's
54
+ * `orgSlug` param and needs nothing else: `createTenancy()`. Pass it to the
55
+ * `Router`:
56
+ *
57
+ * <Router useAuth={useAuth} tenancy={tenancy}>
58
+ */
59
+ export declare const tenancy: Tenancy;
60
+ //# sourceMappingURL=createTenancy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createTenancy.d.ts","sourceRoot":"","sources":["../../src/web/createTenancy.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAoB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AASxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAOlD;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC3B,SAAS,GAAG,IAAI,CAAA;AAErB,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;;;;;OAeG;IACH,MAAM,CAAC,EAAE,aAAa,CAAA;IACtB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CACtC;AAiJD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAwBzE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,SAAkB,CAAA"}
@@ -0,0 +1,125 @@
1
+ import { Fragment, jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { ApolloProvider } from "@apollo/client/react";
4
+ import { HasPermissionContext } from "@cedarjs/auth";
5
+ import { navigate, useLocation, useParams } from "@cedarjs/router";
6
+ import { useCreateApolloClient } from "@cedarjs/web/apollo";
7
+ import { isOrgMembership } from "./getMemberships.js";
8
+ import { hasOrgRole } from "./hasOrgRole.js";
9
+ import { getOrgClient } from "./orgClients.js";
10
+ import { OrgContext } from "./OrgContext.js";
11
+ import { useOrgBySlug } from "./useOrgBySlug.js";
12
+ import { getUserId, useOrgClientTeardown } from "./useOrgClientTeardown.js";
13
+ const CEDAR_ORG_HEADER = "cedar-org";
14
+ function OrgProviderImplementation({
15
+ useAuth,
16
+ membership: routerMembership,
17
+ memberships: routerMemberships,
18
+ notAMember,
19
+ onSetOrg,
20
+ children
21
+ }) {
22
+ const params = useParams();
23
+ const location = useLocation();
24
+ const createClient = useCreateApolloClient();
25
+ const { currentUser } = useAuth();
26
+ const userId = getUserId(currentUser);
27
+ const membership = isOrgMembership(routerMembership) ? routerMembership : void 0;
28
+ const memberships = routerMemberships.filter(isOrgMembership);
29
+ const setOrg = React.useCallback(
30
+ (idOrSlug) => {
31
+ if (onSetOrg) {
32
+ onSetOrg(idOrSlug);
33
+ return;
34
+ }
35
+ const currentSlug = params["orgSlug"];
36
+ if (!currentSlug) {
37
+ throw new Error(
38
+ "useCurrentOrg().setOrg has no orgSlug route param to replace and no onSetOrg option was given to createTenancy"
39
+ );
40
+ }
41
+ const target = routerMemberships.filter(isOrgMembership).find(
42
+ (m) => m.organizationId === idOrSlug || m.organization.slug === idOrSlug
43
+ );
44
+ const nextSlug = target?.organization.slug ?? idOrSlug;
45
+ let replaced = false;
46
+ const nextPathname = location.pathname.split("/").map((segment) => {
47
+ if (!replaced && segment === currentSlug) {
48
+ replaced = true;
49
+ return nextSlug;
50
+ }
51
+ return segment;
52
+ }).join("/");
53
+ navigate(nextPathname + location.search + location.hash);
54
+ },
55
+ [
56
+ onSetOrg,
57
+ params,
58
+ routerMemberships,
59
+ location.pathname,
60
+ location.search,
61
+ location.hash
62
+ ]
63
+ );
64
+ const membershipPermissions = membership?.permissions;
65
+ const hasPermission = React.useCallback(
66
+ (permissionToCheck) => membershipPermissions?.includes(permissionToCheck) ?? false,
67
+ [membershipPermissions]
68
+ );
69
+ if (!membership || !userId) {
70
+ return /* @__PURE__ */ jsx(Fragment, { children: notAMember });
71
+ }
72
+ const org = {
73
+ id: membership.organizationId,
74
+ slug: membership.organization.slug,
75
+ name: membership.organization.name,
76
+ role: membership.role,
77
+ membershipId: membership.id,
78
+ permissions: membership.permissions ?? []
79
+ };
80
+ const orgClient = getOrgClient({
81
+ userId,
82
+ organizationId: membership.organizationId,
83
+ role: membership.role,
84
+ createClient: () => createClient({
85
+ headers: { [CEDAR_ORG_HEADER]: membership.organizationId }
86
+ })
87
+ });
88
+ const contextValue = {
89
+ org,
90
+ memberships,
91
+ hasOrgRole: (roles, organizationId) => hasOrgRole(memberships, roles, organizationId ?? org.id),
92
+ setOrg
93
+ };
94
+ return /* @__PURE__ */ jsx(ApolloProvider, { client: orgClient, children: /* @__PURE__ */ jsx(OrgContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(HasPermissionContext.Provider, { value: hasPermission, children }) }) });
95
+ }
96
+ const useOrgFromParams = (useAuth, params) => useOrgBySlug(
97
+ useAuth,
98
+ Object.hasOwn(params, "orgSlug") ? params["orgSlug"] : void 0
99
+ );
100
+ function createTenancy(options = {}) {
101
+ const {
102
+ useOrg: useFindOrg = useOrgFromParams,
103
+ notAMember,
104
+ onSetOrg
105
+ } = options;
106
+ const useOrg = (useAuth, params) => {
107
+ useOrgClientTeardown(useAuth);
108
+ return useFindOrg(useAuth, params);
109
+ };
110
+ const OrgProvider = (props) => /* @__PURE__ */ jsx(
111
+ OrgProviderImplementation,
112
+ {
113
+ ...props,
114
+ notAMember,
115
+ onSetOrg
116
+ }
117
+ );
118
+ OrgProvider.displayName = "OrgProvider";
119
+ return { useOrg, OrgProvider, notAMember };
120
+ }
121
+ const tenancy = createTenancy();
122
+ export {
123
+ createTenancy,
124
+ tenancy
125
+ };
@@ -0,0 +1,8 @@
1
+ import type { OrgMembership } from './types.js';
2
+ /**
3
+ * The membership in the organization whose slug is `orgSlug`, if the user
4
+ * has one. The default `useOrg` of `createTenancy` finds the organization
5
+ * with it.
6
+ */
7
+ export declare function findMembership(memberships: readonly OrgMembership[], orgSlug: string | undefined): OrgMembership | undefined;
8
+ //# sourceMappingURL=findMembership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findMembership.d.ts","sourceRoot":"","sources":["../../src/web/findMembership.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,SAAS,aAAa,EAAE,EACrC,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,aAAa,GAAG,SAAS,CAM3B"}
@@ -0,0 +1,9 @@
1
+ function findMembership(memberships, orgSlug) {
2
+ if (!orgSlug) {
3
+ return void 0;
4
+ }
5
+ return memberships.find((m) => m.organization.slug === orgSlug);
6
+ }
7
+ export {
8
+ findMembership
9
+ };
@@ -1,4 +1,10 @@
1
1
  import type { OrgMembership } from './types.js';
2
+ /**
3
+ * Type guard for one entry of `currentUser.memberships`, since `currentUser`
4
+ * comes from the app's own `useAuth()` and is typed as `unknown` shape at
5
+ * this boundary.
6
+ */
7
+ export declare function isOrgMembership(value: unknown): value is OrgMembership;
2
8
  /**
3
9
  * Reads `currentUser.memberships` defensively, since `currentUser` is
4
10
  * `unknown` shape from the app's `useAuth()`. Returns `[]` when it is
@@ -1 +1 @@
1
- {"version":3,"file":"getMemberships.d.ts","sourceRoot":"","sources":["../../src/web/getMemberships.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAmC/C;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,OAAO,GAAG,aAAa,EAAE,CAYpE"}
1
+ {"version":3,"file":"getMemberships.d.ts","sourceRoot":"","sources":["../../src/web/getMemberships.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,aAAa,CA0BtE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,OAAO,GAAG,aAAa,EAAE,CAYpE"}
@@ -22,5 +22,6 @@ function getMemberships(currentUser) {
22
22
  return memberships.filter(isOrgMembership);
23
23
  }
24
24
  export {
25
- getMemberships
25
+ getMemberships,
26
+ isOrgMembership
26
27
  };
@@ -8,5 +8,5 @@ import type { OrgMembership } from './types.js';
8
8
  * `roles` list returns `true` when a membership exists, matching
9
9
  * `@cedarjs/tenancy`'s server-side `hasOrgRole`.
10
10
  */
11
- export declare function hasOrgRole(memberships: OrgMembership[] | undefined, roles: string | string[], organizationId: string): boolean;
11
+ export declare function hasOrgRole(memberships: readonly OrgMembership[] | undefined, roles: string | string[], organizationId: string): boolean;
12
12
  //# sourceMappingURL=hasOrgRole.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hasOrgRole.d.ts","sourceRoot":"","sources":["../../src/web/hasOrgRole.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,aAAa,EAAE,GAAG,SAAS,EACxC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,cAAc,EAAE,MAAM,GACrB,OAAO,CAgBT"}
1
+ {"version":3,"file":"hasOrgRole.d.ts","sourceRoot":"","sources":["../../src/web/hasOrgRole.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,SAAS,aAAa,EAAE,GAAG,SAAS,EACjD,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EACxB,cAAc,EAAE,MAAM,GACrB,OAAO,CAgBT"}
@@ -1,8 +1,10 @@
1
+ export type { CreateTenancyOptions, UseTenancyOrg } from './createTenancy.js';
2
+ export { createTenancy, tenancy } from './createTenancy.js';
1
3
  export { getMemberships } from './getMemberships.js';
2
4
  export { hasOrgRole } from './hasOrgRole.js';
3
- export type { OrgScopeProps } from './OrgScope.js';
4
- export { OrgScope } from './OrgScope.js';
5
5
  export { clearOrgClients } from './orgClients.js';
6
6
  export type { CurrentOrgSummary, OrgContextValue, OrgMembership, } from './types.js';
7
7
  export { useCurrentOrg } from './useCurrentOrg.js';
8
+ export type { OrgBySlug } from './useOrgBySlug.js';
9
+ export { useOrgBySlug } from './useOrgBySlug.js';
8
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EACV,iBAAiB,EACjB,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA"}
package/dist/web/index.js CHANGED
@@ -1,12 +1,15 @@
1
+ import { createTenancy, tenancy } from "./createTenancy.js";
1
2
  import { getMemberships } from "./getMemberships.js";
2
3
  import { hasOrgRole } from "./hasOrgRole.js";
3
- import { OrgScope } from "./OrgScope.js";
4
4
  import { clearOrgClients } from "./orgClients.js";
5
5
  import { useCurrentOrg } from "./useCurrentOrg.js";
6
+ import { useOrgBySlug } from "./useOrgBySlug.js";
6
7
  export {
7
- OrgScope,
8
8
  clearOrgClients,
9
+ createTenancy,
9
10
  getMemberships,
10
11
  hasOrgRole,
11
- useCurrentOrg
12
+ tenancy,
13
+ useCurrentOrg,
14
+ useOrgBySlug
12
15
  };
@@ -2,19 +2,31 @@ import type { ApolloClient } from '@apollo/client';
2
2
  export interface GetOrgClientOptions {
3
3
  userId: string;
4
4
  organizationId: string;
5
+ /**
6
+ * The user's role in the organization. A client cached for another role is
7
+ * replaced, so data cached under that role doesn't outlive it, whether or
8
+ * not a provider was mounted to see the role change.
9
+ */
10
+ role: string;
5
11
  /** Builds a new client. Only called on a cache miss. */
6
12
  createClient: () => ApolloClient;
7
13
  }
14
+ /**
15
+ * Drops, after clearing their caches, the clients of every user but
16
+ * `userId`, so another user's data doesn't outlive their session, also when
17
+ * nothing saw them sign out.
18
+ */
19
+ export declare function dropOtherUsersClients(userId: string): void;
8
20
  /**
9
21
  * Returns the Apollo client for one user's membership in one organization,
10
22
  * creating and caching it on first use so returning to an organization
11
23
  * reuses its client and cache.
12
24
  */
13
- export declare function getOrgClient({ userId, organizationId, createClient, }: GetOrgClientOptions): ApolloClient;
25
+ export declare function getOrgClient({ userId, organizationId, role, createClient, }: GetOrgClientOptions): ApolloClient;
14
26
  /**
15
27
  * Drops one organization's client, clearing its cache first. Called when a
16
- * memberships refresh shows the membership gone or its role changed, so
17
- * data cached under the previous authorization does not outlive it.
28
+ * memberships refresh shows the membership gone, so data cached under it
29
+ * does not outlive it. (A role change is handled by `getOrgClient`.)
18
30
  */
19
31
  export declare function dropOrgClient(userId: string, organizationId: string): Promise<void>;
20
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"orgClients.d.ts","sourceRoot":"","sources":["../../src/web/orgClients.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAgBlD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,CAAA;IACtB,wDAAwD;IACxD,YAAY,EAAE,MAAM,YAAY,CAAA;CACjC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,cAAc,EACd,YAAY,GACb,EAAE,mBAAmB,GAAG,YAAY,CAYpC;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAKrD"}
1
+ {"version":3,"file":"orgClients.d.ts","sourceRoot":"","sources":["../../src/web/orgClients.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAoBlD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAA;IACZ,wDAAwD;IACxD,YAAY,EAAE,MAAM,YAAY,CAAA;CACjC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAO1D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,cAAc,EACd,IAAI,EACJ,YAAY,GACb,EAAE,mBAAmB,GAAG,YAAY,CAkBpC;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAKrD"}
@@ -2,36 +2,50 @@ const clients = /* @__PURE__ */ new Map();
2
2
  function clientKey(userId, organizationId) {
3
3
  return `${userId}:${organizationId}`;
4
4
  }
5
+ function dropOtherUsersClients(userId) {
6
+ for (const [key, entry] of clients) {
7
+ if (entry.userId !== userId) {
8
+ clients.delete(key);
9
+ void entry.client.clearStore();
10
+ }
11
+ }
12
+ }
5
13
  function getOrgClient({
6
14
  userId,
7
15
  organizationId,
16
+ role,
8
17
  createClient
9
18
  }) {
19
+ dropOtherUsersClients(userId);
10
20
  const key = clientKey(userId, organizationId);
11
21
  const existing = clients.get(key);
22
+ if (existing?.role === role) {
23
+ return existing.client;
24
+ }
12
25
  if (existing) {
13
- return existing;
26
+ void existing.client.clearStore();
14
27
  }
15
28
  const client = createClient();
16
- clients.set(key, client);
29
+ clients.set(key, { userId, client, role });
17
30
  return client;
18
31
  }
19
32
  async function dropOrgClient(userId, organizationId) {
20
33
  const key = clientKey(userId, organizationId);
21
- const client = clients.get(key);
22
- if (!client) {
34
+ const entry = clients.get(key);
35
+ if (!entry) {
23
36
  return;
24
37
  }
25
38
  clients.delete(key);
26
- await client.clearStore();
39
+ await entry.client.clearStore();
27
40
  }
28
41
  async function clearOrgClients() {
29
42
  const clientsToClear = Array.from(clients.values());
30
43
  clients.clear();
31
- await Promise.all(clientsToClear.map((client) => client.clearStore()));
44
+ await Promise.all(clientsToClear.map(({ client }) => client.clearStore()));
32
45
  }
33
46
  export {
34
47
  clearOrgClients,
35
48
  dropOrgClient,
49
+ dropOtherUsersClients,
36
50
  getOrgClient
37
51
  };
@@ -20,7 +20,7 @@ export interface OrgMembership {
20
20
  };
21
21
  }
22
22
  /**
23
- * The organization `OrgScope` has resolved for the current route, combining
23
+ * The organization the router has resolved for the current route, combining
24
24
  * the matching membership with the organization it belongs to.
25
25
  */
26
26
  export interface CurrentOrgSummary {
@@ -33,7 +33,7 @@ export interface CurrentOrgSummary {
33
33
  permissions: string[];
34
34
  }
35
35
  /**
36
- * What `OrgScope` provides below it, read through `useCurrentOrg()`.
36
+ * What the organization provider provides below it, read through `useCurrentOrg()`.
37
37
  */
38
38
  export interface OrgContextValue {
39
39
  org: CurrentOrgSummary | undefined;
@@ -1,10 +1,12 @@
1
1
  import type { OrgContextValue } from './types.js';
2
2
  /**
3
- * Reads the organization `OrgScope` resolved for the current route: the
3
+ * Reads the organization the router resolved for the current route: the
4
4
  * current organization, the full memberships snapshot, a role check scoped
5
5
  * to it, and `setOrg` to switch organizations.
6
6
  *
7
- * Must be called from a component rendered under `OrgScope`.
7
+ * Must be called from a component rendered by a `PrivateSet` whose route is
8
+ * in an organization (by default, one with `{orgSlug}` in its path), with
9
+ * `tenancy` on the `Router`.
8
10
  */
9
11
  export declare function useCurrentOrg(): OrgContextValue;
10
12
  //# sourceMappingURL=useCurrentOrg.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useCurrentOrg.d.ts","sourceRoot":"","sources":["../../src/web/useCurrentOrg.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,eAAe,CAQ/C"}
1
+ {"version":3,"file":"useCurrentOrg.d.ts","sourceRoot":"","sources":["../../src/web/useCurrentOrg.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,IAAI,eAAe,CAY/C"}
@@ -3,7 +3,9 @@ import { OrgContext } from "./OrgContext.js";
3
3
  function useCurrentOrg() {
4
4
  const context = React.useContext(OrgContext);
5
5
  if (!context) {
6
- throw new Error("useCurrentOrg must be used within an OrgScope");
6
+ throw new Error(
7
+ "useCurrentOrg must be used under an organization route: in a component rendered by a PrivateSet whose route has `{orgSlug}` in its path, with `tenancy` on the Router"
8
+ );
7
9
  }
8
10
  return context;
9
11
  }
@@ -0,0 +1,20 @@
1
+ import type { RouterOrg, UseOrg } from '@cedarjs/router';
2
+ import type { OrgMembership } from './types.js';
3
+ /** What `useOrgBySlug` finds: the router's organization, with full memberships */
4
+ export interface OrgBySlug extends RouterOrg {
5
+ membership: OrgMembership | null;
6
+ memberships: OrgMembership[];
7
+ }
8
+ /**
9
+ * Finds the organization with the slug `orgSlug` in the signed-in user's
10
+ * `currentUser.memberships`, for a `useOrg` of `createTenancy`. It is what
11
+ * `createTenancy` uses by default, with the route's `orgSlug` param.
12
+ *
13
+ * Returns `null` when `orgSlug` is `undefined`, so there's no organization.
14
+ * Otherwise the `membership` is `null` when the user isn't signed in or isn't
15
+ * a member of that organization.
16
+ *
17
+ * A hook: it reads the user with the router's `useAuth`.
18
+ */
19
+ export declare function useOrgBySlug(useAuth: Parameters<UseOrg>[0], orgSlug: string | undefined): OrgBySlug | null;
20
+ //# sourceMappingURL=useOrgBySlug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOrgBySlug.d.ts","sourceRoot":"","sources":["../../src/web/useOrgBySlug.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAIxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,kFAAkF;AAClF,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,UAAU,EAAE,aAAa,GAAG,IAAI,CAAA;IAChC,WAAW,EAAE,aAAa,EAAE,CAAA;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAC9B,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,SAAS,GAAG,IAAI,CAalB"}
@@ -0,0 +1,16 @@
1
+ import { findMembership } from "./findMembership.js";
2
+ import { getMemberships } from "./getMemberships.js";
3
+ function useOrgBySlug(useAuth, orgSlug) {
4
+ const { isAuthenticated, currentUser } = useAuth();
5
+ if (orgSlug === void 0) {
6
+ return null;
7
+ }
8
+ const memberships = isAuthenticated ? getMemberships(currentUser) : [];
9
+ return {
10
+ membership: findMembership(memberships, orgSlug) ?? null,
11
+ memberships
12
+ };
13
+ }
14
+ export {
15
+ useOrgBySlug
16
+ };
@@ -0,0 +1,15 @@
1
+ import type { UseOrg } from '@cedarjs/router';
2
+ /** `currentUser.id`, which only exists when the app's `getCurrentUser` returns it */
3
+ export declare function getUserId(currentUser: unknown): string | undefined;
4
+ /**
5
+ * Clears the per-organization Apollo clients when the user signs out or
6
+ * changes, and drops the client of an organization the user is no longer a
7
+ * member of. (A role change is handled by `getOrgClient`.)
8
+ *
9
+ * It runs from the tenancy's `useOrg`, which the router calls for every
10
+ * private route, so it still sees the change when the organization's
11
+ * provider is no longer rendered because the user has signed out or left the
12
+ * organization.
13
+ */
14
+ export declare function useOrgClientTeardown(useAuth: Parameters<UseOrg>[0]): void;
15
+ //# sourceMappingURL=useOrgClientTeardown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useOrgClientTeardown.d.ts","sourceRoot":"","sources":["../../src/web/useOrgClientTeardown.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAA;AAmB7C,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAWlE;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAwDlE"}
@@ -0,0 +1,56 @@
1
+ import React from "react";
2
+ import { getMemberships } from "./getMemberships.js";
3
+ import {
4
+ clearOrgClients,
5
+ dropOrgClient,
6
+ dropOtherUsersClients
7
+ } from "./orgClients.js";
8
+ function membershipRoleMap(memberships) {
9
+ return new Map(memberships.map((m) => [m.organizationId, m.role]));
10
+ }
11
+ function getUserId(currentUser) {
12
+ if (currentUser && typeof currentUser === "object" && "id" in currentUser && typeof currentUser.id === "string") {
13
+ return currentUser.id;
14
+ }
15
+ return void 0;
16
+ }
17
+ function useOrgClientTeardown(useAuth) {
18
+ const { isAuthenticated, currentUser } = useAuth();
19
+ const userId = getUserId(currentUser);
20
+ const memberships = getMemberships(currentUser);
21
+ const membershipSignature = memberships.map((m) => `${m.organizationId}:${m.role}`).sort().join(",");
22
+ const lastUserIdRef = React.useRef(userId);
23
+ const lastMembershipsRef = React.useRef(
24
+ membershipRoleMap(memberships)
25
+ );
26
+ React.useEffect(() => {
27
+ const previousUserId = lastUserIdRef.current;
28
+ const userChanged = previousUserId !== void 0 && previousUserId !== userId;
29
+ if (!isAuthenticated) {
30
+ lastUserIdRef.current = userId;
31
+ lastMembershipsRef.current = /* @__PURE__ */ new Map();
32
+ void clearOrgClients();
33
+ return;
34
+ }
35
+ if (userChanged && userId) {
36
+ lastUserIdRef.current = userId;
37
+ lastMembershipsRef.current = membershipRoleMap(memberships);
38
+ dropOtherUsersClients(userId);
39
+ return;
40
+ }
41
+ lastUserIdRef.current = userId;
42
+ const nextMemberships = membershipRoleMap(memberships);
43
+ if (userId) {
44
+ for (const organizationId of lastMembershipsRef.current.keys()) {
45
+ if (!nextMemberships.has(organizationId)) {
46
+ void dropOrgClient(userId, organizationId);
47
+ }
48
+ }
49
+ }
50
+ lastMembershipsRef.current = nextMemberships;
51
+ }, [isAuthenticated, userId, membershipSignature]);
52
+ }
53
+ export {
54
+ getUserId,
55
+ useOrgClientTeardown
56
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/tenancy",
3
- "version": "8.0.0-canary.3541",
3
+ "version": "8.0.0-canary.3545",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/cedarjs/cedar.git",
@@ -44,17 +44,17 @@
44
44
  "test:watch": "vitest watch"
45
45
  },
46
46
  "dependencies": {
47
- "@cedarjs/api": "8.0.0-canary.3541",
48
- "@cedarjs/context": "8.0.0-canary.3541",
49
- "@cedarjs/graphql-server": "8.0.0-canary.3541"
47
+ "@cedarjs/api": "8.0.0-canary.3545",
48
+ "@cedarjs/context": "8.0.0-canary.3545",
49
+ "@cedarjs/graphql-server": "8.0.0-canary.3545"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@apollo/client": "4.3.5",
53
53
  "@arethetypeswrong/cli": "0.18.5",
54
- "@cedarjs/auth": "8.0.0-canary.3541",
55
- "@cedarjs/framework-tools": "8.0.0-canary.3541",
56
- "@cedarjs/router": "8.0.0-canary.3541",
57
- "@cedarjs/web": "8.0.0-canary.3541",
54
+ "@cedarjs/auth": "8.0.0-canary.3545",
55
+ "@cedarjs/framework-tools": "8.0.0-canary.3545",
56
+ "@cedarjs/router": "8.0.0-canary.3545",
57
+ "@cedarjs/web": "8.0.0-canary.3545",
58
58
  "@prisma/adapter-better-sqlite3": "7.10.1",
59
59
  "@prisma/client": "7.10.1",
60
60
  "@testing-library/dom": "10.4.2",
@@ -80,9 +80,9 @@
80
80
  },
81
81
  "peerDependencies": {
82
82
  "@apollo/client": "4.3.5",
83
- "@cedarjs/auth": "8.0.0-canary.3541",
84
- "@cedarjs/router": "8.0.0-canary.3541",
85
- "@cedarjs/web": "8.0.0-canary.3541",
83
+ "@cedarjs/auth": "8.0.0-canary.3545",
84
+ "@cedarjs/router": "8.0.0-canary.3545",
85
+ "@cedarjs/web": "8.0.0-canary.3545",
86
86
  "@prisma/client": "7.10.1",
87
87
  "react": "19.3.1"
88
88
  },
@@ -1,50 +0,0 @@
1
- import React from 'react';
2
- import type { PermissionName, UseAuth } from '@cedarjs/auth';
3
- export interface OrgScopeProps {
4
- /** Overrides the `orgSlug` route param. */
5
- orgSlug?: string;
6
- /** Rendered when the user has no membership matching the slug. */
7
- notAMember?: React.ReactNode;
8
- /**
9
- * Only members with one of these roles in the organization get in.
10
- * Checked after the organization is resolved, which `<PrivateSet roles>`
11
- * around `OrgScope` can't do.
12
- */
13
- roles?: string | string[];
14
- /**
15
- * Only members whose membership has this permission get in (see
16
- * `createPermissions` in `@cedarjs/tenancy`). Checked like `roles`.
17
- */
18
- permission?: PermissionName;
19
- /**
20
- * The name of the route a member without one of `roles`, or without
21
- * `permission`, is redirected to. It gets the `orgSlug` param when its
22
- * path has one, so it can be a page of the same organization. Without
23
- * it, they get `notAMember`.
24
- */
25
- unauthorized?: string;
26
- /**
27
- * The app's `useAuth`, so `OrgScope` can read `currentUser.memberships`
28
- * without importing app code. Defaults to `useNoAuth`.
29
- */
30
- useAuth?: UseAuth;
31
- /**
32
- * Called by `useCurrentOrg().setOrg` instead of navigating, for apps that
33
- * select the organization from state rather than a URL segment.
34
- */
35
- onSetOrg?: (idOrSlug: string) => void;
36
- children: React.ReactNode;
37
- }
38
- /**
39
- * Provides the per-organization Apollo client and `OrgContext` for the
40
- * organization matching `orgSlug` (a prop, or else the `orgSlug` route
41
- * param). Renders `notAMember` when the current user has no membership in
42
- * that organization (or not one with `roles`, see `unauthorized`);
43
- * otherwise wraps `children` in that organization's own `ApolloProvider`,
44
- * so every Cell, `useQuery` and `useMutation` under it carries the
45
- * `cedar-org` header and reads from that organization's cache. Under it,
46
- * `useAuth().hasPermission` checks the user's membership in that
47
- * organization.
48
- */
49
- export declare function OrgScope({ orgSlug: orgSlugProp, notAMember, roles, permission, unauthorized, useAuth, onSetOrg, children, }: OrgScopeProps): React.JSX.Element;
50
- //# sourceMappingURL=OrgScope.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"OrgScope.d.ts","sourceRoot":"","sources":["../../src/web/OrgScope.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,OAAO,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAqB5D,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kEAAkE;IAClE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAwCD;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EAAE,WAAW,EACpB,UAAiB,EACjB,KAAK,EACL,UAAU,EACV,YAAY,EACZ,OAAmB,EACnB,QAAQ,EACR,QAAQ,GACT,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA8KnC"}
@@ -1,148 +0,0 @@
1
- import { Fragment, jsx } from "react/jsx-runtime";
2
- import React from "react";
3
- import { ApolloProvider } from "@apollo/client/react";
4
- import { HasPermissionContext, useNoAuth } from "@cedarjs/auth";
5
- import {
6
- navigate,
7
- Redirect,
8
- routes,
9
- useLocation,
10
- useParams
11
- } from "@cedarjs/router";
12
- import { useCreateApolloClient } from "@cedarjs/web/apollo";
13
- import { getMemberships } from "./getMemberships.js";
14
- import { hasOrgRole } from "./hasOrgRole.js";
15
- import { clearOrgClients, dropOrgClient, getOrgClient } from "./orgClients.js";
16
- import { OrgContext } from "./OrgContext.js";
17
- const CEDAR_ORG_HEADER = "cedar-org";
18
- const toList = (roles) => Array.isArray(roles) ? roles : [roles];
19
- const REPLACE = { replace: true };
20
- function isNamedRoute(value) {
21
- return typeof value === "function";
22
- }
23
- function unauthorizedPath(route, orgSlug) {
24
- try {
25
- return route();
26
- } catch (e) {
27
- if (e instanceof Error && e.message.includes("Missing parameter")) {
28
- return route({ orgSlug });
29
- }
30
- throw e;
31
- }
32
- }
33
- function membershipRoleMap(memberships) {
34
- return new Map(memberships.map((m) => [m.organizationId, m.role]));
35
- }
36
- function OrgScope({
37
- orgSlug: orgSlugProp,
38
- notAMember = null,
39
- roles,
40
- permission,
41
- unauthorized,
42
- useAuth = useNoAuth,
43
- onSetOrg,
44
- children
45
- }) {
46
- const params = useParams();
47
- const location = useLocation();
48
- const createClient = useCreateApolloClient();
49
- const { isAuthenticated, currentUser } = useAuth();
50
- const orgSlug = orgSlugProp ?? params["orgSlug"];
51
- const memberships = getMemberships(currentUser);
52
- const userId = currentUser && "id" in currentUser && typeof currentUser.id === "string" ? currentUser.id : void 0;
53
- const membershipSignature = memberships.map((m) => `${m.organizationId}:${m.role}`).sort().join(",");
54
- const lastAuthUserIdRef = React.useRef(userId);
55
- const lastMembershipsRef = React.useRef(
56
- membershipRoleMap(memberships)
57
- );
58
- const [, forceRenderAfterTeardown] = React.useReducer((c) => c + 1, 0);
59
- React.useEffect(() => {
60
- const previousUserId = lastAuthUserIdRef.current;
61
- const userChanged = previousUserId !== void 0 && previousUserId !== userId;
62
- if (!isAuthenticated || userChanged) {
63
- lastAuthUserIdRef.current = userId;
64
- lastMembershipsRef.current = /* @__PURE__ */ new Map();
65
- void clearOrgClients();
66
- forceRenderAfterTeardown();
67
- return;
68
- }
69
- lastAuthUserIdRef.current = userId;
70
- const nextMemberships = membershipRoleMap(memberships);
71
- let dropped = false;
72
- if (userId) {
73
- for (const [organizationId, role] of lastMembershipsRef.current) {
74
- const currentRole = nextMemberships.get(organizationId);
75
- if (currentRole === void 0 || currentRole !== role) {
76
- void dropOrgClient(userId, organizationId);
77
- dropped = true;
78
- }
79
- }
80
- }
81
- lastMembershipsRef.current = nextMemberships;
82
- if (dropped) {
83
- forceRenderAfterTeardown();
84
- }
85
- }, [isAuthenticated, userId, membershipSignature]);
86
- const setOrg = React.useCallback(
87
- (idOrSlug) => {
88
- if (onSetOrg) {
89
- onSetOrg(idOrSlug);
90
- return;
91
- }
92
- const currentSlug = params["orgSlug"];
93
- if (!currentSlug) {
94
- throw new Error(
95
- "useCurrentOrg().setOrg has no orgSlug route param to replace and no onSetOrg prop was given to OrgScope"
96
- );
97
- }
98
- const nextPathname = location.pathname.split("/").map((segment) => segment === currentSlug ? idOrSlug : segment).join("/");
99
- navigate(nextPathname + location.search + location.hash);
100
- },
101
- [onSetOrg, params, location.pathname, location.search, location.hash]
102
- );
103
- const membership = orgSlug ? memberships.find((m) => m.organization.slug === orgSlug) : void 0;
104
- const membershipPermissions = membership?.permissions;
105
- const hasPermission = React.useCallback(
106
- (permissionToCheck) => membershipPermissions?.includes(permissionToCheck) ?? false,
107
- [membershipPermissions]
108
- );
109
- if (!membership || !userId || !orgSlug) {
110
- return /* @__PURE__ */ jsx(Fragment, { children: notAMember });
111
- }
112
- const allowed = (roles === void 0 || toList(roles).includes(membership.role)) && (permission === void 0 || hasPermission(permission));
113
- if (!allowed) {
114
- if (!unauthorized) {
115
- return /* @__PURE__ */ jsx(Fragment, { children: notAMember });
116
- }
117
- const route = Object.hasOwn(routes, unauthorized) ? Reflect.get(routes, unauthorized) : void 0;
118
- if (!isNamedRoute(route)) {
119
- throw new Error(`We could not find a route named ${unauthorized}`);
120
- }
121
- return /* @__PURE__ */ jsx(Redirect, { to: unauthorizedPath(route, orgSlug), options: REPLACE });
122
- }
123
- const org = {
124
- id: membership.organizationId,
125
- slug: membership.organization.slug,
126
- name: membership.organization.name,
127
- role: membership.role,
128
- membershipId: membership.id,
129
- permissions: membership.permissions ?? []
130
- };
131
- const orgClient = getOrgClient({
132
- userId,
133
- organizationId: membership.organizationId,
134
- createClient: () => createClient({
135
- headers: { [CEDAR_ORG_HEADER]: membership.organizationId }
136
- })
137
- });
138
- const contextValue = {
139
- org,
140
- memberships,
141
- hasOrgRole: (roles2, organizationId) => hasOrgRole(memberships, roles2, organizationId ?? org.id),
142
- setOrg
143
- };
144
- return /* @__PURE__ */ jsx(ApolloProvider, { client: orgClient, children: /* @__PURE__ */ jsx(OrgContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(HasPermissionContext.Provider, { value: hasPermission, children }) }) });
145
- }
146
- export {
147
- OrgScope
148
- };