convex-cms 0.0.1
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/cli/commands/admin.d.ts +16 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +88 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/detectConvexUrl.d.ts +13 -0
- package/dist/cli/utils/detectConvexUrl.d.ts.map +1 -0
- package/dist/cli/utils/detectConvexUrl.js +48 -0
- package/dist/cli/utils/detectConvexUrl.js.map +1 -0
- package/dist/cli/utils/openBrowser.d.ts +7 -0
- package/dist/cli/utils/openBrowser.d.ts.map +1 -0
- package/dist/cli/utils/openBrowser.js +17 -0
- package/dist/cli/utils/openBrowser.js.map +1 -0
- package/dist/client/admin-config.d.ts +126 -0
- package/dist/client/admin-config.d.ts.map +1 -0
- package/dist/client/admin-config.js +117 -0
- package/dist/client/admin-config.js.map +1 -0
- package/dist/client/adminApi.d.ts +2273 -0
- package/dist/client/adminApi.d.ts.map +1 -0
- package/dist/client/adminApi.js +716 -0
- package/dist/client/adminApi.js.map +1 -0
- package/dist/client/agentTools.d.ts +933 -0
- package/dist/client/agentTools.d.ts.map +1 -0
- package/dist/client/agentTools.js +1004 -0
- package/dist/client/agentTools.js.map +1 -0
- package/dist/client/argTypes.d.ts +212 -0
- package/dist/client/argTypes.d.ts.map +1 -0
- package/dist/client/argTypes.js +5 -0
- package/dist/client/argTypes.js.map +1 -0
- package/dist/client/field-types.d.ts +55 -0
- package/dist/client/field-types.d.ts.map +1 -0
- package/dist/client/field-types.js +152 -0
- package/dist/client/field-types.js.map +1 -0
- package/dist/client/index.d.ts +189 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +668 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/queryBuilder.d.ts +765 -0
- package/dist/client/queryBuilder.d.ts.map +1 -0
- package/dist/client/queryBuilder.js +970 -0
- package/dist/client/queryBuilder.js.map +1 -0
- package/dist/client/schema/codegen.d.ts +128 -0
- package/dist/client/schema/codegen.d.ts.map +1 -0
- package/dist/client/schema/codegen.js +318 -0
- package/dist/client/schema/codegen.js.map +1 -0
- package/dist/client/schema/defineContentType.d.ts +221 -0
- package/dist/client/schema/defineContentType.d.ts.map +1 -0
- package/dist/client/schema/defineContentType.js +380 -0
- package/dist/client/schema/defineContentType.js.map +1 -0
- package/dist/client/schema/index.d.ts +85 -0
- package/dist/client/schema/index.d.ts.map +1 -0
- package/dist/client/schema/index.js +92 -0
- package/dist/client/schema/index.js.map +1 -0
- package/dist/client/schema/schemaDrift.d.ts +199 -0
- package/dist/client/schema/schemaDrift.d.ts.map +1 -0
- package/dist/client/schema/schemaDrift.js +340 -0
- package/dist/client/schema/schemaDrift.js.map +1 -0
- package/dist/client/schema/typedClient.d.ts +401 -0
- package/dist/client/schema/typedClient.d.ts.map +1 -0
- package/dist/client/schema/typedClient.js +269 -0
- package/dist/client/schema/typedClient.js.map +1 -0
- package/dist/client/schema/types.d.ts +477 -0
- package/dist/client/schema/types.d.ts.map +1 -0
- package/dist/client/schema/types.js +39 -0
- package/dist/client/schema/types.js.map +1 -0
- package/dist/client/types.d.ts +449 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +149 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/workflows.d.ts +51 -0
- package/dist/client/workflows.d.ts.map +1 -0
- package/dist/client/workflows.js +103 -0
- package/dist/client/workflows.js.map +1 -0
- package/dist/client/wrapper.d.ts +2198 -0
- package/dist/client/wrapper.d.ts.map +1 -0
- package/dist/client/wrapper.js +2651 -0
- package/dist/client/wrapper.js.map +1 -0
- package/dist/component/_generated/api.d.ts +124 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +4321 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/auditLog.d.ts +410 -0
- package/dist/component/auditLog.d.ts.map +1 -0
- package/dist/component/auditLog.js +607 -0
- package/dist/component/auditLog.js.map +1 -0
- package/dist/component/authorization.d.ts +323 -0
- package/dist/component/authorization.d.ts.map +1 -0
- package/dist/component/authorization.js +464 -0
- package/dist/component/authorization.js.map +1 -0
- package/dist/component/authorizationHooks.d.ts +184 -0
- package/dist/component/authorizationHooks.d.ts.map +1 -0
- package/dist/component/authorizationHooks.js +521 -0
- package/dist/component/authorizationHooks.js.map +1 -0
- package/dist/component/bulkOperations.d.ts +200 -0
- package/dist/component/bulkOperations.d.ts.map +1 -0
- package/dist/component/bulkOperations.js +568 -0
- package/dist/component/bulkOperations.js.map +1 -0
- package/dist/component/contentEntries.d.ts +719 -0
- package/dist/component/contentEntries.d.ts.map +1 -0
- package/dist/component/contentEntries.js +1617 -0
- package/dist/component/contentEntries.js.map +1 -0
- package/dist/component/contentEntryMutations.d.ts +505 -0
- package/dist/component/contentEntryMutations.d.ts.map +1 -0
- package/dist/component/contentEntryMutations.js +1009 -0
- package/dist/component/contentEntryMutations.js.map +1 -0
- package/dist/component/contentEntryValidation.d.ts +115 -0
- package/dist/component/contentEntryValidation.d.ts.map +1 -0
- package/dist/component/contentEntryValidation.js +546 -0
- package/dist/component/contentEntryValidation.js.map +1 -0
- package/dist/component/contentLock.d.ts +328 -0
- package/dist/component/contentLock.d.ts.map +1 -0
- package/dist/component/contentLock.js +471 -0
- package/dist/component/contentLock.js.map +1 -0
- package/dist/component/contentTypeMigration.d.ts +411 -0
- package/dist/component/contentTypeMigration.d.ts.map +1 -0
- package/dist/component/contentTypeMigration.js +805 -0
- package/dist/component/contentTypeMigration.js.map +1 -0
- package/dist/component/contentTypeMutations.d.ts +975 -0
- package/dist/component/contentTypeMutations.d.ts.map +1 -0
- package/dist/component/contentTypeMutations.js +768 -0
- package/dist/component/contentTypeMutations.js.map +1 -0
- package/dist/component/contentTypes.d.ts +538 -0
- package/dist/component/contentTypes.d.ts.map +1 -0
- package/dist/component/contentTypes.js +304 -0
- package/dist/component/contentTypes.js.map +1 -0
- package/dist/component/convex.config.d.ts +42 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +43 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/documentTypes.d.ts +186 -0
- package/dist/component/documentTypes.d.ts.map +1 -0
- package/dist/component/documentTypes.js +23 -0
- package/dist/component/documentTypes.js.map +1 -0
- package/dist/component/eventEmitter.d.ts +281 -0
- package/dist/component/eventEmitter.d.ts.map +1 -0
- package/dist/component/eventEmitter.js +300 -0
- package/dist/component/eventEmitter.js.map +1 -0
- package/dist/component/exportImport.d.ts +1120 -0
- package/dist/component/exportImport.d.ts.map +1 -0
- package/dist/component/exportImport.js +931 -0
- package/dist/component/exportImport.js.map +1 -0
- package/dist/component/index.d.ts +28 -0
- package/dist/component/index.d.ts.map +1 -0
- package/dist/component/index.js +142 -0
- package/dist/component/index.js.map +1 -0
- package/dist/component/lib/deepReferenceResolver.d.ts +252 -0
- package/dist/component/lib/deepReferenceResolver.d.ts.map +1 -0
- package/dist/component/lib/deepReferenceResolver.js +601 -0
- package/dist/component/lib/deepReferenceResolver.js.map +1 -0
- package/dist/component/lib/errors.d.ts +306 -0
- package/dist/component/lib/errors.d.ts.map +1 -0
- package/dist/component/lib/errors.js +407 -0
- package/dist/component/lib/errors.js.map +1 -0
- package/dist/component/lib/index.d.ts +10 -0
- package/dist/component/lib/index.d.ts.map +1 -0
- package/dist/component/lib/index.js +33 -0
- package/dist/component/lib/index.js.map +1 -0
- package/dist/component/lib/mediaReferenceResolver.d.ts +217 -0
- package/dist/component/lib/mediaReferenceResolver.d.ts.map +1 -0
- package/dist/component/lib/mediaReferenceResolver.js +326 -0
- package/dist/component/lib/mediaReferenceResolver.js.map +1 -0
- package/dist/component/lib/metadataExtractor.d.ts +245 -0
- package/dist/component/lib/metadataExtractor.d.ts.map +1 -0
- package/dist/component/lib/metadataExtractor.js +548 -0
- package/dist/component/lib/metadataExtractor.js.map +1 -0
- package/dist/component/lib/mutationAuth.d.ts +95 -0
- package/dist/component/lib/mutationAuth.d.ts.map +1 -0
- package/dist/component/lib/mutationAuth.js +146 -0
- package/dist/component/lib/mutationAuth.js.map +1 -0
- package/dist/component/lib/queries.d.ts +17 -0
- package/dist/component/lib/queries.d.ts.map +1 -0
- package/dist/component/lib/queries.js +49 -0
- package/dist/component/lib/queries.js.map +1 -0
- package/dist/component/lib/ragContentChunker.d.ts +423 -0
- package/dist/component/lib/ragContentChunker.d.ts.map +1 -0
- package/dist/component/lib/ragContentChunker.js +897 -0
- package/dist/component/lib/ragContentChunker.js.map +1 -0
- package/dist/component/lib/referenceResolver.d.ts +175 -0
- package/dist/component/lib/referenceResolver.d.ts.map +1 -0
- package/dist/component/lib/referenceResolver.js +293 -0
- package/dist/component/lib/referenceResolver.js.map +1 -0
- package/dist/component/lib/slugGenerator.d.ts +71 -0
- package/dist/component/lib/slugGenerator.d.ts.map +1 -0
- package/dist/component/lib/slugGenerator.js +207 -0
- package/dist/component/lib/slugGenerator.js.map +1 -0
- package/dist/component/lib/slugUniqueness.d.ts +131 -0
- package/dist/component/lib/slugUniqueness.d.ts.map +1 -0
- package/dist/component/lib/slugUniqueness.js +229 -0
- package/dist/component/lib/slugUniqueness.js.map +1 -0
- package/dist/component/lib/softDelete.d.ts +18 -0
- package/dist/component/lib/softDelete.d.ts.map +1 -0
- package/dist/component/lib/softDelete.js +29 -0
- package/dist/component/lib/softDelete.js.map +1 -0
- package/dist/component/localeFallbackChain.d.ts +410 -0
- package/dist/component/localeFallbackChain.d.ts.map +1 -0
- package/dist/component/localeFallbackChain.js +467 -0
- package/dist/component/localeFallbackChain.js.map +1 -0
- package/dist/component/localeFields.d.ts +508 -0
- package/dist/component/localeFields.d.ts.map +1 -0
- package/dist/component/localeFields.js +592 -0
- package/dist/component/localeFields.js.map +1 -0
- package/dist/component/mediaAssetMutations.d.ts +235 -0
- package/dist/component/mediaAssetMutations.d.ts.map +1 -0
- package/dist/component/mediaAssetMutations.js +558 -0
- package/dist/component/mediaAssetMutations.js.map +1 -0
- package/dist/component/mediaAssets.d.ts +168 -0
- package/dist/component/mediaAssets.d.ts.map +1 -0
- package/dist/component/mediaAssets.js +618 -0
- package/dist/component/mediaAssets.js.map +1 -0
- package/dist/component/mediaFolderMutations.d.ts +642 -0
- package/dist/component/mediaFolderMutations.d.ts.map +1 -0
- package/dist/component/mediaFolderMutations.js +849 -0
- package/dist/component/mediaFolderMutations.js.map +1 -0
- package/dist/component/mediaUploadMutations.d.ts +136 -0
- package/dist/component/mediaUploadMutations.d.ts.map +1 -0
- package/dist/component/mediaUploadMutations.js +205 -0
- package/dist/component/mediaUploadMutations.js.map +1 -0
- package/dist/component/mediaVariantMutations.d.ts +468 -0
- package/dist/component/mediaVariantMutations.d.ts.map +1 -0
- package/dist/component/mediaVariantMutations.js +737 -0
- package/dist/component/mediaVariantMutations.js.map +1 -0
- package/dist/component/mediaVariants.d.ts +525 -0
- package/dist/component/mediaVariants.d.ts.map +1 -0
- package/dist/component/mediaVariants.js +661 -0
- package/dist/component/mediaVariants.js.map +1 -0
- package/dist/component/ragContentIndexer.d.ts +595 -0
- package/dist/component/ragContentIndexer.d.ts.map +1 -0
- package/dist/component/ragContentIndexer.js +794 -0
- package/dist/component/ragContentIndexer.js.map +1 -0
- package/dist/component/rateLimitHooks.d.ts +266 -0
- package/dist/component/rateLimitHooks.d.ts.map +1 -0
- package/dist/component/rateLimitHooks.js +412 -0
- package/dist/component/rateLimitHooks.js.map +1 -0
- package/dist/component/roles.d.ts +649 -0
- package/dist/component/roles.d.ts.map +1 -0
- package/dist/component/roles.js +884 -0
- package/dist/component/roles.js.map +1 -0
- package/dist/component/scheduledPublish.d.ts +182 -0
- package/dist/component/scheduledPublish.d.ts.map +1 -0
- package/dist/component/scheduledPublish.js +304 -0
- package/dist/component/scheduledPublish.js.map +1 -0
- package/dist/component/schema.d.ts +4114 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +469 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/taxonomies.d.ts +476 -0
- package/dist/component/taxonomies.d.ts.map +1 -0
- package/dist/component/taxonomies.js +785 -0
- package/dist/component/taxonomies.js.map +1 -0
- package/dist/component/taxonomyMutations.d.ts +206 -0
- package/dist/component/taxonomyMutations.d.ts.map +1 -0
- package/dist/component/taxonomyMutations.js +1001 -0
- package/dist/component/taxonomyMutations.js.map +1 -0
- package/dist/component/trash.d.ts +265 -0
- package/dist/component/trash.d.ts.map +1 -0
- package/dist/component/trash.js +621 -0
- package/dist/component/trash.js.map +1 -0
- package/dist/component/types.d.ts +4 -0
- package/dist/component/types.d.ts.map +1 -0
- package/dist/component/types.js +2 -0
- package/dist/component/types.js.map +1 -0
- package/dist/component/userContext.d.ts +508 -0
- package/dist/component/userContext.d.ts.map +1 -0
- package/dist/component/userContext.js +615 -0
- package/dist/component/userContext.js.map +1 -0
- package/dist/component/validation.d.ts +387 -0
- package/dist/component/validation.d.ts.map +1 -0
- package/dist/component/validation.js +1052 -0
- package/dist/component/validation.js.map +1 -0
- package/dist/component/validators.d.ts +4645 -0
- package/dist/component/validators.d.ts.map +1 -0
- package/dist/component/validators.js +641 -0
- package/dist/component/validators.js.map +1 -0
- package/dist/component/versionMutations.d.ts +216 -0
- package/dist/component/versionMutations.d.ts.map +1 -0
- package/dist/component/versionMutations.js +321 -0
- package/dist/component/versionMutations.js.map +1 -0
- package/dist/component/webhookTrigger.d.ts +770 -0
- package/dist/component/webhookTrigger.d.ts.map +1 -0
- package/dist/component/webhookTrigger.js +1413 -0
- package/dist/component/webhookTrigger.js.map +1 -0
- package/dist/react/index.d.ts +316 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +558 -0
- package/dist/react/index.js.map +1 -0
- package/dist/test.d.ts +2230 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +1107 -0
- package/dist/test.js.map +1 -0
- package/package.json +95 -0
- package/src/cli/commands/admin.ts +104 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/utils/detectConvexUrl.ts +54 -0
- package/src/cli/utils/openBrowser.ts +16 -0
- package/src/client/admin-config.ts +138 -0
- package/src/client/adminApi.ts +942 -0
- package/src/client/agentTools.ts +1311 -0
- package/src/client/argTypes.ts +316 -0
- package/src/client/field-types.ts +187 -0
- package/src/client/index.ts +1301 -0
- package/src/client/queryBuilder.ts +1100 -0
- package/src/client/schema/codegen.ts +500 -0
- package/src/client/schema/defineContentType.ts +501 -0
- package/src/client/schema/index.ts +169 -0
- package/src/client/schema/schemaDrift.ts +574 -0
- package/src/client/schema/typedClient.ts +688 -0
- package/src/client/schema/types.ts +666 -0
- package/src/client/types.ts +723 -0
- package/src/client/workflows.ts +141 -0
- package/src/client/wrapper.ts +4304 -0
- package/src/component/_generated/api.ts +140 -0
- package/src/component/_generated/component.ts +5029 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/authorization.ts +647 -0
- package/src/component/authorizationHooks.ts +668 -0
- package/src/component/bulkOperations.ts +687 -0
- package/src/component/contentEntries.ts +1976 -0
- package/src/component/contentEntryMutations.ts +1223 -0
- package/src/component/contentEntryValidation.ts +707 -0
- package/src/component/contentLock.ts +550 -0
- package/src/component/contentTypeMigration.ts +1064 -0
- package/src/component/contentTypeMutations.ts +969 -0
- package/src/component/contentTypes.ts +346 -0
- package/src/component/convex.config.ts +44 -0
- package/src/component/documentTypes.ts +240 -0
- package/src/component/eventEmitter.ts +485 -0
- package/src/component/exportImport.ts +1169 -0
- package/src/component/index.ts +491 -0
- package/src/component/lib/deepReferenceResolver.ts +999 -0
- package/src/component/lib/errors.ts +816 -0
- package/src/component/lib/index.ts +145 -0
- package/src/component/lib/mediaReferenceResolver.ts +495 -0
- package/src/component/lib/metadataExtractor.ts +792 -0
- package/src/component/lib/mutationAuth.ts +199 -0
- package/src/component/lib/queries.ts +79 -0
- package/src/component/lib/ragContentChunker.ts +1371 -0
- package/src/component/lib/referenceResolver.ts +430 -0
- package/src/component/lib/slugGenerator.ts +262 -0
- package/src/component/lib/slugUniqueness.ts +333 -0
- package/src/component/lib/softDelete.ts +44 -0
- package/src/component/localeFallbackChain.ts +673 -0
- package/src/component/localeFields.ts +896 -0
- package/src/component/mediaAssetMutations.ts +725 -0
- package/src/component/mediaAssets.ts +932 -0
- package/src/component/mediaFolderMutations.ts +1046 -0
- package/src/component/mediaUploadMutations.ts +224 -0
- package/src/component/mediaVariantMutations.ts +900 -0
- package/src/component/mediaVariants.ts +793 -0
- package/src/component/ragContentIndexer.ts +1067 -0
- package/src/component/rateLimitHooks.ts +572 -0
- package/src/component/roles.ts +1360 -0
- package/src/component/scheduledPublish.ts +358 -0
- package/src/component/schema.ts +617 -0
- package/src/component/taxonomies.ts +949 -0
- package/src/component/taxonomyMutations.ts +1210 -0
- package/src/component/trash.ts +724 -0
- package/src/component/userContext.ts +898 -0
- package/src/component/validation.ts +1388 -0
- package/src/component/validators.ts +949 -0
- package/src/component/versionMutations.ts +392 -0
- package/src/component/webhookTrigger.ts +1922 -0
- package/src/react/index.ts +898 -0
- package/src/test.ts +1580 -0
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RBAC Default Roles Configuration
|
|
3
|
+
*
|
|
4
|
+
* This module defines the default role configurations for the CMS:
|
|
5
|
+
* - admin: Full access to all CMS features
|
|
6
|
+
* - editor: Can manage all content and media, but not settings
|
|
7
|
+
* - author: Can create and manage own content
|
|
8
|
+
* - viewer: Read-only access to published content
|
|
9
|
+
*
|
|
10
|
+
* Roles are exported as constants for easy customization. Developers can
|
|
11
|
+
* extend or override these defaults using the custom roles feature.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { DEFAULT_ROLES, hasPermission, type RoleName } from '@convex-cms/core';
|
|
16
|
+
*
|
|
17
|
+
* // Check if a role has a specific permission
|
|
18
|
+
* if (hasPermission('editor', { resource: 'contentEntries', action: 'update' })) {
|
|
19
|
+
* // Allow the action
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* // Get all permissions for a role
|
|
23
|
+
* const adminPerms = getRolePermissions('admin');
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
import { v } from "convex/values";
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Role Name Constants
|
|
29
|
+
// =============================================================================
|
|
30
|
+
/**
|
|
31
|
+
* All built-in role names in the CMS.
|
|
32
|
+
* Custom roles can be added by developers, but these are always available.
|
|
33
|
+
*/
|
|
34
|
+
export const roleNames = ["admin", "editor", "author", "viewer"];
|
|
35
|
+
/**
|
|
36
|
+
* Convex validator for role names.
|
|
37
|
+
* Use this in function arguments to validate role input.
|
|
38
|
+
*/
|
|
39
|
+
export const roleNameValidator = v.union(v.literal("admin"), v.literal("editor"), v.literal("author"), v.literal("viewer"));
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Resource and Action Constants
|
|
42
|
+
// =============================================================================
|
|
43
|
+
/**
|
|
44
|
+
* All resources that can be protected by RBAC.
|
|
45
|
+
*/
|
|
46
|
+
export const resources = [
|
|
47
|
+
"contentTypes",
|
|
48
|
+
"contentEntries",
|
|
49
|
+
"mediaItems",
|
|
50
|
+
"settings",
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Convex validator for resources.
|
|
54
|
+
*/
|
|
55
|
+
export const resourceValidator = v.union(v.literal("contentTypes"), v.literal("contentEntries"), v.literal("mediaItems"), v.literal("settings"));
|
|
56
|
+
/**
|
|
57
|
+
* All actions that can be performed on resources.
|
|
58
|
+
*/
|
|
59
|
+
export const actions = [
|
|
60
|
+
"create",
|
|
61
|
+
"read",
|
|
62
|
+
"update",
|
|
63
|
+
"delete",
|
|
64
|
+
"publish",
|
|
65
|
+
"unpublish",
|
|
66
|
+
"restore",
|
|
67
|
+
"manage", // Special action for full control (e.g., settings)
|
|
68
|
+
"move", // Move items between folders (media)
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Convex validator for actions.
|
|
72
|
+
*/
|
|
73
|
+
export const actionValidator = v.union(v.literal("create"), v.literal("read"), v.literal("update"), v.literal("delete"), v.literal("publish"), v.literal("unpublish"), v.literal("restore"), v.literal("manage"), v.literal("move"));
|
|
74
|
+
/**
|
|
75
|
+
* Convex validator for a permission object.
|
|
76
|
+
*/
|
|
77
|
+
export const permissionValidator = v.object({
|
|
78
|
+
resource: resourceValidator,
|
|
79
|
+
action: actionValidator,
|
|
80
|
+
scope: v.optional(v.union(v.literal("all"), v.literal("own"))),
|
|
81
|
+
});
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Permission Factory Helpers
|
|
84
|
+
// =============================================================================
|
|
85
|
+
/**
|
|
86
|
+
* Helper to create a full CRUD permission set for a resource.
|
|
87
|
+
*/
|
|
88
|
+
function fullCrud(resource, scope = "all") {
|
|
89
|
+
return [
|
|
90
|
+
{ resource, action: "create", scope },
|
|
91
|
+
{ resource, action: "read", scope },
|
|
92
|
+
{ resource, action: "update", scope },
|
|
93
|
+
{ resource, action: "delete", scope },
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Helper to create read-only permission for a resource.
|
|
98
|
+
*/
|
|
99
|
+
function readOnly(resource, scope = "all") {
|
|
100
|
+
return [{ resource, action: "read", scope }];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Helper to create publish permissions for content.
|
|
104
|
+
*/
|
|
105
|
+
function publishPermissions(scope = "all") {
|
|
106
|
+
return [
|
|
107
|
+
{ resource: "contentEntries", action: "publish", scope },
|
|
108
|
+
{ resource: "contentEntries", action: "unpublish", scope },
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Default Role Definitions
|
|
113
|
+
// =============================================================================
|
|
114
|
+
/**
|
|
115
|
+
* Admin role - Full access to all CMS features.
|
|
116
|
+
*
|
|
117
|
+
* Admins can:
|
|
118
|
+
* - Create, read, update, and delete all content types
|
|
119
|
+
* - Manage all content entries regardless of author
|
|
120
|
+
* - Publish and unpublish any content
|
|
121
|
+
* - Manage all media assets and folders
|
|
122
|
+
* - Access and modify CMS settings
|
|
123
|
+
*/
|
|
124
|
+
export const ADMIN_ROLE = {
|
|
125
|
+
name: "admin",
|
|
126
|
+
displayName: "Administrator",
|
|
127
|
+
description: "Full access to all CMS features including settings and content type management",
|
|
128
|
+
isSystem: true,
|
|
129
|
+
permissions: [
|
|
130
|
+
// Content types - full management
|
|
131
|
+
...fullCrud("contentTypes"),
|
|
132
|
+
// Content entries - full CRUD + publish
|
|
133
|
+
...fullCrud("contentEntries"),
|
|
134
|
+
...publishPermissions(),
|
|
135
|
+
{ resource: "contentEntries", action: "restore" },
|
|
136
|
+
// Media - full management
|
|
137
|
+
...fullCrud("mediaItems"),
|
|
138
|
+
// Settings - full access
|
|
139
|
+
{ resource: "settings", action: "manage" },
|
|
140
|
+
...readOnly("settings"),
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Editor role - Can manage all content and media, but not settings or content types.
|
|
145
|
+
*
|
|
146
|
+
* Editors can:
|
|
147
|
+
* - Read content type definitions
|
|
148
|
+
* - Create, read, update, and delete all content entries
|
|
149
|
+
* - Publish and unpublish any content
|
|
150
|
+
* - Manage all media assets and folders
|
|
151
|
+
* - Cannot modify CMS settings or content type schemas
|
|
152
|
+
*/
|
|
153
|
+
export const EDITOR_ROLE = {
|
|
154
|
+
name: "editor",
|
|
155
|
+
displayName: "Editor",
|
|
156
|
+
description: "Can manage all content and media, but cannot modify settings or content types",
|
|
157
|
+
isSystem: true,
|
|
158
|
+
permissions: [
|
|
159
|
+
// Content types - read only
|
|
160
|
+
...readOnly("contentTypes"),
|
|
161
|
+
// Content entries - full CRUD + publish
|
|
162
|
+
...fullCrud("contentEntries"),
|
|
163
|
+
...publishPermissions(),
|
|
164
|
+
{ resource: "contentEntries", action: "restore" },
|
|
165
|
+
// Media - full management
|
|
166
|
+
...fullCrud("mediaItems"),
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Author role - Can create and manage own content.
|
|
171
|
+
*
|
|
172
|
+
* Authors can:
|
|
173
|
+
* - Read content type definitions
|
|
174
|
+
* - Create content entries
|
|
175
|
+
* - Read, update, and delete their own content entries
|
|
176
|
+
* - Publish and unpublish their own content (subject to workflow settings)
|
|
177
|
+
* - Upload and manage their own media assets
|
|
178
|
+
* - Read all media (for embedding in content)
|
|
179
|
+
* - Cannot manage other users' content or CMS settings
|
|
180
|
+
*/
|
|
181
|
+
export const AUTHOR_ROLE = {
|
|
182
|
+
name: "author",
|
|
183
|
+
displayName: "Author",
|
|
184
|
+
description: "Can create and manage own content and media",
|
|
185
|
+
isSystem: true,
|
|
186
|
+
permissions: [
|
|
187
|
+
// Content types - read only
|
|
188
|
+
...readOnly("contentTypes"),
|
|
189
|
+
// Content entries - own content only
|
|
190
|
+
{ resource: "contentEntries", action: "create" },
|
|
191
|
+
{ resource: "contentEntries", action: "read", scope: "own" },
|
|
192
|
+
{ resource: "contentEntries", action: "update", scope: "own" },
|
|
193
|
+
{ resource: "contentEntries", action: "delete", scope: "own" },
|
|
194
|
+
// Authors can publish/unpublish their own content
|
|
195
|
+
{ resource: "contentEntries", action: "publish", scope: "own" },
|
|
196
|
+
{ resource: "contentEntries", action: "unpublish", scope: "own" },
|
|
197
|
+
// Media - can create and manage own, read all (for embedding)
|
|
198
|
+
{ resource: "mediaItems", action: "create" },
|
|
199
|
+
{ resource: "mediaItems", action: "read", scope: "all" }, // Can read all for embedding
|
|
200
|
+
{ resource: "mediaItems", action: "update", scope: "own" },
|
|
201
|
+
{ resource: "mediaItems", action: "delete", scope: "own" },
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Viewer role - Read-only access to published content.
|
|
206
|
+
*
|
|
207
|
+
* Viewers can:
|
|
208
|
+
* - Read content type definitions
|
|
209
|
+
* - Read published content entries only
|
|
210
|
+
* - View media assets
|
|
211
|
+
* - Cannot create, update, delete, or publish any content
|
|
212
|
+
*/
|
|
213
|
+
export const VIEWER_ROLE = {
|
|
214
|
+
name: "viewer",
|
|
215
|
+
displayName: "Viewer",
|
|
216
|
+
description: "Read-only access to published content and media",
|
|
217
|
+
isSystem: true,
|
|
218
|
+
permissions: [
|
|
219
|
+
// Content types - read only
|
|
220
|
+
...readOnly("contentTypes"),
|
|
221
|
+
// Content entries - read published only (scope: "all" means all published)
|
|
222
|
+
...readOnly("contentEntries"),
|
|
223
|
+
// Media - read only
|
|
224
|
+
...readOnly("mediaItems"),
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
// =============================================================================
|
|
228
|
+
// Default Roles Collection
|
|
229
|
+
// =============================================================================
|
|
230
|
+
/**
|
|
231
|
+
* All default roles indexed by role name.
|
|
232
|
+
* Use this to look up role definitions or iterate over all roles.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```typescript
|
|
236
|
+
* // Get the admin role definition
|
|
237
|
+
* const adminDef = DEFAULT_ROLES.admin;
|
|
238
|
+
*
|
|
239
|
+
* // Iterate over all roles
|
|
240
|
+
* for (const [name, role] of Object.entries(DEFAULT_ROLES)) {
|
|
241
|
+
* console.log(`${name}: ${role.description}`);
|
|
242
|
+
* }
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
export const DEFAULT_ROLES = {
|
|
246
|
+
admin: ADMIN_ROLE,
|
|
247
|
+
editor: EDITOR_ROLE,
|
|
248
|
+
author: AUTHOR_ROLE,
|
|
249
|
+
viewer: VIEWER_ROLE,
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Array of all default role definitions.
|
|
253
|
+
* Useful for UI rendering or iterating over roles.
|
|
254
|
+
*/
|
|
255
|
+
export const DEFAULT_ROLES_LIST = Object.values(DEFAULT_ROLES);
|
|
256
|
+
// =============================================================================
|
|
257
|
+
// Permission Check Utilities
|
|
258
|
+
// =============================================================================
|
|
259
|
+
/**
|
|
260
|
+
* Check if a permission matches a requested permission.
|
|
261
|
+
* Handles scope matching (own scope only matches if requested scope is also own).
|
|
262
|
+
*
|
|
263
|
+
* @param granted - The permission that was granted to the role
|
|
264
|
+
* @param requested - The permission being requested
|
|
265
|
+
* @returns True if the granted permission satisfies the requested permission
|
|
266
|
+
*/
|
|
267
|
+
export function permissionMatches(granted, requested) {
|
|
268
|
+
// Resource and action must match
|
|
269
|
+
if (granted.resource !== requested.resource ||
|
|
270
|
+
granted.action !== requested.action) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
// Scope matching:
|
|
274
|
+
// - If granted scope is "all" (or undefined), it covers both "all" and "own" requests
|
|
275
|
+
// - If granted scope is "own", it only covers "own" requests
|
|
276
|
+
const grantedScope = granted.scope ?? "all";
|
|
277
|
+
const requestedScope = requested.scope ?? "all";
|
|
278
|
+
if (grantedScope === "all") {
|
|
279
|
+
return true; // "all" scope covers everything
|
|
280
|
+
}
|
|
281
|
+
// "own" scope only matches "own" requests
|
|
282
|
+
return requestedScope === "own";
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Check if a role has a specific permission.
|
|
286
|
+
*
|
|
287
|
+
* @param roleName - The name of the role to check
|
|
288
|
+
* @param permission - The permission to check for (resource + action + optional scope)
|
|
289
|
+
* @param customRoles - Optional custom roles to check in addition to defaults
|
|
290
|
+
* @returns True if the role has the permission
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* // Check if editor can update content entries
|
|
295
|
+
* hasPermission('editor', { resource: 'contentEntries', action: 'update' }); // true
|
|
296
|
+
*
|
|
297
|
+
* // Check if author can publish their own content
|
|
298
|
+
* hasPermission('author', { resource: 'contentEntries', action: 'publish', scope: 'own' }); // true
|
|
299
|
+
*
|
|
300
|
+
* // Check if viewer can update content
|
|
301
|
+
* hasPermission('viewer', { resource: 'contentEntries', action: 'update' }); // false
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
export function hasPermission(roleName, permission, customRoles) {
|
|
305
|
+
// Look up role in default roles first, then custom roles
|
|
306
|
+
const role = DEFAULT_ROLES[roleName] ?? customRoles?.[roleName];
|
|
307
|
+
if (!role) {
|
|
308
|
+
return false; // Unknown role has no permissions
|
|
309
|
+
}
|
|
310
|
+
// Check if any granted permission matches the requested permission
|
|
311
|
+
return role.permissions.some((p) => permissionMatches(p, permission));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get all permissions for a role.
|
|
315
|
+
*
|
|
316
|
+
* @param roleName - The name of the role
|
|
317
|
+
* @param customRoles - Optional custom roles to check in addition to defaults
|
|
318
|
+
* @returns Array of permissions, or empty array if role not found
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```typescript
|
|
322
|
+
* const editorPerms = getRolePermissions('editor');
|
|
323
|
+
* console.log(editorPerms.length); // Number of permissions
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
export function getRolePermissions(roleName, customRoles) {
|
|
327
|
+
const role = DEFAULT_ROLES[roleName] ?? customRoles?.[roleName];
|
|
328
|
+
return role?.permissions ?? [];
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get the role definition for a role name.
|
|
332
|
+
*
|
|
333
|
+
* @param roleName - The name of the role
|
|
334
|
+
* @param customRoles - Optional custom roles to check in addition to defaults
|
|
335
|
+
* @returns The role definition, or undefined if not found
|
|
336
|
+
*/
|
|
337
|
+
export function getRole(roleName, customRoles) {
|
|
338
|
+
return DEFAULT_ROLES[roleName] ?? customRoles?.[roleName];
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Check if a role name is a valid built-in role.
|
|
342
|
+
*
|
|
343
|
+
* @param name - The role name to check
|
|
344
|
+
* @returns True if it's a valid built-in role name
|
|
345
|
+
*/
|
|
346
|
+
export function isBuiltInRole(name) {
|
|
347
|
+
return roleNames.includes(name);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get all permissions for a specific resource across a role.
|
|
351
|
+
*
|
|
352
|
+
* @param roleName - The name of the role
|
|
353
|
+
* @param resource - The resource to filter by
|
|
354
|
+
* @param customRoles - Optional custom roles to check in addition to defaults
|
|
355
|
+
* @returns Array of permissions for the specified resource
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```typescript
|
|
359
|
+
* // Get all content entry permissions for editor
|
|
360
|
+
* const contentPerms = getResourcePermissions('editor', 'contentEntries');
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
export function getResourcePermissions(roleName, resource, customRoles) {
|
|
364
|
+
return getRolePermissions(roleName, customRoles).filter((p) => p.resource === resource);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Check if a role can perform any action on a resource.
|
|
368
|
+
*
|
|
369
|
+
* @param roleName - The name of the role
|
|
370
|
+
* @param resource - The resource to check
|
|
371
|
+
* @param customRoles - Optional custom roles to check in addition to defaults
|
|
372
|
+
* @returns True if the role h permission on the resource
|
|
373
|
+
*/
|
|
374
|
+
export function canAccessResource(roleName, resource, customRoles) {
|
|
375
|
+
return getResourcePermissions(roleName, resource, customRoles).length > 0;
|
|
376
|
+
}
|
|
377
|
+
// =============================================================================
|
|
378
|
+
// Custom Role Factory Functions
|
|
379
|
+
// =============================================================================
|
|
380
|
+
/**
|
|
381
|
+
* Creates a new custom role from configuration.
|
|
382
|
+
*
|
|
383
|
+
* @param config - The custom role configuration
|
|
384
|
+
* @returns A role definition ready to use with the RBAC system
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```typescript
|
|
388
|
+
* const blogAuthor = createCustomRole({
|
|
389
|
+
* name: "blog-author",
|
|
390
|
+
* displayName: "Blog Author",
|
|
391
|
+
* description: "Can create and manage blog posts",
|
|
392
|
+
* permissions: [
|
|
393
|
+
* { resource: "contentTypes", action: "read" },
|
|
394
|
+
* { resource: "contentEntries", action: "create", contentTypes: ["blog_post"] },
|
|
395
|
+
* { resource: "contentEntries", action: "read", scope: "own", contentTypes: ["blog_post"] },
|
|
396
|
+
* { resource: "contentEntries", action: "update", scope: "own", contentTypes: ["blog_post"] },
|
|
397
|
+
* { resource: "contentEntries", action: "delete", scope: "own", contentTypes: ["blog_post"] },
|
|
398
|
+
* { resource: "mediaItems", action: "create" },
|
|
399
|
+
* { resource: "mediaItems", action: "read" },
|
|
400
|
+
* ],
|
|
401
|
+
* });
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
export function createCustomRole(config) {
|
|
405
|
+
// Validate the configuration
|
|
406
|
+
if (!config.name || config.name.trim() === "") {
|
|
407
|
+
throw new Error("Custom role name is required");
|
|
408
|
+
}
|
|
409
|
+
if (isBuiltInRole(config.name)) {
|
|
410
|
+
throw new Error(`Cannot create custom role with built-in role name '${config.name}'. ` +
|
|
411
|
+
"Use extendRole() to extend a built-in role, or choose a different name.");
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
name: config.name,
|
|
415
|
+
displayName: config.displayName,
|
|
416
|
+
description: config.description,
|
|
417
|
+
permissions: config.permissions,
|
|
418
|
+
isSystem: config.isSystem ?? false,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Extends an existing role with additional or removed permissions.
|
|
423
|
+
*
|
|
424
|
+
* This function creates a new role based on an existing one, allowing you to:
|
|
425
|
+
* - Add new permissions
|
|
426
|
+
* - Remove existing permissions
|
|
427
|
+
* - Restrict all contentEntries permissions to specific content types
|
|
428
|
+
*
|
|
429
|
+
* @param config - The extend role configuration
|
|
430
|
+
* @param customRoles - Optional existing custom roles to look up the base role from
|
|
431
|
+
* @returns A new role definition with the modified permissions
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```typescript
|
|
435
|
+
* // Create a senior author who can publish their own content
|
|
436
|
+
* const seniorAuthor = extendRole({
|
|
437
|
+
* name: "senior-author",
|
|
438
|
+
* displayName: "Senior Author",
|
|
439
|
+
* description: "Author with publishing rights",
|
|
440
|
+
* extends: "author",
|
|
441
|
+
* addPermissions: [
|
|
442
|
+
* { resource: "contentEntries", action: "publish", scope: "own" },
|
|
443
|
+
* { resource: "contentEntries", action: "unpublish", scope: "own" },
|
|
444
|
+
* ],
|
|
445
|
+
* });
|
|
446
|
+
*
|
|
447
|
+
* // Create a blog-only editor
|
|
448
|
+
* const blogEditor = extendRole({
|
|
449
|
+
* name: "blog-editor",
|
|
450
|
+
* displayName: "Blog Editor",
|
|
451
|
+
* description: "Can only edit blog content",
|
|
452
|
+
* extends: "editor",
|
|
453
|
+
* restrictToContentTypes: ["blog_post", "blog_category"],
|
|
454
|
+
* });
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
export function extendRole(config, customRoles) {
|
|
458
|
+
// Validate the configuration
|
|
459
|
+
if (!config.name || config.name.trim() === "") {
|
|
460
|
+
throw new Error("Extended role name is required");
|
|
461
|
+
}
|
|
462
|
+
if (config.name === config.extends) {
|
|
463
|
+
throw new Error("Extended role name must be different from the base role name");
|
|
464
|
+
}
|
|
465
|
+
// Get the base role
|
|
466
|
+
const baseRole = getRole(config.extends, customRoles);
|
|
467
|
+
if (!baseRole) {
|
|
468
|
+
throw new Error(`Cannot extend unknown role '${config.extends}'. ` +
|
|
469
|
+
"Ensure the role exists as a built-in role or is defined in customRoles.");
|
|
470
|
+
}
|
|
471
|
+
// Start with base permissions
|
|
472
|
+
let permissions = [...baseRole.permissions];
|
|
473
|
+
// Remove specified permissions
|
|
474
|
+
if (config.removePermissions && config.removePermissions.length > 0) {
|
|
475
|
+
permissions = permissions.filter((p) => {
|
|
476
|
+
return !config.removePermissions.some((r) => r.resource === p.resource && r.action === p.action);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// Add new permissions
|
|
480
|
+
if (config.addPermissions && config.addPermissions.length > 0) {
|
|
481
|
+
permissions = [...permissions, ...config.addPermissions];
|
|
482
|
+
}
|
|
483
|
+
// Apply content type restrictions to all contentEntries permissions
|
|
484
|
+
if (config.restrictToContentTypes &&
|
|
485
|
+
config.restrictToContentTypes.length > 0) {
|
|
486
|
+
permissions = permissions.map((p) => {
|
|
487
|
+
if (p.resource === "contentEntries") {
|
|
488
|
+
return {
|
|
489
|
+
...p,
|
|
490
|
+
contentTypes: config.restrictToContentTypes,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return p;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
name: config.name,
|
|
498
|
+
displayName: config.displayName,
|
|
499
|
+
description: config.description,
|
|
500
|
+
permissions,
|
|
501
|
+
isSystem: config.isSystem ?? false,
|
|
502
|
+
extendsRole: config.extends,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Merges custom roles with the default roles.
|
|
507
|
+
*
|
|
508
|
+
* Creates a combined role registry that includes both default and custom roles.
|
|
509
|
+
* Custom roles do NOT override default roles - they exist alongside them.
|
|
510
|
+
*
|
|
511
|
+
* @param customRoles - Array of custom role definitions
|
|
512
|
+
* @returns A record of all roles (default + custom)
|
|
513
|
+
*
|
|
514
|
+
* @example
|
|
515
|
+
* ```typescript
|
|
516
|
+
* const blogAuthor = createCustomRole({...});
|
|
517
|
+
* const seniorAuthor = extendRole({...});
|
|
518
|
+
*
|
|
519
|
+
* const allRoles = mergeRolesWithDefaults([blogAuthor, seniorAuthor]);
|
|
520
|
+
* // allRoles contains: admin, editor, author, viewer, blog-author, senior-author
|
|
521
|
+
* ```
|
|
522
|
+
*/
|
|
523
|
+
export function mergeRolesWithDefaults(customRoles) {
|
|
524
|
+
const result = {
|
|
525
|
+
...DEFAULT_ROLES,
|
|
526
|
+
};
|
|
527
|
+
for (const role of customRoles) {
|
|
528
|
+
if (isBuiltInRole(role.name)) {
|
|
529
|
+
console.warn(`Warning: Custom role '${role.name}' has the same name as a built-in role. ` +
|
|
530
|
+
"The built-in role will take precedence.");
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
result[role.name] = role;
|
|
534
|
+
}
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Creates a custom roles record from an array of role definitions.
|
|
539
|
+
* Use this to pass custom roles to permission checking functions.
|
|
540
|
+
*
|
|
541
|
+
* @param roles - Array of custom role definitions
|
|
542
|
+
* @returns A record indexed by role name
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```typescript
|
|
546
|
+
* const customRoles = buildCustomRolesRecord([blogAuthor, seniorAuthor]);
|
|
547
|
+
* hasPermission("blog-author", { resource: "contentEntries", action: "create" }, customRoles);
|
|
548
|
+
* ```
|
|
549
|
+
*/
|
|
550
|
+
export function buildCustomRolesRecord(roles) {
|
|
551
|
+
const result = {};
|
|
552
|
+
for (const role of roles) {
|
|
553
|
+
result[role.name] = role;
|
|
554
|
+
}
|
|
555
|
+
return result;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Checks if a permission applies to a specific content type.
|
|
559
|
+
*
|
|
560
|
+
* @param permission - The permission to check
|
|
561
|
+
* @param contentTypeName - The content type name to check against
|
|
562
|
+
* @returns True if the permission applies to this content type
|
|
563
|
+
*/
|
|
564
|
+
function permissionAppliesToContentType(permission, contentTypeName) {
|
|
565
|
+
// If no content type specified, the permission applies
|
|
566
|
+
if (!contentTypeName) {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
// If permission has a whitelist, check if content type is in it
|
|
570
|
+
if (permission.contentTypes && permission.contentTypes.length > 0) {
|
|
571
|
+
return permission.contentTypes.includes(contentTypeName);
|
|
572
|
+
}
|
|
573
|
+
// If permission has a blacklist, check if content type is NOT in it
|
|
574
|
+
if (permission.excludeContentTypes &&
|
|
575
|
+
permission.excludeContentTypes.length > 0) {
|
|
576
|
+
return !permission.excludeContentTypes.includes(contentTypeName);
|
|
577
|
+
}
|
|
578
|
+
// No restrictions, permission applies
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Extended permission check that includes content-type-specific restrictions.
|
|
583
|
+
*
|
|
584
|
+
* Use this function when you need to check if a role can perform an action
|
|
585
|
+
* on a specific content type.
|
|
586
|
+
*
|
|
587
|
+
* @param roleName - The name of the role to check
|
|
588
|
+
* @param permission - The permission to check (resource + action + optional scope)
|
|
589
|
+
* @param options - Additional options including custom roles and content type
|
|
590
|
+
* @returns True if the role has the permission for the specified content type
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```typescript
|
|
594
|
+
* // Check if blog-author can create blog posts
|
|
595
|
+
* hasContentTypePermission("blog-author", {
|
|
596
|
+
* resource: "contentEntries",
|
|
597
|
+
* action: "create",
|
|
598
|
+
* }, {
|
|
599
|
+
* customRoles: allRoles,
|
|
600
|
+
* contentTypeName: "blog_post",
|
|
601
|
+
* }); // true
|
|
602
|
+
*
|
|
603
|
+
* // Check if blog-author can create legal documents
|
|
604
|
+
* hasContentTypePermission("blog-author", {
|
|
605
|
+
* resource: "contentEntries",
|
|
606
|
+
* action: "create",
|
|
607
|
+
* }, {
|
|
608
|
+
* customRoles: allRoles,
|
|
609
|
+
* contentTypeName: "legal_document",
|
|
610
|
+
* }); // false (restricted to blog_post only)
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
export function hasContentTypePermission(roleName, permission, options) {
|
|
614
|
+
// Get the role definition
|
|
615
|
+
const role = getRole(roleName, options?.customRoles);
|
|
616
|
+
if (!role) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
// Check if any granted permission matches
|
|
620
|
+
return role.permissions.some((p) => {
|
|
621
|
+
// Check basic permission match (resource, action, scope)
|
|
622
|
+
if (!permissionMatches(p, permission)) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
// Check content type restrictions
|
|
626
|
+
const extendedPerm = p;
|
|
627
|
+
return permissionAppliesToContentType(extendedPerm, options?.contentTypeName);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Gets all content types that a role can perform an action on.
|
|
632
|
+
*
|
|
633
|
+
* @param roleName - The name of the role
|
|
634
|
+
* @param action - The action to check
|
|
635
|
+
* @param options - Additional options
|
|
636
|
+
* @returns Array of content type names, or ["*"] if unrestricted, or [] if no permission
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* ```typescript
|
|
640
|
+
* // Get content types the blog-author can create
|
|
641
|
+
* getPermittedContentTypes("blog-author", "create", { customRoles });
|
|
642
|
+
* // Returns: ["blog_post"]
|
|
643
|
+
*
|
|
644
|
+
* // Get content types the editor can update
|
|
645
|
+
* getPermittedContentTypes("editor", "update", { customRoles });
|
|
646
|
+
* // Returns: ["*"] (unrestricted)
|
|
647
|
+
* ```
|
|
648
|
+
*/
|
|
649
|
+
export function getPermittedContentTypes(roleName, action, options) {
|
|
650
|
+
const role = getRole(roleName, options?.customRoles);
|
|
651
|
+
if (!role) {
|
|
652
|
+
return [];
|
|
653
|
+
}
|
|
654
|
+
// Find all contentEntries permissions for this action
|
|
655
|
+
const contentPerms = role.permissions.filter((p) => p.resource === "contentEntries" && p.action === action);
|
|
656
|
+
if (contentPerms.length === 0) {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
// Check if any permission is unrestricted
|
|
660
|
+
const hasUnrestricted = contentPerms.some((p) => (!p.contentTypes || p.contentTypes.length === 0) &&
|
|
661
|
+
(!p.excludeContentTypes || p.excludeContentTypes.length === 0));
|
|
662
|
+
if (hasUnrestricted) {
|
|
663
|
+
return ["*"]; // Unrestricted access
|
|
664
|
+
}
|
|
665
|
+
// Collect all permitted content types
|
|
666
|
+
const permitted = new Set();
|
|
667
|
+
for (const perm of contentPerms) {
|
|
668
|
+
if (perm.contentTypes) {
|
|
669
|
+
perm.contentTypes.forEach((ct) => permitted.add(ct));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return Array.from(permitted);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Gets all content types that a role is excluded from for an action.
|
|
676
|
+
*
|
|
677
|
+
* @param roleName - The name of the role
|
|
678
|
+
* @param action - The action to check
|
|
679
|
+
* @param options - Additional options
|
|
680
|
+
* @returns Array of excluded content type names, or [] if none
|
|
681
|
+
*/
|
|
682
|
+
export function getExcludedContentTypes(roleName, action, options) {
|
|
683
|
+
const role = getRole(roleName, options?.customRoles);
|
|
684
|
+
if (!role) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
// Find all contentEntries permissions for this action
|
|
688
|
+
const contentPerms = role.permissions.filter((p) => p.resource === "contentEntries" && p.action === action);
|
|
689
|
+
// Collect all excluded content types
|
|
690
|
+
const excluded = new Set();
|
|
691
|
+
for (const perm of contentPerms) {
|
|
692
|
+
if (perm.excludeContentTypes) {
|
|
693
|
+
perm.excludeContentTypes.forEach((ct) => excluded.add(ct));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return Array.from(excluded);
|
|
697
|
+
}
|
|
698
|
+
// =============================================================================
|
|
699
|
+
// Permission Factory Helpers for Custom Roles
|
|
700
|
+
// =============================================================================
|
|
701
|
+
/**
|
|
702
|
+
* Helper to create a full CRUD permission set for a resource with optional content type restriction.
|
|
703
|
+
*
|
|
704
|
+
* @param resource - The resource to grant permissions on
|
|
705
|
+
* @param options - Optional scope and content type restrictions
|
|
706
|
+
* @returns Array of permissions
|
|
707
|
+
*
|
|
708
|
+
* @example
|
|
709
|
+
* ```typescript
|
|
710
|
+
* // Full CRUD on contentEntries for blog_post only
|
|
711
|
+
* fullCrudForContentType("contentEntries", {
|
|
712
|
+
* contentTypes: ["blog_post"],
|
|
713
|
+
* scope: "own",
|
|
714
|
+
* });
|
|
715
|
+
* ```
|
|
716
|
+
*/
|
|
717
|
+
export function fullCrudForContentType(resource, options) {
|
|
718
|
+
const scope = options?.scope ?? "all";
|
|
719
|
+
const base = {
|
|
720
|
+
scope,
|
|
721
|
+
contentTypes: options?.contentTypes,
|
|
722
|
+
excludeContentTypes: options?.excludeContentTypes,
|
|
723
|
+
};
|
|
724
|
+
return [
|
|
725
|
+
{ resource, action: "create", ...base },
|
|
726
|
+
{ resource, action: "read", ...base },
|
|
727
|
+
{ resource, action: "update", ...base },
|
|
728
|
+
{ resource, action: "delete", ...base },
|
|
729
|
+
];
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Helper to create publish permissions with optional content type restriction.
|
|
733
|
+
*
|
|
734
|
+
* @param options - Optional scope and content type restrictions
|
|
735
|
+
* @returns Array of publish/unpublish permissions
|
|
736
|
+
*/
|
|
737
|
+
export function publishPermissionsForContentType(options) {
|
|
738
|
+
const scope = options?.scope ?? "all";
|
|
739
|
+
const base = {
|
|
740
|
+
scope,
|
|
741
|
+
contentTypes: options?.contentTypes,
|
|
742
|
+
excludeContentTypes: options?.excludeContentTypes,
|
|
743
|
+
};
|
|
744
|
+
return [
|
|
745
|
+
{ resource: "contentEntries", action: "publish", ...base },
|
|
746
|
+
{ resource: "contentEntries", action: "unpublish", ...base },
|
|
747
|
+
];
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Helper to create read-only permission with optional content type restriction.
|
|
751
|
+
*
|
|
752
|
+
* @param resource - The resource to grant read permission on
|
|
753
|
+
* @param options - Optional scope and content type restrictions
|
|
754
|
+
* @returns Array with single read permission
|
|
755
|
+
*/
|
|
756
|
+
export function readOnlyForContentType(resource, options) {
|
|
757
|
+
return [
|
|
758
|
+
{
|
|
759
|
+
resource,
|
|
760
|
+
action: "read",
|
|
761
|
+
scope: options?.scope ?? "all",
|
|
762
|
+
contentTypes: options?.contentTypes,
|
|
763
|
+
excludeContentTypes: options?.excludeContentTypes,
|
|
764
|
+
},
|
|
765
|
+
];
|
|
766
|
+
}
|
|
767
|
+
// =============================================================================
|
|
768
|
+
// Role Validation Utilities
|
|
769
|
+
// =============================================================================
|
|
770
|
+
/**
|
|
771
|
+
* Validates a custom role configuration.
|
|
772
|
+
*
|
|
773
|
+
* @param config - The custom role configuration to validate
|
|
774
|
+
* @returns An object with isValid boolean and optional error messages
|
|
775
|
+
*/
|
|
776
|
+
export function validateCustomRoleConfig(config) {
|
|
777
|
+
const errors = [];
|
|
778
|
+
// Check required fields
|
|
779
|
+
if (!config.name || config.name.trim() === "") {
|
|
780
|
+
errors.push("Role name is required");
|
|
781
|
+
}
|
|
782
|
+
if (!config.displayName || config.displayName.trim() === "") {
|
|
783
|
+
errors.push("Display name is required");
|
|
784
|
+
}
|
|
785
|
+
if (!config.description || config.description.trim() === "") {
|
|
786
|
+
errors.push("Description is required");
|
|
787
|
+
}
|
|
788
|
+
// Check for built-in name conflict
|
|
789
|
+
if (config.name && isBuiltInRole(config.name)) {
|
|
790
|
+
errors.push(`Role name '${config.name}' conflicts with a built-in role`);
|
|
791
|
+
}
|
|
792
|
+
// Validate permissions
|
|
793
|
+
if (!config.permissions || !Array.isArray(config.permissions)) {
|
|
794
|
+
errors.push("Permissions must be an array");
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
for (let i = 0; i < config.permissions.length; i++) {
|
|
798
|
+
const perm = config.permissions[i];
|
|
799
|
+
if (!resources.includes(perm.resource)) {
|
|
800
|
+
errors.push(`Permission ${i}: Invalid resource '${perm.resource}'`);
|
|
801
|
+
}
|
|
802
|
+
if (!actions.includes(perm.action)) {
|
|
803
|
+
errors.push(`Permission ${i}: Invalid action '${perm.action}'`);
|
|
804
|
+
}
|
|
805
|
+
if (perm.scope && perm.scope !== "all" && perm.scope !== "own") {
|
|
806
|
+
errors.push(`Permission ${i}: Invalid scope '${perm.scope}'`);
|
|
807
|
+
}
|
|
808
|
+
// Check for conflicting content type restrictions
|
|
809
|
+
if (perm.contentTypes && perm.excludeContentTypes) {
|
|
810
|
+
if (perm.contentTypes.length > 0 &&
|
|
811
|
+
perm.excludeContentTypes.length > 0) {
|
|
812
|
+
errors.push(`Permission ${i}: Cannot specify both contentTypes and excludeContentTypes`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
isValid: errors.length === 0,
|
|
819
|
+
errors,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Validates an extend role configuration.
|
|
824
|
+
*
|
|
825
|
+
* @param config - The extend role configuration to validate
|
|
826
|
+
* @param customRoles - Optional custom roles to check the base role in
|
|
827
|
+
* @returns An object with isValid boolean and optional error messages
|
|
828
|
+
*/
|
|
829
|
+
export function validateExtendRoleConfig(config, customRoles) {
|
|
830
|
+
const errors = [];
|
|
831
|
+
// Check required fields
|
|
832
|
+
if (!config.name || config.name.trim() === "") {
|
|
833
|
+
errors.push("Role name is required");
|
|
834
|
+
}
|
|
835
|
+
if (!config.displayName || config.displayName.trim() === "") {
|
|
836
|
+
errors.push("Display name is required");
|
|
837
|
+
}
|
|
838
|
+
if (!config.description || config.description.trim() === "") {
|
|
839
|
+
errors.push("Description is required");
|
|
840
|
+
}
|
|
841
|
+
if (!config.extends || config.extends.trim() === "") {
|
|
842
|
+
errors.push("Base role name (extends) is required");
|
|
843
|
+
}
|
|
844
|
+
// Check for self-reference
|
|
845
|
+
if (config.name === config.extends) {
|
|
846
|
+
errors.push("Cannot extend a role with itself");
|
|
847
|
+
}
|
|
848
|
+
// Check if base role exists
|
|
849
|
+
if (config.extends) {
|
|
850
|
+
const baseRole = getRole(config.extends, customRoles);
|
|
851
|
+
if (!baseRole) {
|
|
852
|
+
errors.push(`Base role '${config.extends}' does not exist`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// Validate addPermissions if provided
|
|
856
|
+
if (config.addPermissions) {
|
|
857
|
+
for (let i = 0; i < config.addPermissions.length; i++) {
|
|
858
|
+
const perm = config.addPermissions[i];
|
|
859
|
+
if (!resources.includes(perm.resource)) {
|
|
860
|
+
errors.push(`addPermissions[${i}]: Invalid resource '${perm.resource}'`);
|
|
861
|
+
}
|
|
862
|
+
if (!actions.includes(perm.action)) {
|
|
863
|
+
errors.push(`addPermissions[${i}]: Invalid action '${perm.action}'`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// Validate removePermissions if provided
|
|
868
|
+
if (config.removePermissions) {
|
|
869
|
+
for (let i = 0; i < config.removePermissions.length; i++) {
|
|
870
|
+
const perm = config.removePermissions[i];
|
|
871
|
+
if (!resources.includes(perm.resource)) {
|
|
872
|
+
errors.push(`removePermissions[${i}]: Invalid resource '${perm.resource}'`);
|
|
873
|
+
}
|
|
874
|
+
if (!actions.includes(perm.action)) {
|
|
875
|
+
errors.push(`removePermissions[${i}]: Invalid action '${perm.action}'`);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
isValid: errors.length === 0,
|
|
881
|
+
errors,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
//# sourceMappingURL=roles.js.map
|