@cedarjs/cli 8.0.0-canary.3538 → 8.0.0-canary.3539

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,6 +7,7 @@ import prompts from "prompts";
7
7
  import { colors as c } from "@cedarjs/cli-helpers/colors";
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";
10
11
  import {
11
12
  generateTemplate,
12
13
  getPaths,
@@ -119,14 +120,18 @@ async function handler({
119
120
  exampleFile,
120
121
  exampleSubscriptionTemplateContent
121
122
  );
122
- const blankTemplateContent = await generateTemplate(
123
- setupScriptContent,
124
- templateVariables(name)
123
+ const blankTemplateContent = withAuthDirective(
124
+ await generateTemplate(setupScriptContent, templateVariables(name)),
125
+ // Not about a model, so the safe default
126
+ { belongsToOrg: true }
125
127
  );
126
128
  return [
127
129
  writeFile(
128
130
  sdlFile,
129
- await generateTemplate(sdlContent, templateVariables(name)),
131
+ withAuthDirective(
132
+ await generateTemplate(sdlContent, templateVariables(name)),
133
+ { belongsToOrg: true }
134
+ ),
130
135
  {
131
136
  overwriteExisting: force
132
137
  }
@@ -176,7 +181,10 @@ async function handler({
176
181
  return [
177
182
  writeFile(
178
183
  sdlFile,
179
- await generateTemplate(sdlContent, templateVariables(name)),
184
+ withAuthDirective(
185
+ await generateTemplate(sdlContent, templateVariables(name)),
186
+ { belongsToOrg: true }
187
+ ),
180
188
  {
181
189
  overwriteExisting: force
182
190
  }
@@ -9,6 +9,10 @@ import { generate as generateTypes } from "@cedarjs/internal/dist/generate/gener
9
9
  import { getConfig } from "@cedarjs/project-config";
10
10
  import { errorTelemetry } from "@cedarjs/telemetry";
11
11
  import { pluralize } from "@cedarjs/utils/cedarPluralize";
12
+ import {
13
+ belongsToOrganization,
14
+ withAuthDirective
15
+ } from "../../../lib/authDirective.js";
12
16
  import { transformTSToJS } from "../../../lib/index.js";
13
17
  import {
14
18
  prepareForRollback,
@@ -159,7 +163,8 @@ const sdlFromSchemaModel = async (name, crud, docs = false) => {
159
163
  idType: idType(model, crud),
160
164
  idName: idName(model, crud),
161
165
  relations: relationsForModel(model),
162
- enums
166
+ enums,
167
+ belongsToOrg: belongsToOrganization(model)
163
168
  };
164
169
  };
165
170
  const files = async ({
@@ -180,7 +185,10 @@ const files = async ({
180
185
  templateVars: { docs, name, crud, ...sdlData },
181
186
  outputPath: `${camelcase(pluralize(name))}.sdl.${extension}`
182
187
  });
183
- const template = typescript ? content : await transformTSToJS(outputPath, content);
188
+ const sdl = withAuthDirective(content, {
189
+ belongsToOrg: sdlData.belongsToOrg
190
+ });
191
+ const template = typescript ? sdl : await transformTSToJS(outputPath, sdl);
184
192
  return {
185
193
  [outputPath]: template,
186
194
  ...await serviceFiles({
@@ -7,6 +7,7 @@ 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";
10
11
  import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
11
12
  import { isTypeScriptProject, serverFileExists } from "../../../lib/project.js";
12
13
  import { setupServerFileTasks } from "../server-file/serverFileHandler.js";
@@ -97,9 +98,13 @@ async function handler(args) {
97
98
  exampleSubscriptionTemplateContent
98
99
  );
99
100
  return [
100
- writeFile(exampleFile, setupScriptContent, {
101
- overwriteExisting: force
102
- })
101
+ writeFile(
102
+ exampleFile,
103
+ withAuthDirective(setupScriptContent, { belongsToOrg: false }),
104
+ {
105
+ overwriteExisting: force
106
+ }
107
+ )
103
108
  ];
104
109
  }
105
110
  },
@@ -158,15 +163,23 @@ async function handler(args) {
158
163
  exampleSubscriptionTemplateContent
159
164
  );
160
165
  return [
161
- writeFile(sdlFile, sdlContent, {
162
- overwriteExisting: force
163
- }),
166
+ writeFile(
167
+ sdlFile,
168
+ withAuthDirective(sdlContent, { belongsToOrg: false }),
169
+ {
170
+ overwriteExisting: force
171
+ }
172
+ ),
164
173
  writeFile(serviceFile, serviceContent, {
165
174
  overwriteExisting: force
166
175
  }),
167
- writeFile(exampleFile, setupScriptContent, {
168
- overwriteExisting: force
169
- })
176
+ writeFile(
177
+ exampleFile,
178
+ withAuthDirective(setupScriptContent, { belongsToOrg: false }),
179
+ {
180
+ overwriteExisting: force
181
+ }
182
+ )
170
183
  ];
171
184
  }
172
185
  },
@@ -206,9 +219,13 @@ async function handler(args) {
206
219
  );
207
220
  const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
208
221
  return [
209
- writeFile(sdlFile, sdlContent, {
210
- overwriteExisting: force
211
- }),
222
+ writeFile(
223
+ sdlFile,
224
+ withAuthDirective(sdlContent, { belongsToOrg: false }),
225
+ {
226
+ overwriteExisting: force
227
+ }
228
+ ),
212
229
  writeFile(serviceFile, serviceContent, {
213
230
  overwriteExisting: force
214
231
  })
@@ -251,9 +268,13 @@ async function handler(args) {
251
268
  );
252
269
  const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
253
270
  return [
254
- writeFile(sdlFile, sdlContent, {
255
- overwriteExisting: force
256
- }),
271
+ writeFile(
272
+ sdlFile,
273
+ withAuthDirective(sdlContent, { belongsToOrg: false }),
274
+ {
275
+ overwriteExisting: force
276
+ }
277
+ ),
257
278
  writeFile(serviceFile, serviceContent, {
258
279
  overwriteExisting: force
259
280
  })
@@ -296,9 +317,13 @@ async function handler(args) {
296
317
  );
297
318
  const serviceContent = ts ? exampleServiceTemplateContent : await transformTSToJS(serviceFile, exampleServiceTemplateContent);
298
319
  return [
299
- writeFile(sdlFile, sdlContent, {
300
- overwriteExisting: force
301
- }),
320
+ writeFile(
321
+ sdlFile,
322
+ withAuthDirective(sdlContent, { belongsToOrg: false }),
323
+ {
324
+ overwriteExisting: force
325
+ }
326
+ ),
302
327
  writeFile(serviceFile, serviceContent, {
303
328
  overwriteExisting: force
304
329
  })
@@ -18,6 +18,59 @@ 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
+ }
21
74
  function transform(fileInfo) {
22
75
  const root = j.withParser("tsx")(fileInfo.source);
23
76
  let selectFound = false;
@@ -55,29 +108,52 @@ function transform(fileInfo) {
55
108
  if (!selectFound) {
56
109
  throw new Error("CEDAR_CODEMOD_ERR_AUTH_SHAPE_NOT_FOUND");
57
110
  }
58
- const exported = /* @__PURE__ */ new Set();
59
- root.find(j.ExportNamedDeclaration, { source: { value: "@cedarjs/tenancy" } }).forEach((path) => {
60
- for (const specifier of path.node.specifiers ?? []) {
61
- if (j.Identifier.check(specifier.exported)) {
62
- exported.add(specifier.exported.name);
63
- }
64
- }
111
+ const alreadyWired = root.find(j.ImportSpecifier, { imported: { name: "createTenancyAuth" } }).length > 0;
112
+ if (alreadyWired) {
113
+ return changed ? root.toSource() : fileInfo.source;
114
+ }
115
+ const hasRole = findExportedConst(root, "hasRole");
116
+ const requireAuth = findExportedConst(root, "requireAuth");
117
+ if (hasRole.length !== 1 || requireAuth.length !== 1) {
118
+ throw new Error("CEDAR_CODEMOD_ERR_AUTH_CHECKS_NOT_FOUND");
119
+ }
120
+ if (!isDefaultRequireAuth(requireAuth) || !isDefaultHasRole(hasRole)) {
121
+ throw new Error("CEDAR_CODEMOD_ERR_AUTH_CHECKS_CHANGED");
122
+ }
123
+ const tenancyAuth = j.withParser("ts")(TENANCY_AUTH_EXPORT).find(j.ExportNamedDeclaration).nodes()[0];
124
+ requireAuth.replaceWith(tenancyAuth);
125
+ hasRole.remove();
126
+ root.find(j.Identifier, { name: "hasRole" }).filter(
127
+ (path) => !(j.MemberExpression.check(path.parent.node) && path.parent.node.property === path.node)
128
+ ).forEach((path) => {
129
+ path.node.name = "hasUserRole";
65
130
  });
66
- const missing = ["hasOrgRole", "requireMembership"].filter(
67
- (name) => !exported.has(name)
131
+ const isUnused = (name) => root.find(j.Identifier, { name }).filter(
132
+ (path) => !j.ImportSpecifier.check(path.parent.node) && !j.TSTypeAliasDeclaration.check(path.parent.node)
133
+ ).length === 0;
134
+ root.find(j.TSTypeAliasDeclaration, { id: { name: "AllowedRoles" } }).filter(() => isUnused("AllowedRoles")).remove();
135
+ root.find(j.ImportDeclaration, { source: { value: "@cedarjs/graphql-server" } }).forEach((path) => {
136
+ path.node.specifiers = (path.node.specifiers ?? []).filter(
137
+ (specifier) => !j.ImportSpecifier.check(specifier) || !j.Identifier.check(specifier.local) || !isUnused(specifier.local.name)
138
+ );
139
+ }).filter((path) => (path.node.specifiers ?? []).length === 0).remove();
140
+ const tenancyImport = j.withParser("ts")(`import { createTenancyAuth } from '@cedarjs/tenancy'
141
+ `).find(j.ImportDeclaration).nodes()[0];
142
+ const imports = root.find(j.ImportDeclaration);
143
+ const cedarImports = imports.filter(
144
+ (path) => String(path.node.source.value).startsWith("@cedarjs/")
68
145
  );
69
- if (missing.length > 0) {
70
- const tenancyExport = j.withParser("ts")(
71
- `export { ${missing.join(", ")} } from '@cedarjs/tenancy'
72
- `
73
- ).find(j.ExportNamedDeclaration).nodes()[0];
146
+ const anchor = cedarImports.length > 0 ? cedarImports : imports;
147
+ if (anchor.length > 0) {
148
+ anchor.at(anchor.length - 1).insertAfter(tenancyImport);
149
+ } else {
74
150
  root.find(j.Program).forEach((path) => {
75
- path.node.body.push(tenancyExport);
151
+ path.node.body.unshift(tenancyImport);
76
152
  });
77
- changed = true;
78
153
  }
79
- return changed ? root.toSource() : fileInfo.source;
154
+ return root.toSource();
80
155
  }
81
156
  export {
157
+ TENANCY_AUTH_EXPORT,
82
158
  transform as default
83
159
  };
@@ -0,0 +1,101 @@
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
+ };
@@ -7,6 +7,13 @@ import { useAuth } from 'src/auth'
7
7
  export interface OrgScopeProps {
8
8
  /** Overrides the `orgSlug` route param. */
9
9
  orgSlug?: string
10
+ /** Only members with one of these roles in the organization get in. */
11
+ roles?: string | string[]
12
+ /**
13
+ * The name of the route a member without one of `roles` is sent to. It
14
+ * gets the `orgSlug` param when its path has one.
15
+ */
16
+ unauthorized?: string
10
17
  children: ReactNode
11
18
  }
12
19
 
@@ -19,15 +26,37 @@ export interface OrgScopeProps {
19
26
  * <Route path="/org/{orgSlug}/projects" page={ProjectsPage} name="projects" />
20
27
  * </Set>
21
28
  *
29
+ * `useCurrentOrg().hasOrgRole` checks the user's role in the organization,
30
+ * and `useAuth().hasUserRole` checks `currentUser.roles`. For pages only some
31
+ * roles may open, wrap them in a component that passes `roles` (there's no
32
+ * `<PrivateSet roles>` with tenancy):
33
+ *
34
+ * const OwnersOnly = ({ children }: { children: ReactNode }) => (
35
+ * <OrgScope roles="owner" unauthorized="projects">
36
+ * {children}
37
+ * </OrgScope>
38
+ * )
39
+ *
40
+ * <Set wrap={OwnersOnly}>
41
+ * <Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
42
+ * </Set>
43
+ *
22
44
  * Routes under `OrgScope` must not carry `prerender`: `OrgScope`'s nested
23
45
  * Apollo client shadows the per-route client the prerenderer injects above
24
46
  * `App`, so a prerendered query under here would not go through it.
25
47
  */
26
- export const OrgScope = ({ orgSlug, children }: OrgScopeProps) => {
48
+ export const OrgScope = ({
49
+ orgSlug,
50
+ roles,
51
+ unauthorized,
52
+ children,
53
+ }: OrgScopeProps) => {
27
54
  return (
28
55
  <TenancyOrgScope
29
56
  useAuth={useAuth}
30
57
  orgSlug={orgSlug}
58
+ roles={roles}
59
+ unauthorized={unauthorized}
31
60
  notAMember={
32
61
  <div>
33
62
  <p>You don&apos;t have access to this organization.</p>
@@ -43,16 +43,16 @@ export const schema = gql`
43
43
 
44
44
  type Query {
45
45
  "Every membership belonging to the signed-in user, across all organizations they belong to."
46
- myMemberships: [Membership!]! @requireAuth
46
+ myMemberships: [Membership!]! @requireUserAuth
47
47
 
48
48
  "The current organization, resolved by resolveCurrentOrg for this request."
49
- organization(id: String!): Organization @requireMembership
49
+ organization(id: String!): Organization @requireOrgAuth
50
50
  }
51
51
 
52
52
  type Mutation {
53
53
  "Creates a new organization with the caller as its owner."
54
54
  createOrganization(input: CreateOrganizationInput!): Organization!
55
- @requireAuth
55
+ @requireUserAuth
56
56
 
57
57
  """
58
58
  Creates a pending membership (an invitation) in the current organization,
@@ -64,7 +64,7 @@ export const schema = gql`
64
64
  the invite shows up if the inviter is also the invitee.
65
65
  """
66
66
  inviteMember(input: InviteMemberInput!): Invitation!
67
- @requireMembership(roles: ["owner", "admin"])
67
+ @requireOrgAuth(roles: ["owner", "admin"])
68
68
 
69
69
  """
70
70
  Attaches the pending membership matching token to the signed-in user, or
@@ -73,7 +73,8 @@ export const schema = gql`
73
73
  Web callers must call reauthenticate() from useAuth() in onCompleted so
74
74
  the new membership is reflected in currentUser.memberships.
75
75
  """
76
- acceptInvitation(token: String!): Organization! @requireAuth
76
+ acceptInvitation(token: String!): Organization!
77
+ @requireUserAuth
77
78
 
78
79
  """
79
80
  Removes a membership from the current organization, for a role the
@@ -84,6 +85,6 @@ export const schema = gql`
84
85
  removing their own membership.
85
86
  """
86
87
  removeMember(membershipId: String!): Membership!
87
- @requireMembership(roles: ["owner", "admin"])
88
+ @requireOrgAuth(roles: ["owner", "admin"])
88
89
  }
89
90
  `
@@ -1,5 +1,3 @@
1
- import { setContext } from '@cedarjs/context'
2
-
3
1
  import { db } from 'src/lib/db'
4
2
 
5
3
  import {
@@ -18,39 +16,37 @@ import type { StandardScenario } from './organizations.scenarios.js'
18
16
  // CedarJS Testing Docs:
19
17
  // https://cedarjs.com/docs/testing#testing-services
20
18
 
21
- /**
22
- * `context.currentUser` is typed from this app's `getCurrentUser`, which
23
- * selects more fields than these tests need.
24
- */
25
- const asCurrentUser = (user: { id: string; memberships?: unknown[] }) =>
26
- user as unknown as typeof context.currentUser
19
+ /** The id of the user a scenario membership belongs to */
20
+ const userIdOf = (membership: { userId: string | null }) => {
21
+ if (!membership.userId) {
22
+ throw new Error('Expected the scenario membership to have a user')
23
+ }
24
+
25
+ return membership.userId
26
+ }
27
27
 
28
28
  /**
29
- * Sets `context.currentUser` and `context.currentOrg` the way the GraphQL
30
- * `context` function does for an authenticated, in-organization request.
29
+ * Signs in as `membership`'s user, in its organization, the way the GraphQL
30
+ * `context` function does for an in-organization request
31
31
  */
32
32
  const asMember = (
33
33
  scenario: StandardScenario,
34
34
  membership: { id: string; userId: string | null; role: string },
35
35
  ) => {
36
- setContext({
37
- currentUser: asCurrentUser({ id: membership.userId! }),
38
- currentOrg: {
39
- id: scenario.organization.acme.id,
40
- slug: scenario.organization.acme.slug,
41
- role: membership.role,
42
- membershipId: membership.id,
43
- },
44
- })
36
+ mockCurrentUser({ id: userIdOf(membership) })
37
+ context.currentOrg = {
38
+ id: scenario.organization.acme.id,
39
+ slug: scenario.organization.acme.slug,
40
+ role: membership.role,
41
+ membershipId: membership.id,
42
+ }
45
43
  }
46
44
 
47
45
  describe('organizations service', () => {
48
46
  scenario(
49
47
  "myMemberships returns the signed-in user's memberships",
50
48
  async (scenario: StandardScenario) => {
51
- setContext({
52
- currentUser: asCurrentUser({ id: scenario.membership.owner.userId! }),
53
- })
49
+ mockCurrentUser({ id: userIdOf(scenario.membership.owner) })
54
50
 
55
51
  const result = await myMemberships()
56
52
 
@@ -235,7 +231,7 @@ describe('organizations service', () => {
235
231
  },
236
232
  })
237
233
 
238
- setContext({ currentUser: asCurrentUser({ id: invitee.id }) })
234
+ mockCurrentUser({ id: invitee.id })
239
235
 
240
236
  const result = await acceptInvitation({
241
237
  token: scenario.membership.pending.invitationToken!,
@@ -256,7 +252,7 @@ describe('organizations service', () => {
256
252
  },
257
253
  })
258
254
 
259
- setContext({ currentUser: asCurrentUser({ id: invitee.id }) })
255
+ mockCurrentUser({ id: invitee.id })
260
256
 
261
257
  const token = scenario.membership.pending.invitationToken!
262
258
 
@@ -287,10 +283,10 @@ describe('organizations service', () => {
287
283
 
288
284
  const token = scenario.membership.pending.invitationToken!
289
285
 
290
- setContext({ currentUser: asCurrentUser({ id: firstUser.id }) })
286
+ mockCurrentUser({ id: firstUser.id })
291
287
  await acceptInvitation({ token })
292
288
 
293
- setContext({ currentUser: asCurrentUser({ id: secondUser.id }) })
289
+ mockCurrentUser({ id: secondUser.id })
294
290
  await expect(acceptInvitation({ token })).rejects.toThrow()
295
291
  },
296
292
  )
@@ -308,7 +304,7 @@ describe('organizations service', () => {
308
304
 
309
305
  const token = scenario.membership.pending.invitationToken!
310
306
 
311
- setContext({ currentUser: asCurrentUser({ id: invitee.id }) })
307
+ mockCurrentUser({ id: invitee.id })
312
308
  await acceptInvitation({ token })
313
309
 
314
310
  const claimed = await db.membership.findUnique({
@@ -321,7 +317,7 @@ describe('organizations service', () => {
321
317
  )
322
318
 
323
319
  it('rejects a token that never matched a pending invitation', async () => {
324
- setContext({ currentUser: asCurrentUser({ id: 'some-user-id' }) })
320
+ mockCurrentUser({ id: 'some-user-id' })
325
321
 
326
322
  await expect(
327
323
  acceptInvitation({ token: 'not-a-real-token' }),
@@ -62,7 +62,7 @@ const canGrantRole = (granterRole: string, targetRole: string): boolean =>
62
62
  * organizations. `Membership` is a global model, so this reads plain `db`.
63
63
  */
64
64
  export const myMemberships: QueryResolvers['myMemberships'] = () => {
65
- // @requireAuth guarantees a signed-in user before this resolver runs.
65
+ // @requireUserAuth guarantees a signed-in user before this resolver runs.
66
66
  return db.membership.findMany({
67
67
  where: { userId: context.currentUser!.id },
68
68
  orderBy: { createdAt: 'asc' },
@@ -72,8 +72,8 @@ export const myMemberships: QueryResolvers['myMemberships'] = () => {
72
72
  /**
73
73
  * The current organization (the one `resolveCurrentOrg` resolved for this
74
74
  * request). `id` is required so callers state which organization they mean,
75
- * but it must match the current organization: `@requireMembership` only
76
- * checks that the caller belongs to *some* organization, not to `id`.
75
+ * but it must match the current organization: `@requireOrgAuth` checks the
76
+ * caller's membership in the current organization, not in `id`.
77
77
  */
78
78
  export const organization: QueryResolvers['organization'] = async ({ id }) => {
79
79
  const currentOrg = requireCurrentOrg()
@@ -89,7 +89,7 @@ export const organization: QueryResolvers['organization'] = async ({ id }) => {
89
89
  export const createOrganization: MutationResolvers['createOrganization'] =
90
90
  async ({ input }) => {
91
91
  const baseSlug = slugify(input.name)
92
- // @requireAuth guarantees a signed-in user before this resolver runs.
92
+ // @requireUserAuth guarantees a signed-in user before this resolver runs.
93
93
  const userId = context.currentUser!.id
94
94
 
95
95
  let slug = baseSlug
@@ -135,7 +135,7 @@ export const createOrganization: MutationResolvers['createOrganization'] =
135
135
  * Creates a pending membership (an invitation) in the current organization,
136
136
  * for a role the caller's own role may grant per `ROLE_GRANTS` -- an
137
137
  * `admin` can invite an `admin` or a `member`, but not an `owner`, even
138
- * though `@requireMembership(roles: ["owner", "admin"])` lets an admin call
138
+ * though `@requireOrgAuth(roles: ["owner", "admin"])` lets an admin call
139
139
  * this mutation at all. A role that isn't in `ROLE_GRANTS` is rejected
140
140
  * outright, so a typo in `input.role` can never create a membership nobody
141
141
  * can check against.
@@ -232,7 +232,7 @@ const claimInvitation = async (
232
232
  export const acceptInvitation: MutationResolvers['acceptInvitation'] = async ({
233
233
  token,
234
234
  }) => {
235
- // @requireAuth guarantees a signed-in user before this resolver runs.
235
+ // @requireUserAuth guarantees a signed-in user before this resolver runs.
236
236
  const userId = context.currentUser!.id
237
237
 
238
238
  const claim = await claimInvitation(token, userId)
@@ -0,0 +1,56 @@
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
+ })
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,26 @@
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
+ })
@@ -0,0 +1,26 @@
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
@@ -20,6 +20,12 @@ 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";
23
29
  import {
24
30
  authNotSetUpMessage,
25
31
  hasWebAuthFile,
@@ -33,8 +39,11 @@ import {
33
39
  ORGANIZATION_MODEL,
34
40
  requiredUserFields
35
41
  } from "./schemaPrisma.js";
42
+ import { renameUseAuthHasRole } from "./webHasRole.js";
36
43
  const DEFAULT_TENANT_FIELD = "organizationId";
37
44
  const AUTH_SNIPPET = `
45
+ import { createTenancyAuth } from '@cedarjs/tenancy'
46
+
38
47
  export const getCurrentUser = async (session) => {
39
48
  return await db.user.findUnique({
40
49
  where: { id: session.id },
@@ -52,8 +61,8 @@ const AUTH_SNIPPET = `
52
61
  })
53
62
  }
54
63
 
55
- export { hasOrgRole, requireMembership } from '@cedarjs/tenancy'
56
- `;
64
+ // In place of your hasRole and requireAuth
65
+ ${TENANCY_AUTH_EXPORT}`;
57
66
  const SIGNUP_SNIPPET = `
58
67
  handler: async ({ username, hashedPassword, salt, userAttributes }) => {
59
68
  const user = await db.user.create({
@@ -202,7 +211,7 @@ Re-run with --force to have this command append its own versions beside them ins
202
211
  }
203
212
  },
204
213
  {
205
- title: "Adding memberships to getCurrentUser and re-exporting tenancy helpers...",
214
+ title: "Adding memberships to getCurrentUser and the tenancy auth checks to api/src/lib/auth...",
206
215
  task: async (_ctx, task) => {
207
216
  if (!fs.existsSync(authPath)) {
208
217
  task.skip("No api/src/lib/auth file found; skipping.");
@@ -212,7 +221,10 @@ Re-run with --force to have this command append its own versions beside them ins
212
221
  transformPath: path.join(import.meta.dirname, "authCodemod.js"),
213
222
  targetPaths: [authPath]
214
223
  });
215
- if (transformResult.error) {
224
+ if (transformResult.error?.includes("AUTH_CHECKS_CHANGED")) {
225
+ 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:
226
+ ${AUTH_SNIPPET}`;
227
+ } else if (transformResult.error) {
216
228
  task.output = `Could not update api/src/lib/auth automatically. Please add this by hand:
217
229
  ${AUTH_SNIPPET}`;
218
230
  }
@@ -253,20 +265,54 @@ ${SIGNUP_SNIPPET}`;
253
265
  skip: () => isDbAuthProject ? false : "Not a dbAuth project; skipping."
254
266
  },
255
267
  {
256
- title: "Adding the requireMembership directive...",
257
- task: async () => {
258
- const directiveDir = path.join(
259
- paths.api.directives,
260
- "requireMembership"
261
- );
262
- await writeTemplateFile(
263
- "requireMembership.ts.template",
264
- path.join(directiveDir, `requireMembership.${ext}`)
268
+ title: "Switching @requireAuth to @requireOrgAuth and @requireUserAuth, and hasRole to hasUserRole...",
269
+ task: async (ctx) => {
270
+ const oldDirectiveDir = path.join(paths.api.directives, "requireAuth");
271
+ const isInOldDirective = (file) => file.startsWith(oldDirectiveDir + path.sep);
272
+ const oldDirectivePath = path.join(
273
+ oldDirectiveDir,
274
+ `requireAuth.${ext}`
265
275
  );
266
- await writeTemplateFile(
267
- "requireMembership.test.ts.template",
268
- path.join(directiveDir, `requireMembership.test.${ext}`)
276
+ const keepOldDirective = fs.existsSync(oldDirectivePath) && !isDefaultRequireAuthDirective(
277
+ fs.readFileSync(oldDirectivePath, "utf-8")
269
278
  );
279
+ const sourceFilesIn = (dir) => fs.readdirSync(dir, { recursive: true, withFileTypes: true }).filter(
280
+ (entry) => entry.isFile() && /\.[jt]sx?$/.test(entry.name)
281
+ ).map((entry) => path.join(entry.parentPath, entry.name));
282
+ for (const file of sourceFilesIn(paths.api.src)) {
283
+ const source = fs.readFileSync(file, "utf-8");
284
+ const renamed = renameLibAuthImports(
285
+ keepOldDirective || isInOldDirective(file) ? source : renameRequireAuthDirective(source)
286
+ );
287
+ if (renamed !== source) {
288
+ fs.writeFileSync(file, renamed);
289
+ writtenFiles.push(file);
290
+ }
291
+ }
292
+ for (const file of sourceFilesIn(paths.web.src)) {
293
+ const source = fs.readFileSync(file, "utf-8");
294
+ const renamed = renameUseAuthHasRole(source);
295
+ if (renamed !== source) {
296
+ fs.writeFileSync(file, renamed);
297
+ writtenFiles.push(file);
298
+ }
299
+ }
300
+ if (keepOldDirective) {
301
+ ctx.keptRequireAuthDirective = true;
302
+ } else if (fs.existsSync(oldDirectivePath)) {
303
+ fs.rmSync(oldDirectiveDir, { recursive: true, force: true });
304
+ }
305
+ for (const name of ["requireOrgAuth", "requireUserAuth"]) {
306
+ const directiveDir = path.join(paths.api.directives, name);
307
+ await writeTemplateFile(
308
+ `${name}.ts.template`,
309
+ path.join(directiveDir, `${name}.${ext}`)
310
+ );
311
+ await writeTemplateFile(
312
+ `${name}.test.ts.template`,
313
+ path.join(directiveDir, `${name}.test.${ext}`)
314
+ );
315
+ }
270
316
  }
271
317
  },
272
318
  {
@@ -424,7 +470,8 @@ ${SIGNUP_SNIPPET}`;
424
470
  User/Organization/Membership. For each existing model (e.g.
425
471
  Contact) whose rows belong to one organization, add
426
472
  ${c.highlight("organizationId String")} plus ${c.highlight("@@index([organizationId])")} to it in
427
- schema.prisma; for each one that stays global, add it
473
+ schema.prisma, and switch its fields from ${c.highlight("@requireUserAuth")}
474
+ to ${c.highlight("@requireOrgAuth")}. For each one that stays global, add it
428
475
  to the ${c.highlight("allExcept")} list in api/src/lib/db instead.
429
476
  3. Wrap your organization routes with the generated OrgScope in
430
477
  web/src/Routes.tsx:
@@ -441,6 +488,20 @@ ${SIGNUP_SNIPPET}`;
441
488
  tenant-owned models with ${c.highlight("withTenancy")} from ${c.highlight(
442
489
  "@cedarjs/tenancy"
443
490
  )}.
491
+ 5. ${c.highlight("@requireAuth")} is gone. Your fields use ${c.highlight("@requireUserAuth")} now,
492
+ which checks what ${c.highlight("@requireAuth")} did: there were no organizations
493
+ before. ${c.highlight("@requireOrgAuth")} needs a current organization, and checks
494
+ the user's role in it.${ctx.keptRequireAuthDirective ? `
495
+ api/src/directives/requireAuth was changed from Cedar's, so
496
+ it and your @requireAuth fields were left as they are: move
497
+ its checks to requireUserAuth or requireOrgAuth, switch the
498
+ fields over, then delete it.` : ""}
499
+ 6. Every role check names its kind now. ${c.highlight("hasRole")} is
500
+ ${c.highlight("hasUserRole")} on both sides (your existing uses were renamed),
501
+ checking ${c.highlight("currentUser.roles")}; ${c.highlight("hasOrgRole")} checks the role in
502
+ the organization. ${c.highlight("<PrivateSet roles>")} isn't available with
503
+ tenancy: guard routes by role with ${c.highlight("OrgScope")}'s ${c.highlight("roles")} and
504
+ ${c.highlight("unauthorized")}, from a small wrapper component.
444
505
 
445
506
  ${keptFiles.length > 0 ? `
446
507
  Kept these existing files rather than overwriting them (run with
@@ -0,0 +1,45 @@
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
+ };
@@ -14,6 +14,7 @@ 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";
17
18
  import { getPaths, transformTSToJS, writeFile } from "../../../lib/index.js";
18
19
  import { isTypeScriptProject } from "../../../lib/project.js";
19
20
  import { generateSecret } from "../../generate/secret/secret.js";
@@ -112,6 +113,9 @@ const handler = async ({ targets, force }) => {
112
113
  for (const [token, value] of Object.entries(replacements)) {
113
114
  templateContent = templateContent.replaceAll(token, value);
114
115
  }
116
+ templateContent = withAuthDirective(templateContent, {
117
+ belongsToOrg: false
118
+ });
115
119
  const content = projectIsTypescript ? templateContent : await transformTSToJS(outputPath, templateContent);
116
120
  return writeFile(outputPath, content, { overwriteExisting: force });
117
121
  };
@@ -0,0 +1,37 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getPaths } from "./index.js";
4
+ function hasTenancyAuthDirectives() {
5
+ return fs.existsSync(path.join(getPaths().api.directives, "requireOrgAuth"));
6
+ }
7
+ function tenantField() {
8
+ for (const ext of ["ts", "js"]) {
9
+ const dbPath = path.join(getPaths().api.lib, `db.${ext}`);
10
+ if (fs.existsSync(dbPath)) {
11
+ const match = fs.readFileSync(dbPath, "utf-8").match(/\btenantField:\s*['"](\w+)['"]/);
12
+ return match?.[1] ?? "organizationId";
13
+ }
14
+ }
15
+ return "organizationId";
16
+ }
17
+ function belongsToOrganization(model) {
18
+ const field = tenantField();
19
+ return model.fields.some(
20
+ ({ name, type }) => name === field || type === "Organization"
21
+ );
22
+ }
23
+ function withAuthDirective(source, { belongsToOrg }) {
24
+ if (!hasTenancyAuthDirectives()) {
25
+ return source;
26
+ }
27
+ return source.replace(
28
+ /@requireAuth(?![\w$])/g,
29
+ belongsToOrg ? "@requireOrgAuth" : "@requireUserAuth"
30
+ );
31
+ }
32
+ export {
33
+ belongsToOrganization,
34
+ hasTenancyAuthDirectives,
35
+ tenantField,
36
+ withAuthDirective
37
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/cli",
3
- "version": "8.0.0-canary.3538",
3
+ "version": "8.0.0-canary.3539",
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.3538",
40
- "@cedarjs/babel-config": "8.0.0-canary.3538",
41
- "@cedarjs/cli-helpers": "8.0.0-canary.3538",
42
- "@cedarjs/internal": "8.0.0-canary.3538",
43
- "@cedarjs/prerender": "8.0.0-canary.3538",
44
- "@cedarjs/project-config": "8.0.0-canary.3538",
45
- "@cedarjs/structure": "8.0.0-canary.3538",
46
- "@cedarjs/telemetry": "8.0.0-canary.3538",
47
- "@cedarjs/utils": "8.0.0-canary.3538",
48
- "@cedarjs/vite": "8.0.0-canary.3538",
49
- "@cedarjs/web-server": "8.0.0-canary.3538",
39
+ "@cedarjs/api-server": "8.0.0-canary.3539",
40
+ "@cedarjs/babel-config": "8.0.0-canary.3539",
41
+ "@cedarjs/cli-helpers": "8.0.0-canary.3539",
42
+ "@cedarjs/internal": "8.0.0-canary.3539",
43
+ "@cedarjs/prerender": "8.0.0-canary.3539",
44
+ "@cedarjs/project-config": "8.0.0-canary.3539",
45
+ "@cedarjs/structure": "8.0.0-canary.3539",
46
+ "@cedarjs/telemetry": "8.0.0-canary.3539",
47
+ "@cedarjs/utils": "8.0.0-canary.3539",
48
+ "@cedarjs/vite": "8.0.0-canary.3539",
49
+ "@cedarjs/web-server": "8.0.0-canary.3539",
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.3538",
96
+ "@cedarjs/framework-tools": "8.0.0-canary.3539",
97
97
  "@prisma/dmmf": "7.10.1",
98
98
  "@types/archiver": "^7.0.0",
99
99
  "memfs": "4.80.0",
@@ -1,35 +0,0 @@
1
- import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api'
2
-
3
- import requireMembership from './requireMembership.js'
4
-
5
- /**
6
- * `context.currentUser` is typed from this app's `getCurrentUser`, which
7
- * selects more fields than these tests need.
8
- */
9
- const asCurrentUser = (user: { id: string; memberships?: unknown[] }) =>
10
- user as unknown as NonNullable<typeof context.currentUser>
11
-
12
- describe('requireMembership directive', () => {
13
- it('declares the directive sdl as schema, with the correct name', () => {
14
- expect(requireMembership.schema).toBeTruthy()
15
- expect(getDirectiveName(requireMembership.schema)).toBe(
16
- 'requireMembership',
17
- )
18
- })
19
-
20
- it('throws when there is no current user', () => {
21
- const mockExecution = mockRedwoodDirective(requireMembership, {
22
- context: {},
23
- })
24
-
25
- expect(mockExecution).toThrowError()
26
- })
27
-
28
- it('throws when there is a current user but no current organization', () => {
29
- const mockExecution = mockRedwoodDirective(requireMembership, {
30
- context: { currentUser: asCurrentUser({ id: '1' }) },
31
- })
32
-
33
- expect(mockExecution).toThrowError()
34
- })
35
- })
@@ -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 { requireMembership as applicationRequireMembership } from 'src/lib/auth'
7
-
8
- export const schema = gql`
9
- """
10
- Use to check whether or not the current user is a member of the current
11
- organization, optionally restricted to a set of roles.
12
- """
13
- directive @requireMembership(roles: [String]) on FIELD_DEFINITION
14
- `
15
-
16
- type RequireMembershipValidate = ValidatorDirectiveFunc<{ roles?: string[] }>
17
-
18
- const validate: RequireMembershipValidate = ({ directiveArgs }) => {
19
- const { roles } = directiveArgs
20
- applicationRequireMembership({ roles })
21
- }
22
-
23
- const requireMembership = createValidatorDirective(schema, validate)
24
-
25
- export default requireMembership