@cedarjs/cli 8.0.0-canary.3567 → 8.0.0-canary.3569
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/setup/realtime/realtimeHandler.js +18 -43
- package/dist/commands/setup/tenancy/authCodemod.js +1 -128
- package/dist/commands/setup/tenancy/templates/organizations.sdl.ts.template +3 -3
- package/dist/commands/setup/tenancy/templates/organizations.ts.template +3 -3
- package/dist/commands/setup/tenancy/templates/permissionsPackage.index.ts.template +1 -1
- package/dist/commands/setup/tenancy/tenancyHandler.js +12 -87
- package/dist/commands/setup/uploads/uploadsHandler.js +0 -4
- package/dist/lib/authDirective.js +13 -5
- package/package.json +13 -13
- package/dist/commands/setup/tenancy/authDirectives.js +0 -101
- package/dist/commands/setup/tenancy/templates/requireUserAuth.test.ts.template +0 -26
- package/dist/commands/setup/tenancy/templates/requireUserAuth.ts.template +0 -26
- package/dist/commands/setup/tenancy/webHasRole.js +0 -45
|
@@ -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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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,58 +18,6 @@ function buildMembershipsSelectProperty() {
|
|
|
18
18
|
}
|
|
19
19
|
return property;
|
|
20
20
|
}
|
|
21
|
-
const TENANCY_AUTH_EXPORT = `export const { hasUserRole, requireUserAuth } = createTenancyAuth()
|
|
22
|
-
`;
|
|
23
|
-
const DEFAULT_REQUIRE_AUTH_STATEMENTS = [
|
|
24
|
-
/^if \(!isAuthenticated\(\)\) \{ throw new AuthenticationError\([^)]*\);? \}$/,
|
|
25
|
-
/^if \(roles && !hasRole\(roles\)\) \{ throw new ForbiddenError\([^)]*\);? \}$/,
|
|
26
|
-
/^return isAuthenticated\(\);?$/
|
|
27
|
-
];
|
|
28
|
-
const DEFAULT_HAS_ROLE_NAMES = /* @__PURE__ */ new Set([
|
|
29
|
-
"AllowedRoles",
|
|
30
|
-
"Array",
|
|
31
|
-
"allowedRole",
|
|
32
|
-
"allowedRoles",
|
|
33
|
-
"context",
|
|
34
|
-
"currentUser",
|
|
35
|
-
"currentUserRoles",
|
|
36
|
-
"hasRole",
|
|
37
|
-
"includes",
|
|
38
|
-
"isArray",
|
|
39
|
-
"isAuthenticated",
|
|
40
|
-
"role",
|
|
41
|
-
"roles",
|
|
42
|
-
"some",
|
|
43
|
-
"undefined"
|
|
44
|
-
]);
|
|
45
|
-
function exportedValue(declaration) {
|
|
46
|
-
const node = declaration.nodes()[0];
|
|
47
|
-
return j.VariableDeclaration.check(node?.declaration) ? node.declaration.declarations[0] : void 0;
|
|
48
|
-
}
|
|
49
|
-
function isDefaultHasRole(hasRole) {
|
|
50
|
-
return hasRole.find(j.Identifier).nodes().every((identifier) => DEFAULT_HAS_ROLE_NAMES.has(identifier.name));
|
|
51
|
-
}
|
|
52
|
-
function isDefaultRequireAuth(requireAuth) {
|
|
53
|
-
const declarator = exportedValue(requireAuth);
|
|
54
|
-
const fn = j.VariableDeclarator.check(declarator) ? declarator.init : null;
|
|
55
|
-
if (!j.ArrowFunctionExpression.check(fn) || !j.BlockStatement.check(fn.body)) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
return fn.body.body.every((statement) => {
|
|
59
|
-
const source = j(statement).toSource().replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim();
|
|
60
|
-
return DEFAULT_REQUIRE_AUTH_STATEMENTS.some(
|
|
61
|
-
(pattern) => pattern.test(source)
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
function findExportedConst(root, name) {
|
|
66
|
-
return root.find(j.ExportNamedDeclaration, {
|
|
67
|
-
declaration: {
|
|
68
|
-
type: "VariableDeclaration",
|
|
69
|
-
declarations: [{ id: { type: "Identifier", name } }]
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
21
|
const USER_LOOKUP = {
|
|
74
22
|
callee: {
|
|
75
23
|
type: "MemberExpression",
|
|
@@ -81,33 +29,6 @@ const USER_LOOKUP = {
|
|
|
81
29
|
property: { name: "findUnique" }
|
|
82
30
|
}
|
|
83
31
|
};
|
|
84
|
-
function addContextImport(root) {
|
|
85
|
-
const usesContext = root.find(j.Identifier, { name: "context" }).filter(
|
|
86
|
-
(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)
|
|
87
|
-
).length > 0;
|
|
88
|
-
const declaresContext = root.find(j.ImportDeclaration, {
|
|
89
|
-
source: { value: "@cedarjs/graphql-server" }
|
|
90
|
-
}).length > 0 || root.find(j.ImportSpecifier, { local: { name: "context" } }).length > 0 || root.find(j.VariableDeclarator, { id: { name: "context" } }).length > 0;
|
|
91
|
-
if (!usesContext || declaresContext) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
const contextImport = j.withParser("ts")(`import { context } from '@cedarjs/context'
|
|
95
|
-
`).find(j.ImportDeclaration).nodes()[0];
|
|
96
|
-
const cedarImports = root.find(j.ImportDeclaration).filter((path) => String(path.node.source.value).startsWith("@cedarjs/"));
|
|
97
|
-
const before = cedarImports.filter(
|
|
98
|
-
(path) => String(path.node.source.value) > "@cedarjs/context"
|
|
99
|
-
);
|
|
100
|
-
if (before.length > 0) {
|
|
101
|
-
before.at(0).insertBefore(contextImport);
|
|
102
|
-
} else if (cedarImports.length > 0) {
|
|
103
|
-
cedarImports.at(cedarImports.length - 1).insertAfter(contextImport);
|
|
104
|
-
} else {
|
|
105
|
-
root.find(j.Program).forEach((path) => {
|
|
106
|
-
path.node.body.unshift(contextImport);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
32
|
function addWithPermissions(root, lookups) {
|
|
112
33
|
const isWithPermissionsCall = (node) => j.CallExpression.check(node) && j.Identifier.check(node.callee) && node.callee.name === "withPermissions";
|
|
113
34
|
let wrapped = false;
|
|
@@ -192,56 +113,8 @@ function transform(fileInfo) {
|
|
|
192
113
|
if (addWithPermissions(root, membershipLookups)) {
|
|
193
114
|
changed = true;
|
|
194
115
|
}
|
|
195
|
-
|
|
196
|
-
if (alreadyWired) {
|
|
197
|
-
if (addContextImport(root)) {
|
|
198
|
-
changed = true;
|
|
199
|
-
}
|
|
200
|
-
return addPermissionsImport(changed ? root.toSource() : fileInfo.source);
|
|
201
|
-
}
|
|
202
|
-
const hasRole = findExportedConst(root, "hasRole");
|
|
203
|
-
const requireAuth = findExportedConst(root, "requireAuth");
|
|
204
|
-
if (hasRole.length !== 1 || requireAuth.length !== 1) {
|
|
205
|
-
throw new Error("CEDAR_CODEMOD_ERR_AUTH_CHECKS_NOT_FOUND");
|
|
206
|
-
}
|
|
207
|
-
if (!isDefaultRequireAuth(requireAuth) || !isDefaultHasRole(hasRole)) {
|
|
208
|
-
throw new Error("CEDAR_CODEMOD_ERR_AUTH_CHECKS_CHANGED");
|
|
209
|
-
}
|
|
210
|
-
const tenancyAuth = j.withParser("ts")(TENANCY_AUTH_EXPORT).find(j.ExportNamedDeclaration).nodes()[0];
|
|
211
|
-
requireAuth.replaceWith(tenancyAuth);
|
|
212
|
-
hasRole.remove();
|
|
213
|
-
root.find(j.Identifier, { name: "hasRole" }).filter(
|
|
214
|
-
(path) => !(j.MemberExpression.check(path.parent.node) && path.parent.node.property === path.node)
|
|
215
|
-
).forEach((path) => {
|
|
216
|
-
path.node.name = "hasUserRole";
|
|
217
|
-
});
|
|
218
|
-
const isUnused = (name) => root.find(j.Identifier, { name }).filter(
|
|
219
|
-
(path) => !j.ImportSpecifier.check(path.parent.node) && !j.TSTypeAliasDeclaration.check(path.parent.node)
|
|
220
|
-
).length === 0;
|
|
221
|
-
root.find(j.TSTypeAliasDeclaration, { id: { name: "AllowedRoles" } }).filter(() => isUnused("AllowedRoles")).remove();
|
|
222
|
-
root.find(j.ImportDeclaration, { source: { value: "@cedarjs/graphql-server" } }).forEach((path) => {
|
|
223
|
-
path.node.specifiers = (path.node.specifiers ?? []).filter(
|
|
224
|
-
(specifier) => !j.ImportSpecifier.check(specifier) || !j.Identifier.check(specifier.local) || !isUnused(specifier.local.name)
|
|
225
|
-
);
|
|
226
|
-
}).filter((path) => (path.node.specifiers ?? []).length === 0).remove();
|
|
227
|
-
const tenancyImport = j.withParser("ts")(`import { createTenancyAuth } from '@cedarjs/tenancy'
|
|
228
|
-
`).find(j.ImportDeclaration).nodes()[0];
|
|
229
|
-
const imports = root.find(j.ImportDeclaration);
|
|
230
|
-
const cedarImports = imports.filter(
|
|
231
|
-
(path) => String(path.node.source.value).startsWith("@cedarjs/")
|
|
232
|
-
);
|
|
233
|
-
const anchor = cedarImports.length > 0 ? cedarImports : imports;
|
|
234
|
-
if (anchor.length > 0) {
|
|
235
|
-
anchor.at(anchor.length - 1).insertAfter(tenancyImport);
|
|
236
|
-
} else {
|
|
237
|
-
root.find(j.Program).forEach((path) => {
|
|
238
|
-
path.node.body.unshift(tenancyImport);
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
addContextImport(root);
|
|
242
|
-
return addPermissionsImport(root.toSource());
|
|
116
|
+
return addPermissionsImport(changed ? root.toSource() : fileInfo.source);
|
|
243
117
|
}
|
|
244
118
|
export {
|
|
245
|
-
TENANCY_AUTH_EXPORT,
|
|
246
119
|
transform as default
|
|
247
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!]! @
|
|
48
|
+
myMemberships: [Membership!]! @requireAuth
|
|
49
49
|
|
|
50
50
|
"The current organization, resolved by resolveCurrentOrg for this request."
|
|
51
51
|
organization(id: String!): Organization
|
|
@@ -55,7 +55,7 @@ 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
|
-
@
|
|
58
|
+
@requireAuth
|
|
59
59
|
|
|
60
60
|
"""
|
|
61
61
|
Creates a pending membership (an invitation) in the current organization,
|
|
@@ -77,7 +77,7 @@ export const schema = gql`
|
|
|
77
77
|
the new membership is reflected in currentUser.memberships.
|
|
78
78
|
"""
|
|
79
79
|
acceptInvitation(token: String!): Organization!
|
|
80
|
-
@
|
|
80
|
+
@requireAuth
|
|
81
81
|
|
|
82
82
|
"""
|
|
83
83
|
Removes a membership from the current organization, for a role the
|
|
@@ -45,7 +45,7 @@ const membershipsForUser = (userId: string): Promise<MembershipSummary[]> =>
|
|
|
45
45
|
* organizations. `Membership` is a global model, so this reads plain `db`.
|
|
46
46
|
*/
|
|
47
47
|
export const myMemberships: QueryResolvers['myMemberships'] = () => {
|
|
48
|
-
// @
|
|
48
|
+
// @requireAuth guarantees a signed-in user before this resolver runs.
|
|
49
49
|
return db.membership.findMany({
|
|
50
50
|
where: { userId: context.currentUser!.id },
|
|
51
51
|
orderBy: { createdAt: 'asc' },
|
|
@@ -72,7 +72,7 @@ export const organization: QueryResolvers['organization'] = async ({ id }) => {
|
|
|
72
72
|
export const createOrganization: MutationResolvers['createOrganization'] =
|
|
73
73
|
async ({ input }) => {
|
|
74
74
|
const baseSlug = slugify(input.name)
|
|
75
|
-
// @
|
|
75
|
+
// @requireAuth guarantees a signed-in user before this resolver runs.
|
|
76
76
|
const userId = context.currentUser!.id
|
|
77
77
|
|
|
78
78
|
let slug = baseSlug
|
|
@@ -244,7 +244,7 @@ const isExpiredInvitation = async (token: string): Promise<boolean> => {
|
|
|
244
244
|
export const acceptInvitation: MutationResolvers['acceptInvitation'] = async ({
|
|
245
245
|
token,
|
|
246
246
|
}) => {
|
|
247
|
-
// @
|
|
247
|
+
// @requireAuth guarantees a signed-in user before this resolver runs.
|
|
248
248
|
const userId = context.currentUser!.id
|
|
249
249
|
|
|
250
250
|
const claim = await claimInvitation(token, userId)
|
|
@@ -16,7 +16,7 @@ import type { PermissionName } from '@cedarjs/tenancy/permissions'
|
|
|
16
16
|
*
|
|
17
17
|
* `roles` are per organization: a membership's role decides what the user
|
|
18
18
|
* may do in that organization. Fields about the user rather than an
|
|
19
|
-
* organization (their own account, say) use `@
|
|
19
|
+
* organization (their own account, say) use `@requireAuth` instead.
|
|
20
20
|
*
|
|
21
21
|
* This package is shared: the api checks these permissions
|
|
22
22
|
* (api/src/lib/permissions), and the web can import the names. The web gets each
|
|
@@ -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
|
|
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
|
|
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,58 +255,6 @@ ${SIGNUP_SNIPPET}`;
|
|
|
270
255
|
},
|
|
271
256
|
skip: () => isDbAuthProject ? false : "Not a dbAuth project; skipping."
|
|
272
257
|
},
|
|
273
|
-
{
|
|
274
|
-
title: "Switching @requireAuth to @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
|
-
const directiveDir = path.join(
|
|
312
|
-
paths.api.directives,
|
|
313
|
-
"requireUserAuth"
|
|
314
|
-
);
|
|
315
|
-
await writeTemplateFile(
|
|
316
|
-
"requireUserAuth.ts.template",
|
|
317
|
-
path.join(directiveDir, `requireUserAuth.${ext}`)
|
|
318
|
-
);
|
|
319
|
-
await writeTemplateFile(
|
|
320
|
-
"requireUserAuth.test.ts.template",
|
|
321
|
-
path.join(directiveDir, `requireUserAuth.test.${ext}`)
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
258
|
{
|
|
326
259
|
title: "Adding the permissions package...",
|
|
327
260
|
task: () => addPermissionsPackageTasks({
|
|
@@ -537,7 +470,7 @@ ${SIGNUP_SNIPPET}`;
|
|
|
537
470
|
User/Organization/Membership. For each existing model (e.g.
|
|
538
471
|
Contact) whose rows belong to one organization, add
|
|
539
472
|
${c.highlight("organizationId String")} plus ${c.highlight("@@index([organizationId])")} to it in
|
|
540
|
-
schema.prisma, and switch its fields from ${c.highlight("@
|
|
473
|
+
schema.prisma, and switch its fields from ${c.highlight("@requireAuth")}
|
|
541
474
|
to ${c.highlight('@requirePermission(permission: "organization.read")')}, or
|
|
542
475
|
a permission of its own. For each one that stays global, add it
|
|
543
476
|
to the ${c.highlight("allExcept")} list in api/src/lib/db instead.
|
|
@@ -576,22 +509,14 @@ ${ctx.routerNeedsTenancy ? `
|
|
|
576
509
|
permissions on the web with ${c.highlight("useAuth().hasPermission()")}. Fields
|
|
577
510
|
any member may use ask for ${c.highlight("organization.read")}, which every
|
|
578
511
|
role lists.
|
|
579
|
-
6. ${c.highlight("@requireAuth")} is
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
7. Every role check names its kind now. ${c.highlight("hasRole")} is
|
|
588
|
-
${c.highlight("hasUserRole")} on both sides (your existing uses were renamed),
|
|
589
|
-
checking ${c.highlight("currentUser.roles")}. In the organization, check
|
|
590
|
-
permissions instead. ${c.highlight("<PrivateSet roles>")} isn't available with
|
|
591
|
-
tenancy: guard routes with ${c.highlight("<PrivateSet userRoles>")} for
|
|
592
|
-
${c.highlight("currentUser.roles")}, and with ${c.highlight("<PrivateSet orgPermission>")} for a
|
|
593
|
-
permission of their membership in the route's organization. A
|
|
594
|
-
member who lacks it is sent to ${c.highlight("unauthorized")}:
|
|
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")}:
|
|
595
520
|
|
|
596
521
|
<PrivateSet unauthenticated="login" unauthorized="projects" orgPermission="organization.manage">
|
|
597
522
|
<Route path="/org/{orgSlug}/settings" page={SettingsPage} name="settings" />
|
|
@@ -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,8 +1,16 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getPaths } from "./index.js";
|
|
4
|
-
function
|
|
5
|
-
|
|
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
|
}
|
|
7
15
|
const MEMBER_PERMISSION = "organization.read";
|
|
8
16
|
function tenantField() {
|
|
@@ -22,18 +30,18 @@ function belongsToOrganization(model) {
|
|
|
22
30
|
);
|
|
23
31
|
}
|
|
24
32
|
function withAuthDirective(source, { belongsToOrg }) {
|
|
25
|
-
if (!
|
|
33
|
+
if (!belongsToOrg || !hasTenancy()) {
|
|
26
34
|
return source;
|
|
27
35
|
}
|
|
28
36
|
return source.replace(
|
|
29
37
|
/@requireAuth(?![\w$])/g,
|
|
30
|
-
|
|
38
|
+
`@requirePermission(permission: "${MEMBER_PERMISSION}")`
|
|
31
39
|
);
|
|
32
40
|
}
|
|
33
41
|
export {
|
|
34
42
|
MEMBER_PERMISSION,
|
|
35
43
|
belongsToOrganization,
|
|
36
|
-
|
|
44
|
+
hasTenancy,
|
|
37
45
|
tenantField,
|
|
38
46
|
withAuthDirective
|
|
39
47
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/cli",
|
|
3
|
-
"version": "8.0.0-canary.
|
|
3
|
+
"version": "8.0.0-canary.3569",
|
|
4
4
|
"description": "The CedarJS Command Line",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"@babel/preset-typescript": "7.29.7",
|
|
37
37
|
"@babel/traverse": "7.29.10",
|
|
38
38
|
"@babel/types": "7.29.11",
|
|
39
|
-
"@cedarjs/api-server": "8.0.0-canary.
|
|
40
|
-
"@cedarjs/babel-config": "8.0.0-canary.
|
|
41
|
-
"@cedarjs/cli-helpers": "8.0.0-canary.
|
|
42
|
-
"@cedarjs/internal": "8.0.0-canary.
|
|
43
|
-
"@cedarjs/prerender": "8.0.0-canary.
|
|
44
|
-
"@cedarjs/project-config": "8.0.0-canary.
|
|
45
|
-
"@cedarjs/structure": "8.0.0-canary.
|
|
46
|
-
"@cedarjs/telemetry": "8.0.0-canary.
|
|
47
|
-
"@cedarjs/utils": "8.0.0-canary.
|
|
48
|
-
"@cedarjs/vite": "8.0.0-canary.
|
|
49
|
-
"@cedarjs/web-server": "8.0.0-canary.
|
|
39
|
+
"@cedarjs/api-server": "8.0.0-canary.3569",
|
|
40
|
+
"@cedarjs/babel-config": "8.0.0-canary.3569",
|
|
41
|
+
"@cedarjs/cli-helpers": "8.0.0-canary.3569",
|
|
42
|
+
"@cedarjs/internal": "8.0.0-canary.3569",
|
|
43
|
+
"@cedarjs/prerender": "8.0.0-canary.3569",
|
|
44
|
+
"@cedarjs/project-config": "8.0.0-canary.3569",
|
|
45
|
+
"@cedarjs/structure": "8.0.0-canary.3569",
|
|
46
|
+
"@cedarjs/telemetry": "8.0.0-canary.3569",
|
|
47
|
+
"@cedarjs/utils": "8.0.0-canary.3569",
|
|
48
|
+
"@cedarjs/vite": "8.0.0-canary.3569",
|
|
49
|
+
"@cedarjs/web-server": "8.0.0-canary.3569",
|
|
50
50
|
"@listr2/prompt-adapter-enquirer": "4.4.3",
|
|
51
51
|
"@opentelemetry/api": "1.9.1",
|
|
52
52
|
"@opentelemetry/core": "2.12.0",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"yargs": "18.2.0"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
|
-
"@cedarjs/framework-tools": "8.0.0-canary.
|
|
96
|
+
"@cedarjs/framework-tools": "8.0.0-canary.3569",
|
|
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,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
|
-
};
|