@cedarjs/cli 8.0.0-canary.3565 → 8.0.0-canary.3567
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/dist/commands/setup/tenancy/authCodemod.js +1 -2
- package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +2 -3
- package/dist/commands/setup/tenancy/templates/organizations.test.ts.template +13 -0
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +15 -38
- package/dist/commands/setup/tenancy/templates/permissions.ts.template +4 -2
- package/dist/commands/setup/tenancy/templates/permissionsPackage.index.ts.template +12 -6
- package/dist/commands/setup/tenancy/templates/requirePermission.ts.template +1 -3
- package/dist/commands/setup/tenancy/templates/tenancy.tsx.template +2 -6
- package/dist/commands/setup/tenancy/tenancyHandler.js +32 -28
- package/dist/lib/authDirective.js +4 -2
- package/package.json +13 -13
- package/dist/commands/setup/tenancy/templates/requireOrgAuth.test.ts.template +0 -56
- package/dist/commands/setup/tenancy/templates/requireOrgAuth.ts.template +0 -25
|
@@ -18,8 +18,7 @@ function buildMembershipsSelectProperty() {
|
|
|
18
18
|
}
|
|
19
19
|
return property;
|
|
20
20
|
}
|
|
21
|
-
const TENANCY_AUTH_EXPORT = `export const { hasUserRole,
|
|
22
|
-
createTenancyAuth()
|
|
21
|
+
const TENANCY_AUTH_EXPORT = `export const { hasUserRole, requireUserAuth } = createTenancyAuth()
|
|
23
22
|
`;
|
|
24
23
|
const DEFAULT_REQUIRE_AUTH_STATEMENTS = [
|
|
25
24
|
/^if \(!isAuthenticated\(\)\) \{ throw new AuthenticationError\([^)]*\);? \}$/,
|
|
@@ -59,9 +59,8 @@ export const schema = gql`
|
|
|
59
59
|
|
|
60
60
|
"""
|
|
61
61
|
Creates a pending membership (an invitation) in the current organization,
|
|
62
|
-
for a role the caller
|
|
63
|
-
|
|
64
|
-
owner. The app is responsible for emailing input.email a link built from
|
|
62
|
+
for a role whose permissions the caller all holds -- an admin can invite
|
|
63
|
+
an admin or a member, but not an owner. The app is responsible for emailing input.email a link built from
|
|
65
64
|
the returned invitationToken; Membership has no email column of its own.
|
|
66
65
|
The invitation can be claimed until the returned invitationExpiresAt.
|
|
67
66
|
Web callers must call reauthenticate() from useAuth() in onCompleted so
|
|
@@ -120,6 +120,19 @@ describe('organizations service', () => {
|
|
|
120
120
|
},
|
|
121
121
|
)
|
|
122
122
|
|
|
123
|
+
scenario(
|
|
124
|
+
'an admin can invite an admin',
|
|
125
|
+
async (scenario: StandardScenario) => {
|
|
126
|
+
asMember(scenario, scenario.membership.admin)
|
|
127
|
+
|
|
128
|
+
const result = await inviteMember({
|
|
129
|
+
input: { email: 'new-admin@example.com', role: 'admin' },
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
expect(result.invitationToken).toBeTruthy()
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
123
136
|
scenario(
|
|
124
137
|
'an admin cannot invite an owner',
|
|
125
138
|
async (scenario: StandardScenario) => {
|
|
@@ -6,10 +6,13 @@ import type {
|
|
|
6
6
|
MembershipRelationResolvers,
|
|
7
7
|
} from 'types/graphql'
|
|
8
8
|
|
|
9
|
+
import { permissions } from '__PERMISSIONS_PACKAGE__'
|
|
10
|
+
|
|
9
11
|
import { ForbiddenError, ValidationError } from '@cedarjs/graphql-server'
|
|
10
12
|
import { requireCurrentOrg } from '@cedarjs/tenancy'
|
|
11
13
|
|
|
12
14
|
import { Prisma, db } from 'src/lib/db'
|
|
15
|
+
import { canGrantRole } from 'src/lib/permissions'
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* How long an invitation stays claimable after `inviteMember` creates it.
|
|
@@ -37,32 +40,6 @@ const membershipsForUser = (userId: string): Promise<MembershipSummary[]> =>
|
|
|
37
40
|
select: { id: true, organizationId: true, role: true },
|
|
38
41
|
})
|
|
39
42
|
|
|
40
|
-
/**
|
|
41
|
-
* Roles this generated service knows how to check, and which of them a
|
|
42
|
-
* membership with that role may grant to someone else (`inviteMember`) or
|
|
43
|
-
* revoke (`removeMember`). `owner` may grant or revoke any known role;
|
|
44
|
-
* `admin` may grant or revoke `admin` and `member`, but never `owner`, so
|
|
45
|
-
* an admin can never create or remove an owner. `member` grants nothing;
|
|
46
|
-
* it's listed anyway so `KNOWN_ROLES` below covers every role a membership
|
|
47
|
-
* can actually hold, which is what makes the "unknown role" check on
|
|
48
|
-
* `inviteMember` meaningful.
|
|
49
|
-
*
|
|
50
|
-
* Add a role here when your app adds one. A role missing from this map can
|
|
51
|
-
* never be granted to, or removed from, anyone by `inviteMember` /
|
|
52
|
-
* `removeMember` -- including by an owner.
|
|
53
|
-
*/
|
|
54
|
-
export const ROLE_GRANTS: Readonly<Record<string, readonly string[]>> = {
|
|
55
|
-
owner: ['owner', 'admin', 'member'],
|
|
56
|
-
admin: ['admin', 'member'],
|
|
57
|
-
member: [],
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const KNOWN_ROLES = Object.keys(ROLE_GRANTS)
|
|
61
|
-
|
|
62
|
-
/** Whether a membership with `granterRole` may grant or revoke `targetRole`. */
|
|
63
|
-
const canGrantRole = (granterRole: string, targetRole: string): boolean =>
|
|
64
|
-
ROLE_GRANTS[granterRole]?.includes(targetRole) ?? false
|
|
65
|
-
|
|
66
43
|
/**
|
|
67
44
|
* Every membership belonging to the signed-in user, across all
|
|
68
45
|
* organizations. `Membership` is a global model, so this reads plain `db`.
|
|
@@ -139,12 +116,12 @@ export const createOrganization: MutationResolvers['createOrganization'] =
|
|
|
139
116
|
|
|
140
117
|
/**
|
|
141
118
|
* Creates a pending membership (an invitation) in the current organization,
|
|
142
|
-
* for a role the caller
|
|
143
|
-
* `admin` can invite an `admin` or a `member`, but not an
|
|
144
|
-
* though `@requirePermission(permission: "members.manage")`
|
|
145
|
-
* this mutation at all. A role that isn't in
|
|
146
|
-
* outright, so a typo in `input.role`
|
|
147
|
-
* can
|
|
119
|
+
* for a role the caller may grant (`canGrantRole`): one whose permissions
|
|
120
|
+
* they all hold. An `admin` can invite an `admin` or a `member`, but not an
|
|
121
|
+
* `owner`, even though `@requirePermission(permission: "members.manage")`
|
|
122
|
+
* lets an admin call this mutation at all. A role that isn't in the
|
|
123
|
+
* permission definitions is rejected outright, so a typo in `input.role`
|
|
124
|
+
* can never create a membership with no permissions.
|
|
148
125
|
*
|
|
149
126
|
* `Membership` has no email column: the caller is responsible for emailing
|
|
150
127
|
* `input.email` a link built from the returned `invitationToken`, with its
|
|
@@ -158,14 +135,14 @@ export const inviteMember: MutationResolvers['inviteMember'] = async ({
|
|
|
158
135
|
}) => {
|
|
159
136
|
const currentOrg = requireCurrentOrg()
|
|
160
137
|
|
|
161
|
-
if (!
|
|
138
|
+
if (!Object.hasOwn(permissions.roles, input.role)) {
|
|
162
139
|
throw new ValidationError(
|
|
163
|
-
`"${input.role}" is not a known role. Add it to
|
|
164
|
-
'
|
|
140
|
+
`"${input.role}" is not a known role. Add it to your permission ` +
|
|
141
|
+
'definitions before inviting with it.',
|
|
165
142
|
)
|
|
166
143
|
}
|
|
167
144
|
|
|
168
|
-
if (!canGrantRole(
|
|
145
|
+
if (!canGrantRole(input.role)) {
|
|
169
146
|
throw new ForbiddenError(
|
|
170
147
|
`A "${currentOrg.role}" cannot invite a "${input.role}".`,
|
|
171
148
|
)
|
|
@@ -288,7 +265,7 @@ export const acceptInvitation: MutationResolvers['acceptInvitation'] = async ({
|
|
|
288
265
|
/**
|
|
289
266
|
* Removes a membership from the current organization. A caller may only
|
|
290
267
|
* remove a membership whose role they could grant via `inviteMember` (the
|
|
291
|
-
* same `
|
|
268
|
+
* same `canGrantRole` rule), so an admin can never remove an owner, and the
|
|
292
269
|
* organization's last owner can never be removed -- including by
|
|
293
270
|
* themselves -- so an organization always keeps at least one owner who has
|
|
294
271
|
* accepted their invitation.
|
|
@@ -306,7 +283,7 @@ export const removeMember: MutationResolvers['removeMember'] = async ({
|
|
|
306
283
|
throw new ForbiddenError("You don't have access to that membership.")
|
|
307
284
|
}
|
|
308
285
|
|
|
309
|
-
if (!canGrantRole(
|
|
286
|
+
if (!canGrantRole(membership.role)) {
|
|
310
287
|
throw new ForbiddenError(
|
|
311
288
|
`A "${currentOrg.role}" cannot remove a "${membership.role}" member.`,
|
|
312
289
|
)
|
|
@@ -9,12 +9,14 @@ import { createPermissions } from '@cedarjs/tenancy'
|
|
|
9
9
|
* `useAuth().hasPermission()`. The web only decides what to show; these
|
|
10
10
|
* checks are the guard.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* `canGrantRole` is who may give someone a role, or take it away: whoever
|
|
13
|
+
* holds every permission it has (`inviteMember` and `removeMember` in
|
|
14
|
+
* api/src/services/organizations).
|
|
14
15
|
*/
|
|
15
16
|
export const {
|
|
16
17
|
hasPermission,
|
|
17
18
|
requirePermission,
|
|
19
|
+
canGrantRole,
|
|
18
20
|
permissionsOf,
|
|
19
21
|
withPermissions,
|
|
20
22
|
} = createPermissions(permissions)
|
|
@@ -2,12 +2,18 @@ import { definePermissions } from '@cedarjs/tenancy/permissions'
|
|
|
2
2
|
import type { PermissionName } from '@cedarjs/tenancy/permissions'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Every permission the app checks, and what each role may do
|
|
6
|
-
* for a permission
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Every permission the app checks, and what each role may do: a role is a
|
|
6
|
+
* named bundle of permissions. Fields ask for a permission
|
|
7
|
+
* (`@requirePermission(permission: "members.manage")`, or
|
|
8
|
+
* `requirePermission('members.manage')` in a service), never a role, so a
|
|
9
|
+
* new role is one entry here: it can do exactly what its list says, and a
|
|
9
10
|
* field added later is closed to it unless its permission is on the list.
|
|
10
11
|
*
|
|
12
|
+
* `organization.read` is "any member": every role lists it, and fields any
|
|
13
|
+
* member may use ask for it. `organization.manage` is the owner's alone,
|
|
14
|
+
* which also keeps admins from making someone an owner: a role can only be
|
|
15
|
+
* given by someone who holds every permission it has.
|
|
16
|
+
*
|
|
11
17
|
* `roles` are per organization: a membership's role decides what the user
|
|
12
18
|
* may do in that organization. Fields about the user rather than an
|
|
13
19
|
* organization (their own account, say) use `@requireUserAuth` instead.
|
|
@@ -17,9 +23,9 @@ import type { PermissionName } from '@cedarjs/tenancy/permissions'
|
|
|
17
23
|
* membership's permissions from the api, through `getCurrentUser`.
|
|
18
24
|
*/
|
|
19
25
|
export const permissions = definePermissions({
|
|
20
|
-
permissions: ['organization.read', 'members.manage'],
|
|
26
|
+
permissions: ['organization.read', 'organization.manage', 'members.manage'],
|
|
21
27
|
roles: {
|
|
22
|
-
owner: ['organization.read', 'members.manage'],
|
|
28
|
+
owner: ['organization.read', 'organization.manage', 'members.manage'],
|
|
23
29
|
admin: ['organization.read', 'members.manage'],
|
|
24
30
|
member: ['organization.read'],
|
|
25
31
|
},
|
|
@@ -22,9 +22,7 @@ type RequirePermissionValidate = ValidatorDirectiveFunc<{
|
|
|
22
22
|
const validate: RequirePermissionValidate = ({ directiveArgs }) => {
|
|
23
23
|
// A name the permissions map doesn't know throws, so a typo in the SDL is
|
|
24
24
|
// an error rather than a field nobody can use
|
|
25
|
-
applicationRequirePermission(
|
|
26
|
-
permission: directiveArgs.permission as Permission,
|
|
27
|
-
})
|
|
25
|
+
applicationRequirePermission(directiveArgs.permission as Permission)
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
const requirePermission = createValidatorDirective(schema, validate)
|
|
@@ -16,12 +16,8 @@ import { createTenancy } from '@cedarjs/tenancy/web'
|
|
|
16
16
|
*
|
|
17
17
|
* A user who isn't signed in is sent to log in, and one who isn't a member
|
|
18
18
|
* of the organization sees `notAMember`. Pages only some members may open
|
|
19
|
-
* take `
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* <PrivateSet unauthenticated="login" unauthorized="projects" orgRoles="owner">
|
|
23
|
-
* <Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
|
|
24
|
-
* </PrivateSet>
|
|
19
|
+
* take `orgPermission` (see packages/permissions), and the one a member
|
|
20
|
+
* without it is sent to is `unauthorized`:
|
|
25
21
|
*
|
|
26
22
|
* <PrivateSet unauthenticated="login" unauthorized="projects" orgPermission="members.manage">
|
|
27
23
|
* <Route path="/org/{orgSlug}/members" page={MembersPage} name="members" />
|
|
@@ -271,7 +271,7 @@ ${SIGNUP_SNIPPET}`;
|
|
|
271
271
|
skip: () => isDbAuthProject ? false : "Not a dbAuth project; skipping."
|
|
272
272
|
},
|
|
273
273
|
{
|
|
274
|
-
title: "Switching @requireAuth to @
|
|
274
|
+
title: "Switching @requireAuth to @requireUserAuth, and hasRole to hasUserRole...",
|
|
275
275
|
task: async (ctx) => {
|
|
276
276
|
const oldDirectiveDir = path.join(paths.api.directives, "requireAuth");
|
|
277
277
|
const isInOldDirective = (file) => file.startsWith(oldDirectiveDir + path.sep);
|
|
@@ -308,17 +308,18 @@ ${SIGNUP_SNIPPET}`;
|
|
|
308
308
|
} else if (fs.existsSync(oldDirectivePath)) {
|
|
309
309
|
fs.rmSync(oldDirectiveDir, { recursive: true, force: true });
|
|
310
310
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
311
|
+
const directiveDir = path.join(
|
|
312
|
+
paths.api.directives,
|
|
313
|
+
"requireUserAuth"
|
|
314
|
+
);
|
|
315
|
+
await writeTemplateFile(
|
|
316
|
+
"requireUserAuth.ts.template",
|
|
317
|
+
path.join(directiveDir, `requireUserAuth.${ext}`)
|
|
318
|
+
);
|
|
319
|
+
await writeTemplateFile(
|
|
320
|
+
"requireUserAuth.test.ts.template",
|
|
321
|
+
path.join(directiveDir, `requireUserAuth.test.${ext}`)
|
|
322
|
+
);
|
|
322
323
|
}
|
|
323
324
|
},
|
|
324
325
|
{
|
|
@@ -375,7 +376,8 @@ ${SIGNUP_SNIPPET}`;
|
|
|
375
376
|
const withUserFields = (firstNumber) => (content) => addRequiredUserFields(content, userFields, { firstNumber });
|
|
376
377
|
await writeTemplateFile(
|
|
377
378
|
"organizations.ts.template",
|
|
378
|
-
path.join(serviceDir, `organizations.${ext}`)
|
|
379
|
+
path.join(serviceDir, `organizations.${ext}`),
|
|
380
|
+
{ __PERMISSIONS_PACKAGE__: permissionsPackageName() }
|
|
379
381
|
);
|
|
380
382
|
await writeTemplateFile(
|
|
381
383
|
"organizations.test.ts.template",
|
|
@@ -536,7 +538,8 @@ ${SIGNUP_SNIPPET}`;
|
|
|
536
538
|
Contact) whose rows belong to one organization, add
|
|
537
539
|
${c.highlight("organizationId String")} plus ${c.highlight("@@index([organizationId])")} to it in
|
|
538
540
|
schema.prisma, and switch its fields from ${c.highlight("@requireUserAuth")}
|
|
539
|
-
to ${c.highlight(
|
|
541
|
+
to ${c.highlight('@requirePermission(permission: "organization.read")')}, or
|
|
542
|
+
a permission of its own. For each one that stays global, add it
|
|
540
543
|
to the ${c.highlight("allExcept")} list in api/src/lib/db instead.
|
|
541
544
|
3. Put your organization routes in a PrivateSet in web/src/Routes.tsx.
|
|
542
545
|
The Router renders the organization for it, using the generated
|
|
@@ -567,29 +570,30 @@ ${ctx.routerNeedsTenancy ? `
|
|
|
567
570
|
)}.
|
|
568
571
|
5. Your permissions, and what each role may do, are in
|
|
569
572
|
${c.highlight("packages/permissions")}, shared by the api and web sides${ctx.packagesWorkspaceTurnedOn ? ` (this
|
|
570
|
-
turned on ${c.highlight("experimental.packagesWorkspace")} in cedar.toml)` : ""}.
|
|
571
|
-
organization fields with
|
|
572
|
-
|
|
573
|
-
web with ${c.highlight("useAuth().hasPermission()")}.
|
|
573
|
+
turned on ${c.highlight("experimental.packagesWorkspace")} in cedar.toml)` : ""}. A role is a bundle of
|
|
574
|
+
permissions: guard organization fields with
|
|
575
|
+
${c.highlight('@requirePermission(permission: "...")')}, never a role, and check
|
|
576
|
+
permissions on the web with ${c.highlight("useAuth().hasPermission()")}. Fields
|
|
577
|
+
any member may use ask for ${c.highlight("organization.read")}, which every
|
|
578
|
+
role lists.
|
|
574
579
|
6. ${c.highlight("@requireAuth")} is gone. Your fields use ${c.highlight("@requireUserAuth")} now,
|
|
575
580
|
which checks what ${c.highlight("@requireAuth")} did: there were no organizations
|
|
576
|
-
before. ${c.highlight("@
|
|
577
|
-
the user's role
|
|
581
|
+
before. ${c.highlight("@requirePermission")} needs a current organization, and
|
|
582
|
+
checks what the user's role there may do.${ctx.keptRequireAuthDirective ? `
|
|
578
583
|
api/src/directives/requireAuth was changed from Cedar's, so
|
|
579
584
|
it and your @requireAuth fields were left as they are: move
|
|
580
|
-
its checks to requireUserAuth or
|
|
585
|
+
its checks to requireUserAuth or requirePermission, switch the
|
|
581
586
|
fields over, then delete it.` : ""}
|
|
582
587
|
7. Every role check names its kind now. ${c.highlight("hasRole")} is
|
|
583
588
|
${c.highlight("hasUserRole")} on both sides (your existing uses were renamed),
|
|
584
|
-
checking ${c.highlight("currentUser.roles")}
|
|
585
|
-
|
|
589
|
+
checking ${c.highlight("currentUser.roles")}. In the organization, check
|
|
590
|
+
permissions instead. ${c.highlight("<PrivateSet roles>")} isn't available with
|
|
586
591
|
tenancy: guard routes with ${c.highlight("<PrivateSet userRoles>")} for
|
|
587
|
-
${c.highlight("currentUser.roles")}, and with ${c.highlight("<PrivateSet
|
|
588
|
-
|
|
589
|
-
to ${c.highlight("unauthorized")}
|
|
590
|
-
permission of their membership the same way:
|
|
592
|
+
${c.highlight("currentUser.roles")}, and with ${c.highlight("<PrivateSet orgPermission>")} for a
|
|
593
|
+
permission of their membership in the route's organization. A
|
|
594
|
+
member who lacks it is sent to ${c.highlight("unauthorized")}:
|
|
591
595
|
|
|
592
|
-
<PrivateSet unauthenticated="login" unauthorized="projects"
|
|
596
|
+
<PrivateSet unauthenticated="login" unauthorized="projects" orgPermission="organization.manage">
|
|
593
597
|
<Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
|
|
594
598
|
</PrivateSet>
|
|
595
599
|
|
|
@@ -2,8 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getPaths } from "./index.js";
|
|
4
4
|
function hasTenancyAuthDirectives() {
|
|
5
|
-
return fs.existsSync(path.join(getPaths().api.directives, "
|
|
5
|
+
return fs.existsSync(path.join(getPaths().api.directives, "requireUserAuth"));
|
|
6
6
|
}
|
|
7
|
+
const MEMBER_PERMISSION = "organization.read";
|
|
7
8
|
function tenantField() {
|
|
8
9
|
for (const ext of ["ts", "js"]) {
|
|
9
10
|
const dbPath = path.join(getPaths().api.lib, `db.${ext}`);
|
|
@@ -26,10 +27,11 @@ function withAuthDirective(source, { belongsToOrg }) {
|
|
|
26
27
|
}
|
|
27
28
|
return source.replace(
|
|
28
29
|
/@requireAuth(?![\w$])/g,
|
|
29
|
-
belongsToOrg ? "
|
|
30
|
+
belongsToOrg ? `@requirePermission(permission: "${MEMBER_PERMISSION}")` : "@requireUserAuth"
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
33
|
export {
|
|
34
|
+
MEMBER_PERMISSION,
|
|
33
35
|
belongsToOrganization,
|
|
34
36
|
hasTenancyAuthDirectives,
|
|
35
37
|
tenantField,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "8.0.0-canary.
|
|
3
|
+
"version": "8.0.0-canary.3567",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"@babel/preset-typescript": "7.29.7",
|
|
37
37
|
"@babel/traverse": "7.29.10",
|
|
38
38
|
"@babel/types": "7.29.11",
|
|
39
|
-
"@cedarjs/api-server": "8.0.0-canary.
|
|
40
|
-
"@cedarjs/babel-config": "8.0.0-canary.
|
|
41
|
-
"@cedarjs/cli-helpers": "8.0.0-canary.
|
|
42
|
-
"@cedarjs/internal": "8.0.0-canary.
|
|
43
|
-
"@cedarjs/prerender": "8.0.0-canary.
|
|
44
|
-
"@cedarjs/project-config": "8.0.0-canary.
|
|
45
|
-
"@cedarjs/structure": "8.0.0-canary.
|
|
46
|
-
"@cedarjs/telemetry": "8.0.0-canary.
|
|
47
|
-
"@cedarjs/utils": "8.0.0-canary.
|
|
48
|
-
"@cedarjs/vite": "8.0.0-canary.
|
|
49
|
-
"@cedarjs/web-server": "8.0.0-canary.
|
|
39
|
+
"@cedarjs/api-server": "8.0.0-canary.3567",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3567",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3567",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3567",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3567",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3567",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3567",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3567",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3567",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3567",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3567",
|
|
50
50
|
"@listr2/prompt-adapter-enquirer": "4.4.3",
|
|
51
51
|
"@opentelemetry/api": "1.9.1",
|
|
52
52
|
"@opentelemetry/core": "2.12.0",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"yargs": "18.2.0"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
96
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3567",
|
|
97
97
|
"@prisma/dmmf": "7.10.1",
|
|
98
98
|
"@types/archiver": "^7.0.0",
|
|
99
99
|
"memfs": "4.80.0",
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
|
|
2
|
-
|
|
3
|
-
import requireOrgAuth from './requireOrgAuth.js'
|
|
4
|
-
|
|
5
|
-
const currentOrg = (role: string) => ({
|
|
6
|
-
id: 'org-1',
|
|
7
|
-
slug: 'org-1',
|
|
8
|
-
role,
|
|
9
|
-
membershipId: 'membership-1',
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
describe('requireOrgAuth directive', () => {
|
|
13
|
-
it('declares the directive sdl as schema, with the correct name', () => {
|
|
14
|
-
expect(requireOrgAuth.schema).toBeTruthy()
|
|
15
|
-
expect(getDirectiveName(requireOrgAuth.schema)).toBe('requireOrgAuth')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('throws when there is no current user', () => {
|
|
19
|
-
const mockExecution = mockRedwoodDirective(requireOrgAuth, { context: {} })
|
|
20
|
-
|
|
21
|
-
expect(mockExecution).toThrowError("You don't have permission to do that.")
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('throws when there is no current organization', () => {
|
|
25
|
-
const mockExecution = mockRedwoodDirective(requireOrgAuth, {
|
|
26
|
-
context: { currentUser: { id: 'user-1' } },
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
expect(mockExecution).toThrowError(
|
|
30
|
-
'You must be a member of an organization to do that.',
|
|
31
|
-
)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('does not throw for a member of the current organization', () => {
|
|
35
|
-
const mockExecution = mockRedwoodDirective(requireOrgAuth, {
|
|
36
|
-
context: {
|
|
37
|
-
currentUser: { id: 'user-1' },
|
|
38
|
-
currentOrg: currentOrg('member'),
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
expect(mockExecution).not.toThrowError()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it("throws when the role in the current organization isn't listed", () => {
|
|
46
|
-
const mockExecution = mockRedwoodDirective(requireOrgAuth, {
|
|
47
|
-
context: {
|
|
48
|
-
currentUser: { id: 'user-1' },
|
|
49
|
-
currentOrg: currentOrg('member'),
|
|
50
|
-
},
|
|
51
|
-
directiveArgs: { roles: ['owner'] },
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
expect(mockExecution).toThrowError("You don't have access to do that.")
|
|
55
|
-
})
|
|
56
|
-
})
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { gql } from 'graphql-tag'
|
|
2
|
-
|
|
3
|
-
import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
|
|
4
|
-
import { createValidatorDirective } from '@cedarjs/graphql-server'
|
|
5
|
-
|
|
6
|
-
import { requireOrgAuth as applicationRequireOrgAuth } from 'src/lib/auth'
|
|
7
|
-
|
|
8
|
-
export const schema = gql`
|
|
9
|
-
"""
|
|
10
|
-
Use to check that the signed-in user is a member of the current
|
|
11
|
-
organization, with one of an optional set of roles there.
|
|
12
|
-
"""
|
|
13
|
-
directive @requireOrgAuth(roles: [String]) on FIELD_DEFINITION
|
|
14
|
-
`
|
|
15
|
-
|
|
16
|
-
type RequireOrgAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
|
|
17
|
-
|
|
18
|
-
const validate: RequireOrgAuthValidate = ({ directiveArgs }) => {
|
|
19
|
-
const { roles } = directiveArgs
|
|
20
|
-
applicationRequireOrgAuth({ roles })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const requireOrgAuth = createValidatorDirective(schema, validate)
|
|
24
|
-
|
|
25
|
-
export default requireOrgAuth
|