mirror of https://github.com/buster-so/buster.git
create security endpoints strucutre
This commit is contained in:
parent
f5bdb9d8da
commit
9bd7824586
|
@ -0,0 +1,83 @@
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { securityQueryKeys } from '@/api/query_keys/security';
|
||||||
|
import {
|
||||||
|
getWorkspaceSettings,
|
||||||
|
getInviteLink,
|
||||||
|
getApprovedDomains,
|
||||||
|
updateWorkspaceSettings,
|
||||||
|
updateInviteLinks,
|
||||||
|
refreshInviteLink,
|
||||||
|
addApprovedDomain,
|
||||||
|
removeApprovedDomain
|
||||||
|
} from './requests';
|
||||||
|
|
||||||
|
export const useGetWorkspaceSettings = () => {
|
||||||
|
return useQuery({
|
||||||
|
...securityQueryKeys.securityGetWorkspaceSettings,
|
||||||
|
queryFn: getWorkspaceSettings
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetInviteLink = () => {
|
||||||
|
return useQuery({
|
||||||
|
...securityQueryKeys.securityInviteLink,
|
||||||
|
queryFn: getInviteLink
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetApprovedDomains = () => {
|
||||||
|
return useQuery({
|
||||||
|
...securityQueryKeys.securityApprovedDomains,
|
||||||
|
queryFn: getApprovedDomains
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateWorkspaceSettings = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: updateWorkspaceSettings,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(securityQueryKeys.securityGetWorkspaceSettings.queryKey, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateInviteLinks = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: updateInviteLinks,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(securityQueryKeys.securityInviteLink.queryKey, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRefreshInviteLink = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: refreshInviteLink,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(securityQueryKeys.securityInviteLink.queryKey, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAddApprovedDomain = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: addApprovedDomain,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(securityQueryKeys.securityApprovedDomains.queryKey, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoveApprovedDomain = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: removeApprovedDomain,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData(securityQueryKeys.securityApprovedDomains.queryKey, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { mainApiV2 } from '../instances';
|
||||||
|
import {
|
||||||
|
type UpdateInviteLinkRequest,
|
||||||
|
type AddApprovedDomainRequest,
|
||||||
|
type RemoveApprovedDomainRequest,
|
||||||
|
type UpdateWorkspaceSettingsRequest,
|
||||||
|
type GetApprovedDomainsResponse,
|
||||||
|
type GetInviteLinkResponse,
|
||||||
|
type RefreshInviteLinkResponse,
|
||||||
|
type UpdateInviteLinkResponse,
|
||||||
|
type AddApprovedDomainsResponse,
|
||||||
|
type GetWorkspaceSettingsResponse,
|
||||||
|
type UpdateWorkspaceSettingsResponse
|
||||||
|
} from '@buster/server-shared/security';
|
||||||
|
|
||||||
|
export const updateInviteLinks = async (request: UpdateInviteLinkRequest) => {
|
||||||
|
return await mainApiV2
|
||||||
|
.post<UpdateInviteLinkResponse>('/security/invite-links', request)
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshInviteLink = async () => {
|
||||||
|
return await mainApiV2
|
||||||
|
.post<RefreshInviteLinkResponse>('/security/invite-links/refresh')
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getInviteLink = async () => {
|
||||||
|
return await mainApiV2
|
||||||
|
.get<GetInviteLinkResponse>('/security/invite-links')
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApprovedDomains = async () => {
|
||||||
|
return await mainApiV2
|
||||||
|
.get<GetApprovedDomainsResponse>('/security/approved-domains')
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addApprovedDomain = async (request: AddApprovedDomainRequest) => {
|
||||||
|
return await mainApiV2
|
||||||
|
.post<AddApprovedDomainsResponse>('/security/approved-domains', request)
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeApprovedDomain = async (request: RemoveApprovedDomainRequest) => {
|
||||||
|
return await mainApiV2
|
||||||
|
.delete<GetApprovedDomainsResponse>('/security/approved-domains', {
|
||||||
|
data: request
|
||||||
|
})
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWorkspaceSettings = async () => {
|
||||||
|
return await mainApiV2
|
||||||
|
.get<GetWorkspaceSettingsResponse>('/security/settings')
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateWorkspaceSettings = async (request: UpdateWorkspaceSettingsRequest) => {
|
||||||
|
return await mainApiV2
|
||||||
|
.put<UpdateWorkspaceSettingsResponse>('/security/settings', request)
|
||||||
|
.then((res) => res.data);
|
||||||
|
};
|
|
@ -10,6 +10,7 @@ import { permissionGroupQueryKeys } from './permission_groups';
|
||||||
import { searchQueryKeys } from './search';
|
import { searchQueryKeys } from './search';
|
||||||
import { termsQueryKeys } from './terms';
|
import { termsQueryKeys } from './terms';
|
||||||
import { userQueryKeys } from './users';
|
import { userQueryKeys } from './users';
|
||||||
|
import { securityQueryKeys } from './security';
|
||||||
|
|
||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
...datasetQueryKeys,
|
...datasetQueryKeys,
|
||||||
|
@ -23,5 +24,6 @@ export const queryKeys = {
|
||||||
...datasourceQueryKeys,
|
...datasourceQueryKeys,
|
||||||
...datasetGroupQueryKeys,
|
...datasetGroupQueryKeys,
|
||||||
...permissionGroupQueryKeys,
|
...permissionGroupQueryKeys,
|
||||||
...currencyQueryKeys
|
...currencyQueryKeys,
|
||||||
|
...securityQueryKeys
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import type {
|
||||||
|
GetWorkspaceSettingsResponse,
|
||||||
|
GetApprovedDomainsResponse,
|
||||||
|
GetInviteLinkResponse
|
||||||
|
} from '@buster/server-shared/security';
|
||||||
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const securityGetWorkspaceSettings = queryOptions<GetWorkspaceSettingsResponse>({
|
||||||
|
queryKey: ['security', 'workspace-settings']
|
||||||
|
});
|
||||||
|
|
||||||
|
export const securityApprovedDomains = queryOptions<GetApprovedDomainsResponse>({
|
||||||
|
queryKey: ['security', 'approved-domains']
|
||||||
|
});
|
||||||
|
|
||||||
|
export const securityInviteLink = queryOptions<GetInviteLinkResponse>({
|
||||||
|
queryKey: ['security', 'invite-link']
|
||||||
|
});
|
||||||
|
|
||||||
|
export const securityQueryKeys = {
|
||||||
|
securityGetWorkspaceSettings,
|
||||||
|
securityApprovedDomains,
|
||||||
|
securityInviteLink
|
||||||
|
};
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { SettingsPageHeader } from '../../../_components/SettingsPageHeader';
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div>
|
||||||
|
<SettingsPageHeader
|
||||||
|
title="Security"
|
||||||
|
description="Manage security and general permission settings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { InviteLinks } from './InviteLinks';
|
||||||
|
|
||||||
|
const meta: Meta<typeof InviteLinks> = {
|
||||||
|
title: 'Features/InviteLinks',
|
||||||
|
component: InviteLinks,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'A security feature component that allows administrators to manage invite links for workspace access. Users can enable/disable invite links, generate new links, and copy them to clipboard.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof InviteLinks>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
name: 'Default',
|
||||||
|
parameters: {
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
story:
|
||||||
|
'The default state of the InviteLinks component showing the toggle switch, link input field, and action buttons.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React, { useState } 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';
|
||||||
|
|
||||||
|
export const InviteLinks = () => {
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
const [link, setLink] = useState('');
|
||||||
|
const { openInfoMessage } = useBusterNotifications();
|
||||||
|
|
||||||
|
const onClickCopy = () => {
|
||||||
|
navigator.clipboard.writeText(link);
|
||||||
|
openInfoMessage('Invite link copied to clipboard');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickRefresh = () => {
|
||||||
|
setLink(Math.random().toString(36).substring(2, 15));
|
||||||
|
openInfoMessage('Invite link refreshed');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SecurityCards
|
||||||
|
title="Invite links"
|
||||||
|
description="A uniquely generated invite link allows anyone with the link to join your workspace"
|
||||||
|
cards={[
|
||||||
|
{
|
||||||
|
sections: [
|
||||||
|
<div key="title" className="flex items-center justify-between">
|
||||||
|
<Text>Enable invite links</Text>
|
||||||
|
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||||
|
</div>,
|
||||||
|
enabled && (
|
||||||
|
<div key="link" className="flex items-center justify-between space-x-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
className="w-full bg-transparent!"
|
||||||
|
disabled
|
||||||
|
value={link}
|
||||||
|
placeholder="Invite link"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-1/2 right-1 -translate-y-1/2">
|
||||||
|
<AppTooltip title="Refresh the invite link" side="top" sideOffset={8}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size={'small'}
|
||||||
|
onClick={onClickRefresh}
|
||||||
|
suffix={<Refresh />}
|
||||||
|
/>
|
||||||
|
</AppTooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="outlined" size={'tall'} onClick={onClickCopy} suffix={<Copy2 />}>
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
].filter(Boolean)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,268 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { SecurityCards } from './SecurityCards';
|
||||||
|
import { Button } from '@/components/ui/buttons';
|
||||||
|
import { Pill } from '@/components/ui/pills/Pill';
|
||||||
|
import { Text } from '@/components/ui/typography';
|
||||||
|
|
||||||
|
const meta: Meta<typeof SecurityCards> = {
|
||||||
|
title: 'Features/SecurityCards',
|
||||||
|
component: SecurityCards,
|
||||||
|
parameters: {
|
||||||
|
layout: 'padded',
|
||||||
|
docs: {
|
||||||
|
description: {
|
||||||
|
component:
|
||||||
|
'A security cards component that displays security-related information in structured card sections.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof SecurityCards>;
|
||||||
|
|
||||||
|
// Mock data for different use cases
|
||||||
|
const basicSections = [
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Two-Factor Authentication</Text>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Add an extra layer of security to your account
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small" variant="outlined">
|
||||||
|
Enable
|
||||||
|
</Button>
|
||||||
|
</div>,
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">API Keys</Text>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Manage your API access tokens
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small" variant="outlined">
|
||||||
|
Manage
|
||||||
|
</Button>
|
||||||
|
</div>,
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Invite Links</Text>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Share your account with others
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small" variant="outlined">
|
||||||
|
Manage
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
|
||||||
|
const detailedSections = [
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 flex items-center gap-2">
|
||||||
|
<Text className="font-medium">Password Policy</Text>
|
||||||
|
<Pill variant="success">Active</Pill>
|
||||||
|
</div>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Minimum 8 characters with uppercase, lowercase, numbers, and symbols
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small">Update</Button>
|
||||||
|
</div>,
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="mb-1 flex items-center gap-2">
|
||||||
|
<Text className="font-medium">Session Timeout</Text>
|
||||||
|
<Pill variant="gray">30 minutes</Pill>
|
||||||
|
</div>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Automatic logout after period of inactivity
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small" variant="outlined">
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
</div>,
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">IP Restrictions</Text>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Limit access to specific IP addresses
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small" variant="ghost">
|
||||||
|
Setup
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
|
||||||
|
const accessLogSections = [
|
||||||
|
<div>
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<Text className="font-medium">Recent Login Activity</Text>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Last 7 days
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between py-1">
|
||||||
|
<div>
|
||||||
|
<Text size="sm">Chrome on macOS</Text>
|
||||||
|
<Text variant="secondary" size="xs">
|
||||||
|
192.168.1.100 • 2 hours ago
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Pill variant="success">Current</Pill>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between py-1">
|
||||||
|
<div>
|
||||||
|
<Text size="sm">Safari on iPhone</Text>
|
||||||
|
<Text variant="secondary" size="xs">
|
||||||
|
10.0.1.50 • 1 day ago
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small" variant="ghost">
|
||||||
|
Revoke
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Account Security',
|
||||||
|
description: 'Manage your security settings and authentication methods',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
sections: basicSections
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultipleCards: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Security Overview',
|
||||||
|
description: 'Complete security configuration for your organization',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
sections: detailedSections
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sections: accessLogSections
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SingleSection: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Quick Setup',
|
||||||
|
description: 'Essential security setting that needs immediate attention',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
sections: [
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Enable Two-Factor Authentication</Text>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Protect your account with an additional verification step
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button>Get Started</Button>
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmptyState: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Security Settings',
|
||||||
|
description: 'No security configurations available',
|
||||||
|
cards: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ComplexContent: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Advanced Security Configuration',
|
||||||
|
description: 'Comprehensive security settings with detailed controls',
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
sections: [
|
||||||
|
<div>
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Organization Policies</Text>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Configure security policies for all team members
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Button size="small">Manage</Button>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Text size="sm" className="font-medium">
|
||||||
|
Password Requirements
|
||||||
|
</Text>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||||
|
<Text size="xs" variant="secondary">
|
||||||
|
Minimum 12 characters
|
||||||
|
</Text>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
|
||||||
|
<Text size="xs" variant="secondary">
|
||||||
|
Special characters required
|
||||||
|
</Text>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Text size="sm" className="font-medium">
|
||||||
|
Access Controls
|
||||||
|
</Text>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-blue-500" />
|
||||||
|
<Text size="xs" variant="secondary">
|
||||||
|
Role-based permissions
|
||||||
|
</Text>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-blue-500" />
|
||||||
|
<Text size="xs" variant="secondary">
|
||||||
|
IP whitelisting enabled
|
||||||
|
</Text>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Text className="font-medium">Audit Logging</Text>
|
||||||
|
<Text variant="secondary" size="sm">
|
||||||
|
Track all security-related events and user actions
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Pill variant="success">Enabled</Pill>
|
||||||
|
<Button size="small" variant="outlined">
|
||||||
|
View Logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Title, Paragraph } from '@/components/ui/typography';
|
||||||
|
import { cn } from '@/lib/classMerge';
|
||||||
|
|
||||||
|
interface SecurityCardsProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
cards: {
|
||||||
|
sections: React.ReactNode[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecurityCards: React.FC<SecurityCardsProps> = ({ title, description, cards }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-3.5">
|
||||||
|
<div className="flex flex-col space-y-1.5">
|
||||||
|
<Title as="h3" className="text-lg">
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
<Paragraph variant="secondary">{description}</Paragraph>
|
||||||
|
</div>
|
||||||
|
{cards.map((card, index) => (
|
||||||
|
<SecurityCard key={index} sections={card.sections} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SecurityCard = ({ sections }: { sections: React.ReactNode[] }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col rounded border">
|
||||||
|
{sections.map((section, index) => (
|
||||||
|
<div key={index} className={cn(index !== sections.length - 1 && 'border-b', 'px-4 py-2.5')}>
|
||||||
|
{section}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -52,6 +52,11 @@ const permissionAndSecurityItems = (currentParentRoute: BusterRoutes): ISidebarG
|
||||||
id: 'permission-and-security',
|
id: 'permission-and-security',
|
||||||
icon: <LockCircle />,
|
icon: <LockCircle />,
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Security',
|
||||||
|
route: createBusterRoute({ route: BusterRoutes.SETTINGS_SECURITY }),
|
||||||
|
id: createBusterRoute({ route: BusterRoutes.SETTINGS_SECURITY })
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
route: createBusterRoute({ route: BusterRoutes.SETTINGS_USERS }),
|
route: createBusterRoute({ route: BusterRoutes.SETTINGS_USERS }),
|
||||||
|
@ -89,8 +94,18 @@ export const SidebarSettings: React.FC = React.memo(() => {
|
||||||
return (
|
return (
|
||||||
<Sidebar
|
<Sidebar
|
||||||
content={content}
|
content={content}
|
||||||
header={useMemo(() => <SidebarSettingsHeader />, [])}
|
header={useMemo(
|
||||||
footer={useMemo(() => <SidebarUserFooter />, [])}
|
() => (
|
||||||
|
<SidebarSettingsHeader />
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)}
|
||||||
|
footer={useMemo(
|
||||||
|
() => (
|
||||||
|
<SidebarUserFooter />
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -56,6 +56,10 @@
|
||||||
"./teams": {
|
"./teams": {
|
||||||
"types": "./dist/teams/index.d.ts",
|
"types": "./dist/teams/index.d.ts",
|
||||||
"default": "./dist/teams/index.js"
|
"default": "./dist/teams/index.js"
|
||||||
|
},
|
||||||
|
"./security": {
|
||||||
|
"types": "./dist/security/index.d.ts",
|
||||||
|
"default": "./dist/security/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './requests';
|
||||||
|
export * from './responses';
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { z } from 'zod/v4';
|
||||||
|
import { OrganizationRoleSchema } from '../organization';
|
||||||
|
|
||||||
|
export const UpdateInviteLinkRequestSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
refresh_link: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateInviteLinkRequest = z.infer<typeof UpdateInviteLinkRequestSchema>;
|
||||||
|
|
||||||
|
export const AddApprovedDomainRequestSchema = z.object({
|
||||||
|
domains: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AddApprovedDomainRequest = z.infer<typeof AddApprovedDomainRequestSchema>;
|
||||||
|
|
||||||
|
export const RemoveApprovedDomainRequestSchema = z.object({
|
||||||
|
domains: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RemoveApprovedDomainRequest = z.infer<typeof RemoveApprovedDomainRequestSchema>;
|
||||||
|
|
||||||
|
export const UpdateWorkspaceSettingsRequestSchema = z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
default_role: OrganizationRoleSchema.optional(),
|
||||||
|
// this can either be a uuid or "all"
|
||||||
|
default_datasets_ids: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.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'),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UpdateWorkspaceSettingsRequest = z.infer<typeof UpdateWorkspaceSettingsRequestSchema>;
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { OrganizationRoleSchema } from "../organization";
|
||||||
|
|
||||||
|
export const GetInviteLinkResponseSchema = z.object({
|
||||||
|
link: z.string(),
|
||||||
|
enabled: z.boolean(),
|
||||||
|
});
|
||||||
|
export const UpdateInviteLinkResponseSchema = GetInviteLinkResponseSchema;
|
||||||
|
export const RefreshInviteLinkResponseSchema = GetInviteLinkResponseSchema;
|
||||||
|
|
||||||
|
export const GetApprovedDomainsResponseSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
domain: z.string(),
|
||||||
|
created_at: z.string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export const AddApprovedDomainsResponseSchema =
|
||||||
|
GetApprovedDomainsResponseSchema;
|
||||||
|
export const UpdateApprovedDomainsResponseSchema =
|
||||||
|
GetApprovedDomainsResponseSchema;
|
||||||
|
export const RemoveApprovedDomainsResponseSchema =
|
||||||
|
GetApprovedDomainsResponseSchema;
|
||||||
|
|
||||||
|
export const GetWorkspaceSettingsResponseSchema = z.object({
|
||||||
|
enabled: z.boolean(),
|
||||||
|
default_role: OrganizationRoleSchema,
|
||||||
|
default_datasets: z.array(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
export const UpdateWorkspaceSettingsResponseSchema =
|
||||||
|
GetWorkspaceSettingsResponseSchema;
|
||||||
|
|
||||||
|
export type RefreshInviteLinkResponse = z.infer<
|
||||||
|
typeof RefreshInviteLinkResponseSchema
|
||||||
|
>;
|
||||||
|
export type UpdateInviteLinkResponse = z.infer<
|
||||||
|
typeof UpdateInviteLinkResponseSchema
|
||||||
|
>;
|
||||||
|
export type GetInviteLinkResponse = z.infer<typeof GetInviteLinkResponseSchema>;
|
||||||
|
export type GetApprovedDomainsResponse = z.infer<
|
||||||
|
typeof GetApprovedDomainsResponseSchema
|
||||||
|
>;
|
||||||
|
export type AddApprovedDomainsResponse = z.infer<
|
||||||
|
typeof AddApprovedDomainsResponseSchema
|
||||||
|
>;
|
||||||
|
export type UpdateApprovedDomainsResponse = z.infer<
|
||||||
|
typeof UpdateApprovedDomainsResponseSchema
|
||||||
|
>;
|
||||||
|
export type RemoveApprovedDomainsResponse = z.infer<
|
||||||
|
typeof RemoveApprovedDomainsResponseSchema
|
||||||
|
>;
|
||||||
|
export type GetWorkspaceSettingsResponse = z.infer<
|
||||||
|
typeof GetWorkspaceSettingsResponseSchema
|
||||||
|
>;
|
||||||
|
export type UpdateWorkspaceSettingsResponse = z.infer<
|
||||||
|
typeof UpdateWorkspaceSettingsResponseSchema
|
||||||
|
>;
|
Loading…
Reference in New Issue