diff --git a/apps/web/src/api/buster_rest/security/queryRequests.ts b/apps/web/src/api/buster_rest/security/queryRequests.ts
index 6fee80cc9..1b93a6337 100644
--- a/apps/web/src/api/buster_rest/security/queryRequests.ts
+++ b/apps/web/src/api/buster_rest/security/queryRequests.ts
@@ -10,6 +10,7 @@ import {
addApprovedDomain,
removeApprovedDomain
} from './requests';
+import type { GetApprovedDomainsResponse } from '@buster/server-shared/security';
export const useGetWorkspaceSettings = () => {
return useQuery({
@@ -46,6 +47,15 @@ export const useUpdateInviteLinks = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateInviteLinks,
+ onMutate: (variables) => {
+ queryClient.setQueryData(securityQueryKeys.securityInviteLink.queryKey, (prev) => {
+ if (!prev) return prev;
+ return {
+ ...prev,
+ ...variables
+ };
+ });
+ },
onSuccess: (data) => {
queryClient.setQueryData(securityQueryKeys.securityInviteLink.queryKey, data);
}
@@ -66,6 +76,18 @@ export const useAddApprovedDomain = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addApprovedDomain,
+ onMutate: (variables) => {
+ queryClient.setQueryData(securityQueryKeys.securityApprovedDomains.queryKey, (prev) => {
+ if (!prev) return prev;
+ return {
+ ...prev,
+ ...variables.domains.map((domain) => ({
+ domain,
+ created_at: new Date().toISOString()
+ }))
+ } satisfies GetApprovedDomainsResponse;
+ });
+ },
onSuccess: (data) => {
queryClient.setQueryData(securityQueryKeys.securityApprovedDomains.queryKey, data);
}
@@ -76,6 +98,14 @@ export const useRemoveApprovedDomain = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: removeApprovedDomain,
+ onMutate: (variables) => {
+ queryClient.setQueryData(securityQueryKeys.securityApprovedDomains.queryKey, (prev) => {
+ if (!prev) return prev;
+ return prev.filter(
+ (domain) => !variables.domains.includes(domain.domain)
+ ) satisfies GetApprovedDomainsResponse;
+ });
+ },
onSuccess: (data) => {
queryClient.setQueryData(securityQueryKeys.securityApprovedDomains.queryKey, data);
}
diff --git a/apps/web/src/app/app/(settings_layout)/settings/(restricted-width)/(admin-only)/security/page.tsx b/apps/web/src/app/app/(settings_layout)/settings/(restricted-width)/(admin-only)/security/page.tsx
index 07a1c2526..034d04800 100644
--- a/apps/web/src/app/app/(settings_layout)/settings/(restricted-width)/(admin-only)/security/page.tsx
+++ b/apps/web/src/app/app/(settings_layout)/settings/(restricted-width)/(admin-only)/security/page.tsx
@@ -1,4 +1,6 @@
+import { InviteLinks } from '@/components/features/security/InviteLinks';
import { SettingsPageHeader } from '../../../_components/SettingsPageHeader';
+import { ApprovedEmailDomains } from '@/components/features/security/ApprovedEmailDomains';
export default function Page() {
return (
@@ -8,6 +10,11 @@ export default function Page() {
title="Security"
description="Manage security and general permission settings"
/>
+
+
);
diff --git a/apps/web/src/components/features/security/ApprovedEmailDomains.tsx b/apps/web/src/components/features/security/ApprovedEmailDomains.tsx
new file mode 100644
index 000000000..cdb2671c5
--- /dev/null
+++ b/apps/web/src/components/features/security/ApprovedEmailDomains.tsx
@@ -0,0 +1,173 @@
+'use client';
+
+import React, { useMemo, useState } from 'react';
+import { SecurityCards } from './SecurityCards';
+import { Input } from '@/components/ui/inputs';
+import { Button } from '@/components/ui/buttons';
+import { Text } from '@/components/ui/typography';
+import { Plus, Dots } from '@/components/ui/icons';
+import { useBusterNotifications } from '@/context/BusterNotifications';
+import { Dropdown } from '@/components/ui/dropdown';
+import {
+ useGetApprovedDomains,
+ useAddApprovedDomain,
+ useRemoveApprovedDomain
+} from '@/api/buster_rest/security/queryRequests';
+import pluralize from 'pluralize';
+import { useMemoizedFn } from '@/hooks';
+
+interface ApprovedDomain {
+ domain: string;
+ created_at: string;
+}
+
+export const ApprovedEmailDomains = React.memo(() => {
+ const { data: approvedDomains = [] } = useGetApprovedDomains();
+ const { mutateAsync: removeDomain } = useRemoveApprovedDomain();
+
+ const [isAddingDomain, setIsAddingDomain] = useState(false);
+
+ const { openInfoMessage, openErrorMessage } = useBusterNotifications();
+
+ const domainCount = approvedDomains.length;
+ const countText = pluralize('approved email domain', domainCount, true);
+
+ const handleRemoveDomain = useMemoizedFn(async (domain: string) => {
+ try {
+ await removeDomain({ domains: [domain] });
+ openInfoMessage('Domain removed successfully');
+ } catch (error) {
+ openErrorMessage('Failed to remove domain');
+ }
+ });
+
+ const sections = useMemo(
+ () =>
+ [
+ // Header section with count and add button
+
+
{countText}
+
+
,
+
+ // Add domain input section (when adding)
+ isAddingDomain && ,
+
+ // Domain list sections
+ ...approvedDomains.map((domainData) => (
+
+ ))
+ ].filter(Boolean),
+ [countText, isAddingDomain, approvedDomains, handleRemoveDomain]
+ );
+
+ return (
+
+ );
+});
+
+ApprovedEmailDomains.displayName = 'ApprovedEmailDomains';
+
+const AddDomainInput = React.memo(
+ ({ setIsAddingDomain }: { setIsAddingDomain: (isAddingDomain: boolean) => void }) => {
+ const { mutateAsync: addDomain } = useAddApprovedDomain();
+ const { openInfoMessage, openErrorMessage } = useBusterNotifications();
+
+ const [newDomain, setNewDomain] = useState('');
+
+ const handleAddDomain = useMemoizedFn(async () => {
+ if (!newDomain.trim()) return;
+
+ try {
+ await addDomain({ domains: [newDomain.trim()] });
+ setNewDomain('');
+ setIsAddingDomain(false);
+ openInfoMessage('Domain added successfully');
+ } catch (error) {
+ openErrorMessage('Failed to add domain');
+ }
+ });
+
+ return (
+
+ setNewDomain(e.target.value)}
+ placeholder="Enter domain (e.g., company.com)"
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ handleAddDomain();
+ } else if (e.key === 'Escape') {
+ setIsAddingDomain(false);
+ setNewDomain('');
+ }
+ }}
+ autoFocus
+ />
+
+
+
+ );
+ }
+);
+
+AddDomainInput.displayName = 'AddDomainInput';
+
+interface DomainListItemProps {
+ domainData: ApprovedDomain;
+ onRemove: (domain: string) => Promise;
+}
+
+const DomainListItem = React.memo(({ domainData, onRemove }) => {
+ const formatDate = (dateString: string) => {
+ return new Date(dateString).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric'
+ });
+ };
+
+ return (
+
+
+ {domainData.domain}
+
+ Added {formatDate(domainData.created_at)}
+
+
+
onRemove(domainData.domain)
+ }
+ ]}>
+ } />
+
+
+ );
+});
+
+DomainListItem.displayName = 'DomainListItem';
diff --git a/apps/web/src/components/features/security/InviteLinks.tsx b/apps/web/src/components/features/security/InviteLinks.tsx
index bb617acaa..7b3db6d42 100644
--- a/apps/web/src/components/features/security/InviteLinks.tsx
+++ b/apps/web/src/components/features/security/InviteLinks.tsx
@@ -1,17 +1,22 @@
-import React, { useState } from 'react';
+'use client';
+
+import React from 'react';
import { SecurityCards } from './SecurityCards';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/inputs';
import { Button } from '@/components/ui/buttons';
import { Text } from '@/components/ui/typography';
-import { cn } from '@/lib/classMerge';
import { Copy2, Refresh } from '@/components/ui/icons';
import { useBusterNotifications } from '@/context/BusterNotifications';
import { AppTooltip } from '@/components/ui/tooltip';
+import { useGetInviteLink, useUpdateInviteLinks } from '@/api/buster_rest/security/queryRequests';
+
+export const InviteLinks = React.memo(() => {
+ const { data: inviteLink } = useGetInviteLink();
+ const { mutateAsync: updateInviteLink } = useUpdateInviteLinks();
+ const enabled = inviteLink?.enabled ?? false;
+ const link = inviteLink?.link ?? '';
-export const InviteLinks = () => {
- const [enabled, setEnabled] = useState(false);
- const [link, setLink] = useState('');
const { openInfoMessage } = useBusterNotifications();
const onClickCopy = () => {
@@ -19,11 +24,15 @@ export const InviteLinks = () => {
openInfoMessage('Invite link copied to clipboard');
};
- const onClickRefresh = () => {
- setLink(Math.random().toString(36).substring(2, 15));
+ const onClickRefresh = async () => {
+ await updateInviteLink({ refresh_link: true });
openInfoMessage('Invite link refreshed');
};
+ const onToggleEnabled = (enabled: boolean) => {
+ updateInviteLink({ enabled });
+ };
+
return (
{
sections: [
Enable invite links
-
+
,
enabled && (
@@ -65,4 +74,6 @@ export const InviteLinks = () => {
]}
/>
);
-};
+});
+
+InviteLinks.displayName = 'InviteLinks';
diff --git a/apps/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/interfaces.ts b/apps/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/interfaces.ts
index d0edcd8d2..f6cb979b4 100644
--- a/apps/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/interfaces.ts
+++ b/apps/web/src/layouts/ChatLayout/FileContainer/FileContainerHeader/interfaces.ts
@@ -5,7 +5,7 @@ export type FileContainerSegmentProps = {
selectedFileId: string | undefined;
chatId: string | undefined;
overrideOldVersionMessage?: boolean;
- isVersionHistoryMode: boolean;
+ isVersionHistoryMode?: boolean;
};
export type FileContainerButtonsProps = {
diff --git a/packages/server-shared/src/security/requests.ts b/packages/server-shared/src/security/requests.ts
index ed6691999..1d6e8cd88 100644
--- a/packages/server-shared/src/security/requests.ts
+++ b/packages/server-shared/src/security/requests.ts
@@ -1,24 +1,30 @@
-import { z } from 'zod/v4';
-import { OrganizationRoleSchema } from '../organization';
+import { z } from "zod/v4";
+import { OrganizationRoleSchema } from "../organization";
export const UpdateInviteLinkRequestSchema = z.object({
- enabled: z.boolean(),
+ enabled: z.boolean().optional(),
refresh_link: z.boolean().optional(),
});
-export type UpdateInviteLinkRequest = z.infer;
+export type UpdateInviteLinkRequest = z.infer<
+ typeof UpdateInviteLinkRequestSchema
+>;
export const AddApprovedDomainRequestSchema = z.object({
domains: z.array(z.string()),
});
-export type AddApprovedDomainRequest = z.infer;
+export type AddApprovedDomainRequest = z.infer<
+ typeof AddApprovedDomainRequestSchema
+>;
export const RemoveApprovedDomainRequestSchema = z.object({
domains: z.array(z.string()),
});
-export type RemoveApprovedDomainRequest = z.infer;
+export type RemoveApprovedDomainRequest = z.infer<
+ typeof RemoveApprovedDomainRequestSchema
+>;
export const UpdateWorkspaceSettingsRequestSchema = z.object({
enabled: z.boolean().optional(),
@@ -32,10 +38,12 @@ export const UpdateWorkspaceSettingsRequestSchema = z.object({
.regex(
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/
),
- z.literal('all'),
+ z.literal("all"),
])
)
.optional(),
});
-export type UpdateWorkspaceSettingsRequest = z.infer;
+export type UpdateWorkspaceSettingsRequest = z.infer<
+ typeof UpdateWorkspaceSettingsRequestSchema
+>;