Skip to content

feat: add checkPermission to all loyalty resolvers and define permiss#7622

Merged
munkhsaikhan merged 8 commits into
mainfrom
feat/loyalty-permission-checks
May 19, 2026
Merged

feat: add checkPermission to all loyalty resolvers and define permiss#7622
munkhsaikhan merged 8 commits into
mainfrom
feat/loyalty-permission-checks

Conversation

@sekulya
Copy link
Copy Markdown
Collaborator

@sekulya sekulya commented May 8, 2026

…ions config

Summary by CodeRabbit

  • New Features

    • Role-based access control for loyalty with default admin/user/viewer groups and comprehensive permission sets.
  • Refactor

    • GraphQL endpoints across loyalty modules now require and enforce permission checks for viewing, creating, editing, removing, buying/sharing/refunding, and campaign operations.
  • Bug Fixes

    • Campaign and search filters now escape special characters to ensure correct, literal matching.

Review Change Stack

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @sekulya, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an exported permissions config for the loyalty plugin and inserts awaited checkPermission(...) calls into GraphQL resolvers across loyalty modules; campaign search regexes are escaped to treat user input as literal text.

Changes

Loyalty Plugin Authorization System

Layer / File(s) Summary
Permission configuration
backend/plugins/loyalty_api/src/meta/permissions.ts
Adds exported permissions object enumerating loyalty modules, scopes, actions (view actions flagged always: true), ownerFields for some modules, and defaultGroups (loyalty:admin, loyalty:user, loyalty:viewer).
Resolver permission gates & campaign filter fixes
backend/plugins/loyalty_api/src/modules/*/graphql/resolvers/{mutations,queries}/*.ts
Every affected GraphQL resolver now destructures checkPermission from IContext and awaits operation-specific permission strings before calling models.*; campaign search filters updated to use escapeRegExp(params.searchValue) with case-insensitive regexes where applicable. Applies across agent, assignment, assignmentCampaign, config, loyalty, coupon, couponCampaign, donate, donateCampaign, lottery, lotteryCampaign, pricing, pricingPlan, scoreCampaign, scoreLog, spin, spinCampaign, voucher, voucherCampaign.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • erxes/erxes#7684: Frontend GraphQL/UI work for score campaign owner-score checking and refunds that depends on backend score resolver gates.

Poem

🐰 I hopped through resolvers, one by one,

Awaiting gates before they run.
Permissions set and searches tamed,
A tidy plugin—no calls unnamed.
🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is truncated and incomplete. It reads 'feat: add checkPermission to all loyalty resolvers and define permiss' but cuts off mid-word, making it impossible to understand the complete change. Complete the title with the full phrase (e.g., 'feat: add checkPermission to all loyalty resolvers and define permissions config') to clearly summarize the entire changeset.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/loyalty-permission-checks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

🌗 Pull Request Overview

This PR adds comprehensive permission checks to all loyalty plugin GraphQL resolvers and defines a complete permission configuration with modules, actions, and default groups (admin, user, viewer).

Reviewed Changes
Kimi performed full review on 33 changed files and found 6 issues.

Show a summary per file
File Description
backend/plugins/loyalty_api/src/meta/permissions.ts New comprehensive permission configuration file defining all modules, actions, scopes, and default permission groups
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/mutations/agent.ts Added checkPermission calls to all agent mutation resolvers (createAgent, updateAgent, removeAgent, etc.)
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/queries/agent.ts Added checkPermission('agentView') to all agent query resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignment.ts Added checkPermission calls to assignment mutation resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts Added checkPermission calls to assignment campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts Added checkPermission('assignmentView') to assignment query resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts Added checkPermission('assignmentCampaignView') to assignment campaign query resolvers
backend/plugins/loyalty_api/src/modules/config/graphql/mutations/config.ts Added checkPermission('loyaltyConfigUpdate') to loyalty config update mutation
backend/plugins/loyalty_api/src/modules/config/graphql/mutations/loyalty.ts Added checkPermission to loyalty share score and confirm voucher mutations
backend/plugins/loyalty_api/src/modules/config/graphql/queries/config.ts Added checkPermission('loyaltyConfigView') to loyalty config query
backend/plugins/loyalty_api/src/modules/config/graphql/queries/loyalty.ts Added checkPermission to loyalty check and view queries
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/coupon.ts Added checkPermission calls to coupon mutation resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts Added checkPermission calls to coupon campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/coupon.ts Added checkPermission('couponView') to coupon query resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/couponCampaign.ts Added checkPermission('couponCampaignView') to coupon campaign query resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donate.ts Added checkPermission calls to donate mutation resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donateCampaign.ts Added checkPermission calls to donate campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donate.ts Added checkPermission('donateView') to donate query resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts Added checkPermission('donateCampaignView') to donate campaign query resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts CRITICAL: File content completely replaced with query logic - all mutations lost
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts CRITICAL: File content completely replaced with query logic - all mutations lost
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lottery.ts Added checkPermission('lotteryView') to lottery query resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts Added checkPermission('lotteryCampaignView') to lottery campaign query resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricing.ts Added checkPermission calls to pricing mutation resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricingPlan.ts Added checkPermission calls to pricing plan mutation resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricing.ts Added checkPermission('pricingView') to pricing query resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricingPlan.ts Added checkPermission('pricingPlanView') to pricing plan query resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreCampaign.ts Added checkPermission calls to score campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreLog.ts Added checkPermission('scoreLogChange') to score log change mutation
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreCampaign.ts Added checkPermission('scoreCampaignView') to score campaign query resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreLog.ts Added checkPermission('scoreLogView') to score log query resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts Added checkPermission calls to spin mutation resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spinCampaign.ts Added checkPermission calls to spin campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spin.ts Added checkPermission('spinView') to spin query resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts Added checkPermission('spinCampaignView') to spin campaign query resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucher.ts Added checkPermission calls to voucher mutation resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts Added checkPermission calls to voucher campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucher.ts Added checkPermission('voucherView') to voucher query resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucherCampaign.ts Added checkPermission('voucherCampaignView') to voucher campaign query resolvers

📋 Review Findings

📄 backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts

🔴 CRITICAL functionality: File content completely replaced - all mutations lost

Lines 1-77

The entire file content was replaced with query logic instead of mutations. The original mutation resolvers (lotteriesAdd, lotteriesEdit, lotteriesRemove, buyLottery) are completely lost and replaced with query functions (lotteries, lotteriesMain). This will break the API.

Original mutations that were deleted:

  • lotteriesAdd - Create lottery
  • lotteriesEdit - Update lottery
  • lotteriesRemove - Remove lotteries
  • buyLottery - Buy lottery ticket

💡 Suggested fix:

Restore the original mutations file with permission checks:

import { ILottery } from '@/lottery/@types/lottery';
import { IContext } from '~/connectionResolvers';
import { IBuyParams } from '~/utils';

export const lotteryMutations = {
  async lotteriesAdd(_root: undefined, doc: ILottery, { models, checkPermission }: IContext) {
    await checkPermission('lotteryCreate');
    return models.Lotteries.createLottery(doc);
  },

  async lotteriesEdit(
    _root: undefined,
    { _id, ...doc }: ILottery & { _id: string },
    { models, user, checkPermission }: IContext,
  ) {
    await checkPermission('lotteryEdit');
    return models.Lotteries.updateLottery(_id, { ...doc, userId: user._id });
  },

  async lotteriesRemove(
    _root: undefined,
    { _ids }: { _ids: string[] },
    { models, checkPermission }: IContext,
  ) {
    await checkPermission('lotteryRemove');
    return models.Lotteries.removeLotteries(_ids);
  },

  async buyLottery(_root: undefined, param: IBuyParams, { models, checkPermission }: IContext) {
    await checkPermission('lotteryBuy');
    return models.Lotteries.buyLottery(param);
  },
};

📄 backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts

🔴 CRITICAL functionality: File content completely replaced - all mutations lost

Lines 1-102

Same issue as above - the entire mutation file was replaced with query logic. Original mutations (lotteryCampaignsAdd, lotteryCampaignsEdit, lotteryCampaignsRemove, doLottery, doLotteryMultiple, getNextChar) are lost.

Original mutations that were deleted:

  • lotteryCampaignsAdd - Create lottery campaign
  • lotteryCampaignsEdit - Update lottery campaign
  • lotteryCampaignsRemove - Remove lottery campaigns
  • doLottery - Perform lottery draw
  • doLotteryMultiple - Multiple lottery draw
  • getNextChar - Get next lottery character

💡 Suggested fix:

Restore the original mutations file with permission checks:

import { ILotteryCampaign } from '@/lottery/@types/lotteryCampaign';
import { IContext } from '~/connectionResolvers';

export const lotteryCampaignMutations = {
  async lotteryCampaignsAdd(
    _root: undefined,
    doc: ILotteryCampaign,
    { models, checkPermission }: IContext,
  ) {
    await checkPermission('lotteryCampaignCreate');
    return models.LotteryCampaigns.createLotteryCampaign(doc);
  },

  async lotteryCampaignsEdit(
    _root: undefined,
    { _id, ...doc }: ILotteryCampaign & { _id: string },
    { models, checkPermission }: IContext,
  ) {
    await checkPermission('lotteryCampaignEdit');
    return models.LotteryCampaigns.updateLotteryCampaign(_id, doc);
  },

  async lotteryCampaignsRemove(
    _root: undefined,
    { _ids }: { _ids: string[] },
    { models, checkPermission }: IContext,
  ) {
    await checkPermission('lotteryCampaignRemove');
    return models.LotteryCampaigns.removeLotteryCampaigns(_ids);
  },

  async doLottery(
    _root: undefined,
    params: { campaignId: string; awardId: string },
    { models, checkPermission }: IContext,
  ) {
    await checkPermission('lotteryCampaignDo');
    return models.LotteryCampaigns.doLottery(params);
  },

  async doLotteryMultiple(
    _root: undefined,
    params: { campaignId: string; awardId: string; multiple: number },
    { models, checkPermission }: IContext,
  ) {
    await checkPermission('lotteryCampaignDo');
    return models.LotteryCampaigns.multipleDoLottery(params);
  },

  async getNextChar(
    _root: undefined,
    params: { campaignId: string; awardId: string; prevChars: string },
    { models, checkPermission }: IContext,
  ) {
    await checkPermission('lotteryCampaignDo');
    return models.LotteryCampaigns.getNextChar(params);
  },
};

📄 backend/plugins/loyalty_api/src/meta/permissions.ts

🟡 MEDIUM code_quality: Missing newline at end of file

Line 1024

The file is missing a newline at the end. While not critical, it's a POSIX standard convention.

💡 Suggested fix:

Add a newline at the end of the file.


📄 backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts

🟡 MEDIUM logic: checkAssignment query may need loyaltyCheck permission instead

Line 55-58

The checkAssignment query uses checkPermission('assignmentView') but based on the permissions defined, there is a separate loyaltyCheck permission. If this query is used for checking voucher eligibility, it might need the loyaltyCheck permission instead.

Current code:

await checkPermission('assignmentView');

However, looking at the context and how similar queries work, this might be intentional since it's fetching assignment data. Verify this is the correct permission.


📄 backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donate.ts

🟡 MEDIUM logic: Inconsistent scope for cpDonatesAdd vs donatesAdd

Lines 23-24 and 33-34

The donatesAdd uses scope 'own' in the defaultGroups for Loyalty User, but cpDonatesAdd (customer portal) calls the same permission check without considering the user context difference. This may be intentional but should be verified that the customer portal mutations properly validate the user context.


📄 backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts

🟢 LOW naming: Inconsistent naming - permission uses 'couponCampaignEdit' but mutation uses 'couponCampaignEdit'

Lines 17-18

The permission name is couponCampaignEdit (consistent with action name), but note that in permissions.ts the action is defined as couponCampaignEdit while other modules use Edit suffix. This is actually consistent - just noting for verification.


Summary

The PR contains 2 CRITICAL issues where mutation files were completely replaced with query logic, resulting in loss of all mutation functionality for the lottery module. These files need to be restored with the original mutation logic plus the permission checks added.

The remaining issues are minor (missing newline, permission naming verification).

Action Required:

  1. Restore backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts with original mutations
  2. Restore backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts with original mutations

Powered by Kimi | Model: kimi-k2.5

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts (1)

42-55: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

cpDonateCampaigns should not gate on an admin permission.

The cp prefix in erxes conventionally identifies customer portal (clientportal) endpoints, which are served to unauthenticated customers/contacts — not to staff users with RBAC permissions. Applying await checkPermission('donateCampaignView') here will throw a "permission denied" error for every customer portal request, effectively disabling the feature for end-users.

The admin permission guard should only be on donateCampaigns and donateCampaignDetail. For cpDonateCampaigns, either remove the permission check entirely or replace it with a customer-portal authentication guard (e.g., checking context.cpUser), consistent with how other cp* resolvers in the codebase are secured.

🐛 Proposed fix
  async cpDonateCampaigns(
    _root: undefined,
    _args: undefined,
-   { models, checkPermission }: IContext,
+   { models }: IContext,
  ) {
-   await checkPermission('donateCampaignView');
    const now = new Date();
    return models.DonateCampaigns.find({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts`
around lines 42 - 55, The cpDonateCampaigns resolver currently calls await
checkPermission('donateCampaignView') which blocks unauthenticated
customer-portal access; remove that admin RBAC guard from cpDonateCampaigns (or
replace it with a customer-portal check such as verifying context.cpUser) so
that the resolver simply queries models.DonateCampaigns for active campaigns
(like donateCampaigns/donateCampaignDetail use admin guards but cp* endpoints
use cpUser). Ensure any replacement uses the same cp authentication pattern as
other cp* resolvers in the codebase rather than checkPermission.
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts (1)

51-64: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

cpSpinCampaigns must not gate on an admin permission — this will break the customer portal.

The cp prefix in erxes marks a customer portal resolver called by end-customers, not admins. Applying checkPermission('spinCampaignView') (an admin-level action) here fails all customer requests, making the spin campaign list inaccessible. This pattern is established across the codebase: cpForms, cpBmsOrders, cpDeals, cpPayments, cpWebPages, and many other cp* resolvers in frontline_api, sales_api, payment_api, and other plugins do not call permission checks. The sales_api plugin even includes an explicit comment: "Client portal – no permission check."

Remove the permission check from cpSpinCampaigns:

Suggested diff
  async cpSpinCampaigns(
    _root: undefined,
    _args: undefined,
-   { models, checkPermission }: IContext,
+   { models }: IContext,
  ) {
-   await checkPermission('spinCampaignView');
    const now = new Date();

Note: This issue also affects other cp* resolvers in loyalty_api (cpVoucherCampaigns, cpDonateCampaigns, cpLotteryCampaigns, cpAssignmentCampaigns) which similarly gate on admin permissions and should be reviewed for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts`
around lines 51 - 64, The resolver cpSpinCampaigns is incorrectly enforcing an
admin permission by calling checkPermission('spinCampaignView'); remove that
permission check from the cpSpinCampaigns function so the customer portal can
access active campaigns (keep the date filtering and sort logic intact in
models.SpinCampaigns.find(...).sort(...)). Also audit and remove similar
checkPermission calls in other customer-facing resolvers in this file/plugin
(cpVoucherCampaigns, cpDonateCampaigns, cpLotteryCampaigns,
cpAssignmentCampaigns) to match the established cp* pattern of no permission
gating.
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts (1)

59-207: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

checkAssignment performs write operations but is gated on a read-only assignmentView permission.

checkAssignment is declared as a query but internally it:

  1. Calls awardAssignmentCampaign() which creates Voucher and Assignment records (lines 75-89)
  2. Issues a tRPC updateOne mutation that writes to customer customFieldsData (lines 164–182)

Protecting these write side-effects behind only assignmentView violates least-privilege: any user with view-only access can trigger actual data creation. The permission should be assignmentCreate to match the actual operation semantics, since assignmentCreate already exists in the permission model and is used for other mutations in this module.

-   await checkPermission('assignmentView');
+   await checkPermission('assignmentCreate');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts`
around lines 59 - 207, The resolver checkAssignment (in this file) performs
writes via models.AssignmentCampaigns.awardAssignmentCampaign and
sendTRPCMessage(..., action: 'updateOne') but only checks
checkPermission('assignmentView'); change the permission check to
checkPermission('assignmentCreate') and also ensure the resolver is exposed as a
mutation (not a read-only query) in your GraphQL schema/registration so its
intent matches side effects (move or re-register checkAssignment from queries to
mutations if applicable); keep all existing calls to awardAssignmentCampaign,
sendTRPCMessage and generateFieldMaxValue unchanged.
🧹 Nitpick comments (3)
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucher.ts (1)

5-46: ⚡ Quick win

Inconsistent permission action naming across modules (voucherEdit vs *Update elsewhere).

The voucher mutations use voucherEdit (Line 21) while sibling modules use the *Update suffix (e.g., agentUpdate, pricingPlanUpdate). This inconsistency complicates permission management — admins assigning permissions must know which modules use Edit vs Update.

Consider standardising to either voucherUpdate or align the other modules' permission strings to *Edit within the central permissions.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucher.ts`
around lines 5 - 46, The voucherMutations file is using the permission string
'voucherEdit' in the vouchersEdit resolver (checked via checkPermission), which
is inconsistent with the rest of the codebase that uses a '*Update' suffix;
change the permission check in vouchersEdit from 'voucherEdit' to
'voucherUpdate' (i.e., update the argument passed to checkPermission in
vouchersEdit) and then update the central permissions list in permissions.ts to
include 'voucherUpdate' (remove or alias 'voucherEdit' if present) so permission
names are consistent across modules.
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/mutations/agent.ts (1)

4-49: ⚡ Quick win

Three pairs of fully duplicate resolver implementations — DRY violation.

createAgent/agentsAdd, updateAgent/agentsEdit, and removeAgent/agentsRemove each share exactly the same permission check and model call. Any future change (added validation, error handling, permission rename) must be applied in both places, and having them diverge silently is a real maintenance risk.

If both names must exist for backwards compatibility, delegate to a shared implementation:

♻️ Proposed refactor
 export const agentMutations = {
-  async createAgent(_root: undefined, doc: IAgent, { models, checkPermission }: IContext) {
-    await checkPermission('agentCreate')
-    return models.Agents.createAgent(doc)
-  },
-
-  async updateAgent(
-    _root: undefined,
-    { _id, ...doc }: { _id: string } & IAgent,
-    { models, checkPermission }: IContext,
-  ) {
-    await checkPermission('agentUpdate')
-    return models.Agents.updateAgent(_id, doc)
-  },
-
-  async removeAgent(
-    _root: undefined,
-    { _id }: { _id: string },
-    { models, checkPermission }: IContext,
-  ) {
-    await checkPermission('agentRemove')
-    return models.Agents.removeAgent(_id)
-  },
-
   async agentsAdd(_root: undefined, doc: IAgent, { models, checkPermission }: IContext) {
     await checkPermission('agentCreate')
     return models.Agents.createAgent(doc)
   },

+  createAgent: agentMutations.agentsAdd,

   async agentsEdit(
     _root: undefined,
     { _id, ...doc }: { _id: string } & IAgent,
     { models, checkPermission }: IContext,
   ) {
     await checkPermission('agentUpdate')
     return models.Agents.updateAgent(_id, doc)
   },

+  updateAgent: agentMutations.agentsEdit,

   async agentsRemove(
     _root: undefined,
     { _id }: { _id: string },
     { models, checkPermission }: IContext,
   ) {
     await checkPermission('agentRemove')
     return models.Agents.removeAgent(_id)
   },
+
+  removeAgent: agentMutations.agentsRemove,
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/mutations/agent.ts`
around lines 4 - 49, The three resolver pairs (createAgent/agentsAdd,
updateAgent/agentsEdit, removeAgent/agentsRemove) are exact duplicates causing a
DRY violation; refactor by extracting a single implementation per operation
(e.g., doCreateAgent, doUpdateAgent, doRemoveAgent or keep
createAgent/updateAgent/removeAgent as the canonical functions) and have the
duplicate names delegate to those implementations (agentsAdd -> createAgent,
agentsEdit -> updateAgent, agentsRemove -> removeAgent) so permission checks and
model calls live in one place (refer to createAgent, updateAgent, removeAgent,
agentsAdd, agentsEdit, agentsRemove).
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts (1)

30-31: ⚡ Quick win

Use the correct type narrowing for voucherCampaignId in generateFilter.

voucherCampaignId is defined on ILotteryMainParams, but the code casts to ILotteryParams, which is misleading and weakens type safety.

Suggested fix
-  if ((params as ILotteryParams).voucherCampaignId) {
-    filter.voucherCampaignId = (params as ILotteryParams).voucherCampaignId;
+  if ('voucherCampaignId' in params && params.voucherCampaignId) {
+    filter.voucherCampaignId = params.voucherCampaignId;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts`
around lines 30 - 31, The code in generateFilter incorrectly narrows params to
ILotteryParams when checking voucherCampaignId; voucherCampaignId actually lives
on ILotteryMainParams—change the type guard/cast to use ILotteryMainParams (or
refine the params union check) so you read (params as
ILotteryMainParams).voucherCampaignId (or perform an instanceof/type-predicate
check) before assigning to filter.voucherCampaignId to restore correct type
safety and avoid the misleading ILotteryParams cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/plugins/loyalty_api/src/meta/permissions.ts`:
- Around line 956-1022: The loyalty:viewer permission set is missing campaign-
and pricing-related read permissions, causing incomplete read-only access;
update the permissions array inside the object with id 'loyalty:viewer' to add
entries for modules voucherCampaign, lotteryCampaign, spinCampaign,
couponCampaign, donateCampaign, assignmentCampaign and pricing/pricingPlan each
granting their respective view actions (e.g. voucherCampaignView,
lotteryCampaignView, spinCampaignView, couponCampaignView, donateCampaignView,
assignmentCampaignView, pricingView/pricingPlanView) with scope 'all' so viewers
can see both entities and their campaign/configuration context.
- Around line 9-16: The permission entries for the donate and scoreLog modules
(and any other module configured with scope 'own' in defaultGroups such as the
loyalty:user group) currently have ownerFields: [] so the 'own' scope cannot
enforce ownership; update the module permission objects for "donate" and
"scoreLog" to set ownerFields: ['ownerId'] (and similarly populate
ownerFields:['ownerId'] for any other modules listed with scope: 'own' in
defaultGroups) so the permission framework can filter records by ownerId; locate
the module definitions named donate and scoreLog in permissions.ts and replace
their ownerFields arrays accordingly.

In
`@backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts`:
- Around line 30-31: The resolver functions in lotteryCampaign.ts destructure
context as "{ models, checkPermission }: IContext" but omit subdomain; update
each resolver parameter to destructure subdomain as well (e.g., "{ models,
checkPermission, subdomain }: IContext") so tenant-scoped model access uses the
correct tenant context; ensure every occurrence mentioned (the resolver
parameter locations corresponding to lines around the existing "{ models,
checkPermission }: IContext" entries) is updated consistently and any downstream
calls that rely on context pass/consume subdomain as needed.
- Around line 13-15: The current code directly builds a RegExp from
params.searchValue and assigns it to filter.name, which risks ReDoS and
performance issues; change this to escape any regex metacharacters before
constructing the regex (e.g., implement or import an escapeRegExp function and
use it on params.searchValue) or avoid RegExp entirely by using a safe DB text
match (e.g., use a $regex value built from the escaped string with the 'i' flag
or use a case-insensitive index/search method). Update the assignment that sets
filter.name (where params.searchValue is read) to use the escaped value or safe
search approach so untrusted input cannot inject regex patterns.

In
`@backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts`:
- Around line 34-37: The resolver doSpin is receiving the entire GraphQL args
object as the spinId parameter, so models.Spins.doSpin gets { spinId: "..." }
instead of a string; update the doSpin signature to destructure the args object
to extract the spinId string (e.g., change the second parameter from spinId:
string to { spinId }: { spinId: string } or similar), keep the permission check
via checkPermission('spinDo'), and then call models.Spins.doSpin(spinId) with
the extracted string.

---

Outside diff comments:
In
`@backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts`:
- Around line 59-207: The resolver checkAssignment (in this file) performs
writes via models.AssignmentCampaigns.awardAssignmentCampaign and
sendTRPCMessage(..., action: 'updateOne') but only checks
checkPermission('assignmentView'); change the permission check to
checkPermission('assignmentCreate') and also ensure the resolver is exposed as a
mutation (not a read-only query) in your GraphQL schema/registration so its
intent matches side effects (move or re-register checkAssignment from queries to
mutations if applicable); keep all existing calls to awardAssignmentCampaign,
sendTRPCMessage and generateFieldMaxValue unchanged.

In
`@backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts`:
- Around line 42-55: The cpDonateCampaigns resolver currently calls await
checkPermission('donateCampaignView') which blocks unauthenticated
customer-portal access; remove that admin RBAC guard from cpDonateCampaigns (or
replace it with a customer-portal check such as verifying context.cpUser) so
that the resolver simply queries models.DonateCampaigns for active campaigns
(like donateCampaigns/donateCampaignDetail use admin guards but cp* endpoints
use cpUser). Ensure any replacement uses the same cp authentication pattern as
other cp* resolvers in the codebase rather than checkPermission.

In
`@backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts`:
- Around line 51-64: The resolver cpSpinCampaigns is incorrectly enforcing an
admin permission by calling checkPermission('spinCampaignView'); remove that
permission check from the cpSpinCampaigns function so the customer portal can
access active campaigns (keep the date filtering and sort logic intact in
models.SpinCampaigns.find(...).sort(...)). Also audit and remove similar
checkPermission calls in other customer-facing resolvers in this file/plugin
(cpVoucherCampaigns, cpDonateCampaigns, cpLotteryCampaigns,
cpAssignmentCampaigns) to match the established cp* pattern of no permission
gating.

---

Nitpick comments:
In
`@backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/mutations/agent.ts`:
- Around line 4-49: The three resolver pairs (createAgent/agentsAdd,
updateAgent/agentsEdit, removeAgent/agentsRemove) are exact duplicates causing a
DRY violation; refactor by extracting a single implementation per operation
(e.g., doCreateAgent, doUpdateAgent, doRemoveAgent or keep
createAgent/updateAgent/removeAgent as the canonical functions) and have the
duplicate names delegate to those implementations (agentsAdd -> createAgent,
agentsEdit -> updateAgent, agentsRemove -> removeAgent) so permission checks and
model calls live in one place (refer to createAgent, updateAgent, removeAgent,
agentsAdd, agentsEdit, agentsRemove).

In
`@backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts`:
- Around line 30-31: The code in generateFilter incorrectly narrows params to
ILotteryParams when checking voucherCampaignId; voucherCampaignId actually lives
on ILotteryMainParams—change the type guard/cast to use ILotteryMainParams (or
refine the params union check) so you read (params as
ILotteryMainParams).voucherCampaignId (or perform an instanceof/type-predicate
check) before assigning to filter.voucherCampaignId to restore correct type
safety and avoid the misleading ILotteryParams cast.

In
`@backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucher.ts`:
- Around line 5-46: The voucherMutations file is using the permission string
'voucherEdit' in the vouchersEdit resolver (checked via checkPermission), which
is inconsistent with the rest of the codebase that uses a '*Update' suffix;
change the permission check in vouchersEdit from 'voucherEdit' to
'voucherUpdate' (i.e., update the argument passed to checkPermission in
vouchersEdit) and then update the central permissions list in permissions.ts to
include 'voucherUpdate' (remove or alias 'voucherEdit' if present) so permission
names are consistent across modules.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 088830bb-1623-43cd-917e-f9119492331f

📥 Commits

Reviewing files that changed from the base of the PR and between b215a23 and 57acea5.

📒 Files selected for processing (39)
  • backend/plugins/loyalty_api/src/meta/permissions.ts
  • backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/mutations/agent.ts
  • backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/queries/agent.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignment.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts
  • backend/plugins/loyalty_api/src/modules/config/graphql/mutations/config.ts
  • backend/plugins/loyalty_api/src/modules/config/graphql/mutations/loyalty.ts
  • backend/plugins/loyalty_api/src/modules/config/graphql/queries/config.ts
  • backend/plugins/loyalty_api/src/modules/config/graphql/queries/loyalty.ts
  • backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/coupon.ts
  • backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts
  • backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/coupon.ts
  • backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/couponCampaign.ts
  • backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donate.ts
  • backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donateCampaign.ts
  • backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donate.ts
  • backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lottery.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricing.ts
  • backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricingPlan.ts
  • backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricing.ts
  • backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricingPlan.ts
  • backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreCampaign.ts
  • backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreLog.ts
  • backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreCampaign.ts
  • backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreLog.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spinCampaign.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spin.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts
  • backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucher.ts
  • backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts
  • backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucher.ts
  • backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucherCampaign.ts

Comment thread backend/plugins/loyalty_api/src/meta/permissions.ts
Comment thread backend/plugins/loyalty_api/src/meta/permissions.ts
Comment on lines +30 to 31
{ models, checkPermission }: IContext,
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include subdomain in resolver context for tenant-isolated data access.

These resolvers access tenant-scoped models but omit subdomain in context destructuring, which violates the backend multi-tenancy rule.

As per coding guidelines, "backend/plugins//src/**/.ts: Always include subdomain in context for data access to maintain multi-tenancy isolation; models are automatically scoped to subdomain collections" and "backend/**/*.ts: Always use the subdomain context parameter for tenant-isolated data access in multi-tenant resolvers and services".

Also applies to: 45-46, 60-61, 69-70, 88-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts`
around lines 30 - 31, The resolver functions in lotteryCampaign.ts destructure
context as "{ models, checkPermission }: IContext" but omit subdomain; update
each resolver parameter to destructure subdomain as well (e.g., "{ models,
checkPermission, subdomain }: IContext") so tenant-scoped model access uses the
correct tenant context; ensure every occurrence mentioned (the resolver
parameter locations corresponding to lines around the existing "{ models,
checkPermission }: IContext" entries) is updated consistently and any downstream
calls that rely on context pass/consume subdomain as needed.

Comment thread backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts Outdated
@github-actions
Copy link
Copy Markdown

🌗 Pull Request Overview

This PR adds permission checks (checkPermission) to all GraphQL resolvers in the loyalty plugin and defines a comprehensive permission configuration file. It also includes a security fix for RegExp injection vulnerabilities by using escapeRegExp on search values.

Reviewed Changes
Kimi performed full review on 35 changed files and found 2 issues.

Show a summary per file
File Description
backend/plugins/loyalty_api/src/meta/permissions.ts New permission configuration file defining all modules, actions, scopes, and default permission groups for the loyalty plugin
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/mutations/agent.ts Added checkPermission calls to all agent mutation resolvers (createAgent, updateAgent, removeAgent, agentsAdd, agentsEdit, agentsRemove)
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/queries/agent.ts Added checkPermission calls to all agent query resolvers (agents, agentsMain, agentDetail)
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignment.ts Added checkPermission calls to assignment mutation resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts Added checkPermission calls to assignment campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts Added checkPermission calls to assignment query resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts Added checkPermission calls and escapeRegExp security fix for searchValue filtering
backend/plugins/loyalty_api/src/modules/config/graphql/mutations/config.ts Added checkPermission call to loyaltyConfigsUpdate mutation
backend/plugins/loyalty_api/src/modules/config/graphql/mutations/loyalty.ts Added checkPermission calls to loyalty mutations (shareScore, confirmVoucherSale)
backend/plugins/loyalty_api/src/modules/config/graphql/queries/config.ts Added checkPermission call to loyaltyConfigs query
backend/plugins/loyalty_api/src/modules/config/graphql/queries/loyalty.ts Added checkPermission calls to loyalty queries (checkLoyalties, loyalties)
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/coupon.ts Added checkPermission calls to coupon mutation resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts Added checkPermission calls to coupon campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/coupon.ts Added checkPermission calls to coupon query resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/couponCampaign.ts Added checkPermission calls to coupon campaign query resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donate.ts Added checkPermission calls to donate mutation resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donateCampaign.ts Added checkPermission calls to donate campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donate.ts Added checkPermission calls to donate query resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts Added checkPermission calls and escapeRegExp security fix for searchValue filtering
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts Added checkPermission calls to lottery mutation resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts Added checkPermission calls to lottery campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lottery.ts Added checkPermission calls to lottery query resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts Added checkPermission calls and escapeRegExp security fix for searchValue filtering
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricing.ts Added checkPermission calls to pricing mutation resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricingPlan.ts Added checkPermission calls to pricing plan mutation resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricing.ts Added checkPermission calls to pricing query resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricingPlan.ts Added checkPermission calls to pricing plan query resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreCampaign.ts Added checkPermission calls to score campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreLog.ts Added checkPermission call to changeScore mutation
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreCampaign.ts Added checkPermission calls to score campaign query resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreLog.ts Added checkPermission calls to score log query resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts Added checkPermission calls to spin mutation resolvers; BREAKING CHANGE: Changed doSpin function signature
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spinCampaign.ts Added checkPermission calls to spin campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spin.ts Added checkPermission calls to spin query resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts Added checkPermission calls and escapeRegExp security fix for searchValue filtering
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucher.ts Added checkPermission calls to voucher mutation resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts Added checkPermission calls to voucher campaign mutation resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucher.ts Added checkPermission calls to voucher query resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucherCampaign.ts Added checkPermission calls to voucher campaign query resolvers

📋 Review Findings

📄 backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts

🔴 CRITICAL breaking-change: Function signature change breaks GraphQL API

Lines 38-44

The doSpin mutation resolver's second parameter was changed from a simple string to an object { spinId: string }. This is a breaking change that will cause existing GraphQL mutations to fail.

💡 Suggested fix:

Current code:

async doSpin(
  _root: undefined,
  { spinId }: { spinId: string },
  { models, checkPermission }: IContext,
) {
  await checkPermission('spinDo');
  return models.Spins.doSpin(spinId);
}

If the GraphQL schema defines doSpin(spinId: String!) (scalar argument), keep the original signature:

async doSpin(
  _root: undefined,
  spinId: string,
  { models, checkPermission }: IContext,
) {
  await checkPermission('spinDo');
  return models.Spins.doSpin(spinId);
}

If the schema was updated to doSpin(spinId: SpinInput!) (input object), this change is correct, but ensure the frontend is updated accordingly.


📄 backend/plugins/loyalty_api/src/meta/permissions.ts

🟡 MEDIUM consistency: Inconsistent action naming convention

Multiple locations throughout file

The permission action names use inconsistent naming patterns:

  • Some use Create/Update/Remove (e.g., agentCreate, agentUpdate, agentRemove)
  • Some use Create/Edit/Remove (e.g., couponCreate, couponEdit, couponRemove)
  • Some use View/Create/Update/Remove (e.g., pricingView, pricingCreate, pricingUpdate, pricingRemove)

This inconsistency could lead to confusion when assigning permissions. Consider standardizing on a consistent naming convention (e.g., verbNoun pattern: createAgent, updateAgent, deleteAgent).

💡 Suggested fix:

Standardize action names. For example:

// Instead of mixed patterns:
actions: [
  { name: 'agentCreate', ... },      // Create pattern
  { name: 'couponEdit', ... },       // Edit pattern  
  { name: 'pricingUpdate', ... },    // Update pattern
  { name: 'donateRemove', ... },     // Remove pattern
]

// Use consistent pattern:
actions: [
  { name: 'createAgent', title: 'Create agent', ... },
  { name: 'updateCoupon', title: 'Edit coupon', ... },
  { name: 'updatePricing', title: 'Edit pricing', ... },
  { name: 'deleteDonate', title: 'Delete donation', ... },
]

📄 Multiple files (assignmentCampaign.ts, donateCampaign.ts, lotteryCampaign.ts, spinCampaign.ts)

🔵 LOW style: Missing newline at end of files

Last line of multiple files

Several files are missing the trailing newline at the end of file. While this doesn't affect functionality, it's a common convention to include a final newline.


✅ Summary

The overall implementation is solid and follows the established patterns. The permission checks are correctly added to all resolvers, and the security fix using escapeRegExp is properly implemented in all search query handlers.

Key concerns to address:

  1. CRITICAL: Verify the doSpin function signature change is intentional and matches the GraphQL schema definition
  2. MEDIUM: Consider standardizing permission action naming for long-term maintainability

Powered by Kimi | Model: kimi-k2.5

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts (1)

8-9: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include subdomain in context for multi-tenant data access.

All resolvers in this file destructure { models, checkPermission } but omit subdomain, which is required for tenant-isolated data access when calling models.LotteryCampaigns.* operations.

As per coding guidelines, "backend/plugins//src/**/.ts: Always include subdomain in context for data access to maintain multi-tenancy isolation" and "backend/**/*.ts: Always use the subdomain context parameter for tenant-isolated data access in multi-tenant resolvers and services".

🔧 Proposed fix to include subdomain in all resolvers
   async lotteryCampaignsAdd(
     _root: undefined,
     doc: ILotteryCampaign,
-    { models, checkPermission }: IContext,
+    { models, checkPermission, subdomain }: IContext,
   ) {

   async lotteryCampaignsEdit(
     _root: undefined,
     { _id, ...doc }: ILotteryCampaign & { _id: string },
-    { models, checkPermission }: IContext,
+    { models, checkPermission, subdomain }: IContext,
   ) {

   async lotteryCampaignsRemove(
     _root: undefined,
     { _ids }: { _ids: string[] },
-    { models, checkPermission }: IContext,
+    { models, checkPermission, subdomain }: IContext,
   ) {

   async doLottery(
     _root: undefined,
     params: { campaignId: string; awardId: string },
-    { models, checkPermission }: IContext,
+    { models, checkPermission, subdomain }: IContext,
   ) {

   async doLotteryMultiple(
     _root: undefined,
     params: { campaignId: string; awardId: string; multiple: number },
-    { models, checkPermission }: IContext,
+    { models, checkPermission, subdomain }: IContext,
   ) {

   async getNextChar(
     _root: undefined,
     params: { campaignId: string; awardId: string; prevChars: string },
-    { models, checkPermission }: IContext,
+    { models, checkPermission, subdomain }: IContext,
   ) {

Also applies to: 17-18, 26-27, 35-36, 44-45, 53-54

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts`
around lines 8 - 9, Resolver functions currently destructure "{ models,
checkPermission }" but omit "subdomain"; update each resolver (the mutation
handlers that call models.LotteryCampaigns.*) to destructure "{ models,
checkPermission, subdomain }" and pass that subdomain into the
models.LotteryCampaigns method calls (e.g., create, update, find, delete) so
tenant-isolated data access is used. Ensure every occurrence where
models.LotteryCampaigns.* is invoked uses the subdomain parameter and update any
function signatures or local variables accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts`:
- Line 6: The resolver signatures (e.g., lotteriesAdd) destructure context but
omit subdomain; update each resolver in this file to include subdomain in the
context destructuring (for example change "{ models, checkPermission }:
IContext" to "{ models, checkPermission, subdomain }: IContext") and then pass
that subdomain into any tenant-isolated model calls (models.Lotteries.*) so the
database operations use the correct tenant; apply the same change to the other
resolvers referenced in this file.

---

Duplicate comments:
In
`@backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts`:
- Around line 8-9: Resolver functions currently destructure "{ models,
checkPermission }" but omit "subdomain"; update each resolver (the mutation
handlers that call models.LotteryCampaigns.*) to destructure "{ models,
checkPermission, subdomain }" and pass that subdomain into the
models.LotteryCampaigns method calls (e.g., create, update, find, delete) so
tenant-isolated data access is used. Ensure every occurrence where
models.LotteryCampaigns.* is invoked uses the subdomain parameter and update any
function signatures or local variables accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc2cb107-023e-41fc-b325-2724a42fdac3

📥 Commits

Reviewing files that changed from the base of the PR and between 57acea5 and 272550a.

📒 Files selected for processing (8)
  • backend/plugins/loyalty_api/src/meta/permissions.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts
  • backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts
  • backend/plugins/loyalty_api/src/meta/permissions.ts


export const lotteryMutations = {
async lotteriesAdd(_root: undefined, doc: ILottery, { models }: IContext) {
async lotteriesAdd(_root: undefined, doc: ILottery, { models, checkPermission }: IContext) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include subdomain in context for multi-tenant data access.

All resolvers in this file destructure context but omit subdomain, which is required for tenant-isolated data access when calling models.Lotteries.* operations.

As per coding guidelines, "backend/plugins//src/**/.ts: Always include subdomain in context for data access to maintain multi-tenancy isolation" and "backend/**/*.ts: Always use the subdomain context parameter for tenant-isolated data access in multi-tenant resolvers and services".

🔧 Proposed fix to include subdomain in all resolvers
-  async lotteriesAdd(_root: undefined, doc: ILottery, { models, checkPermission }: IContext) {
+  async lotteriesAdd(_root: undefined, doc: ILottery, { models, checkPermission, subdomain }: IContext) {

   async lotteriesEdit(
     _root: undefined,
     { _id, ...doc }: ILottery & { _id: string },
-    { models, user, checkPermission }: IContext,
+    { models, user, checkPermission, subdomain }: IContext,
   ) {

   async lotteriesRemove(
     _root: undefined,
     { _ids }: { _ids: string[] },
-    { models, checkPermission }: IContext,
+    { models, checkPermission, subdomain }: IContext,
   ) {

-  async buyLottery(_root: undefined, param: IBuyParams, { models, checkPermission }: IContext) {
+  async buyLottery(_root: undefined, param: IBuyParams, { models, checkPermission, subdomain }: IContext) {

Also applies to: 14-14, 23-23, 29-29

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts`
at line 6, The resolver signatures (e.g., lotteriesAdd) destructure context but
omit subdomain; update each resolver in this file to include subdomain in the
context destructuring (for example change "{ models, checkPermission }:
IContext" to "{ models, checkPermission, subdomain }: IContext") and then pass
that subdomain into any tenant-isolated model calls (models.Lotteries.*) so the
database operations use the correct tenant; apply the same change to the other
resolvers referenced in this file.

@github-actions
Copy link
Copy Markdown

🌗 Pull Request Overview

This PR adds comprehensive permission checks to all GraphQL resolvers in the loyalty plugin and defines a detailed permission configuration. It introduces role-based access control with three default groups (admin, user, viewer) and fixes a potential ReDoS vulnerability by escaping regex patterns.

Reviewed Changes
Kimi performed full review on 41 changed files and found 3 issues.

Show a summary per file
File Description
backend/plugins/loyalty_api/src/meta/permissions.ts New comprehensive permission configuration with 19 modules and 3 default permission groups
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/mutations/agent.ts Added checkPermission calls to 6 mutation resolvers
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/queries/agent.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignment.ts Added checkPermission calls to 4 mutation resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts Added checkPermission calls to 3 query resolvers and fixed ReDoS vulnerability with escapeRegExp
backend/plugins/loyalty_api/src/modules/config/graphql/mutations/config.ts Added checkPermission call to loyaltyConfigsUpdate resolver
backend/plugins/loyalty_api/src/modules/config/graphql/mutations/loyalty.ts Added checkPermission calls to 2 mutation resolvers
backend/plugins/loyalty_api/src/modules/config/graphql/queries/config.ts Added checkPermission call to loyaltyConfigs query resolver
backend/plugins/loyalty_api/src/modules/config/graphql/queries/loyalty.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/coupon.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/coupon.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/couponCampaign.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donate.ts Added checkPermission calls to 5 mutation resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donateCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donate.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts Added checkPermission calls to 3 query resolvers and fixed ReDoS vulnerability with escapeRegExp
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts Added checkPermission calls to 4 mutation resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts Added checkPermission calls to 6 mutation resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lottery.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts Added checkPermission calls to 5 query resolvers and fixed ReDoS vulnerability with escapeRegExp
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricing.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricingPlan.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricing.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricingPlan.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreCampaign.ts Added checkPermission calls to 5 mutation resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreLog.ts Added checkPermission call to changeScore mutation resolver
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreCampaign.ts Added checkPermission calls to 5 query resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreLog.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts Added checkPermission calls to 5 mutation resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spinCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spin.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts Added checkPermission calls to 3 query resolvers and fixed ReDoS vulnerability with escapeRegExp
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucher.ts Added checkPermission calls to 6 mutation resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucher.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucherCampaign.ts Added checkPermission calls to 3 query resolvers

📋 Review Findings

📄 backend/plugins/loyalty_api/src/meta/permissions.ts

🔴 HIGH security: Missing newline at end of file

Line 1072

Files should end with a newline character to comply with POSIX standards and avoid potential issues with some tools.

💡 Suggested fix:

Add a newline at the end of the file.

};

📄 backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts

🔴 CRITICAL bug: Unused subdomain parameter added without usage

Line 38

The doSpin resolver adds subdomain to the context destructuring but never uses it. This appears to be incomplete code - either the subdomain should be used, or it shouldn't be extracted.

💡 Suggested fix:

Either remove subdomain if not needed:

async doSpin(
  _root: undefined,
  spinId: string,
  { models, checkPermission }: IContext,
) {
  await checkPermission('spinDo');
  return models.Spins.doSpin(spinId);
},

Or pass subdomain to doSpin if the model method requires it:

async doSpin(
  _root: undefined,
  spinId: string,
  { models, checkPermission, subdomain }: IContext,
) {
  await checkPermission('spinDo');
  return models.Spins.doSpin(spinId, subdomain);
},

📄 backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts & other files

🟡 MEDIUM code-quality: Duplicate import statement

Line 9

The escapeRegExp function is imported twice - once on line 9 (import { escapeRegExp } from 'erxes-api-shared/utils';) and also via the existing import on line 6 (import { cursorPaginate } from 'erxes-api-shared/utils';). These should be consolidated.

Same issue exists in:

  • donateCampaign.ts (lines 9, 6)
  • lotteryCampaign.ts (lines 9, 6)

💡 Suggested fix:

Consolidate imports:

import { cursorPaginate, escapeRegExp } from 'erxes-api-shared/utils';

✅ Summary

Overall, this is a well-structured PR that adds comprehensive security through permission checks. The main issues are:

  1. Minor formatting issue - Missing newline at end of permissions.ts
  2. Potential incomplete code - Unused subdomain parameter in doSpin mutation
  3. Code style - Duplicate import statements in 3 files

The ReDoS vulnerability fixes using escapeRegExp() are correctly implemented. The permission structure is comprehensive and follows the project's patterns.


Powered by Kimi | Model: kimi-k2.5

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts (1)

42-54: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't gate the customer-portal campaign feed behind a staff permission.

cpDonateCampaigns only returns active, date-bounded campaigns and doesn't accept any back-office filters, so this looks like the customer-facing feed. Requiring loyaltyCampaignView here will reject portal users who don't carry loyalty staff permissions and can break campaign discovery/redeem flows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts`
around lines 42 - 54, The cpDonateCampaigns resolver is incorrectly requiring
the staff permission checkPermission('loyaltyCampaignView'), which blocks
customer-portal access; remove the await checkPermission('loyaltyCampaignView')
call from the cpDonateCampaigns function so the resolver returns active,
date-bounded campaigns to portal users as intended (leave the
models.DonateCampaigns.find(...) and sorting logic unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/plugins/loyalty_api/src/meta/permissions.ts`:
- Around line 367-389: The loyalty:viewer role (id: 'loyalty:viewer') is missing
the config read permission; add a permission entry granting the config module's
read action to that role by appending { plugin: 'loyalty', module: 'config',
actions: ['loyaltyConfigView'], scope: 'all' } to the permissions array for the
'loyalty:viewer' role so it retains read-only access to loyalty settings when
resolvers enforce loyaltyConfigView.

---

Outside diff comments:
In
`@backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts`:
- Around line 42-54: The cpDonateCampaigns resolver is incorrectly requiring the
staff permission checkPermission('loyaltyCampaignView'), which blocks
customer-portal access; remove the await checkPermission('loyaltyCampaignView')
call from the cpDonateCampaigns function so the resolver returns active,
date-bounded campaigns to portal users as intended (leave the
models.DonateCampaigns.find(...) and sorting logic unchanged).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7c3dbee9-a98b-4530-9c70-21d423a60df2

📥 Commits

Reviewing files that changed from the base of the PR and between ef38ad3 and 64830d5.

📒 Files selected for processing (15)
  • backend/plugins/loyalty_api/src/meta/permissions.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts
  • backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts
  • backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/couponCampaign.ts
  • backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donateCampaign.ts
  • backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreCampaign.ts
  • backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreCampaign.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spinCampaign.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts
  • backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts
  • backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucherCampaign.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donateCampaign.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts
  • backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts
  • backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts
  • backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts
  • backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spinCampaign.ts

Comment thread backend/plugins/loyalty_api/src/meta/permissions.ts
@github-actions
Copy link
Copy Markdown

🌗 Pull Request Overview

This PR adds comprehensive permission checks (checkPermission) to all GraphQL resolvers in the loyalty plugin, defines a complete permission configuration with default groups (admin, user, viewer), and integrates event logging (sendDbEventLog) into campaign model operations for audit trails. It also fixes several minor issues like regex escaping and async/await bugs.

Reviewed Changes
Kimi performed full review on 39 changed files and found 6 issues.

Show a summary per file
File Description
backend/plugins/loyalty_api/src/connectionResolvers.ts Added event dispatcher initialization for all 7 campaign types and injected into model loaders
backend/plugins/loyalty_api/src/meta/permissions.ts New comprehensive permission config with modules, actions, scopes, and 3 default groups
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/mutations/agent.ts Added checkPermission calls to 6 mutation resolvers
backend/plugins/loyalty_api/src/modules/agent/graphql/resolvers/queries/agent.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/assignment/db/models/AssignmentCampaign.ts Added event logging for create, update, soft-delete, and hard-delete operations
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignment.ts Added checkPermission calls to 4 mutation resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignment.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts Added checkPermission calls to 3 query resolvers; fixed regex escaping with escapeRegExp
backend/plugins/loyalty_api/src/modules/config/graphql/mutations/config.ts Added checkPermission call to loyaltyConfigsUpdate resolver
backend/plugins/loyalty_api/src/modules/config/graphql/mutations/loyalty.ts Added checkPermission calls to 2 mutation resolvers
backend/plugins/loyalty_api/src/modules/config/graphql/queries/config.ts Added checkPermission call to loyaltyConfigs query
backend/plugins/loyalty_api/src/modules/config/graphql/queries/loyalty.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/coupon/db/models/CouponCampaign.ts Added event logging for create, update, soft-delete, and hard-delete operations
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/coupon.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/coupon.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/coupon/graphql/resolvers/queries/couponCampaign.ts Added checkPermission calls to 2 query resolvers; fixed regex escaping with escapeRegExp
backend/plugins/loyalty_api/src/modules/donate/db/models/DonateCampaign.ts Added event logging for create, update, soft-delete, and hard-delete operations
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donate.ts Added checkPermission calls to 5 mutation resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/mutations/donateCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donate.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/donate/graphql/resolvers/queries/donateCampaign.ts Added checkPermission calls to 3 query resolvers; fixed regex escaping with escapeRegExp
backend/plugins/loyalty_api/src/modules/lottery/db/models/LotteryCampaign.ts Added event logging for create, update, soft-delete, and hard-delete operations
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lottery.ts Added checkPermission calls to 4 mutation resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts Added checkPermission calls to 6 mutation resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lottery.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts Added checkPermission calls to 5 query resolvers; fixed regex escaping with escapeRegExp
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricing.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/mutations/pricingPlan.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricing.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/pricing/graphql/resolvers/queries/pricingPlan.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/score/db/models/ScoreCampaign.ts Added event logging for create, update, remove operations; fixed removeScoreCampaigns parameter type
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreCampaign.ts Added checkPermission calls to 5 mutation resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/mutations/scoreLog.ts Added checkPermission call to changeScore resolver
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreCampaign.ts Added checkPermission calls to 5 query resolvers
backend/plugins/loyalty_api/src/modules/score/graphql/resolvers/queries/scoreLog.ts Added checkPermission calls to 4 query resolvers
backend/plugins/loyalty_api/src/modules/spin/db/models/SpinCampaign.ts Added event logging for create, update, soft-delete, and hard-delete operations
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts Added checkPermission calls to 5 mutation resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spinCampaign.ts Added checkPermission calls to 3 mutation resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spin.ts Added checkPermission calls to 2 query resolvers
backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/queries/spinCampaign.ts Added checkPermission calls to 3 query resolvers; fixed regex escaping with escapeRegExp
backend/plugins/loyalty_api/src/modules/voucher/db/models/VoucherCampaign.ts Added event logging for create, update, soft-delete, and hard-delete operations; fixed async/await bug in used voucher count
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucher.ts Added checkPermission calls to 6 mutation resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts Added checkPermission calls to 3 mutation resolvers; fixed return value for voucherCampaignsEdit
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucher.ts Added checkPermission calls to 3 query resolvers
backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/queries/voucherCampaign.ts Added checkPermission calls to 3 query resolvers; fixed regex escaping with escapeRegExp

📋 Review Findings

📄 backend/plugins/loyalty_api/src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts

🟠 HIGH logic: Permission check uses incorrect action name

Line 18-20

The mutation assignmentCampaignsEdit uses permission assignmentCampaignEdit but this action name doesn't match the standardized campaign action names used elsewhere. Looking at the permissions.ts file, the campaign actions are defined as loyaltyCampaignCreate, loyaltyCampaignUpdate, etc. However, the assignment module appears to have its own specific actions. This inconsistency could cause permission checks to fail.

💡 Suggested fix:

Check if the permission name should be assignmentCampaignUpdate to match the pattern used in permissions.ts (loyaltyCampaignUpdate), or ensure the action name is correctly registered in the permissions config.


📄 backend/plugins/loyalty_api/src/modules/voucher/db/models/VoucherCampaign.ts

🟠 HIGH logic: Missing await in countDocuments calls

Lines 98-116

The usedVoucherCount assignments wrap countDocuments() with Number() but don't await the promises. This was partially fixed in the diff but the pattern is still problematic - countDocuments() returns a Promise, not a number. Wrapping a Promise with Number() will always return NaN.

Current code:

usedVoucherCount = Number(
  await models.Spins.find({
    campaignId: voucherCampaignDB.spinCampaignId,
  }).countDocuments(),
);

Improved code:

usedVoucherCount = await models.Spins.find({
  campaignId: voucherCampaignDB.spinCampaignId,
}).countDocuments();

The Number() wrapper is unnecessary since countDocuments() already returns a number.


📄 backend/plugins/loyalty_api/src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts

🔵 LOW maintainability: Inconsistent return pattern in voucherCampaignsEdit

Lines 20-26

The voucherCampaignsEdit mutation now fetches and returns the updated document after updating. While this is good for GraphQL clients to get the latest data, it's inconsistent with other similar mutations in the codebase that typically just return the update result. Consider if this pattern should be applied consistently across all campaign edit mutations.

Current code:

await checkPermission('loyaltyCampaignUpdate');
await models.VoucherCampaigns.updateVoucherCampaign(_id, doc);
return models.VoucherCampaigns.findOne({ _id });

Suggested consistency check: Either return the result from all edit mutations consistently, or document why voucher campaigns need this special handling.


📄 backend/plugins/loyalty_api/src/modules/score/db/models/ScoreCampaign.ts

🟡 MEDIUM robustness: Parameter type change for removeScoreCampaigns

Line 549

The function signature changed from _ids: string to _ids: string[] with array normalization. However, the user: IUserDocument parameter was also added but isn't being used in the function. This creates a dead parameter.

Current code:

public static async removeScoreCampaigns(_ids: string[], user: IUserDocument) {

Either use the user parameter (perhaps for audit logging in the event logs), or remove it to keep the API clean.


📄 backend/plugins/loyalty_api/src/connectionResolvers.ts

🔵 LOW maintainability: Empty context values in event handlers

Lines 103-142

All createEventHandlers calls pass empty processId and userId in getContext. This limits the usefulness of the audit logs as they won't track which user performed the action.

Current code:

const assignmentDispatcher = createEventHandlers({
  subdomain,
  pluginName: 'loyalty',
  moduleName: 'assignment',
  collectionName: 'assignment_campaigns',
  getContext: () => ({ subdomain, processId: '', userId: '' }),
});

💡 Suggested improvement:

If the user context is available at the model level, consider passing it through. However, this may require significant refactoring since the models are instantiated at the connection level, not per-request.


📄 backend/plugins/loyalty_api/src/modules/spin/graphql/resolvers/mutations/spin.ts

🔵 LOW maintainability: Unused subdomain parameter

Line 46

The doSpin mutation extracts subdomain from context but doesn't use it.

Current code:

async doSpin(
  _root: undefined,
  spinId: string,
  { models, checkPermission, subdomain }: IContext,
) {

💡 Suggested fix:

Remove unused parameter:

async doSpin(
  _root: undefined,
  spinId: string,
  { models, checkPermission }: IContext,
) {

✅ Summary

The PR is well-structured and addresses important security concerns by adding permission checks throughout the loyalty plugin. The event logging integration is a valuable addition for audit trails.

Key issues to address:

  1. HIGH: Fix the unnecessary Number() wrapper around countDocuments() in VoucherCampaign.ts
  2. MEDIUM: Remove or use the unused user parameter in removeScoreCampaigns
  3. LOW: Clean up unused subdomain parameter in doSpin mutation

The permission configuration is comprehensive and the default groups provide good role separation. The addition of escapeRegExp for search filters fixes potential regex injection issues.


Powered by Kimi | Model: kimi-k2.5

@github-actions
Copy link
Copy Markdown

🌗 Pull Request Overview

This PR adds comprehensive permission checks (checkPermission) to all GraphQL resolvers in the loyalty plugin and introduces a new permission configuration file (meta/permissions.ts) defining granular access controls for agents, campaigns, donations, lotteries, spins, vouchers, and scoring. It also adds event logging (audit trails) to campaign model operations via createEventHandlers.

Reviewed Changes
Kimi performed full review on 44 changed files and found 5 issues.

Show a summary per file
File Description
src/connectionResolvers.ts Adds event dispatchers for 7 campaign types and passes them to model loaders
src/meta/permissions.ts New file defining comprehensive permission matrix with modules, actions, and default groups
src/modules/agent/graphql/resolvers/mutations/agent.ts Adds checkPermission calls to 6 agent mutations
src/modules/agent/graphql/resolvers/queries/agent.ts Adds checkPermission('agentView') to 3 queries
src/modules/assignment/db/models/AssignmentCampaign.ts Adds event logging for create/update/delete operations
src/modules/assignment/graphql/resolvers/mutations/assignment.ts Adds checkPermission to 4 assignment mutations
src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts CRITICAL: Uses wrong permission names (assignmentCampaignCreate/Edit/Remove instead of loyaltyCampaign*)
src/modules/assignment/graphql/resolvers/queries/assignment.ts Adds checkPermission to 3 queries
src/modules/assignment/graphql/resolvers/queries/assignmentCampaign.ts Adds checkPermission and escapeRegExp for search
src/modules/config/graphql/mutations/config.ts Adds checkPermission('loyaltyConfigUpdate')
src/modules/config/graphql/mutations/loyalty.ts Adds checkPermission to score sharing and voucher confirmation
src/modules/config/graphql/queries/config.ts Adds checkPermission('loyaltyConfigView')
src/modules/config/graphql/queries/loyalty.ts Adds checkPermission to loyalty check and view queries
src/modules/coupon/db/models/CouponCampaign.ts Adds event logging for campaign operations
src/modules/coupon/graphql/resolvers/mutations/coupon.ts Adds checkPermission to 3 coupon mutations
src/modules/coupon/graphql/resolvers/mutations/couponCampaign.ts Adds checkPermission to 3 campaign mutations
src/modules/coupon/graphql/resolvers/queries/coupon.ts Adds checkPermission to 2 queries
src/modules/coupon/graphql/resolvers/queries/couponCampaign.ts Adds checkPermission and escapeRegExp
src/modules/donate/db/models/DonateCampaign.ts Adds event logging for campaign operations
src/modules/donate/graphql/resolvers/mutations/donate.ts Adds checkPermission to 5 donation mutations
src/modules/donate/graphql/resolvers/mutations/donateCampaign.ts Adds checkPermission to 3 campaign mutations
src/modules/donate/graphql/resolvers/queries/donate.ts Adds checkPermission to 2 queries
src/modules/donate/graphql/resolvers/queries/donateCampaign.ts Adds checkPermission and escapeRegExp
src/modules/lottery/db/models/LotteryCampaign.ts Adds event logging for campaign operations
src/modules/lottery/graphql/resolvers/mutations/lottery.ts Adds checkPermission to 4 lottery mutations
src/modules/lottery/graphql/resolvers/mutations/lotteryCampaign.ts Adds checkPermission to 6 campaign mutations
src/modules/lottery/graphql/resolvers/queries/lottery.ts Adds checkPermission to 2 queries
src/modules/lottery/graphql/resolvers/queries/lotteryCampaign.ts Adds checkPermission and escapeRegExp
src/modules/pricing/graphql/resolvers/mutations/pricing.ts Adds checkPermission to 3 pricing mutations
src/modules/pricing/graphql/resolvers/mutations/pricingPlan.ts Adds checkPermission to 3 plan mutations
src/modules/pricing/graphql/resolvers/queries/pricing.ts Adds checkPermission to 2 queries
src/modules/pricing/graphql/resolvers/queries/pricingPlan.ts Adds checkPermission to 3 queries
src/modules/score/db/models/ScoreCampaign.ts Adds event logging; changes removeScoreCampaigns signature
src/modules/score/graphql/resolvers/mutations/scoreCampaign.ts Adds checkPermission to 5 mutations
src/modules/score/graphql/resolvers/mutations/scoreLog.ts Adds checkPermission to score change mutation
src/modules/score/graphql/resolvers/queries/scoreCampaign.ts Adds checkPermission to 5 queries
src/modules/score/graphql/resolvers/queries/scoreLog.ts Adds checkPermission to 4 queries
src/modules/spin/db/models/SpinCampaign.ts Adds event logging for campaign operations
src/modules/spin/graphql/resolvers/mutations/spin.ts Adds checkPermission to 5 spin mutations
src/modules/spin/graphql/resolvers/mutations/spinCampaign.ts Adds checkPermission to 3 campaign mutations
src/modules/spin/graphql/resolvers/queries/spin.ts Adds checkPermission to 2 queries
src/modules/spin/graphql/resolvers/queries/spinCampaign.ts Adds checkPermission and escapeRegExp
src/modules/voucher/db/models/VoucherCampaign.ts Adds event logging; fixes missing await on count queries
src/modules/voucher/graphql/resolvers/mutations/voucher.ts Adds checkPermission to 6 voucher mutations
src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts HIGH: Changes return behavior in voucherCampaignsEdit
src/modules/voucher/graphql/resolvers/queries/voucher.ts Adds checkPermission to 3 queries
src/modules/voucher/graphql/resolvers/queries/voucherCampaign.ts Adds checkPermission and escapeRegExp

📋 Review Findings

📄 src/modules/assignment/graphql/resolvers/mutations/assignmentCampaign.ts

🔴 CRITICAL bug: Permission name mismatch - checks will fail

Lines 10, 18, 26

The resolver uses permission names assignmentCampaignCreate, assignmentCampaignEdit, assignmentCampaignRemove, but permissions.ts defines loyaltyCampaignCreate, loyaltyCampaignUpdate, loyaltyCampaignRemove. These names don't match, so all permission checks will fail.

💡 Suggested fix:

Current code:

await checkPermission('assignmentCampaignCreate');
// ...
await checkPermission('assignmentCampaignEdit');
// ...
await checkPermission('assignmentCampaignRemove');

Improved code:

await checkPermission('loyaltyCampaignCreate');
// ...
await checkPermission('loyaltyCampaignUpdate');
// ...
await checkPermission('loyaltyCampaignRemove');

📄 src/modules/voucher/graphql/resolvers/mutations/voucherCampaign.ts

🟠 HIGH logic: Changed return behavior in voucherCampaignsEdit

Lines 15-20

The original code returned the result of updateVoucherCampaign directly. The new code ignores the update result, performs a separate findOne query, and returns that. This changes the return value shape and adds an unnecessary database query.

💡 Suggested fix:

Current code:

async voucherCampaignsEdit(
  _root: undefined,
  { _id, ...doc }: { _id: string } & IVoucherCampaign,
  { models, checkPermission }: IContext,
) {
  await checkPermission('loyaltyCampaignUpdate');
  await models.VoucherCampaigns.updateVoucherCampaign(_id, doc);
  return models.VoucherCampaigns.findOne({ _id });
},

Improved code:

async voucherCampaignsEdit(
  _root: undefined,
  { _id, ...doc }: { _id: string } & IVoucherCampaign,
  { models, checkPermission }: IContext,
) {
  await checkPermission('loyaltyCampaignUpdate');
  return models.VoucherCampaigns.updateVoucherCampaign(_id, doc);
},

📄 src/modules/score/db/models/ScoreCampaign.ts

🟠 HIGH breaking change: removeScoreCampaigns signature changed

Line 139

The method signature changed from _ids: string (single string) to _ids: string[] (array), but this may break existing callers passing a single ID string.

💡 Suggested fix:

Current code:

public static async removeScoreCampaigns(_ids: string[], user: IUserDocument) {
  const idsArray = Array.isArray(_ids) ? _ids : [_ids];
  // ...
}

Improved code:

public static async removeScoreCampaigns(_ids: string | string[], user: IUserDocument) {
  const idsArray = Array.isArray(_ids) ? _ids : [_ids];
  // ...
}

📄 src/modules/voucher/db/models/VoucherCampaign.ts

🟡 MEDIUM bug: Missing await added (good fix, but verify)

Lines 98-112

The fix adds await to models.Spins.find(), models.Lotteries.find(), and models.Vouchers.find() calls before .countDocuments(). This is correct since Mongoose queries return promises, but ensure this was an intentional fix and not an accidental side effect.

💡 No change needed - this appears to be a legitimate bug fix, but verify the query behavior is as expected since countDocuments() should work on the query object without explicit await on find().


📄 src/connectionResolvers.ts

🟡 LOW `code quality**: Empty context values in event handlers

Lines 106-143

All event dispatchers use getContext: () => ({ subdomain, processId: '', userId: '' }) with empty processId and userId. This reduces the value of audit logs since they won't track which user performed actions.

💡 Suggested fix:

Pass actual user context from the resolver context instead of hardcoded empty strings, or document why these fields are intentionally empty.

// Consider injecting real user context where available
const assignmentDispatcher = createEventHandlers({
  subdomain,
  pluginName: 'loyalty',
  moduleName: 'assignment',
  collectionName: 'assignment_campaigns',
  getContext: (reqContext) => ({ 
    subdomain, 
    processId: reqContext.processId || '', 
    userId: reqContext.user?._id || '' 
  }),
});

✅ Additional Observations

Good practices found:

  • Consistent use of escapeRegExp() for user-provided search values prevents ReDoS attacks
  • Event logging added comprehensively across all campaign types
  • Permission checks added to all GraphQL resolvers
  • Soft-delete pattern (status → TRASH) properly logs both update and delete events

Powered by Kimi | Model: kimi-k2.5

@munkhsaikhan munkhsaikhan merged commit 4b728af into main May 19, 2026
11 of 14 checks passed
@munkhsaikhan munkhsaikhan deleted the feat/loyalty-permission-checks branch May 19, 2026 08:01
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
25.8% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants