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
1 change: 1 addition & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ UNSPLASH_ACCESS_KEY=
STORAGE_ACCESS_KEY_ID=
STORAGE_SECRET_ACCESS_KEY=
STORAGE_ENDPOINT=
STORAGE_BASE_URL=

# Used for internal monitoring & paging
# You can remove this by removing `DUB_SLACK_HOOK_CRON` and `DUB_SLACK_HOOK_LINKS` from the codebase
Expand Down
21 changes: 10 additions & 11 deletions apps/web/app/admin.dub.co/(dashboard)/commissions/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,13 @@ export default function CommissionsPageClient() {

const totals = useMemo(() => {
return {
commissions: timeseries?.reduce(
(acc, { commissions }) => acc + (commissions || 0),
0,
),
revenue: timeseries?.reduce(
(acc, { revenue }) => acc + (revenue || 0),
0,
),
commissions:
timeseries?.reduce(
(acc, { commissions }) => acc + (commissions || 0),
0,
) ?? 0,
revenue:
timeseries?.reduce((acc, { revenue }) => acc + (revenue || 0), 0) ?? 0,
};
}, [timeseries]);

Expand Down Expand Up @@ -205,9 +204,9 @@ export default function CommissionsPageClient() {
<span>{label}</span>
</div>
<div className="mt-1 flex h-12 items-center">
{totals[id] ? (
{(totals[id] || totals[id] === 0) && !isLoading ? (
<NumberFlow
value={totals[id] / 100}
value={(totals[id] ?? 0) / 100}
className="text-xl font-medium sm:text-3xl"
format={{
style: "currency",
Expand All @@ -217,7 +216,7 @@ export default function CommissionsPageClient() {
}}
/>
) : (
<div className="h-9 w-16 animate-pulse rounded-md bg-neutral-200" />
<div className="h-10 w-24 animate-pulse rounded-md bg-neutral-200" />
)}
</div>
</button>
Expand Down
26 changes: 15 additions & 11 deletions apps/web/app/admin.dub.co/(dashboard)/payouts/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function PayoutsPageClient() {
columns: [
{
id: "date",
header: "Date",
header: "Payment Date",
accessorKey: "date",
cell: ({ row }) => formatDateTime(row.original.date),
},
Expand Down Expand Up @@ -226,16 +226,20 @@ export default function PayoutsPageClient() {
<span>{label}</span>
</div>
<div className="mt-1 flex h-12 items-center">
<NumberFlow
value={totals[id] / 100}
className="text-xl font-medium sm:text-3xl"
format={{
style: "currency",
currency: "USD",
// @ts-ignore – trailingZeroDisplay is a valid option but TS is outdated
trailingZeroDisplay: "stripIfInteger",
}}
/>
{(totals[id] || totals[id] === 0) && !isLoading ? (
<NumberFlow
value={(totals[id] ?? 0) / 100}
className="text-xl font-medium sm:text-3xl"
format={{
style: "currency",
currency: "USD",
// @ts-ignore – trailingZeroDisplay is a valid option but TS is outdated
trailingZeroDisplay: "stripIfInteger",
}}
/>
) : (
<div className="h-10 w-24 animate-pulse rounded-md bg-neutral-200" />
)}
</div>
</button>
);
Expand Down
22 changes: 11 additions & 11 deletions apps/web/app/api/admin/payouts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ export const GET = withAdmin(async ({ searchParams }) => {
programId: {
not: ACME_PROGRAM_ID,
},
status: "completed",
paidAt: {
not: null,
status: {
not: "failed",
},
createdAt: {
gte: startDate,
lte: endDate,
},
Expand All @@ -47,7 +48,7 @@ export const GET = withAdmin(async ({ searchParams }) => {
},
},
orderBy: {
paidAt: "desc",
createdAt: "desc",
},
});

Expand All @@ -59,23 +60,22 @@ export const GET = withAdmin(async ({ searchParams }) => {
{ date: Date; payouts: number; fees: number; total: number }[]
>`
SELECT
DATE_FORMAT(CONVERT_TZ(paidAt, "UTC", ${timezone}), ${dateFormat}) as date,
DATE_FORMAT(CONVERT_TZ(createdAt, "UTC", ${timezone}), ${dateFormat}) as date,
SUM(amount) as payouts,
SUM(fee) as fees,
SUM(total) as total
FROM Invoice
WHERE
programId != ${ACME_PROGRAM_ID}
AND status = 'completed'
AND paidAt IS NOT NULL
AND paidAt >= ${startDate}
AND paidAt <= ${endDate}
GROUP BY DATE_FORMAT(CONVERT_TZ(paidAt, "UTC", ${timezone}), ${dateFormat})
AND status != 'failed'
AND createdAt >= ${startDate}
AND createdAt <= ${endDate}
GROUP BY DATE_FORMAT(CONVERT_TZ(createdAt, "UTC", ${timezone}), ${dateFormat})
ORDER BY date ASC;
`;

const formattedInvoices = invoices.map((invoice) => ({
date: invoice.paidAt,
date: invoice.createdAt,
programName: invoice.program.name,
programLogo: invoice.program.logo,
status: invoice.status,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/domains/[domain]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const PATCH = withWorkspace(
archived,
assetLinks,
appleAppSiteAssociation,
} = updateDomainBodySchema.parse(await parseRequestBody(req));
} = await updateDomainBodySchema.parseAsync(await parseRequestBody(req));

if (workspace.plan === "free") {
if (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/domains/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const POST = withWorkspace(
placeholder,
assetLinks,
appleAppSiteAssociation,
} = createDomainBodySchema.parse(body);
} = await createDomainBodySchema.parseAsync(body);

if (workspace.plan === "free") {
if (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/oauth/apps/[appId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const PATCH = withWorkspace(
logo,
pkce,
screenshots,
} = updateOAuthAppSchema.parse(await parseRequestBody(req));
} = await updateOAuthAppSchema.parseAsync(await parseRequestBody(req));

try {
const integration = await prisma.integration.findUniqueOrThrow({
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/api/oauth/apps/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const POST = withWorkspace(
logo,
pkce,
screenshots,
} = createOAuthAppSchema.parse(await parseRequestBody(req));
} = await createOAuthAppSchema.parseAsync(await parseRequestBody(req));

const integration = await prisma.integration.findUnique({
where: {
Expand Down
110 changes: 74 additions & 36 deletions apps/web/app/api/tokens/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { scopesToName, validateScopesForRole } from "@/lib/api/tokens/scopes";
import { parseRequestBody } from "@/lib/api/utils";
import { hashToken, withWorkspace } from "@/lib/auth";
import { generateRandomName } from "@/lib/names";
import { ratelimit } from "@/lib/upstash";
import { createTokenSchema, tokenSchema } from "@/lib/zod/schemas/token";
import { sendEmail } from "@dub/email";
import { APIKeyCreated } from "@dub/email/templates/api-key-created";
import { prisma } from "@dub/prisma";
import { User } from "@dub/prisma/client";
import { Prisma, User } from "@dub/prisma/client";
import { getCurrentPlan, nanoid } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import { NextResponse } from "next/server";

const MAX_WORKSPACE_TOKENS = 100;

// GET /api/tokens - get all tokens for a workspace
export const GET = withWorkspace(
async ({ workspace }) => {
Expand Down Expand Up @@ -52,6 +55,17 @@ export const GET = withWorkspace(
// POST /api/tokens – create a new token for a workspace
export const POST = withWorkspace(
async ({ req, session, workspace }) => {
const { success } = await ratelimit(1, "5 s").limit(
`create-tokens:${workspace.id}`,
);

if (!success) {
throw new DubApiError({
code: "rate_limit_exceeded",
message: "Too many requests. Please try again later.",
});
}

const { name, isMachine, scopes } = createTokenSchema.parse(
await parseRequestBody(req),
);
Expand Down Expand Up @@ -85,47 +99,71 @@ export const POST = withWorkspace(
});
}

// Create machine user if needed
if (isMachine) {
const randomName = generateRandomName();
machineUser = await prisma.user.create({
data: {
id: createId({ prefix: "user_" }),
name: `${randomName} (Machine User)`,
isMachine: true,
},
select: {
id: true,
},
});

// Add machine user to workspace
await prisma.projectUsers.create({
data: {
role: "member",
userId: machineUser.id,
projectId: workspace.id,
},
});
}

// Create token
const token = `dub_${nanoid(24)}`;
const hashedKey = await hashToken(token);
const partialKey = `${token.slice(0, 3)}...${token.slice(-4)}`;

await prisma.restrictedToken.create({
data: {
name,
hashedKey,
partialKey,
userId: isMachine ? machineUser?.id! : session.user.id,
projectId: workspace.id,
rateLimit: getCurrentPlan(workspace.plan).limits.api,
scopes:
scopes && scopes.length > 0 ? [...new Set(scopes)].join(" ") : null,
await prisma.$transaction(
async (tx) => {
const totalTokens = await tx.restrictedToken.count({
where: {
projectId: workspace.id,
installationId: null, // Skip OAuth installations tokens
},
});

if (totalTokens >= MAX_WORKSPACE_TOKENS) {
throw new DubApiError({
code: "forbidden",
message: `You've reached your limit of ${MAX_WORKSPACE_TOKENS} API keys for this workspace. Please contact support to increase this limit.`,
});
}

// Create machine user if needed
if (isMachine) {
machineUser = await tx.user.create({
data: {
id: createId({ prefix: "user_" }),
name: `${generateRandomName()} (Machine User)`,
isMachine: true,
},
select: {
id: true,
},
});

// Add machine user to workspace
await tx.projectUsers.create({
data: {
role: "member",
userId: machineUser.id,
projectId: workspace.id,
},
});
}

return await tx.restrictedToken.create({
data: {
name,
hashedKey,
partialKey,
userId: isMachine ? machineUser?.id! : session.user.id,
projectId: workspace.id,
rateLimit: getCurrentPlan(workspace.plan).limits.api,
scopes:
scopes && scopes.length > 0
? [...new Set(scopes)].join(" ")
: null,
},
});
},
});
{
isolationLevel: Prisma.TransactionIsolationLevel.ReadUncommitted,
maxWait: 5000,
timeout: 5000,
},
);

waitUntil(
sendEmail({
Expand Down
Loading