Răsfoiți Sursa

feat(core): Expand userHasPermissions docstring; Add new userHasAllPermissions method (#4107)

Housein Abo Shaar 3 zile în urmă
părinte
comite
f2d0359086

+ 111 - 1
packages/core/src/api/common/request-context.spec.ts

@@ -1,4 +1,4 @@
-import { CurrencyCode, LanguageCode } from '@vendure/common/lib/generated-types';
+import { CurrencyCode, LanguageCode, Permission } from '@vendure/common/lib/generated-types';
 import { beforeAll, describe, expect, it } from 'vitest';
 
 import { CachedSession } from '../../config/session-cache/session-cache-strategy';
@@ -130,6 +130,74 @@ describe('RequestContext', () => {
         });
     });
 
+    describe('userHasPermissions', () => {
+        it('returns false when no session', () => {
+            const ctx = createRequestContextWithPermissions([], false);
+            expect(ctx.userHasPermissions([Permission.ReadProduct])).toBe(false);
+        });
+
+        it('returns false when user has no permissions on channel', () => {
+            const ctx = createRequestContextWithPermissions([]);
+            expect(ctx.userHasPermissions([Permission.ReadProduct])).toBe(false);
+        });
+
+        it('returns true if user has ANY of the permissions (OR logic)', () => {
+            const ctx = createRequestContextWithPermissions([Permission.ReadProduct]);
+            expect(ctx.userHasPermissions([Permission.ReadProduct, Permission.UpdateProduct])).toBe(true);
+        });
+
+        it('returns false if user has none of the permissions', () => {
+            const ctx = createRequestContextWithPermissions([Permission.ReadOrder]);
+            expect(ctx.userHasPermissions([Permission.ReadProduct, Permission.UpdateProduct])).toBe(false);
+        });
+
+        it('returns true for single permission match', () => {
+            const ctx = createRequestContextWithPermissions([
+                Permission.ReadProduct,
+                Permission.UpdateProduct,
+            ]);
+            expect(ctx.userHasPermissions([Permission.ReadProduct])).toBe(true);
+        });
+    });
+
+    describe('userHasAllPermissions', () => {
+        it('returns false when no session', () => {
+            const ctx = createRequestContextWithPermissions([], false);
+            expect(ctx.userHasAllPermissions([Permission.ReadProduct])).toBe(false);
+        });
+
+        it('returns false when user has no permissions on channel', () => {
+            const ctx = createRequestContextWithPermissions([]);
+            expect(ctx.userHasAllPermissions([Permission.ReadProduct])).toBe(false);
+        });
+
+        it('returns true if user has ALL of the permissions (AND logic)', () => {
+            const ctx = createRequestContextWithPermissions([
+                Permission.ReadProduct,
+                Permission.UpdateProduct,
+            ]);
+            expect(ctx.userHasAllPermissions([Permission.ReadProduct, Permission.UpdateProduct])).toBe(true);
+        });
+
+        it('returns false if user is missing any permission', () => {
+            const ctx = createRequestContextWithPermissions([Permission.ReadProduct]);
+            expect(ctx.userHasAllPermissions([Permission.ReadProduct, Permission.UpdateProduct])).toBe(false);
+        });
+
+        it('returns true for empty permissions array', () => {
+            const ctx = createRequestContextWithPermissions([Permission.ReadProduct]);
+            expect(ctx.userHasAllPermissions([])).toBe(true);
+        });
+
+        it('returns true for single permission match', () => {
+            const ctx = createRequestContextWithPermissions([
+                Permission.ReadProduct,
+                Permission.UpdateProduct,
+            ]);
+            expect(ctx.userHasAllPermissions([Permission.ReadProduct])).toBe(true);
+        });
+    });
+
     function createRequestContext(req?: any) {
         const activeOrder = new Order({
             id: '55555',
@@ -173,4 +241,46 @@ describe('RequestContext', () => {
             authorizedAsOwnerOnly: false,
         });
     }
+
+    function createRequestContextWithPermissions(permissions: Permission[], withSession = true) {
+        const zone = new Zone({
+            id: '62626',
+            name: 'Europe',
+        });
+        const channel = new Channel({
+            token: 'oiajwodij09au3r',
+            id: '995859',
+            code: '__default_channel__',
+            defaultCurrencyCode: CurrencyCode.EUR,
+            pricesIncludeTax: true,
+            defaultLanguageCode: LanguageCode.en,
+            defaultShippingZone: zone,
+            defaultTaxZone: zone,
+        });
+        const session: CachedSession | undefined = withSession
+            ? {
+                  cacheExpiry: Number.MAX_SAFE_INTEGER,
+                  expires: new Date(),
+                  id: '1234',
+                  token: '2d37187e9e8fc47807fe4f58ca',
+                  activeOrderId: '123',
+                  user: {
+                      id: '8833774',
+                      identifier: 'user',
+                      verified: true,
+                      channelPermissions: [
+                          { id: channel.id, token: channel.token, code: channel.code, permissions },
+                      ],
+                  },
+              }
+            : undefined;
+        return new RequestContext({
+            apiType: 'admin',
+            languageCode: LanguageCode.en,
+            channel,
+            session,
+            isAuthorized: true,
+            authorizedAsOwnerOnly: false,
+        });
+    }
 });

+ 39 - 2
packages/core/src/api/common/request-context.ts

@@ -4,7 +4,7 @@ import { ID, JsonCompatible } from '@vendure/common/lib/shared-types';
 import { isObject } from '@vendure/common/lib/shared-utils';
 import { Request } from 'express';
 import { TFunction } from 'i18next';
-import { ReplicationMode, EntityManager } from 'typeorm';
+import { EntityManager, ReplicationMode } from 'typeorm';
 
 import {
     REQUEST_CONTEXT_KEY,
@@ -254,7 +254,16 @@ export class RequestContext {
     /**
      * @description
      * Returns `true` if there is an active Session & User associated with this request,
-     * and that User has the specified permissions on the active Channel.
+     * and that User has **at least one** of the specified permissions on the active Channel.
+     *
+     * This method uses OR logic - it checks if the user has ANY of the given permissions,
+     * not ALL of them. For AND logic, use {@link userHasAllPermissions}.
+     *
+     * @example
+     * ```ts
+     * // Returns true if user has ReadProduct OR ReadCatalog
+     * ctx.userHasPermissions([Permission.ReadProduct, Permission.ReadCatalog]);
+     * ```
      */
     userHasPermissions(permissions: Permission[]): boolean {
         const user = this.session?.user;
@@ -268,6 +277,34 @@ export class RequestContext {
         return false;
     }
 
+    /**
+     * @description
+     * Returns `true` if there is an active Session & User associated with this request,
+     * and that User has **all** of the specified permissions on the active Channel.
+     *
+     * This method uses AND logic - it checks if the user has EVERY one of the given permissions.
+     * For OR logic (any permission), use {@link userHasPermissions}.
+     *
+     * @example
+     * ```ts
+     * // Returns true only if user has BOTH ReadProduct AND UpdateProduct
+     * ctx.userHasAllPermissions([Permission.ReadProduct, Permission.UpdateProduct]);
+     * ```
+     *
+     * @since 3.6.0
+     */
+    userHasAllPermissions(permissions: Permission[]): boolean {
+        const user = this.session?.user;
+        if (!user || !this.channelId) {
+            return false;
+        }
+        const permissionsOnChannel = user.channelPermissions.find(c => idsAreEqual(c.id, this.channelId));
+        if (permissionsOnChannel) {
+            return permissions.every(permission => permissionsOnChannel.permissions.includes(permission));
+        }
+        return false;
+    }
+
     /**
      * @description
      * Serializes the RequestContext object into a JSON-compatible simple object.

+ 13 - 0
packages/core/src/api/decorators/allow.decorator.ts

@@ -8,6 +8,9 @@ export const PERMISSIONS_METADATA_KEY = '__permissions__';
  * Attaches metadata to the resolver defining which permissions are required to execute the
  * operation, using one or more {@link Permission} values.
  *
+ * When multiple permissions are specified, the user needs only **one** of them (OR logic).
+ * This is useful for operations that can be performed by users with different permission sets.
+ *
  * In a GraphQL context, it can be applied to top-level queries and mutations as well as field resolvers.
  *
  * For REST controllers, it can be applied to route handler.
@@ -32,6 +35,16 @@ export const PERMISSIONS_METADATA_KEY = '__permissions__';
  *  }
  * ```
  *
+ * @example
+ * ```ts
+ *  // User needs ReadProduct OR ReadCatalog (either grants access)
+ *  \@Allow(Permission.ReadProduct, Permission.ReadCatalog)
+ *  \@Query()
+ *  getProducts() {
+ *      // ...
+ *  }
+ * ```
+ *
  * @docsCategory request
  * @docsPage Allow Decorator
  */