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
3 changes: 0 additions & 3 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,6 @@ PLAIN_WEBHOOK_SECRET=
###### DUB.CO INTERNAL USE ONLY ######
######################################

# For the official Dub app
NEXT_PUBLIC_IS_DUB=

# For storing vector embeddings (/api/support/chat)
UPSTASH_VECTOR_REST_URL=
UPSTASH_VECTOR_REST_TOKEN=
Expand Down
19 changes: 18 additions & 1 deletion apps/web/app/(ee)/api/cron/partners/merge-accounts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function POST(req: Request) {
select: {
programId: true,
tenantId: true,
status: true,
},
},
users: {
Expand Down Expand Up @@ -262,8 +263,24 @@ export async function POST(req: Request) {
const targetEnrollment = targetPartnerEnrollments.find(
({ programId }) => programId === sourceEnrollment.programId,
);

await prisma.$transaction(async (tx) => {
// delete old source enrollment
if (
targetEnrollment &&
sourceEnrollment.status === "approved" &&
["pending", "invited"].includes(targetEnrollment.status)
) {
await tx.programEnrollment.update({
where: {
partnerId_programId: {
partnerId: targetPartnerId,
programId: sourceEnrollment.programId,
},
},
data: { status: "approved" },
});
}

await tx.programEnrollment.delete({
where: {
partnerId_programId: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ const partnersColumns = {
all: [
"partner",
"group",
"createdAt",
"tags",
"createdAt",
"status",
"location",
"totalClicks",
Expand Down Expand Up @@ -166,7 +166,7 @@ export function PartnersTable() {
const { groups } = useGroups();

const { columnVisibility, setColumnVisibility } = useColumnVisibility(
"partners-table-columns-v2",
"partners-table-columns-v3",
partnersColumns,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { clientAccessCheck } from "@/lib/client-access-check";
import { MEGA_WORKSPACE_LINKS_LIMIT } from "@/lib/constants/misc";
import useGroupsCount from "@/lib/swr/use-groups-count";
import useTagsCount from "@/lib/swr/use-tags-count";
import { useLinkTagsCount } from "@/lib/swr/use-link-tags-count";
import { useUsageTimeseries } from "@/lib/swr/use-usage-timeseries";
import useWorkspace from "@/lib/swr/use-workspace";
import useWorkspaceUsers from "@/lib/swr/use-workspace-users";
Expand Down Expand Up @@ -88,7 +88,7 @@ export default function PlanUsage() {
const { StartPaidPlanModal, setShowStartPaidPlanModal } =
useStartPaidPlanModal();

const { data: tags } = useTagsCount();
const { data: tags } = useLinkTagsCount();
const { users } = useWorkspaceUsers();

const { groupsCount } = useGroupsCount();
Expand Down Expand Up @@ -367,21 +367,21 @@ export default function PlanUsage() {
)}
>
<UsageCategory
title="Custom Domains"
title="Custom domains"
icon={Globe}
usage={domains?.length}
usageLimit={domainsLimit}
href={`/${slug}/settings/domains`}
/>
<UsageCategory
title="Folders"
title="Link folders"
icon={Folder5}
usage={foldersUsage}
usageLimit={foldersLimit}
href={`/${slug}/settings/library/folders`}
/>
<UsageCategory
title="Tags"
title="Link tags"
icon={Tag}
usage={tags}
usageLimit={tagsLimit}
Expand All @@ -404,7 +404,7 @@ export default function PlanUsage() {
href={`/${slug}/program/partners`}
/>
<UsageCategory
title="Partner Groups"
title="Partner groups"
icon={Users6}
usage={groupsCount ?? 0}
usageLimit={groupsLimit}
Expand All @@ -416,7 +416,7 @@ export default function PlanUsage() {
usage={payoutsUsage}
usageLimit={payoutsLimit}
unit="$"
href={`/${slug}/program/payouts`}
href={`/${slug}/program/payouts?status=pending`}
/>
<UsageCategory
title="Payout fees"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useLinkTagsCount } from "@/lib/swr/use-link-tags-count";
import useTags from "@/lib/swr/use-tags";
import useTagsCount from "@/lib/swr/use-tags-count";
import { TAGS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/tags";
import { useAddEditTagModal } from "@/ui/modals/add-edit-tag-modal";
import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state";
Expand Down Expand Up @@ -40,7 +40,7 @@ export default function WorkspaceTagsClient() {
},
includeLinksCount: true,
});
const { data: tagsCount } = useTagsCount({
const { data: tagsCount } = useLinkTagsCount({
query: { search: search ?? "" },
});

Expand Down
90 changes: 43 additions & 47 deletions apps/web/lib/actions/send-otp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,56 +38,52 @@ export const sendOtpAction = actionClient
throw new Error("Too many requests. Please try again later.");
}

if (email.includes("+") && isGenericEmail(email)) {
throw new Error(
"Email addresses with + are not allowed. Please use your work email instead.",
);
}
const isGenericEmailWithPlus = email.includes("+") && isGenericEmail(email);

const domain = email.split("@")[1];
const emailDomain = email.split("@")[1];

if (process.env.NEXT_PUBLIC_IS_DUB) {
const [isDisposable, emailDomainTerms] = await Promise.all([
redis.sismember("disposableEmailDomains", domain),
process.env.EDGE_CONFIG ? get("emailDomainTerms") : [],
]);
const [isDisposable, emailDomainTerms] = await Promise.all([
redis.sismember("disposableEmailDomains", emailDomain),
process.env.EDGE_CONFIG ? get("emailDomainTerms") : [],
]);

// Only build the regex if we have at least one term; otherwise set to null
const blacklistedEmailDomainTermsRegex =
emailDomainTerms && Array.isArray(emailDomainTerms)
? new RegExp(
emailDomainTerms
.map((term: string) =>
term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
) // replace special characters with escape sequences
.join("|"),
)
: null;

if (
isDisposable ||
(blacklistedEmailDomainTermsRegex &&
blacklistedEmailDomainTermsRegex.test(domain))
) {
// edge case: the user already has a partner account on Dub with this email address,
// or they have an existing application for a program, we can allow them to continue
const [isPartnerAccount, hasExistingApplications] = await Promise.all([
prisma.partner.findUnique({
where: {
email,
},
}),
prisma.programApplication.findFirst({
where: {
email,
},
}),
]);
if (!isPartnerAccount && !hasExistingApplications) {
throw new Error(
"Invalid email address – please use your work email instead. If you think this is a mistake, please contact us at dub.co/support",
);
}
// Only build the regex if we have at least one term; otherwise set to null
const blacklistedEmailDomainTermsRegex =
emailDomainTerms && Array.isArray(emailDomainTerms)
? new RegExp(
emailDomainTerms
.map((term: string) =>
term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
) // replace special characters with escape sequences
.join("|"),
)
: null;

// if any of the flags match, run one final edge case check, before throwing an error
if (
isGenericEmailWithPlus ||
isDisposable ||
(blacklistedEmailDomainTermsRegex &&
blacklistedEmailDomainTermsRegex.test(emailDomain))
) {
// edge case: the user already has a partner account on Dub with this email address,
// or they have an existing application for a program, we can allow them to continue
const [isPartnerAccount, hasExistingApplications] = await Promise.all([
prisma.partner.findUnique({
where: {
email,
},
}),
prisma.programApplication.findFirst({
where: {
email,
},
}),
]);
if (!isPartnerAccount && !hasExistingApplications) {
throw new Error(
"Invalid email address – please use your work email instead. If you think this is a mistake, please contact us at dub.co/support",
);
}
}

Expand Down
4 changes: 2 additions & 2 deletions apps/web/lib/analytics/get-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export const getAnalytics = async (params: AnalyticsFilters) => {
: `SUM(${event}) as ${event}`;

const response = await conn.execute(
`SELECT ${aggregateColumns} FROM Link WHERE id IN (${linkIdPlaceholders})`,
normalizedLinkId.values,
`SELECT ${aggregateColumns} FROM Link WHERE id IN (${linkIdPlaceholders}) AND projectId = ?`,
[...normalizedLinkId.values, workspaceId],
);

return analyticsResponse["count"].parse(response.rows[0]);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/api/links/utils/key-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function keyChecks({
};
}

if (isDubDomain(domain) && process.env.NEXT_PUBLIC_IS_DUB) {
if (isDubDomain(domain)) {
if (domain === "dub.sh" || domain === "dub.link") {
if (DEFAULT_REDIRECTS[key] || RESERVED_SLUGS.includes(key)) {
return {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/edge-config/get-feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const getFeatureFlags = async ({
analyticsSettingsSiteVisitTracking: false,
};

if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) {
if (!process.env.EDGE_CONFIG) {
// return all features as true if edge config is not available
return Object.fromEntries(
Object.entries(workspaceFeatures).map(([key, _v]) => [key, true]),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/edge-config/is-blacklisted-domain.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getAll } from "@vercel/edge-config";

export const isBlacklistedDomain = async (domain: string): Promise<boolean> => {
if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) {
if (!process.env.EDGE_CONFIG) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/edge-config/is-blacklisted-email.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { get } from "@vercel/edge-config";

export const isBlacklistedEmail = async (email: string | string[]) => {
if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) {
if (!process.env.EDGE_CONFIG) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/edge-config/is-blacklisted-key.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { get } from "@vercel/edge-config";

export const isBlacklistedKey = async (key: string) => {
if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) {
if (!process.env.EDGE_CONFIG) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/edge-config/is-blacklisted-referrer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getDomainWithoutWWW } from "@dub/utils";
import { get } from "@vercel/edge-config";

export const isBlacklistedReferrer = async (referrer: string | null) => {
if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) {
if (!process.env.EDGE_CONFIG) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/edge-config/is-reserved-username.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { get } from "@vercel/edge-config";
* Check if a username is reserved – should only be available on Pro+
*/
export const isReservedUsername = async (key: string) => {
if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) {
if (!process.env.EDGE_CONFIG) {
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import useWorkspace from "./use-workspace";

const partialQuerySchema = getTagsCountQuerySchema.partial();

export default function useTagsCount({
export function useLinkTagsCount({
query,
}: { query?: z.infer<typeof partialQuerySchema> } = {}) {
const { id: workspaceId } = useWorkspace();
Expand Down
4 changes: 2 additions & 2 deletions apps/web/ui/links/link-builder/tag-select.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useLinkTagsCount } from "@/lib/swr/use-link-tags-count";
import useTags from "@/lib/swr/use-tags";
import useTagsCount from "@/lib/swr/use-tags-count";
import useWorkspace from "@/lib/swr/use-workspace";
import { TagProps } from "@/lib/types";
import { TAGS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/tags";
Expand Down Expand Up @@ -43,7 +43,7 @@ export const TagSelect = memo(() => {
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebounce(search, 500);

const { data: tagsCount } = useTagsCount();
const { data: tagsCount } = useLinkTagsCount();
const useAsync = tagsCount && tagsCount > TAGS_MAX_PAGE_SIZE;

const { tags: availableTags, loading: loadingTags } = useTags({
Expand Down
4 changes: 2 additions & 2 deletions apps/web/ui/links/use-link-filters.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import useCurrentFolderId from "@/lib/swr/use-current-folder-id";
import { useLinkTagsCount } from "@/lib/swr/use-link-tags-count";
import useLinksCount from "@/lib/swr/use-links-count";
import useTags from "@/lib/swr/use-tags";
import useTagsCount from "@/lib/swr/use-tags-count";
import useWorkspaceUsers from "@/lib/swr/use-workspace-users";
import { TagProps } from "@/lib/types";
import { TAGS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/tags";
Expand Down Expand Up @@ -197,7 +197,7 @@ function useTagFilterOptions({
[searchParamsObj.tagIds],
);

const { data: tagsCount } = useTagsCount();
const { data: tagsCount } = useLinkTagsCount();
const tagsAsync = Boolean(tagsCount && tagsCount > TAGS_MAX_PAGE_SIZE);
const { tags, loading: loadingTags } = useTags({
query: { search: tagsAsync ? search : "" },
Expand Down
Loading
Loading