@cedarjs/cli 8.0.0-canary.3565 → 8.0.0-canary.3568

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.
@@ -7,7 +7,6 @@ import { addApiPackages } from "@cedarjs/cli-helpers/installHelpers";
7
7
  import { runBinSync } from "@cedarjs/cli-helpers/packageManager/exec";
8
8
  import { generate as generateTypes } from "@cedarjs/internal/dist/generate/generate";
9
9
  import { errorTelemetry } from "@cedarjs/telemetry";
10
- import { withAuthDirective } from "../../../lib/authDirective.js";
11
10
  import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
12
11
  import { isTypeScriptProject, serverFileExists } from "../../../lib/project.js";
13
12
  import { setupServerFileTasks } from "../server-file/serverFileHandler.js";
@@ -98,13 +97,9 @@ async function handler(args) {
98
97
  exampleSubscriptionTemplateContent
99
98
  );
100
99
  return [
101
- writeFile(
102
- exampleFile,
103
- withAuthDirective(setupScriptContent, { belongsToOrg: false }),
104
- {
105
- overwriteExisting: force
106
- }
107
- )
100
+ writeFile(exampleFile, setupScriptContent, {
101
+ overwriteExisting: force
102
+ })
108
103
  ];
109
104
  }
110
105
  },
@@ -163,23 +158,15 @@ async function handler(args) {
163
158
  exampleSubscriptionTemplateContent
164
159
  );
165
160
  return [
166
- writeFile(
167
- sdlFile,
168
- withAuthDirective(sdlContent, { belongsToOrg: false }),
169
- {
170
- overwriteExisting: force
171
- }
172
- ),
161
+ writeFile(sdlFile, sdlContent, {
162
+ overwriteExisting: force
163
+ }),
173
164
  writeFile(serviceFile, serviceContent, {
174
165
  overwriteExisting: force
175
166
  }),
176
- writeFile(
177
- exampleFile,
178
- withAuthDirective(setupScriptContent, { belongsToOrg: false }),
179
- {
180
- overwriteExisting: force
181
- }
182
- )
167
+ writeFile(exampleFile, setupScriptContent, {
168
+ overwriteExisting: force
169
+ })
183
170
  ];
184
171
  }
185
172
  },
@@ -219,13 +206,9 @@ async function handler(args) {
219
206
  );
220
207
  const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
221
208
  return [
222
- writeFile(
223
- sdlFile,
224
- withAuthDirective(sdlContent, { belongsToOrg: false }),
225
- {
226
- overwriteExisting: force
227
- }
228
- ),
209
+ writeFile(sdlFile, sdlContent, {
210
+ overwriteExisting: force
211
+ }),
229
212
  writeFile(serviceFile, serviceContent, {
230
213
  overwriteExisting: force
231
214
  })
@@ -268,13 +251,9 @@ async function handler(args) {
268
251
  );
269
252
  const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
270
253
  return [
271
- writeFile(
272
- sdlFile,
273
- withAuthDirective(sdlContent, { belongsToOrg: false }),
274
- {
275
- overwriteExisting: force
276
- }
277
- ),
254
+ writeFile(sdlFile, sdlContent, {
255
+ overwriteExisting: force
256
+ }),
278
257
  writeFile(serviceFile, serviceContent, {
279
258
  overwriteExisting: force
280
259
  })
@@ -317,13 +296,9 @@ async function handler(args) {
317
296
  );
318
297
  const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
319
298
  return [
320
- writeFile(
321
- sdlFile,
322
- withAuthDirective(sdlContent, { belongsToOrg: false }),
323
- {
324
- overwriteExisting: force
325
- }
326
- ),
299
+ writeFile(sdlFile, sdlContent, {
300
+ overwriteExisting: force
301
+ }),
327
302
  writeFile(serviceFile, serviceContent, {
328
303
  overwriteExisting: force
329
304
  })
@@ -18,59 +18,6 @@ function buildMembershipsSelectProperty() {
18
18
  }
19
19
  return property;
20
20
  }
21
- const TENANCY_AUTH_EXPORT = `export const { hasUserRole, hasOrgRole, requireUserAuth, requireOrgAuth } =
22
- createTenancyAuth()
23
- `;
24
- const DEFAULT_REQUIRE_AUTH_STATEMENTS = [
25
- /^if \(!isAuthenticated\(\)\) \{ throw new AuthenticationError\([^)]*\);? \}$/,
26
- /^if \(roles && !hasRole\(roles\)\) \{ throw new ForbiddenError\([^)]*\);? \}$/,
27
- /^return isAuthenticated\(\);?$/
28
- ];
29
- const DEFAULT_HAS_ROLE_NAMES = /* @__PURE__ */ new Set([
30
- "AllowedRoles",
31
- "Array",
32
- "allowedRole",
33
- "allowedRoles",
34
- "context",
35
- "currentUser",
36
- "currentUserRoles",
37
- "hasRole",
38
- "includes",
39
- "isArray",
40
- "isAuthenticated",
41
- "role",
42
- "roles",
43
- "some",
44
- "undefined"
45
- ]);
46
- function exportedValue(declaration) {
47
- const node = declaration.nodes()[0];
48
- return j.VariableDeclaration.check(node?.declaration) ? node.declaration.declarations[0] : void 0;
49
- }
50
- function isDefaultHasRole(hasRole) {
51
- return hasRole.find(j.Identifier).nodes().every((identifier) => DEFAULT_HAS_ROLE_NAMES.has(identifier.name));
52
- }
53
- function isDefaultRequireAuth(requireAuth) {
54
- const declarator = exportedValue(requireAuth);
55
- const fn = j.VariableDeclarator.check(declarator) ? declarator.init : null;
56
- if (!j.ArrowFunctionExpression.check(fn) || !j.BlockStatement.check(fn.body)) {
57
- return false;
58
- }
59
- return fn.body.body.every((statement) => {
60
- const source = j(statement).toSource().replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
61
- return DEFAULT_REQUIRE_AUTH_STATEMENTS.some(
62
- (pattern) => pattern.test(source)
63
- );
64
- });
65
- }
66
- function findExportedConst(root, name) {
67
- return root.find(j.ExportNamedDeclaration, {
68
- declaration: {
69
- type: "VariableDeclaration",
70
- declarations: [{ id: { type: "Identifier", name } }]
71
- }
72
- });
73
- }
74
21
  const USER_LOOKUP = {
75
22
  callee: {
76
23
  type: "MemberExpression",
@@ -82,33 +29,6 @@ const USER_LOOKUP = {
82
29
  property: { name: "findUnique" }
83
30
  }
84
31
  };
85
- function addContextImport(root) {
86
- const usesContext = root.find(j.Identifier, { name: "context" }).filter(
87
- (path) => !(j.MemberExpression.check(path.parent.node) && path.parent.node.property === path.node && !path.parent.node.computed) && !(j.ObjectProperty.check(path.parent.node) && path.parent.node.key === path.node)
88
- ).length > 0;
89
- const declaresContext = root.find(j.ImportDeclaration, {
90
- source: { value: "@cedarjs/graphql-server" }
91
- }).length > 0 || root.find(j.ImportSpecifier, { local: { name: "context" } }).length > 0 || root.find(j.VariableDeclarator, { id: { name: "context" } }).length > 0;
92
- if (!usesContext || declaresContext) {
93
- return false;
94
- }
95
- const contextImport = j.withParser("ts")(`import { context } from '@cedarjs/context'
96
- `).find(j.ImportDeclaration).nodes()[0];
97
- const cedarImports = root.find(j.ImportDeclaration).filter((path) => String(path.node.source.value).startsWith("@cedarjs/"));
98
- const before = cedarImports.filter(
99
- (path) => String(path.node.source.value) > "@cedarjs/context"
100
- );
101
- if (before.length > 0) {
102
- before.at(0).insertBefore(contextImport);
103
- } else if (cedarImports.length > 0) {
104
- cedarImports.at(cedarImports.length - 1).insertAfter(contextImport);
105
- } else {
106
- root.find(j.Program).forEach((path) => {
107
- path.node.body.unshift(contextImport);
108
- });
109
- }
110
- return true;
111
- }
112
32
  function addWithPermissions(root, lookups) {
113
33
  const isWithPermissionsCall = (node) => j.CallExpression.check(node) && j.Identifier.check(node.callee) && node.callee.name === "withPermissions";
114
34
  let wrapped = false;
@@ -193,56 +113,8 @@ function transform(fileInfo) {
193
113
  if (addWithPermissions(root, membershipLookups)) {
194
114
  changed = true;
195
115
  }
196
- const alreadyWired = root.find(j.ImportSpecifier, { imported: { name: "createTenancyAuth" } }).length > 0;
197
- if (alreadyWired) {
198
- if (addContextImport(root)) {
199
- changed = true;
200
- }
201
- return addPermissionsImport(changed ? root.toSource() : fileInfo.source);
202
- }
203
- const hasRole = findExportedConst(root, "hasRole");
204
- const requireAuth = findExportedConst(root, "requireAuth");
205
- if (hasRole.length !== 1 || requireAuth.length !== 1) {
206
- throw new Error("CEDAR_CODEMOD_ERR_AUTH_CHECKS_NOT_FOUND");
207
- }
208
- if (!isDefaultRequireAuth(requireAuth) || !isDefaultHasRole(hasRole)) {
209
- throw new Error("CEDAR_CODEMOD_ERR_AUTH_CHECKS_CHANGED");
210
- }
211
- const tenancyAuth = j.withParser("ts")(TENANCY_AUTH_EXPORT).find(j.ExportNamedDeclaration).nodes()[0];
212
- requireAuth.replaceWith(tenancyAuth);
213
- hasRole.remove();
214
- root.find(j.Identifier, { name: "hasRole" }).filter(
215
- (path) => !(j.MemberExpression.check(path.parent.node) && path.parent.node.property === path.node)
216
- ).forEach((path) => {
217
- path.node.name = "hasUserRole";
218
- });
219
- const isUnused = (name) => root.find(j.Identifier, { name }).filter(
220
- (path) => !j.ImportSpecifier.check(path.parent.node) && !j.TSTypeAliasDeclaration.check(path.parent.node)
221
- ).length === 0;
222
- root.find(j.TSTypeAliasDeclaration, { id: { name: "AllowedRoles" } }).filter(() => isUnused("AllowedRoles")).remove();
223
- root.find(j.ImportDeclaration, { source: { value: "@cedarjs/graphql-server" } }).forEach((path) => {
224
- path.node.specifiers = (path.node.specifiers ?? []).filter(
225
- (specifier) => !j.ImportSpecifier.check(specifier) || !j.Identifier.check(specifier.local) || !isUnused(specifier.local.name)
226
- );
227
- }).filter((path) => (path.node.specifiers ?? []).length === 0).remove();
228
- const tenancyImport = j.withParser("ts")(`import { createTenancyAuth } from '@cedarjs/tenancy'
229
- `).find(j.ImportDeclaration).nodes()[0];
230
- const imports = root.find(j.ImportDeclaration);
231
- const cedarImports = imports.filter(
232
- (path) => String(path.node.source.value).startsWith("@cedarjs/")
233
- );
234
- const anchor = cedarImports.length > 0 ? cedarImports : imports;
235
- if (anchor.length > 0) {
236
- anchor.at(anchor.length - 1).insertAfter(tenancyImport);
237
- } else {
238
- root.find(j.Program).forEach((path) => {
239
- path.node.body.unshift(tenancyImport);
240
- });
241
- }
242
- addContextImport(root);
243
- return addPermissionsImport(root.toSource());
116
+ return addPermissionsImport(changed ? root.toSource() : fileInfo.source);
244
117
  }
245
118
  export {
246
- TENANCY_AUTH_EXPORT,
247
119
  transform as default
248
120
  };
@@ -45,7 +45,7 @@ export const schema = gql`
45
45
 
46
46
  type Query {
47
47
  "Every membership belonging to the signed-in user, across all organizations they belong to."
48
- myMemberships: [Membership!]! @requireUserAuth
48
+ myMemberships: [Membership!]! @requireAuth
49
49
 
50
50
  "The current organization, resolved by resolveCurrentOrg for this request."
51
51
  organization(id: String!): Organization
@@ -55,13 +55,12 @@ export const schema = gql`
55
55
  type Mutation {
56
56
  "Creates a new organization with the caller as its owner."
57
57
  createOrganization(input: CreateOrganizationInput!): Organization!
58
- @requireUserAuth
58
+ @requireAuth
59
59
 
60
60
  """
61
61
  Creates a pending membership (an invitation) in the current organization,
62
- for a role the caller's own role may grant (see ROLE_GRANTS in
63
- organizations.ts) -- an admin can invite an admin or a member, but not an
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
@@ -78,7 +77,7 @@ export const schema = gql`
78
77
  the new membership is reflected in currentUser.memberships.
79
78
  """
80
79
  acceptInvitation(token: String!): Organization!
81
- @requireUserAuth
80
+ @requireAuth
82
81
 
83
82
  """
84
83
  Removes a membership from the current organization, for a role the
@@ -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,38 +40,12 @@ 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`.
69
46
  */
70
47
  export const myMemberships: QueryResolvers['myMemberships'] = () => {
71
- // @requireUserAuth guarantees a signed-in user before this resolver runs.
48
+ // @requireAuth guarantees a signed-in user before this resolver runs.
72
49
  return db.membership.findMany({
73
50
  where: { userId: context.currentUser!.id },
74
51
  orderBy: { createdAt: 'asc' },
@@ -95,7 +72,7 @@ export const organization: QueryResolvers['organization'] = async ({ id }) => {
95
72
  export const createOrganization: MutationResolvers['createOrganization'] =
96
73
  async ({ input }) => {
97
74
  const baseSlug = slugify(input.name)
98
- // @requireUserAuth guarantees a signed-in user before this resolver runs.
75
+ // @requireAuth guarantees a signed-in user before this resolver runs.
99
76
  const userId = context.currentUser!.id
100
77
 
101
78
  let slug = baseSlug
@@ -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's own role may grant per `ROLE_GRANTS` -- an
143
- * `admin` can invite an `admin` or a `member`, but not an `owner`, even
144
- * though `@requirePermission(permission: "members.manage")` lets an admin call
145
- * this mutation at all. A role that isn't in `ROLE_GRANTS` is rejected
146
- * outright, so a typo in `input.role` can never create a membership nobody
147
- * can check against.
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 (!KNOWN_ROLES.includes(input.role)) {
138
+ if (!Object.hasOwn(permissions.roles, input.role)) {
162
139
  throw new ValidationError(
163
- `"${input.role}" is not a known role. Add it to ROLE_GRANTS in ` +
164
- 'organizations.ts before inviting with it.',
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(currentOrg.role, input.role)) {
145
+ if (!canGrantRole(input.role)) {
169
146
  throw new ForbiddenError(
170
147
  `A "${currentOrg.role}" cannot invite a "${input.role}".`,
171
148
  )
@@ -267,7 +244,7 @@ const isExpiredInvitation = async (token: string): Promise<boolean> => {
267
244
  export const acceptInvitation: MutationResolvers['acceptInvitation'] = async ({
268
245
  token,
269
246
  }) => {
270
- // @requireUserAuth guarantees a signed-in user before this resolver runs.
247
+ // @requireAuth guarantees a signed-in user before this resolver runs.
271
248
  const userId = context.currentUser!.id
272
249
 
273
250
  const claim = await claimInvitation(token, userId)
@@ -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 `ROLE_GRANTS` rule), so an admin can never remove an owner, and the
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(currentOrg.role, membership.role)) {
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
- * Who may grant which role is `ROLE_GRANTS` in
13
- * api/src/services/organizations/organizations.
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,24 +2,30 @@ 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. Fields ask
6
- * for a permission (`@requirePermission(permission: "members.manage")`, or
7
- * `requirePermission()` in a service) instead of listing roles, so a new
8
- * role is one entry here: it can do exactly what its list says, and a
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
- * organization (their own account, say) use `@requireUserAuth` instead.
19
+ * organization (their own account, say) use `@requireAuth` instead.
14
20
  *
15
21
  * This package is shared: the api checks these permissions
16
22
  * (api/src/lib/permissions), and the web can import the names. The web gets each
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 `orgRoles` or `orgPermission` (see api/src/lib/permissions), and the
20
- * one a member without them is sent to is `unauthorized`:
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" />
@@ -20,12 +20,6 @@ import { errorTelemetry } from "@cedarjs/telemetry";
20
20
  import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
21
21
  import { isTypeScriptProject } from "../../../lib/project.js";
22
22
  import { runTransform } from "../../../lib/runTransform.js";
23
- import { TENANCY_AUTH_EXPORT } from "./authCodemod.js";
24
- import {
25
- isDefaultRequireAuthDirective,
26
- renameRequireAuthDirective,
27
- renameLibAuthImports
28
- } from "./authDirectives.js";
29
23
  import {
30
24
  addPermissionsPackageTasks,
31
25
  permissionsPackageName
@@ -43,11 +37,8 @@ import {
43
37
  ORGANIZATION_MODEL,
44
38
  requiredUserFields
45
39
  } from "./schemaPrisma.js";
46
- import { renameUseAuthHasRole } from "./webHasRole.js";
47
40
  const DEFAULT_TENANT_FIELD = "organizationId";
48
41
  const AUTH_SNIPPET = `
49
- import { createTenancyAuth } from '@cedarjs/tenancy'
50
-
51
42
  import { withPermissions } from 'src/lib/permissions'
52
43
 
53
44
  export const getCurrentUser = async (session) => {
@@ -65,10 +56,7 @@ const AUTH_SNIPPET = `
65
56
  },
66
57
  },
67
58
  }))
68
- }
69
-
70
- // In place of your hasRole and requireAuth
71
- ${TENANCY_AUTH_EXPORT}`;
59
+ }`;
72
60
  const SIGNUP_SNIPPET = `
73
61
  handler: async ({ username, hashedPassword, salt, userAttributes }) => {
74
62
  const user = await db.user.create({
@@ -217,7 +205,7 @@ Re-run with --force to have this command append its own versions beside them ins
217
205
  }
218
206
  },
219
207
  {
220
- title: "Adding memberships to getCurrentUser and the tenancy auth checks to api/src/lib/auth...",
208
+ title: "Adding memberships and their permissions to getCurrentUser...",
221
209
  task: async (_ctx, task) => {
222
210
  if (!fs.existsSync(authPath)) {
223
211
  task.skip("No api/src/lib/auth file found; skipping.");
@@ -227,10 +215,7 @@ Re-run with --force to have this command append its own versions beside them ins
227
215
  transformPath: path.join(import.meta.dirname, "authCodemod.js"),
228
216
  targetPaths: [authPath]
229
217
  });
230
- if (transformResult.error?.includes("AUTH_CHECKS_CHANGED")) {
231
- task.output = `api/src/lib/auth's requireAuth or hasRole has checks of its own, so it was left as it is. Please add this by hand, keeping your checks:
232
- ${AUTH_SNIPPET}`;
233
- } else if (transformResult.error) {
218
+ if (transformResult.error) {
234
219
  task.output = `Could not update api/src/lib/auth automatically. Please add this by hand:
235
220
  ${AUTH_SNIPPET}`;
236
221
  }
@@ -270,57 +255,6 @@ ${SIGNUP_SNIPPET}`;
270
255
  },
271
256
  skip: () => isDbAuthProject ? false : "Not a dbAuth project; skipping."
272
257
  },
273
- {
274
- title: "Switching @requireAuth to @requireOrgAuth and @requireUserAuth, and hasRole to hasUserRole...",
275
- task: async (ctx) => {
276
- const oldDirectiveDir = path.join(paths.api.directives, "requireAuth");
277
- const isInOldDirective = (file) => file.startsWith(oldDirectiveDir + path.sep);
278
- const oldDirectivePath = path.join(
279
- oldDirectiveDir,
280
- `requireAuth.${ext}`
281
- );
282
- const keepOldDirective = fs.existsSync(oldDirectivePath) && !isDefaultRequireAuthDirective(
283
- fs.readFileSync(oldDirectivePath, "utf-8")
284
- );
285
- const sourceFilesIn = (dir) => fs.readdirSync(dir, { recursive: true, withFileTypes: true }).filter(
286
- (entry) => entry.isFile() && /\.[jt]sx?$/.test(entry.name)
287
- ).map((entry) => path.join(entry.parentPath, entry.name));
288
- for (const file of sourceFilesIn(paths.api.src)) {
289
- const source = fs.readFileSync(file, "utf-8");
290
- const renamed = renameLibAuthImports(
291
- keepOldDirective || isInOldDirective(file) ? source : renameRequireAuthDirective(source)
292
- );
293
- if (renamed !== source) {
294
- fs.writeFileSync(file, renamed);
295
- writtenFiles.push(file);
296
- }
297
- }
298
- for (const file of sourceFilesIn(paths.web.src)) {
299
- const source = fs.readFileSync(file, "utf-8");
300
- const renamed = renameUseAuthHasRole(source);
301
- if (renamed !== source) {
302
- fs.writeFileSync(file, renamed);
303
- writtenFiles.push(file);
304
- }
305
- }
306
- if (keepOldDirective) {
307
- ctx.keptRequireAuthDirective = true;
308
- } else if (fs.existsSync(oldDirectivePath)) {
309
- fs.rmSync(oldDirectiveDir, { recursive: true, force: true });
310
- }
311
- for (const name of ["requireOrgAuth", "requireUserAuth"]) {
312
- const directiveDir = path.join(paths.api.directives, name);
313
- await writeTemplateFile(
314
- `${name}.ts.template`,
315
- path.join(directiveDir, `${name}.${ext}`)
316
- );
317
- await writeTemplateFile(
318
- `${name}.test.ts.template`,
319
- path.join(directiveDir, `${name}.test.${ext}`)
320
- );
321
- }
322
- }
323
- },
324
258
  {
325
259
  title: "Adding the permissions package...",
326
260
  task: () => addPermissionsPackageTasks({
@@ -375,7 +309,8 @@ ${SIGNUP_SNIPPET}`;
375
309
  const withUserFields = (firstNumber) => (content) => addRequiredUserFields(content, userFields, { firstNumber });
376
310
  await writeTemplateFile(
377
311
  "organizations.ts.template",
378
- path.join(serviceDir, `organizations.${ext}`)
312
+ path.join(serviceDir, `organizations.${ext}`),
313
+ { __PERMISSIONS_PACKAGE__: permissionsPackageName() }
379
314
  );
380
315
  await writeTemplateFile(
381
316
  "organizations.test.ts.template",
@@ -535,8 +470,9 @@ ${SIGNUP_SNIPPET}`;
535
470
  User/Organization/Membership. For each existing model (e.g.
536
471
  Contact) whose rows belong to one organization, add
537
472
  ${c.highlight("organizationId String")} plus ${c.highlight("@@index([organizationId])")} to it in
538
- schema.prisma, and switch its fields from ${c.highlight("@requireUserAuth")}
539
- to ${c.highlight("@requireOrgAuth")}. For each one that stays global, add it
473
+ schema.prisma, and switch its fields from ${c.highlight("@requireAuth")}
474
+ to ${c.highlight('@requirePermission(permission: "organization.read")')}, or
475
+ a permission of its own. For each one that stays global, add it
540
476
  to the ${c.highlight("allExcept")} list in api/src/lib/db instead.
541
477
  3. Put your organization routes in a PrivateSet in web/src/Routes.tsx.
542
478
  The Router renders the organization for it, using the generated
@@ -567,29 +503,22 @@ ${ctx.routerNeedsTenancy ? `
567
503
  )}.
568
504
  5. Your permissions, and what each role may do, are in
569
505
  ${c.highlight("packages/permissions")}, shared by the api and web sides${ctx.packagesWorkspaceTurnedOn ? ` (this
570
- turned on ${c.highlight("experimental.packagesWorkspace")} in cedar.toml)` : ""}. Guard
571
- organization fields with ${c.highlight('@requirePermission(permission: "...")')}
572
- (or ${c.highlight("@requireOrgAuth(roles: [...])")}), and check permissions on the
573
- web with ${c.highlight("useAuth().hasPermission()")}.
574
- 6. ${c.highlight("@requireAuth")} is gone. Your fields use ${c.highlight("@requireUserAuth")} now,
575
- which checks what ${c.highlight("@requireAuth")} did: there were no organizations
576
- before. ${c.highlight("@requireOrgAuth")} needs a current organization, and checks
577
- the user's role in it.${ctx.keptRequireAuthDirective ? `
578
- api/src/directives/requireAuth was changed from Cedar's, so
579
- it and your @requireAuth fields were left as they are: move
580
- its checks to requireUserAuth or requireOrgAuth, switch the
581
- fields over, then delete it.` : ""}
582
- 7. Every role check names its kind now. ${c.highlight("hasRole")} is
583
- ${c.highlight("hasUserRole")} on both sides (your existing uses were renamed),
584
- checking ${c.highlight("currentUser.roles")}; ${c.highlight("hasOrgRole")} checks the role in
585
- the organization. ${c.highlight("<PrivateSet roles>")} isn't available with
586
- tenancy: guard routes with ${c.highlight("<PrivateSet userRoles>")} for
587
- ${c.highlight("currentUser.roles")}, and with ${c.highlight("<PrivateSet orgRoles>")} for the
588
- role in the route's organization. A member who lacks it is sent
589
- to ${c.highlight("unauthorized")}, and ${c.highlight("<PrivateSet orgPermission>")} checks a
590
- permission of their membership the same way:
506
+ turned on ${c.highlight("experimental.packagesWorkspace")} in cedar.toml)` : ""}. A role is a bundle of
507
+ permissions: guard organization fields with
508
+ ${c.highlight('@requirePermission(permission: "...")')}, never a role, and check
509
+ permissions on the web with ${c.highlight("useAuth().hasPermission()")}. Fields
510
+ any member may use ask for ${c.highlight("organization.read")}, which every
511
+ role lists.
512
+ 6. ${c.highlight("@requireAuth")} is for fields about the signed-in user.
513
+ ${c.highlight("@requirePermission")} needs a current organization, and checks
514
+ what the user's role there may do.
515
+ 7. Roles are the user's own: ${c.highlight("hasRole")} and ${c.highlight("<PrivateSet roles>")}
516
+ check ${c.highlight("currentUser.roles")}. In an organization, check a
517
+ permission instead: guard routes with ${c.highlight("<PrivateSet orgPermission>")}
518
+ for a permission of their membership in the route's organization.
519
+ A member who lacks it is sent to ${c.highlight("unauthorized")}:
591
520
 
592
- <PrivateSet unauthenticated="login" unauthorized="projects" orgRoles="owner">
521
+ <PrivateSet unauthenticated="login" unauthorized="projects" orgPermission="organization.manage">
593
522
  <Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
594
523
  </PrivateSet>
595
524
 
@@ -14,7 +14,6 @@ import { formatCedarCommand } from "@cedarjs/cli-helpers/packageManager/display"
14
14
  import { addEnvVar } from "@cedarjs/cli-helpers/project";
15
15
  import { getSchemaPath } from "@cedarjs/project-config";
16
16
  import { errorTelemetry } from "@cedarjs/telemetry";
17
- import { withAuthDirective } from "../../../lib/authDirective.js";
18
17
  import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
19
18
  import { isTypeScriptProject } from "../../../lib/project.js";
20
19
  import { generateSecret } from "../../generate/secret/secret.js";
@@ -113,9 +112,6 @@ const handler = async ({ targets, force }) => {
113
112
  for (const [token, value] of Object.entries(replacements)) {
114
113
  templateContent = templateContent.replaceAll(token, value);
115
114
  }
116
- templateContent = withAuthDirective(templateContent, {
117
- belongsToOrg: false
118
- });
119
115
  const content = projectIsTypescript ? templateContent : await transformTSToJS(outputPath, templateContent);
120
116
  return writeFile(outputPath, content, { overwriteExisting: force });
121
117
  };
@@ -1,9 +1,18 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { getPaths } from "./index.js";
4
- function hasTenancyAuthDirectives() {
5
- return fs.existsSync(path.join(getPaths().api.directives, "requireOrgAuth"));
4
+ function hasTenancy() {
5
+ let packageJson;
6
+ try {
7
+ packageJson = JSON.parse(
8
+ fs.readFileSync(path.join(getPaths().api.base, "package.json"), "utf-8")
9
+ );
10
+ } catch {
11
+ return false;
12
+ }
13
+ return !!packageJson.dependencies?.["@cedarjs/tenancy"] || !!packageJson.devDependencies?.["@cedarjs/tenancy"];
6
14
  }
15
+ const MEMBER_PERMISSION = "organization.read";
7
16
  function tenantField() {
8
17
  for (const ext of ["ts", "js"]) {
9
18
  const dbPath = path.join(getPaths().api.lib, `db.${ext}`);
@@ -21,17 +30,18 @@ function belongsToOrganization(model) {
21
30
  );
22
31
  }
23
32
  function withAuthDirective(source, { belongsToOrg }) {
24
- if (!hasTenancyAuthDirectives()) {
33
+ if (!belongsToOrg || !hasTenancy()) {
25
34
  return source;
26
35
  }
27
36
  return source.replace(
28
37
  /@requireAuth(?![\w$])/g,
29
- belongsToOrg ? "@requireOrgAuth" : "@requireUserAuth"
38
+ `@requirePermission(permission: "${MEMBER_PERMISSION}")`
30
39
  );
31
40
  }
32
41
  export {
42
+ MEMBER_PERMISSION,
33
43
  belongsToOrganization,
34
- hasTenancyAuthDirectives,
44
+ hasTenancy,
35
45
  tenantField,
36
46
  withAuthDirective
37
47
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/cli",
3
- "version": "8.0.0-canary.3565",
3
+ "version": "8.0.0-canary.3568",
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.3565",
40
- "@cedarjs/babel-config": "8.0.0-canary.3565",
41
- "@cedarjs/cli-helpers": "8.0.0-canary.3565",
42
- "@cedarjs/internal": "8.0.0-canary.3565",
43
- "@cedarjs/prerender": "8.0.0-canary.3565",
44
- "@cedarjs/project-config": "8.0.0-canary.3565",
45
- "@cedarjs/structure": "8.0.0-canary.3565",
46
- "@cedarjs/telemetry": "8.0.0-canary.3565",
47
- "@cedarjs/utils": "8.0.0-canary.3565",
48
- "@cedarjs/vite": "8.0.0-canary.3565",
49
- "@cedarjs/web-server": "8.0.0-canary.3565",
39
+ "@cedarjs/api-server": "8.0.0-canary.3568",
40
+ "@cedarjs/babel-config": "8.0.0-canary.3568",
41
+ "@cedarjs/cli-helpers": "8.0.0-canary.3568",
42
+ "@cedarjs/internal": "8.0.0-canary.3568",
43
+ "@cedarjs/prerender": "8.0.0-canary.3568",
44
+ "@cedarjs/project-config": "8.0.0-canary.3568",
45
+ "@cedarjs/structure": "8.0.0-canary.3568",
46
+ "@cedarjs/telemetry": "8.0.0-canary.3568",
47
+ "@cedarjs/utils": "8.0.0-canary.3568",
48
+ "@cedarjs/vite": "8.0.0-canary.3568",
49
+ "@cedarjs/web-server": "8.0.0-canary.3568",
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.3565",
96
+ "@cedarjs/framework-tools": "8.0.0-canary.3568",
97
97
  "@prisma/dmmf": "7.10.1",
98
98
  "@types/archiver": "^7.0.0",
99
99
  "memfs": "4.80.0",
@@ -1,101 +0,0 @@
1
- function renameRequireAuthDirective(source) {
2
- return source.replace(/@requireAuth(?![\w$])/g, "@requireUserAuth");
3
- }
4
- const LIB_AUTH_IMPORT = /import\s*\{([^}]*)\}\s*from\s*(['"])((?:src|\.{1,2})(?:\/[^'"]*)?\/lib\/auth(?:\.[jt]s)?)\2/g;
5
- const LIB_AUTH_RENAMES = {
6
- requireAuth: "requireUserAuth",
7
- hasRole: "hasUserRole"
8
- };
9
- function renameLibAuthImports(source) {
10
- let result = source;
11
- for (const [from, to] of Object.entries(LIB_AUTH_RENAMES)) {
12
- let renameUses = false;
13
- result = result.replace(
14
- LIB_AUTH_IMPORT,
15
- (statement, specifiers) => {
16
- const renamed = specifiers.replace(
17
- new RegExp(`(?<![\\w$])${from}(?![\\w$])(\\s+as\\s+[\\w$]+)?`, "g"),
18
- (_specifier, alias) => {
19
- if (!alias) {
20
- renameUses = true;
21
- }
22
- return `${to}${alias ?? ""}`;
23
- }
24
- );
25
- return statement.replace(specifiers, renamed);
26
- }
27
- );
28
- if (renameUses) {
29
- result = result.replace(
30
- new RegExp(`(?<![\\w$@.])${from}(?![\\w$])`, "g"),
31
- to
32
- );
33
- }
34
- }
35
- return result;
36
- }
37
- const DEFAULT_REQUIRE_AUTH_DIRECTIVES = [
38
- `import { gql } from 'graphql-tag'
39
-
40
- import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server'
41
- import { createValidatorDirective } from '@cedarjs/graphql-server'
42
-
43
- import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
44
-
45
- export const schema = gql\`
46
- """
47
- Use to check whether or not a user is authenticated and is associated
48
- with an optional set of roles.
49
- """
50
- directive @requireAuth(roles: [String]) on FIELD_DEFINITION
51
- \`
52
-
53
- type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
54
-
55
- const validate: RequireAuthValidate = ({ directiveArgs }) => {
56
- const { roles } = directiveArgs
57
- applicationRequireAuth({ roles })
58
- }
59
-
60
- const requireAuth = createValidatorDirective(schema, validate)
61
-
62
- export default requireAuth
63
- `,
64
- `import { gql } from 'graphql-tag'
65
-
66
- import { createValidatorDirective } from '@cedarjs/graphql-server'
67
-
68
- import { requireAuth as applicationRequireAuth } from 'src/lib/auth'
69
-
70
- export const schema = gql\`
71
- """
72
- Use to check whether or not a user is authenticated and is associated
73
- with an optional set of roles.
74
- """
75
- directive @requireAuth(roles: [String]) on FIELD_DEFINITION
76
- \`
77
-
78
- const validate = ({ directiveArgs }) => {
79
- const { roles } = directiveArgs
80
- applicationRequireAuth({ roles })
81
- }
82
-
83
- const requireAuth = createValidatorDirective(schema, validate)
84
-
85
- export default requireAuth
86
- `
87
- ];
88
- function withoutFormatting(source) {
89
- return source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, "").replace(/["']/g, "'").replace(/[;\s]/g, "").replace(/,([)\]}>])/g, "$1");
90
- }
91
- function isDefaultRequireAuthDirective(source) {
92
- return DEFAULT_REQUIRE_AUTH_DIRECTIVES.some(
93
- (directive) => withoutFormatting(directive) === withoutFormatting(source)
94
- );
95
- }
96
- export {
97
- DEFAULT_REQUIRE_AUTH_DIRECTIVES,
98
- isDefaultRequireAuthDirective,
99
- renameLibAuthImports,
100
- renameRequireAuthDirective
101
- };
@@ -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
@@ -1,26 +0,0 @@
1
- import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
2
-
3
- import requireUserAuth from './requireUserAuth.js'
4
-
5
- describe('requireUserAuth directive', () => {
6
- it('declares the directive sdl as schema, with the correct name', () => {
7
- expect(requireUserAuth.schema).toBeTruthy()
8
- expect(getDirectiveName(requireUserAuth.schema)).toBe('requireUserAuth')
9
- })
10
-
11
- it('throws when there is no current user', () => {
12
- const mockExecution = mockRedwoodDirective(requireUserAuth, {
13
- context: {},
14
- })
15
-
16
- expect(mockExecution).toThrowError("You don't have permission to do that.")
17
- })
18
-
19
- it('does not throw for a signed-in user, with no organization', () => {
20
- const mockExecution = mockRedwoodDirective(requireUserAuth, {
21
- context: { currentUser: { id: 'user-1' } },
22
- })
23
-
24
- expect(mockExecution).not.toThrowError()
25
- })
26
- })
@@ -1,26 +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 { requireUserAuth as applicationRequireUserAuth } from 'src/lib/auth'
7
-
8
- export const schema = gql`
9
- """
10
- Use to check whether or not a user is authenticated and is associated
11
- with an optional set of roles, in any organization or none. For fields
12
- about the user rather than an organization.
13
- """
14
- directive @requireUserAuth(roles: [String]) on FIELD_DEFINITION
15
- `
16
-
17
- type RequireUserAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
18
-
19
- const validate: RequireUserAuthValidate = ({ directiveArgs }) => {
20
- const { roles } = directiveArgs
21
- applicationRequireUserAuth({ roles })
22
- }
23
-
24
- const requireUserAuth = createValidatorDirective(schema, validate)
25
-
26
- export default requireUserAuth
@@ -1,45 +0,0 @@
1
- const HAS_ROLE = /(?<![\w$])hasRole(?![\w$])/;
2
- const USE_AUTH_DESTRUCTURING = /\{([^{}]*)\}(\s*=\s*useAuth\(\))/g;
3
- const USE_AUTH_VARIABLE = /(?:const|let|var)\s+([\w$]+)\s*=\s*useAuth\(\)/g;
4
- const HAS_ROLE_ON_VARIABLE = /(?<![\w$.])([\w$]+)(\s*\??\.\s*)hasRole(?![\w$])/g;
5
- function renameUseAuthHasRole(source) {
6
- if (!source.includes("useAuth()") || !HAS_ROLE.test(source)) {
7
- return source;
8
- }
9
- let renameUses = false;
10
- let result = source.replace(
11
- /(useAuth\(\)\s*\??\.\s*)hasRole(?![\w$])/g,
12
- "$1hasUserRole"
13
- );
14
- result = result.replace(
15
- USE_AUTH_DESTRUCTURING,
16
- (_statement, specifiers, rest) => {
17
- const renamed = specifiers.replace(
18
- /(?<![\w$])hasRole(?![\w$])(\s*:)?/g,
19
- (_specifier, alias) => {
20
- if (!alias) {
21
- renameUses = true;
22
- }
23
- return `hasUserRole${alias ?? ""}`;
24
- }
25
- );
26
- return `{${renamed}}${rest}`;
27
- }
28
- );
29
- if (renameUses) {
30
- result = result.replace(/(?<![\w$.])hasRole(?![\w$])/g, "hasUserRole");
31
- }
32
- const authVariables = new Set(
33
- Array.from(source.matchAll(USE_AUTH_VARIABLE), ([, name]) => name)
34
- );
35
- if (authVariables.size === 0) {
36
- return result;
37
- }
38
- return result.replace(
39
- HAS_ROLE_ON_VARIABLE,
40
- (match, name, access) => authVariables.has(name) ? `${name}${access}hasUserRole` : match
41
- );
42
- }
43
- export {
44
- renameUseAuthHasRole
45
- };