feat(2FA): add admin security settings pages
This commit is contained in:
1 parent
1b78192253
commit
2359dbad64
5 files changed
+131
-1
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { IconLabel } from '@/components/common/IconLabel';
|
||||
import { NavMenu } from '@/components/common/NavMenu';
|
||||
import { useMessages, useNavigation } from '@/components/hooks';
|
||||
import { ArrowLeft, Globe, User, Users } from '@/components/icons';
|
||||
import { ArrowLeft, Globe, ShieldCheck, User, Users } from '@/components/icons';
|
||||
import { Column, Focusable, Row, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import Link from '@/components/common/Link';
|
||||
|
||||
@@ -31,6 +31,12 @@ export function AdminNav({ onItemClick }: { onItemClick?: () => void }) {
|
||||
path: '/admin/teams',
|
||||
icon: <Users />,
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
label: t(labels.security),
|
||||
path: '/admin/security',
|
||||
icon: <ShieldCheck />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
import { Column, Row, Switch, Text } from '@umami/react-zen';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useMessages, useUpdateQuery, useTwoFactorStatusQuery } from '@/components/hooks';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function AdminSecurityPage() {
|
||||
const { t, labels, messages } = useMessages();
|
||||
const { data: status, isLoading } = useTwoFactorStatusQuery(true);
|
||||
const { mutateAsync: setGlobal } = useUpdateQuery('/admin/2fa/global');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const isGlobalRequired = status?.requiredReason === 'global' || false;
|
||||
|
||||
const handleToggle = async (value: boolean) => {
|
||||
await setGlobal({ required: value });
|
||||
queryClient.invalidateQueries({ queryKey: ['2fa-status'] });
|
||||
};
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
return (
|
||||
<Column gap="6" margin="2">
|
||||
<PageHeader title={t(labels.security)} />
|
||||
<Panel>
|
||||
<Column gap="4">
|
||||
<Column gap="1">
|
||||
<Text weight="bold">{t(labels.twoFactorAuth)}</Text>
|
||||
<Text>{t(messages.twoFactorAdminGlobalDescription)}</Text>
|
||||
</Column>
|
||||
|
||||
<Row alignItems="center" gap="3">
|
||||
<Switch isSelected={isGlobalRequired} onChange={handleToggle} />
|
||||
<Text>{t(labels.twoFactorRequireGlobal)}</Text>
|
||||
</Row>
|
||||
|
||||
{isGlobalRequired && (
|
||||
<Text size="sm" color="muted">
|
||||
{t(messages.twoFactorOverrideNote)}
|
||||
</Text>
|
||||
)}
|
||||
</Column>
|
||||
</Panel>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { AdminSecurityPage } from './AdminSecurityPage';
|
||||
|
||||
export default function () {
|
||||
return <AdminSecurityPage />;
|
||||
}
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Security',
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { UserEditForm } from './UserEditForm';
|
||||
import { UserTwoFactorSettings } from './UserTwoFactorSettings';
|
||||
import { UserWebsites } from './UserWebsites';
|
||||
|
||||
export function UserSettings({ userId }: { userId: string }) {
|
||||
@@ -12,6 +13,7 @@ export function UserSettings({ userId }: { userId: string }) {
|
||||
<TabList>
|
||||
<Tab id="details">{t(labels.details)}</Tab>
|
||||
<Tab id="websites">{t(labels.websites)}</Tab>
|
||||
<Tab id="security">{t(labels.security)}</Tab>
|
||||
</TabList>
|
||||
<TabPanel id="details" style={{ width: 500 }}>
|
||||
<UserEditForm userId={userId} />
|
||||
@@ -19,6 +21,9 @@ export function UserSettings({ userId }: { userId: string }) {
|
||||
<TabPanel id="websites">
|
||||
<UserWebsites userId={userId} />
|
||||
</TabPanel>
|
||||
<TabPanel id="security" style={{ width: 500 }}>
|
||||
<UserTwoFactorSettings userId={userId} />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Column>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
import { Column, Row, Switch, Text, Tooltip, TooltipTrigger } from '@umami/react-zen';
|
||||
import { Badge } from '@/components/common/Badge';
|
||||
import { useMessages, useUpdateQuery, useTwoFactorStatusQuery } from '@/components/hooks';
|
||||
import { useApi } from '@/components/hooks/useApi';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function UserTwoFactorSettings({ userId }: { userId: string }) {
|
||||
const { t, labels, messages } = useMessages();
|
||||
const { get } = useApi();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: globalStatus } = useTwoFactorStatusQuery(true);
|
||||
const isGlobalRequired = globalStatus?.requiredReason === 'global';
|
||||
|
||||
const { data: userTfaData } = useQuery({
|
||||
queryKey: ['user-2fa-status', userId],
|
||||
queryFn: () => get(`/admin/users/${userId}/2fa`),
|
||||
});
|
||||
|
||||
const { data: userData } = useQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => get(`/users/${userId}`),
|
||||
});
|
||||
|
||||
const { mutateAsync: setUserRequired } = useUpdateQuery(`/admin/users/${userId}/2fa`);
|
||||
|
||||
const twoFactorEnabled = !!(userTfaData as any)?.isEnabled;
|
||||
const twoFactorRequired = !!(userData as any)?.twoFactorRequired;
|
||||
|
||||
const handleToggle = async (value: boolean) => {
|
||||
await setUserRequired({ required: value });
|
||||
queryClient.invalidateQueries({ queryKey: ['user', userId] });
|
||||
};
|
||||
|
||||
return (
|
||||
<Column gap="4">
|
||||
<Row alignItems="center" gap="3">
|
||||
<Text weight="bold">{t(labels.twoFactorAuth)}</Text>
|
||||
<Badge variant={twoFactorEnabled ? 'good' : 'gray'}>
|
||||
{twoFactorEnabled ? t(labels.twoFactorActive) : t(labels.twoFactorStatusNotConfigured)}
|
||||
</Badge>
|
||||
</Row>
|
||||
|
||||
<Text size="sm" color="muted">
|
||||
{t(messages.twoFactorRequireUserDescription)}
|
||||
</Text>
|
||||
|
||||
<TooltipTrigger isDisabled={!isGlobalRequired}>
|
||||
<Row alignItems="center" gap="3">
|
||||
<Switch
|
||||
isSelected={isGlobalRequired || twoFactorRequired}
|
||||
isDisabled={isGlobalRequired}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
<Text>{t(labels.twoFactorRequireUser)}</Text>
|
||||
</Row>
|
||||
<Tooltip>{t(labels.twoFactorGlobalActiveTooltip)}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user