Skip to content
Open
Show file tree
Hide file tree
Changes from 85 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
e1a1f49
Partner viewer role and program/link scoping for partner profile
devkiran Apr 1, 2026
7fbf6a2
Delete get-scoped-link-ids.ts
devkiran Apr 1, 2026
a9a1fef
Updated routes to include assigned program IDs in message retrieval, …
devkiran Apr 1, 2026
2f06b45
Refactor partner profile API routes to destructure assigned program I…
devkiran Apr 1, 2026
4966fbc
Enforce partner role permissions on partner server actions
devkiran Apr 1, 2026
980c110
Merge branch 'main' into partner-viewer-role
devkiran Apr 2, 2026
98ef498
Add 'payouts.read' permission to partner profile payout routes
devkiran Apr 2, 2026
4441542
Add missing partner permission checks for links, bounties, and referrals
devkiran Apr 2, 2026
04e7bcb
Add payout_settings.read permission and scope earnings/customers by a…
devkiran Apr 2, 2026
c9817d0
Hide Payouts, Messages, Postbacks tabs from partner viewers
devkiran Apr 2, 2026
9a6db09
Merge branch 'main' into partner-viewer-role
devkiran Apr 3, 2026
12ea11e
Update footer
devkiran Apr 3, 2026
c2d6ca8
Partner profile members: programs column, schema, and per-user assign…
devkiran Apr 3, 2026
43df416
Update route.ts
devkiran Apr 3, 2026
14add7c
assign and unassign the program
devkiran Apr 3, 2026
7281874
Update partner-member-programs-sheet.tsx
devkiran Apr 3, 2026
480bd5c
Update route.ts
devkiran Apr 3, 2026
200cc88
Update route.ts
devkiran Apr 3, 2026
39fd5d6
Add link assignment route for partner user programs
devkiran Apr 3, 2026
87684ec
Merge branch 'main' into partner-viewer-role
devkiran Apr 6, 2026
9a172c4
Add programAccess field to PartnerUser with ProgramAccessScope enum
devkiran Apr 6, 2026
ad162e0
Add per-program link restrictions for partner users with restricted a…
devkiran Apr 6, 2026
2c15629
Update upload-bounty-submission-file.ts
devkiran Apr 6, 2026
f798984
Update withdraw-partner-application.ts
devkiran Apr 6, 2026
e0d2728
Update partner-user-permissions.ts
devkiran Apr 6, 2026
9d4fabe
Update partner.ts
devkiran Apr 6, 2026
e673234
Add program access handling and link restrictions for partner users
devkiran Apr 6, 2026
1ce6fde
Enhance partner profile API to include assigned link filtering
devkiran Apr 6, 2026
6b4698b
Extract linkScopeFilter and linkIncludeFilter helpers for assignedLin…
devkiran Apr 6, 2026
fe46afa
Update route.ts
devkiran Apr 6, 2026
8a28d6c
missed a few spot
devkiran Apr 6, 2026
af779b3
Update route.ts
devkiran Apr 6, 2026
a898a60
Update partner.ts
devkiran Apr 6, 2026
d43d077
Format
devkiran Apr 6, 2026
f45837c
Merge branch 'main' into partner-viewer-role
devkiran Apr 6, 2026
3d554c7
Update partner.ts
devkiran Apr 6, 2026
3b60e7b
Merge branch 'main' into partner-viewer-role
devkiran Apr 7, 2026
8b4be02
Refactor partner profile API routes to include additional conditions …
devkiran Apr 7, 2026
4159f05
Update partner.ts
devkiran Apr 7, 2026
f1a8296
Update route.ts
devkiran Apr 7, 2026
cdf47a0
Merge branch 'main' into partner-viewer-role
devkiran Apr 7, 2026
de0c8f2
Update route.ts
devkiran Apr 7, 2026
b713410
Update route.ts
devkiran Apr 7, 2026
c3326d8
some cleanup
devkiran Apr 7, 2026
3864ad2
Update route.ts
devkiran Apr 7, 2026
fd73d3d
Update get-earnings-for-partner.ts
devkiran Apr 7, 2026
b92bf4a
fix messages API
devkiran Apr 7, 2026
a147724
fix the earning filters
devkiran Apr 7, 2026
a96c405
Update route.ts
devkiran Apr 7, 2026
b88590b
Update route.ts
devkiran Apr 7, 2026
5130081
Update route.ts
devkiran Apr 7, 2026
ba737c3
extract reusable throwIfNoProgramAccess and throwIfNoLinkAccess helpers
devkiran Apr 7, 2026
fc731dd
Update playwright.yaml
devkiran Apr 7, 2026
d38e7c6
Update route.ts
devkiran Apr 7, 2026
1ae540e
Address CR comments
devkiran Apr 7, 2026
a9ff85b
Merge branch 'main' into partner-viewer-role
devkiran Apr 8, 2026
58e4d93
Add "Edit programs" to member row menu and convert program access to …
devkiran Apr 8, 2026
61d6947
Update route.ts
devkiran Apr 8, 2026
3cd2983
Update reject-partner-application-modal.tsx
devkiran Apr 8, 2026
3e095f1
Format
devkiran Apr 8, 2026
d02ed20
Sync the UI with Figma design
devkiran Apr 8, 2026
76e3bd2
adjust the UI
devkiran Apr 8, 2026
6d7f08c
Some cleanups
devkiran Apr 8, 2026
fc0b108
Update partner-member-programs-sheet.tsx
devkiran Apr 8, 2026
5d9ed5f
Update partner-member-programs-sheet.tsx
devkiran Apr 8, 2026
ac6637f
Merge branch 'main' into partner-viewer-role
steven-tey Apr 9, 2026
c9db818
Add workspace-scoped dev seed fixtures and -w/--workspace flag
devkiran Apr 9, 2026
e9d993e
Update example-workspace.json
devkiran Apr 9, 2026
f6faabf
WIP adding tests
devkiran Apr 9, 2026
cd79d50
Update rbac.spec.ts
devkiran Apr 9, 2026
6973ac1
Merge main into partner-viewer-role; resolve partner-profile payouts …
devkiran Apr 10, 2026
2fc3303
Merge branch 'main' into partner-viewer-role
steven-tey Apr 10, 2026
12fc9cb
Consolidate Playwright partner RBAC data into main seed script
devkiran Apr 10, 2026
1c24dec
Add defaultPartnerId to auth options and update seed scripts for new IDs
devkiran Apr 10, 2026
48f0a51
Update route.ts
devkiran Apr 10, 2026
86fad7d
Refactor Playwright RBAC tests for partner roles, adding dynamic endp…
devkiran Apr 10, 2026
1dd6ff1
Merge branch 'partner-viewer-role' of https://github.com/dubinc/dub i…
devkiran Apr 10, 2026
40664d3
Skip partner signup test that conflicts with RBAC seed data
devkiran Apr 10, 2026
16c7da8
Refactor partner RBAC tests and add link limits
devkiran Apr 10, 2026
8d76bce
Add payouts/settings endpoint to RBAC test matrix
devkiran Apr 10, 2026
342757b
Update global-setup.ts
devkiran Apr 10, 2026
ee2c4ed
Update rbac.spec.ts
devkiran Apr 10, 2026
b949614
Update global-setup.ts
devkiran Apr 10, 2026
e05f2e7
Update rbac.spec.ts
devkiran Apr 10, 2026
013cdeb
Update rbac.spec.ts
devkiran Apr 10, 2026
1daa8c5
fix the tests
devkiran Apr 10, 2026
f892451
fix tests one more time
devkiran Apr 10, 2026
eabef59
Update global-setup.ts
devkiran Apr 10, 2026
47c6488
Update playwright.yaml
devkiran Apr 10, 2026
3af5c9e
Update rbac.spec.ts
devkiran Apr 10, 2026
efa952d
Update playwright.yaml
devkiran Apr 10, 2026
d6a50ec
Refactor RBAC tests to restore commented-out endpoints and enhance on…
devkiran Apr 10, 2026
97c0398
Update playwright.yaml
devkiran Apr 10, 2026
d2ce884
Merge branch 'main' into partner-viewer-role
devkiran Apr 13, 2026
2805ece
Update Playwright config to refine test ignore pattern, exclude Playw…
devkiran Apr 13, 2026
6a14bb5
Pass program and link scope objects through partner profile auth
devkiran Apr 13, 2026
236f38f
Format
devkiran Apr 13, 2026
e9f5bc4
Update route.ts
devkiran Apr 13, 2026
1da379c
Merge branch 'main' into partner-viewer-role
steven-tey Apr 14, 2026
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
3 changes: 0 additions & 3 deletions .github/workflows/playwright.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ jobs:
TINYBIRD_API_KEY: "xx"
TINYBIRD_API_URL: "xx"

AXIOM_TOKEN: "xx"
AXIOM_DATASET: "xx"

# serverless-redis-http (SRH) — must match jobs.e2e.services.srh env
SRH_TOKEN: "e2e_srh_token"
UPSTASH_REDIS_REST_URL: "http://127.0.0.1:8079"
Expand Down
32 changes: 25 additions & 7 deletions apps/web/app/(ee)/api/cron/export/events/partner/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { obfuscateCustomerEmail } from "@/lib/api/partner-profile/obfuscate-cust
import { getProgramEnrollmentOrThrow } from "@/lib/api/programs/get-program-enrollment-or-throw";
import { generateExportFilename } from "@/lib/api/utils/generate-export-filename";
import { generateRandomString } from "@/lib/api/utils/generate-random-string";
import { linkIncludeFilter } from "@/lib/auth/partner-users/link-scope-filter";
import { MAX_PARTNER_LINKS_FOR_LOCAL_FILTERING } from "@/lib/constants/partner-profile";
import { verifyQstashSignature } from "@/lib/cron/verify-qstash";
import { generateRandomName } from "@/lib/names";
Expand Down Expand Up @@ -48,30 +49,46 @@ export async function POST(req: Request) {
const { columns, partnerId, programId, userId, ...parsedParams } =
payloadSchema.parse(JSON.parse(rawBody));

const user = await prisma.user.findUnique({
const partnerUser = await prisma.partnerUser.findUnique({
where: {
id: userId,
userId_partnerId: {
userId,
partnerId,
},
},
select: {
email: true,
assignedLinks: true,
user: {
select: {
email: true,
},
},
},
});

if (!user) {
return logAndRespond(`User ${userId} not found. Skipping the export.`);
if (!partnerUser) {
return logAndRespond(
`Partner user ${userId} not found. Skipping the export.`,
);
}

const { user } = partnerUser;

if (!user.email) {
return logAndRespond(`User ${userId} has no email. Skipping the export.`);
}

const assignedLinkIds = partnerUser.assignedLinks.map(
({ linkId }) => linkId,
);

const { program, links, customerDataSharingEnabledAt } =
await getProgramEnrollmentOrThrow({
partnerId,
programId,
include: {
program: true,
links: true,
links: linkIncludeFilter(assignedLinkIds),
},
});

Expand Down Expand Up @@ -116,7 +133,8 @@ export async function POST(req: Request) {
includeMetadata: false,
...(parsedParams.linkId
? { linkId: parsedParams.linkId }
: links.length > MAX_PARTNER_LINKS_FOR_LOCAL_FILTERING
: links.length > MAX_PARTNER_LINKS_FOR_LOCAL_FILTERING &&
assignedLinkIds.length === 0
? { partnerId }
: { linkId: parseFilterValue(links.map((link) => link.id)) }),
dataAvailableFrom: program.startedAt ?? program.createdAt,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/(ee)/api/partner-profile/invites/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const GET = withPartnerProfile(async ({ partner, searchParams }) => {
...invite,
id: null,
name: invite.email,
programAccess: "all",
programs: [],
}),
);

Expand Down
47 changes: 27 additions & 20 deletions apps/web/app/(ee)/api/partner-profile/messages/count/route.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import { withPartnerProfile } from "@/lib/auth/partner";
import { programScopeFilter } from "@/lib/auth/partner-users/program-scope-filter";
import { countMessagesQuerySchema } from "@/lib/zod/schemas/messages";
import { prisma } from "@dub/prisma";
import { NextResponse } from "next/server";

// GET /api/partner-profile/messages/count - count messages for a partner
export const GET = withPartnerProfile(async ({ partner, searchParams }) => {
const { unread } = countMessagesQuerySchema.parse(searchParams);
export const GET = withPartnerProfile(
async ({ partner, searchParams, partnerUser: { assignedProgramIds } }) => {
const { unread } = countMessagesQuerySchema.parse(searchParams);

const count = await prisma.message.count({
where: {
partnerId: partner.id,
...(unread !== undefined && {
// Only count messages from the program
senderPartnerId: null,
readInApp: unread
? // Only count unread messages
null
: {
// Only count read messages
not: null,
},
}),
},
});
const count = await prisma.message.count({
where: {
partnerId: partner.id,
...(unread !== undefined && {
// Only count messages from the program
senderPartnerId: null,
readInApp: unread
? // Only count unread messages
null
: {
// Only count read messages
not: null,
},
}),
...programScopeFilter(assignedProgramIds),
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

return NextResponse.json(count);
});
return NextResponse.json(count);
},
{
requiredPermission: "messages.read",
},
);
217 changes: 117 additions & 100 deletions apps/web/app/(ee)/api/partner-profile/messages/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { withPartnerProfile } from "@/lib/auth/partner";
import { throwIfNoProgramAccess } from "@/lib/auth/partner-users/throw-if-no-access";
import {
ProgramMessagesSchema,
getProgramMessagesQuerySchema,
Expand All @@ -7,120 +8,136 @@ import { prisma } from "@dub/prisma";
import { NextResponse } from "next/server";

// GET /api/partner-profile/messages - get messages grouped by program
export const GET = withPartnerProfile(async ({ partner, searchParams }) => {
const {
programSlug,
sortBy,
sortOrder,
messagesLimit: messagesLimitArg,
} = getProgramMessagesQuerySchema.parse(searchParams);
export const GET = withPartnerProfile(
async ({ partner, searchParams, partnerUser }) => {
const {
programSlug,
sortBy,
sortOrder,
messagesLimit: messagesLimitArg,
} = getProgramMessagesQuerySchema.parse(searchParams);

const messagesLimit = messagesLimitArg ?? (programSlug ? undefined : 10);
const messagesLimit = messagesLimitArg ?? (programSlug ? undefined : 10);

const programs = await prisma.program.findMany({
where: {
// Partner is not banned from the program
partners: {
none: {
partnerId: partner.id,
status: "banned",
},
},
throwIfNoProgramAccess({
programSlug,
partnerUser,
});

...(programSlug
? {
slug: programSlug,
OR: [
// Partner is enrolled in the program
// in this case, return messages regardless of messaging enabled status which is passed to the UI
{
partners: {
some: {
partnerId: partner.id,
const programs = await prisma.program.findMany({
where: {
// Partner is not banned from the program
partners: {
none: {
partnerId: partner.id,
status: "banned",
},
},
...(programSlug
? {
slug: programSlug,
OR: [
// Partner is enrolled in the program
// in this case, return messages regardless of messaging enabled status which is passed to the UI
{
partners: {
some: {
partnerId: partner.id,
},
},
},
},
{
// Partner has received a direct message from the program
messages: {
some: {
partnerId: partner.id,
senderPartnerId: null, // Sent by the program
{
// Partner has received a direct message from the program
messages: {
some: {
partnerId: partner.id,
senderPartnerId: null, // Sent by the program
},
},
},
},
],
}
: {
OR: [
// Program has messaging enabled and partner has 1+ messages with the program
{
messagingEnabledAt: {
not: null,
},
messages: {
some: {
partnerId: partner.id,
],
}
: {
OR: [
// Program has messaging enabled and partner has 1+ messages with the program
{
messagingEnabledAt: {
not: null,
},
messages: {
some: {
partnerId: partner.id,
},
},
},
},

// Partner has received a direct message from the program
{
messages: {
some: {
partnerId: partner.id,
senderPartnerId: null, // Sent by the program
// Partner has received a direct message from the program
{
messages: {
some: {
partnerId: partner.id,
senderPartnerId: null, // Sent by the program
},
},
},
},
],
}),
},
include: {
messages: {
where: {
partnerId: partner.id,
},
include: {
senderPartner: true,
senderUser: true,
},
orderBy: {
[sortBy]: sortOrder,
],
...(partnerUser.assignedProgramIds
? {
id: {
in: partnerUser.assignedProgramIds,
},
}
: {}),
}),
},
include: {
messages: {
where: {
partnerId: partner.id,
},
include: {
senderPartner: true,
senderUser: true,
},
orderBy: {
[sortBy]: sortOrder,
},
take: messagesLimit,
},
take: messagesLimit,
},
},
});
});

return NextResponse.json(
ProgramMessagesSchema.parse(
programs
// Sort by unread first, then by most recent message
.sort((a, b) => {
const aUnread = a.messages.some(
(m) => !m.senderPartnerId && !m.readInApp,
);
const bUnread = b.messages.some(
(m) => !m.senderPartnerId && !m.readInApp,
);
return NextResponse.json(
ProgramMessagesSchema.parse(
programs
// Sort by unread first, then by most recent message
.sort((a, b) => {
const aUnread = a.messages.some(
(m) => !m.senderPartnerId && !m.readInApp,
);
const bUnread = b.messages.some(
(m) => !m.senderPartnerId && !m.readInApp,
);

if (aUnread !== bUnread) {
return aUnread ? -1 : 1;
}
if (aUnread !== bUnread) {
return aUnread ? -1 : 1;
}

return sortOrder === "desc"
? (b.messages?.[0]?.[sortBy]?.getTime() ?? 0) -
(a.messages?.[0]?.[sortBy]?.getTime() ?? 0)
: (a.messages?.[0]?.[sortBy]?.getTime() ?? 0) -
(b.messages?.[0]?.[sortBy]?.getTime() ?? 0);
})
// Map to {program, messages}
.map(({ messages, ...program }) => ({
program,
messages,
})),
),
);
});
return sortOrder === "desc"
? (b.messages?.[0]?.[sortBy]?.getTime() ?? 0) -
(a.messages?.[0]?.[sortBy]?.getTime() ?? 0)
: (a.messages?.[0]?.[sortBy]?.getTime() ?? 0) -
(b.messages?.[0]?.[sortBy]?.getTime() ?? 0);
})
// Map to {program, messages}
.map(({ messages, ...program }) => ({
program,
messages,
})),
),
);
},
{
requiredPermission: "messages.read",
},
);
Loading
Loading