@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 +33 -16
- 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 +3 -3
- package/dist/web/findMembership.d.ts.map +1 -1
- 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 -3
- package/dist/web/index.d.ts.map +1 -1
- package/dist/web/index.js +5 -4
- package/dist/web/orgClients.d.ts +7 -1
- package/dist/web/orgClients.d.ts.map +1 -1
- package/dist/web/orgClients.js +11 -1
- 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 -149
- package/dist/web/useOrgRoles.d.ts +0 -22
- package/dist/web/useOrgRoles.d.ts.map +0 -1
- package/dist/web/useOrgRoles.js +0 -16
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
|
-
|
|
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
|
-
|
|
144
|
-
`
|
|
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
|
-
|
|
149
|
-
|
|
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={
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
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
|
+
};
|
|
@@ -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. `
|
|
5
|
-
*
|
|
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,
|
|
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,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 {
|
|
8
|
+
export type { OrgBySlug } from './useOrgBySlug.js';
|
|
9
|
+
export { useOrgBySlug } from './useOrgBySlug.js';
|
|
9
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,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 {
|
|
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
|
-
|
|
14
|
+
useOrgBySlug
|
|
14
15
|
};
|
package/dist/web/orgClients.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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,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
|
};
|
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. 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"}
|
package/dist/web/OrgScope.js
DELETED
|
@@ -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"}
|
package/dist/web/useOrgRoles.js
DELETED
|
@@ -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
|
-
};
|