@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 +43 -9
- package/dist/prismaExtension.js +1 -1
- package/dist/web/OrgContext.d.ts +2 -2
- package/dist/web/createTenancy.d.ts +60 -0
- package/dist/web/createTenancy.d.ts.map +1 -0
- package/dist/web/createTenancy.js +125 -0
- package/dist/web/findMembership.d.ts +8 -0
- package/dist/web/findMembership.d.ts.map +1 -0
- package/dist/web/findMembership.js +9 -0
- package/dist/web/getMemberships.d.ts +6 -0
- package/dist/web/getMemberships.d.ts.map +1 -1
- package/dist/web/getMemberships.js +2 -1
- package/dist/web/hasOrgRole.d.ts +1 -1
- package/dist/web/hasOrgRole.d.ts.map +1 -1
- package/dist/web/index.d.ts +4 -2
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +6 -3
- package/dist/web/orgClients.d.ts +15 -3
- package/dist/web/orgClients.d.ts.map +1 -1
- package/dist/web/orgClients.js +20 -6
- package/dist/web/types.d.ts +2 -2
- package/dist/web/useCurrentOrg.d.ts +4 -2
- package/dist/web/useCurrentOrg.d.ts.map +1 -1
- package/dist/web/useCurrentOrg.js +3 -1
- package/dist/web/useOrgBySlug.d.ts +20 -0
- package/dist/web/useOrgBySlug.d.ts.map +1 -0
- package/dist/web/useOrgBySlug.js +16 -0
- package/dist/web/useOrgClientTeardown.d.ts +15 -0
- package/dist/web/useOrgClientTeardown.d.ts.map +1 -0
- package/dist/web/useOrgClientTeardown.js +56 -0
- package/package.json +11 -11
- package/dist/web/OrgScope.d.ts +0 -50
- package/dist/web/OrgScope.d.ts.map +0 -1
- package/dist/web/OrgScope.js +0 -148
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
|
-
|
|
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 `
|
|
139
|
-
`getMemberships`/`hasOrgRole` for the client
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
146
|
-
|
|
179
|
+
an app with tenancy. See the
|
|
180
|
+
how-to for the full setup `yarn cedar setup tenancy` generates.
|
package/dist/prismaExtension.js
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/web/OrgContext.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { OrgContextValue } from './types.js';
|
|
3
3
|
/**
|
|
4
|
-
* Carries the organization
|
|
5
|
-
* Read through `useCurrentOrg()`; only defined under
|
|
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"}
|
|
@@ -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;
|
|
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"}
|
package/dist/web/hasOrgRole.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/web/index.d.ts
CHANGED
|
@@ -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
|
package/dist/web/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/web/index.ts"],"names":[],"mappings":"AAAA,
|
|
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
|
-
|
|
12
|
+
tenancy,
|
|
13
|
+
useCurrentOrg,
|
|
14
|
+
useOrgBySlug
|
|
12
15
|
};
|
package/dist/web/orgClients.d.ts
CHANGED
|
@@ -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
|
|
17
|
-
*
|
|
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;
|
|
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"}
|
package/dist/web/orgClients.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
22
|
-
if (!
|
|
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
|
};
|
package/dist/web/types.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface OrgMembership {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* The organization
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
48
|
-
"@cedarjs/context": "8.0.0-canary.
|
|
49
|
-
"@cedarjs/graphql-server": "8.0.0-canary.
|
|
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.
|
|
55
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
56
|
-
"@cedarjs/router": "8.0.0-canary.
|
|
57
|
-
"@cedarjs/web": "8.0.0-canary.
|
|
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.
|
|
84
|
-
"@cedarjs/router": "8.0.0-canary.
|
|
85
|
-
"@cedarjs/web": "8.0.0-canary.
|
|
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
|
},
|
package/dist/web/OrgScope.d.ts
DELETED
|
@@ -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"}
|
package/dist/web/OrgScope.js
DELETED
|
@@ -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
|
-
};
|