+
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0d1269e
WIP reward sheet updates
TWilson023 Jul 22, 2025
bf1f329
Merge branch 'main' into reward-logic
TWilson023 Jul 24, 2025
ce36c8f
Add inline term dropdowns
TWilson023 Jul 24, 2025
0b8aa60
Update add-edit-reward-sheet.tsx
TWilson023 Jul 24, 2025
2d1f815
Clicks+leads config
TWilson023 Jul 24, 2025
407ad1e
Update add-edit-reward-sheet.tsx
TWilson023 Jul 24, 2025
8d8f5c1
Modifier field array
TWilson023 Jul 24, 2025
7c00bfd
Move reward sheet
TWilson023 Jul 24, 2025
e413adf
Reorganize components
TWilson023 Jul 24, 2025
15a0421
Merge branch 'main' into reward-logic
TWilson023 Jul 25, 2025
447bb83
Add operator selector
TWilson023 Jul 25, 2025
e9760f6
WIP conditions
TWilson023 Jul 25, 2025
3a13c2b
WIP inline condition config
TWilson023 Jul 25, 2025
03ba9ee
Update rewards-logic.tsx
TWilson023 Jul 25, 2025
62333f5
Add operators
TWilson023 Jul 25, 2025
bd2ddbf
Add multi-values
TWilson023 Jul 25, 2025
df673d0
Update rewards-logic.tsx
TWilson023 Jul 25, 2025
febf0db
Merge branch 'main' into reward-logic
TWilson023 Jul 28, 2025
81bb21b
Country selection
TWilson023 Jul 28, 2025
3d83003
Merge branch 'main' into reward-logic
steven-tey Jul 28, 2025
06e3542
Add terms configuration
TWilson023 Jul 28, 2025
57c3b7b
Merge branch 'reward-logic' of github.com:dubinc/dub into reward-logic
TWilson023 Jul 28, 2025
377518b
Event entities/logic
TWilson023 Jul 28, 2025
6a4839c
Merge branch 'main' into reward-logic
steven-tey Jul 28, 2025
0fee4f2
Parsing/error improvements
TWilson023 Jul 28, 2025
d072c0f
Merge branch 'reward-logic' of github.com:dubinc/dub into reward-logic
TWilson023 Jul 28, 2025
806a88a
Input tweaks
TWilson023 Jul 28, 2025
7c140e3
Update rewards-logic.tsx
TWilson023 Jul 28, 2025
e3daabe
Merge branch 'main' into reward-logic
steven-tey Jul 28, 2025
8e70d0d
Sort popover items
TWilson023 Jul 29, 2025
771cf17
Merge branch 'reward-logic' of github.com:dubinc/dub into reward-logic
TWilson023 Jul 29, 2025
eef1e6d
Expand/collapse partner selection
TWilson023 Jul 29, 2025
420bc73
Add partner avatar previews
TWilson023 Jul 29, 2025
06d940a
Merge branch 'main' into reward-logic
steven-tey Jul 29, 2025
79f304d
Update reward-partners-card.tsx
TWilson023 Jul 29, 2025
472e9a6
Form state fixes
TWilson023 Jul 29, 2025
9909f9f
Action + amount logic updates
TWilson023 Jul 29, 2025
7663d31
Update rewards-logic.tsx
TWilson023 Jul 29, 2025
189cfc6
Add plan capabilities
TWilson023 Jul 29, 2025
b4337bf
WIP upsell modal
TWilson023 Jul 29, 2025
9ea1395
Merge branch 'main' into reward-logic
TWilson023 Jul 30, 2025
e73bab0
Update rewards-logic.tsx
TWilson023 Jul 30, 2025
d74812b
Update rewards-upgrade-modal.tsx
TWilson023 Jul 30, 2025
439c3ca
Update rewards-logic.tsx
TWilson023 Jul 30, 2025
9429df8
Update add-edit-reward-sheet.tsx
TWilson023 Jul 30, 2025
dee0263
Update reward-partners-card.tsx
TWilson023 Jul 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import usePartnersCount from "@/lib/swr/use-partners-count";
import useRewards from "@/lib/swr/use-rewards";
import type { RewardProps } from "@/lib/types";
import { REWARD_EVENT_COLUMN_MAPPING } from "@/lib/zod/schemas/rewards";
import { useRewardSheet } from "@/ui/partners/add-edit-reward-sheet";
import { REWARD_EVENTS } from "@/ui/partners/constants";
import { ProgramRewardDescription } from "@/ui/partners/program-reward-description";
import { useRewardSheet } from "@/ui/partners/rewards/add-edit-reward-sheet";
import { EventType } from "@dub/prisma/client";
import { Badge, Button, Popover, useKeyboardShortcut } from "@dub/ui";
import { pluralize } from "@dub/utils";
Expand Down
12 changes: 12 additions & 0 deletions apps/web/lib/actions/partners/create-reward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
import { createId } from "@/lib/api/create-id";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { getPlanCapabilities } from "@/lib/plan-capabilities";
import {
createRewardSchema,
REWARD_EVENT_COLUMN_MAPPING,
} from "@/lib/zod/schemas/rewards";
import { prisma } from "@dub/prisma";
import { Prisma } from "@dub/prisma/client";
import { waitUntil } from "@vercel/functions";
import { authActionClient } from "../safe-action";

Expand All @@ -23,6 +25,7 @@ export const createRewardAction = authActionClient
isDefault,
includedPartnerIds,
excludedPartnerIds,
modifiers,
} = parsedInput;

includedPartnerIds = includedPartnerIds || [];
Expand All @@ -47,6 +50,14 @@ export const createRewardAction = authActionClient
}
}

if (
modifiers &&
!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
Comment on lines +53 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Plan capability bypass risk

Same security concern as in update-reward.ts - the plan capability check occurs after extracting modifiers and could be bypassed if workspace data is stale.

Apply the same fix as recommended for update-reward.ts:

+ // Refresh workspace to ensure latest plan data
+ const refreshedWorkspace = await prisma.workspace.findUnique({
+   where: { id: workspace.id },
+   select: { plan: true }
+ });
+ 
  if (
    modifiers &&
-   !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
+   !getPlanCapabilities(refreshedWorkspace?.plan || workspace.plan).canUseAdvancedRewardLogic
  )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
modifiers &&
!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
// Refresh workspace to ensure latest plan data
const refreshedWorkspace = await prisma.workspace.findUnique({
where: { id: workspace.id },
select: { plan: true },
});
if (
modifiers &&
!getPlanCapabilities(refreshedWorkspace?.plan || workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/create-reward.ts around lines 53 to 59, the
plan capability check happens after extracting modifiers, which risks bypass if
workspace data is stale. Move the plan capability check to occur before any
extraction or processing of modifiers to ensure the validation happens first and
prevents unauthorized access to advanced reward logic.


const finalPartnerIds = [...includedPartnerIds, ...excludedPartnerIds];

if (finalPartnerIds && finalPartnerIds.length > 0) {
Expand Down Expand Up @@ -83,6 +94,7 @@ export const createRewardAction = authActionClient
amount,
maxDuration,
default: isDefault,
modifiers: modifiers || Prisma.JsonNull,
},
});

Expand Down
12 changes: 12 additions & 0 deletions apps/web/lib/actions/partners/update-reward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
import { getRewardOrThrow } from "@/lib/api/partners/get-reward-or-throw";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { getPlanCapabilities } from "@/lib/plan-capabilities";
import {
REWARD_EVENT_COLUMN_MAPPING,
updateRewardSchema,
} from "@/lib/zod/schemas/rewards";
import { prisma } from "@dub/prisma";
import { Prisma } from "@dub/prisma/client";
import { Reward } from "@prisma/client";
import { waitUntil } from "@vercel/functions";
import { authActionClient } from "../safe-action";
Expand All @@ -23,6 +25,7 @@ export const updateRewardAction = authActionClient
type,
includedPartnerIds,
excludedPartnerIds,
modifiers,
} = parsedInput;

includedPartnerIds = includedPartnerIds || [];
Expand All @@ -35,6 +38,14 @@ export const updateRewardAction = authActionClient
programId,
});

if (
modifiers &&
!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
Comment on lines +41 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Plan capability bypass risk

The plan capability check occurs after extracting modifiers from user input. If workspace data is stale or manipulated, this validation could be bypassed, allowing unauthorized access to advanced features.

Consider refreshing workspace plan validation and adding server-side double-checking:

+ // Refresh workspace to ensure latest plan data
+ const refreshedWorkspace = await prisma.workspace.findUnique({
+   where: { id: workspace.id },
+   select: { plan: true }
+ });
+ 
  if (
    modifiers &&
-   !getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
+   !getPlanCapabilities(refreshedWorkspace?.plan || workspace.plan).canUseAdvancedRewardLogic
  )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
modifiers &&
!getPlanCapabilities(workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
// Refresh workspace to ensure latest plan data
const refreshedWorkspace = await prisma.workspace.findUnique({
where: { id: workspace.id },
select: { plan: true }
});
if (
modifiers &&
!getPlanCapabilities(refreshedWorkspace?.plan || workspace.plan).canUseAdvancedRewardLogic
)
throw new Error(
"Advanced reward structures are only available on the Advanced plan and above.",
);
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/update-reward.ts around lines 41 to 47, the
plan capability check happens after extracting modifiers, risking bypass if
workspace data is stale or manipulated. To fix this, ensure the workspace plan
is freshly validated from a trusted source before checking capabilities, and add
a server-side double-check to enforce plan restrictions strictly before
processing modifiers or applying advanced reward logic.


const finalPartnerIds = [...includedPartnerIds, ...excludedPartnerIds];

if (finalPartnerIds && finalPartnerIds.length > 0) {
Expand Down Expand Up @@ -66,6 +77,7 @@ export const updateRewardAction = authActionClient
type,
amount,
maxDuration,
modifiers: modifiers === null ? Prisma.JsonNull : modifiers,
},
});

Expand Down
2 changes: 2 additions & 0 deletions apps/web/lib/plan-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ export const getPlanCapabilities = (
canManageProgram: !!plan && !["free", "pro"].includes(plan),
canTrackConversions: !!plan && !["free", "pro"].includes(plan),
canExportAuditLogs: !!plan && ["enterprise"].includes(plan),
canUseAdvancedRewardLogic:
!!plan && ["enterprise", "advanced"].includes(plan),
};
};
70 changes: 41 additions & 29 deletions apps/web/lib/zod/schemas/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,46 @@ export const COMMISSION_TYPES = [
},
] as const;

export const CONDITION_ENTITIES = ["customer", "sale"] as const;

export const CONDITION_CUSTOMER_ATTRIBUTES = ["country"] as const;
export const CONDITION_SALE_ATTRIBUTES = ["productId"] as const;
export const CONDITION_ATTRIBUTES = [
...CONDITION_CUSTOMER_ATTRIBUTES,
...CONDITION_SALE_ATTRIBUTES,
] as const;

export const CONDITION_OPERATORS = [
"equals_to",
"not_equals",
"starts_with",
"ends_with",
"in",
"not_in",
] as const;

export const rewardConditionSchema = z.object({
entity: z.enum(CONDITION_ENTITIES),
attribute: z.enum(CONDITION_ATTRIBUTES),
operator: z.enum(CONDITION_OPERATORS),
value: z.union([
z.string(),
z.number(),
z.array(z.string()),
z.array(z.number()),
]),
});

export const rewardConditionsSchema = z.object({
operator: z.enum(["AND", "OR"]).default("AND"),
conditions: z.array(rewardConditionSchema).min(1),
amount: z.number().int().min(0),
});

export const rewardConditionsArraySchema = z
.array(rewardConditionsSchema)
.min(1);

export const RewardSchema = z.object({
id: z.string(),
event: z.nativeEnum(EventType),
Expand Down Expand Up @@ -42,6 +82,7 @@ export const createOrUpdateRewardSchema = z.object({
.array(z.string())
.nullish()
.describe("Only applicable for default rewards"),
modifiers: rewardConditionsArraySchema.nullish(),
});

export const createRewardSchema = createOrUpdateRewardSchema.superRefine(
Expand Down Expand Up @@ -80,35 +121,6 @@ export const REWARD_EVENT_COLUMN_MAPPING = Object.freeze({
sale: "saleRewardId",
});

export const rewardConditionSchema = z.object({
entity: z.enum(["customer", "sale"]),
attribute: z.enum(["country", "productId"]),
operator: z.enum([
"equals_to",
"not_equals",
"starts_with",
"ends_with",
"in",
"not_in",
]),
value: z.union([
z.string(),
z.number(),
z.array(z.string()),
z.array(z.number()),
]),
});

export const rewardConditionsSchema = z.object({
operator: z.enum(["AND", "OR"]).default("AND"),
conditions: z.array(rewardConditionSchema).min(1),
amount: z.number().int().min(0),
});

export const rewardConditionsArraySchema = z
.array(rewardConditionsSchema)
.min(1);

export const rewardContextSchema = z.object({
customer: z
.object({
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载