diff --git a/app/api/views-dataroom/route.ts b/app/api/views-dataroom/route.ts index 633af43a5..580473b4d 100644 --- a/app/api/views-dataroom/route.ts +++ b/app/api/views-dataroom/route.ts @@ -27,6 +27,7 @@ import { generateOTP } from "@/lib/utils/generate-otp"; import { LOCALHOST_IP } from "@/lib/utils/geo"; import { checkGlobalBlockList } from "@/lib/utils/global-block-list"; import { validateEmail } from "@/lib/utils/validate-email"; +import { isEmailAllowedByAllowList } from "@/lib/utils/allow-list-access"; export async function POST(request: NextRequest) { try { @@ -110,6 +111,12 @@ export async function POST(request: NextRequest) { domainId: true, allowList: true, denyList: true, + allowListGroupId: true, + allowListGroup: { + select: { + allowList: true, + }, + }, enableAgreement: true, agreementId: true, enableWatermark: true, @@ -289,19 +296,24 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "Access denied" }, { status: 403 }); } - // Check if email is allowed to visit the link - if (link.allowList && link.allowList.length > 0) { - // Determine if the email or its domain is allowed - const isAllowed = link.allowList.some((allowed) => - isEmailMatched(email, allowed), - ); + // Check if email is allowed by either link allowList or AllowListGroup + if (email && typeof email === "string" && email.includes("@")) { + const hasAnyAllowList = (link.allowList && link.allowList.length > 0) || + (link.allowListGroup?.allowList && link.allowListGroup.allowList.length > 0); - // Deny access if the email is not allowed - if (!isAllowed) { - return NextResponse.json( - { message: "Unauthorized access" }, - { status: 403 }, + if (hasAnyAllowList) { + const isAllowed = isEmailAllowedByAllowList( + email, + link.allowList, + link.allowListGroup ); + + if (!isAllowed) { + return NextResponse.json( + { message: "Unauthorized access" }, + { status: 403 }, + ); + } } } diff --git a/app/api/views/route.ts b/app/api/views/route.ts index 2ae77def6..4dc8f72c9 100644 --- a/app/api/views/route.ts +++ b/app/api/views/route.ts @@ -19,11 +19,12 @@ import { parseSheet } from "@/lib/sheet"; import { recordLinkView } from "@/lib/tracking/record-link-view"; import { CustomUser, WatermarkConfigSchema } from "@/lib/types"; import { checkPassword, decryptEncrpytedPassword, log } from "@/lib/utils"; -import { extractEmailDomain, isEmailMatched } from "@/lib/utils/email-domain"; import { generateOTP } from "@/lib/utils/generate-otp"; import { LOCALHOST_IP } from "@/lib/utils/geo"; import { checkGlobalBlockList } from "@/lib/utils/global-block-list"; import { validateEmail } from "@/lib/utils/validate-email"; +import { isEmailMatched } from "@/lib/utils/email-domain"; +import { isEmailAllowedByAllowList } from "@/lib/utils/allow-list-access"; export async function POST(request: NextRequest) { try { @@ -93,6 +94,12 @@ export async function POST(request: NextRequest) { slug: true, allowList: true, denyList: true, + allowListGroupId: true, + allowListGroup: { + select: { + allowList: true, + }, + }, enableAgreement: true, agreementId: true, enableWatermark: true, @@ -224,19 +231,24 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: "Access denied" }, { status: 403 }); } - // Check if email is allowed to visit the link - if (link.allowList && link.allowList.length > 0) { - // Determine if the email or its domain is allowed - const isAllowed = link.allowList.some((allowed) => - isEmailMatched(email, allowed), - ); + // Check if email is allowed by either link allowList or AllowListGroup + if (email && typeof email === "string" && email.includes("@")) { + const hasAnyAllowList = (link.allowList && link.allowList.length > 0) || + (link.allowListGroup?.allowList && link.allowListGroup.allowList.length > 0); - // Deny access if the email is not allowed - if (!isAllowed) { - return NextResponse.json( - { message: "Unauthorized access" }, - { status: 403 }, + if (hasAnyAllowList) { + const isAllowed = isEmailAllowedByAllowList( + email, + link.allowList, + link.allowListGroup ); + + if (!isAllowed) { + return NextResponse.json( + { message: "Unauthorized access" }, + { status: 403 }, + ); + } } } diff --git a/components/layouts/breadcrumb.tsx b/components/layouts/breadcrumb.tsx index 64ffe34ff..71697144b 100644 --- a/components/layouts/breadcrumb.tsx +++ b/components/layouts/breadcrumb.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/router"; import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useAllowListGroup } from "@/lib/swr/use-allow-list-groups"; import { useDataroom } from "@/lib/swr/use-dataroom"; import { useDocument } from "@/lib/swr/use-document"; import { useFolderWithParents } from "@/lib/swr/use-folders"; @@ -365,9 +366,7 @@ const VisitorsBreadcrumb = () => { - - Visitors - + Visitors @@ -396,6 +395,28 @@ const SingleVisitorBreadcrumb = () => { ); }; +const SingleVisitorGroupBreadcrumb = ({ groupId }: { groupId: string }) => { + const { allowListGroup } = useAllowListGroup(groupId); + + return ( + + + + + Visitors + + + + + + {allowListGroup?.name || "Loading..."} + + + + + ); +}; + const AnalyticsBreadcrumb = () => { const router = useRouter(); const { type = "links" } = router.query; @@ -435,8 +456,9 @@ const AnalyticsBreadcrumb = () => { export const AppBreadcrumb = () => { const router = useRouter(); const path = router.pathname; - const { id } = router.query as { + const { id, groupId } = router.query as { id?: string; + groupId?: string; }; const breadcrumb = useMemo(() => { @@ -516,8 +538,13 @@ export const AppBreadcrumb = () => { return ; } + // Visitor group route + if (path === "/visitors/groups/[groupId]" && groupId) { + return ; + } + return null; - }, [path, id]); + }, [path, id, groupId]); return breadcrumb; }; diff --git a/components/links/link-sheet/allow-list-section.tsx b/components/links/link-sheet/allow-list-section.tsx index 307e38c3d..55cbe5248 100644 --- a/components/links/link-sheet/allow-list-section.tsx +++ b/components/links/link-sheet/allow-list-section.tsx @@ -1,11 +1,23 @@ +import Link from "next/link"; + import { useEffect, useState } from "react"; import { LinkPreset } from "@prisma/client"; import { motion } from "motion/react"; import { FADE_IN_ANIMATION_SETTINGS } from "@/lib/constants"; +import { useAllowListGroupsAll } from "@/lib/swr/use-allow-list-groups"; import { sanitizeList } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { DEFAULT_LINK_TYPE } from "."; @@ -30,32 +42,20 @@ export default function AllowListSection({ }: LinkUpgradeOptions) => void; presets: LinkPreset | null; }) { - const { emailProtected, allowList } = data; + const { emailProtected, allowList, allowListGroupId } = data; + const { allowListGroups, loading: isLoadingGroups } = useAllowListGroupsAll(); + const [showAllEmails, setShowAllEmails] = useState(false); - // Initialize enabled state based on whether allowList is not null and not empty const [enabled, setEnabled] = useState( - !!allowList && allowList.length > 0, + (!!allowList && allowList.length > 0) || !!allowListGroupId, ); const [allowListInput, setAllowListInput] = useState( allowList?.join("\n") || "", ); - useEffect(() => { - // Update the allowList in the data state when their inputs change - const newAllowList = sanitizeList(allowListInput); - setEnabled((prevEnabled) => prevEnabled && emailProtected); - setData((prevData) => ({ - ...prevData, - allowList: emailProtected && enabled ? newAllowList : [], - })); - }, [allowListInput, emailProtected, enabled, setData]); - - useEffect(() => { - if (isAllowed && presets?.allowList && presets.allowList.length > 0) { - setEnabled(true); - setAllowListInput(presets.allowList.join("\n") || ""); - } - }, [presets, isAllowed]); + const selectedGroup = allowListGroups?.find( + (group) => group.id === allowListGroupId, + ); const handleEnableAllowList = () => { const updatedEnabled = !enabled; @@ -72,10 +72,51 @@ export default function AllowListSection({ setData((prevData) => ({ ...prevData, allowList: [], + allowListGroupId: null, })); } }; + const handleGroupSelection = (selectedGroupId: string) => { + if (selectedGroupId === "none") { + setData((prevData) => ({ + ...prevData, + allowListGroupId: null, + })); + } else { + setData((prevData) => ({ + ...prevData, + allowListGroupId: selectedGroupId, + })); + } + }; + + useEffect(() => { + if (isAllowed && presets) { + // Load preset data if available + if (presets.allowList && presets.allowList.length > 0) { + setEnabled(true); + setAllowListInput(presets.allowList.join("\n") || ""); + } + + if (presets.allowListGroupId) { + setEnabled(true); + setData((prevData) => ({ + ...prevData, + allowListGroupId: presets.allowListGroupId, + })); + } + } + }, [presets, isAllowed, setData]); + + useEffect(() => { + const newAllowList = sanitizeList(allowListInput); + setData((prevData) => ({ + ...prevData, + allowList: emailProtected && enabled ? newAllowList : [], + })); + }, [allowListInput, emailProtected, enabled, setData]); + const handleAllowListChange = ( event: React.ChangeEvent, ) => { @@ -105,18 +146,140 @@ export default function AllowListSection({ {enabled && ( -