Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
127 changes: 88 additions & 39 deletions apps/web/app/api/analytics/dashboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,76 +18,124 @@ export const GET = async (req: Request) => {
const searchParams = getSearchParams(req.url);
const parsedParams = analyticsQuerySchema.parse(searchParams);

const { groupBy, domain, key, interval, start, end } = parsedParams;
const { groupBy, domain, key, folderId, interval, start, end } =
parsedParams;

if (!domain || !key) {
if ((!domain || !key) && !folderId) {
throw new DubApiError({
code: "bad_request",
message: "Missing domain or key query parameter",
message: "Missing domain/key or folderId query parameters",
});
}

let link;
let demoLink, link, folder, workspace;

const demoLink = DUB_DEMO_LINKS.find(
(l) => l.domain === domain && l.key === key,
);

// if it's a demo link
if (demoLink) {
link = {
id: demoLink.id,
projectId: DUB_WORKSPACE_ID,
};
} else {
link = await prisma.link.findUnique({
if (folderId) {
// Folder
folder = await prisma.folder.findUnique({
where: {
domain_key: { domain, key },
id: folderId,
},
select: {
id: true,
dashboard: true,
projectId: true,
project: {
select: {
id: true,
plan: true,
usage: true,
usageLimit: true,
createdAt: true,
},
},
...(domain && key
? {
links: {
select: { id: true },
where: {
domain,
key,
},
},
}
: {}),
},
});

// if the link is explicitly private (publicStats === false)
if (!link?.dashboard) {
if (!folder?.dashboard) {
throw new DubApiError({
code: "forbidden",
message: "This link does not have a public analytics dashboard",
message: "This folder does not have a public analytics dashboard",
});
}

const workspace = link.project;
workspace = folder.project;

assertValidDateRangeForPlan({
plan: workspace?.plan || "free",
dataAvailableFrom: workspace?.createdAt,
interval,
start,
end,
});
if ("links" in folder && folder.links?.length) link = folder.links[0];
} else {
// Link
demoLink = DUB_DEMO_LINKS.find(
(l) => l.domain === domain && l.key === key,
);

if (workspace && workspace.usage > workspace.usageLimit) {
throw new DubApiError({
code: "forbidden",
message: exceededLimitError({
plan: workspace.plan as PlanProps,
limit: workspace.usageLimit,
type: "clicks",
}),
// if it's a demo link
if (demoLink) {
link = {
id: demoLink.id,
projectId: DUB_WORKSPACE_ID,
};
} else {
link = await prisma.link.findUnique({
where: {
domain_key: { domain: domain!, key: key! },
},
select: {
id: true,
dashboard: true,
projectId: true,
project: {
select: {
id: true,
plan: true,
usage: true,
usageLimit: true,
createdAt: true,
},
},
},
});

if (!link?.dashboard) {
throw new DubApiError({
code: "forbidden",
message: "This link does not have a public analytics dashboard",
});
}

workspace = link.project;
}
}

assertValidDateRangeForPlan({
plan: workspace?.plan || "free",
dataAvailableFrom: workspace?.createdAt,
interval,
start,
end,
});

if (workspace && workspace.usage > workspace.usageLimit) {
throw new DubApiError({
code: "forbidden",
message: exceededLimitError({
plan: workspace.plan as PlanProps,
limit: workspace.usageLimit,
type: "clicks",
}),
});
}

// Rate limit in production
if (process.env.NODE_ENV !== "development") {
const ip = ipAddress(req);
Expand All @@ -98,7 +146,7 @@ export const GET = async (req: Request) => {
const { success } = await ratelimit(
demoLink ? 15 : 10,
!demoLink || groupBy === "count" ? "10 s" : "1 m",
).limit(`analytics-dashboard:${link.id}:${ip}:${groupBy}`);
).limit(`analytics-dashboard:${folder?.id || link?.id}:${ip}:${groupBy}`);

if (!success) {
throw new DubApiError({
Expand All @@ -111,8 +159,9 @@ export const GET = async (req: Request) => {
const response = await getAnalytics({
...parsedParams,
// workspaceId can be undefined (for public links that haven't been claimed/synced to a workspace)
...(link.projectId && { workspaceId: link.projectId }),
linkId: link.id,
...(workspace && { workspaceId: workspace.id }),
...(folder && { folderId: folder.id }),
...(link && { linkId: link.id }),
});

return NextResponse.json(response);
Expand Down
91 changes: 64 additions & 27 deletions apps/web/app/api/dashboards/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { createId } from "@/lib/api/create-id";
import { getLinkOrThrow } from "@/lib/api/links/get-link-or-throw";
import { withWorkspace } from "@/lib/auth";
import { verifyFolderAccess } from "@/lib/folder/permissions";
import { dashboardSchema } from "@/lib/zod/schemas/dashboard";
import { domainKeySchema } from "@/lib/zod/schemas/links";
import { getPlanCapabilities } from "@/lib/plan-capabilities";
import {
createDashboardQuerySchema,
dashboardSchema,
} from "@/lib/zod/schemas/dashboard";
import { prisma } from "@dub/prisma";
import { waitUntil } from "@vercel/functions";
import { NextResponse } from "next/server";
Expand All @@ -26,41 +29,75 @@ export const GET = withWorkspace(
// POST /api/dashboards – create a new dashboard
export const POST = withWorkspace(
async ({ searchParams, workspace, session }) => {
const { domain, key } = domainKeySchema.parse(searchParams);
const params = createDashboardQuerySchema.parse(searchParams);

const link = await getLinkOrThrow({
workspaceId: workspace.id,
domain,
key,
});
const { canTrackConversions } = getPlanCapabilities(workspace.plan);

if ("key" in params) {
const { domain, key } = params;
const link = await getLinkOrThrow({
workspaceId: workspace.id,
domain,
key,
});

if (link.folderId) {
await verifyFolderAccess({
workspace,
userId: session.user.id,
folderId: link.folderId,
requiredPermission: "folders.links.write",
});
}

const dashboard = await prisma.dashboard.create({
data: {
id: createId({ prefix: "dash_" }),
linkId: link.id,
projectId: workspace.id,
userId: link.userId,
showConversions: canTrackConversions,
},
});

// for backwards compatibility, we'll update the link to have publicStats = true
waitUntil(
prisma.link.update({
where: { id: link.id },
data: { publicStats: true },
}),
);

return NextResponse.json(dashboardSchema.parse(dashboard));
} else {
const { folderId } = params;

if (link.folderId) {
await verifyFolderAccess({
workspace,
userId: session.user.id,
folderId: link.folderId,
folderId: folderId,
requiredPermission: "folders.links.write",
});
}

const dashboard = await prisma.dashboard.create({
data: {
id: createId({ prefix: "dash_" }),
linkId: link.id,
projectId: workspace.id,
userId: link.userId,
},
});
const dashboard = await prisma.dashboard.create({
data: {
id: createId({ prefix: "dash_" }),
folderId,
projectId: workspace.id,
userId: session.user.id,
showConversions: canTrackConversions,
},
});

// for backwards compatibility, we'll update the link to have publicStats = true
waitUntil(
prisma.link.update({
where: { id: link.id },
data: { publicStats: true },
}),
);
waitUntil(
prisma.link.updateMany({
where: { folderId },
data: { publicStats: true },
}),
);

return NextResponse.json(dashboardSchema.parse(dashboard));
return NextResponse.json(dashboardSchema.parse(dashboard));
}
},
{
requiredPermissions: ["links.write"],
Expand Down
32 changes: 32 additions & 0 deletions apps/web/app/api/folders/[folderId]/dashboard/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { withWorkspace } from "@/lib/auth";
import { verifyFolderAccess } from "@/lib/folder/permissions";
import { dashboardSchema } from "@/lib/zod/schemas/dashboard";
import { prisma } from "@dub/prisma";
import { NextResponse } from "next/server";

// GET /folders/[folderId]/dashboard – get dashboard for a given folder
export const GET = withWorkspace(
async ({ params, workspace, session }) => {
const { folderId } = params;

const folder = await verifyFolderAccess({
workspace,
userId: session.user.id,
folderId,
requiredPermission: "folders.read",
});

const dashboard = await prisma.dashboard.findUnique({
where: {
folderId: folder.id,
},
});

if (!dashboard) return NextResponse.json(null);

return NextResponse.json(dashboardSchema.parse(dashboard));
},
{
requiredPermissions: ["folders.read"],
},
);
Loading