@cedarjs/tenancy 8.0.0-canary.3542 → 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,34 +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`, `useOrgRoles` 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. To guard routes by it, pass
143
- `useOrgRoles` to the router and give a `PrivateSet` `orgRoles` and
144
- `unauthorized`:
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`:
145
146
 
146
147
  <!-- prettier-ignore -->
147
148
  ```tsx
148
- <Router useAuth={useAuth} useOrgRoles={useOrgRoles}>
149
- <PrivateSet wrap={[OrgScope, OrgLayout]} unauthenticated="login">
149
+ import { tenancy } from 'src/tenancy'
150
+
151
+ <Router useAuth={useAuth} tenancy={tenancy}>
152
+ <PrivateSet wrap={OrgLayout} unauthenticated="login">
150
153
  <Route path="/org/{orgSlug}/projects" page={ProjectsPage} name="projects" />
151
154
  </PrivateSet>
152
- <PrivateSet wrap={[OrgScope, OrgLayout]} unauthenticated="login" unauthorized="projects" orgRoles="owner">
155
+ <PrivateSet wrap={OrgLayout} unauthenticated="login" unauthorized="projects" orgRoles="owner">
153
156
  <Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
154
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>
155
161
  </Router>
156
162
  ```
157
163
 
158
- The router checks the role in the organization of the route's `orgSlug` param,
159
- so a `PrivateSet` with `orgRoles` throws without `useOrgRoles` on the router or
160
- an `{orgSlug}` in the route's path. `useAuth().hasUserRole` and
161
- `<PrivateSet userRoles>` check `currentUser.roles`; there's no
162
- `useAuth().hasRole` or `<PrivateSet roles>` in an app with tenancy. See the
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
178
+ `currentUser.roles`; there's no `useAuth().hasRole` or `<PrivateSet roles>` in
179
+ an app with tenancy. See the
163
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
+ };
@@ -1,8 +1,8 @@
1
1
  import type { OrgMembership } from './types.js';
2
2
  /**
3
3
  * The membership in the organization whose slug is `orgSlug`, if the user
4
- * has one. `OrgScope` and `useOrgRoles` both find the organization with it,
5
- * so they agree on which one a route is in.
4
+ * has one. The default `useOrg` of `createTenancy` finds the organization
5
+ * with it.
6
6
  */
7
- export declare function findMembership(memberships: OrgMembership[], orgSlug: string | undefined): OrgMembership | undefined;
7
+ export declare function findMembership(memberships: readonly OrgMembership[], orgSlug: string | undefined): OrgMembership | undefined;
8
8
  //# sourceMappingURL=findMembership.d.ts.map
@@ -1 +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,aAAa,EAAE,EAC5B,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,aAAa,GAAG,SAAS,CAM3B"}
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"}
@@ -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,9 +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 { useOrgRoles } from './useOrgRoles.js';
8
+ export type { OrgBySlug } from './useOrgBySlug.js';
9
+ export { useOrgBySlug } from './useOrgBySlug.js';
9
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;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,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,14 +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 { useOrgRoles } from "./useOrgRoles.js";
6
+ import { useOrgBySlug } from "./useOrgBySlug.js";
7
7
  export {
8
- OrgScope,
9
8
  clearOrgClients,
9
+ createTenancy,
10
10
  getMemberships,
11
11
  hasOrgRole,
12
+ tenancy,
12
13
  useCurrentOrg,
13
- useOrgRoles
14
+ useOrgBySlug
14
15
  };
@@ -5,12 +5,18 @@ export interface GetOrgClientOptions {
5
5
  /**
6
6
  * The user's role in the organization. A client cached for another role is
7
7
  * replaced, so data cached under that role doesn't outlive it, whether or
8
- * not an `OrgScope` was mounted to see the role change.
8
+ * not a provider was mounted to see the role change.
9
9
  */
10
10
  role: string;
11
11
  /** Builds a new client. Only called on a cache miss. */
12
12
  createClient: () => ApolloClient;
13
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;
14
20
  /**
15
21
  * Returns the Apollo client for one user's membership in one organization,
16
22
  * creating and caching it on first use so returning to an organization
@@ -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;AAiBlD,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,YAAY,CAAC,EAC3B,MAAM,EACN,cAAc,EACd,IAAI,EACJ,YAAY,GACb,EAAE,mBAAmB,GAAG,YAAY,CAgBpC;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,12 +2,21 @@ 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,
8
16
  role,
9
17
  createClient
10
18
  }) {
19
+ dropOtherUsersClients(userId);
11
20
  const key = clientKey(userId, organizationId);
12
21
  const existing = clients.get(key);
13
22
  if (existing?.role === role) {
@@ -17,7 +26,7 @@ function getOrgClient({
17
26
  void existing.client.clearStore();
18
27
  }
19
28
  const client = createClient();
20
- clients.set(key, { client, role });
29
+ clients.set(key, { userId, client, role });
21
30
  return client;
22
31
  }
23
32
  async function dropOrgClient(userId, organizationId) {
@@ -37,5 +46,6 @@ async function clearOrgClients() {
37
46
  export {
38
47
  clearOrgClients,
39
48
  dropOrgClient,
49
+ dropOtherUsersClients,
40
50
  getOrgClient
41
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.3542",
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.3542",
48
- "@cedarjs/context": "8.0.0-canary.3542",
49
- "@cedarjs/graphql-server": "8.0.0-canary.3542"
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.3542",
55
- "@cedarjs/framework-tools": "8.0.0-canary.3542",
56
- "@cedarjs/router": "8.0.0-canary.3542",
57
- "@cedarjs/web": "8.0.0-canary.3542",
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.3542",
84
- "@cedarjs/router": "8.0.0-canary.3542",
85
- "@cedarjs/web": "8.0.0-canary.3542",
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. For routes, prefer
11
- * `<PrivateSet orgRoles>`.
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;AAsB5D,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,CA4KnC"}
@@ -1,149 +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 { findMembership } from "./findMembership.js";
14
- import { getMemberships } from "./getMemberships.js";
15
- import { hasOrgRole } from "./hasOrgRole.js";
16
- import { clearOrgClients, dropOrgClient, getOrgClient } from "./orgClients.js";
17
- import { OrgContext } from "./OrgContext.js";
18
- const CEDAR_ORG_HEADER = "cedar-org";
19
- const toList = (roles) => Array.isArray(roles) ? roles : [roles];
20
- const REPLACE = { replace: true };
21
- function isNamedRoute(value) {
22
- return typeof value === "function";
23
- }
24
- function unauthorizedPath(route, orgSlug) {
25
- try {
26
- return route();
27
- } catch (e) {
28
- if (e instanceof Error && e.message.includes("Missing parameter")) {
29
- return route({ orgSlug });
30
- }
31
- throw e;
32
- }
33
- }
34
- function membershipRoleMap(memberships) {
35
- return new Map(memberships.map((m) => [m.organizationId, m.role]));
36
- }
37
- function OrgScope({
38
- orgSlug: orgSlugProp,
39
- notAMember = null,
40
- roles,
41
- permission,
42
- unauthorized,
43
- useAuth = useNoAuth,
44
- onSetOrg,
45
- children
46
- }) {
47
- const params = useParams();
48
- const location = useLocation();
49
- const createClient = useCreateApolloClient();
50
- const { isAuthenticated, currentUser } = useAuth();
51
- const orgSlug = orgSlugProp ?? params["orgSlug"];
52
- const memberships = getMemberships(currentUser);
53
- const userId = currentUser && "id" in currentUser && typeof currentUser.id === "string" ? currentUser.id : void 0;
54
- const membershipSignature = memberships.map((m) => `${m.organizationId}:${m.role}`).sort().join(",");
55
- const lastAuthUserIdRef = React.useRef(userId);
56
- const lastMembershipsRef = React.useRef(
57
- membershipRoleMap(memberships)
58
- );
59
- const [, forceRenderAfterTeardown] = React.useReducer((c) => c + 1, 0);
60
- React.useEffect(() => {
61
- const previousUserId = lastAuthUserIdRef.current;
62
- const userChanged = previousUserId !== void 0 && previousUserId !== userId;
63
- if (!isAuthenticated || userChanged) {
64
- lastAuthUserIdRef.current = userId;
65
- lastMembershipsRef.current = /* @__PURE__ */ new Map();
66
- void clearOrgClients();
67
- forceRenderAfterTeardown();
68
- return;
69
- }
70
- lastAuthUserIdRef.current = userId;
71
- const nextMemberships = membershipRoleMap(memberships);
72
- let dropped = false;
73
- if (userId) {
74
- for (const organizationId of lastMembershipsRef.current.keys()) {
75
- if (!nextMemberships.has(organizationId)) {
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 = findMembership(memberships, orgSlug);
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
- role: membership.role,
135
- createClient: () => createClient({
136
- headers: { [CEDAR_ORG_HEADER]: membership.organizationId }
137
- })
138
- });
139
- const contextValue = {
140
- org,
141
- memberships,
142
- hasOrgRole: (roles2, organizationId) => hasOrgRole(memberships, roles2, organizationId ?? org.id),
143
- setOrg
144
- };
145
- return /* @__PURE__ */ jsx(ApolloProvider, { client: orgClient, children: /* @__PURE__ */ jsx(OrgContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(HasPermissionContext.Provider, { value: hasPermission, children }) }) });
146
- }
147
- export {
148
- OrgScope
149
- };
@@ -1,22 +0,0 @@
1
- import type { UseAuth } from '@cedarjs/auth';
2
- /**
3
- * Checks the user's role in the organization of the current route, for
4
- * `<PrivateSet orgRoles>`. Pass it to the router, next to `useAuth`:
5
- *
6
- * <Router useAuth={useAuth} useOrgRoles={useOrgRoles}>
7
- *
8
- * The router calls it with its `useAuth` and the current route's path
9
- * params. It returns `null` when the route has no `orgSlug` param, so there's
10
- * no organization to check a role in. Otherwise it returns a check of whether
11
- * the user's role in the organization with that slug is one of `roles` (a
12
- * role, or a list of roles): `false` when they aren't signed in or aren't a
13
- * member of it.
14
- *
15
- * An app can pass the router a hook of its own instead, such as one that
16
- * wraps this one with the organization from somewhere other than the path.
17
- * Besides returning a check or `null`, it can throw an error of its own,
18
- * which reaches the app's error boundary like any error thrown while
19
- * rendering. See `UseOrgRoles` in `@cedarjs/router`.
20
- */
21
- export declare function useOrgRoles(useAuth: UseAuth, params: Record<string, string>): ((roles: string | string[]) => boolean) | null;
22
- //# sourceMappingURL=useOrgRoles.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"useOrgRoles.d.ts","sourceRoot":"","sources":["../../src/web/useOrgRoles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAM5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,OAAO,CAAC,GAAG,IAAI,CAehD"}
@@ -1,16 +0,0 @@
1
- import { findMembership } from "./findMembership.js";
2
- import { getMemberships } from "./getMemberships.js";
3
- import { hasOrgRole } from "./hasOrgRole.js";
4
- function useOrgRoles(useAuth, params) {
5
- const { isAuthenticated, currentUser } = useAuth();
6
- const orgSlug = Object.hasOwn(params, "orgSlug") ? params["orgSlug"] : void 0;
7
- if (orgSlug === void 0) {
8
- return null;
9
- }
10
- const memberships = isAuthenticated ? getMemberships(currentUser) : [];
11
- const membership = findMembership(memberships, orgSlug);
12
- return (roles) => !!membership && hasOrgRole(memberships, roles, membership.organizationId);
13
- }
14
- export {
15
- useOrgRoles
16
- };