Skip to content

Commit 01dd1cb

Browse files
mizhml1ttpsdependabot[bot]
authored
feat(user): implement users manager (#289)
* feat(console, admin): add list users for administration * feat(console): redesign user detail sheet * fix(console): fix sorting for server datatable * refactor(console): restyle user-detail-sheet * feat(console): add confirm dialog for action ban * feat(console): add user detail section * fix(console): fix small typo in tls tab (#290) * feat(asset): add tls filter for asset * fix(core): fix asset test * fix(asset): fix based on bot reviews * fix(console): fix small typo in tls tab * fix(console): add missing tls for queryParams in asset context * fix(console): fix tls query hook in dashboard --------- Co-authored-by: Quang Vinh <32523515+l1ttps@users.noreply.github.com> * chore(deps): bump multer from 2.0.2 to 2.1.0 (#292) Bumps [multer](https://github.com/expressjs/multer) from 2.0.2 to 2.1.0. - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](expressjs/multer@v2.0.2...v2.1.0) --- updated-dependencies: - dependency-name: multer dependency-version: 2.1.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(targets): create multiple targets (#291) * refactor(targets): combine logic create target in transaction * feat(targets): add bulk target creation endpoint * feat(targets): add bulk creation support * fix(console): move tabList into component to avoid out of context (#293) Co-authored-by: Quang Vinh <32523515+l1ttps@users.noreply.github.com> * fix(assets): select asset relations in query (#297) * refactor(ui): improve flow onboarding with first workspace creation and re-design settings ui (#299) * feat(console): add all workspaces navigation and improve 404 page UI * refactor(layout): extract header into dedicated HeaderBar component * refactor(console): add workspace-aware header layout * refactor(console): convert workspace creation to page and add route protection * refactor(console): update workspaces UI from table to card layout * feat(workspaces): add member and target counts to workspace list * refactor(settings): reorganize settings page with sidebar layout * feat(settings): add API keys management * refactor(settings): improve API key display layout * fix(screenshot-cell): add type assertion for screenshotPath * refactor(workspaces): use workspace selector hook * feat(auth): add session retry with exponential backoff * chore(agent): migrate ai agent * feat(router): add admin users route * feat(console): implement create user * feat(console): add change name, email and reset password in user detail * fix(console): fix duplicate tlsHosts in context * fix(console): use loading state of data table and improve client user type * fix(console): add admin route * feat(console): Implement role-based access control for settings tabs and sidebar menu items based on user roles. * style(console): update 'Add User' button to outline variant * refactor(console): move add user button to table toolbar * fix(console): add autoComplete to user detail input --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Quang Vinh <32523515+l1ttps@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: l1ttps <l1ttps443@gmail.com>
1 parent 0e66fab commit 01dd1cb

26 files changed

+1192
-203
lines changed

console/src/components/common/layout/menu-bar.tsx

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,32 @@ import {
2828
LayoutDashboard,
2929
SquareTerminal,
3030
Target,
31+
User,
3132
} from 'lucide-react';
3233
import { NavUser } from '../../ui/nav-user';
3334
import { NewBadge } from '../new-badge';
35+
import { useSession } from '@/utils/authClient';
36+
37+
interface SubMenuItem {
38+
title: string;
39+
icon: React.ReactNode;
40+
url: string;
41+
isNew?: boolean;
42+
}
43+
44+
interface NavGroup {
45+
title: string;
46+
url: string;
47+
items: SubMenuItem[];
48+
roles?: string[];
49+
}
3450

3551
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
3652
const location = useLocation();
3753
const { state, isMobile, setOpenMobile } = useSidebar();
54+
const { data } = useSession();
3855

39-
const menu = [
56+
const menu: NavGroup[] = [
4057
{
4158
title: 'Overview',
4259
url: '#',
@@ -48,6 +65,18 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
4865
},
4966
],
5067
},
68+
{
69+
title: 'Admin',
70+
url: '#',
71+
roles: ['admin'],
72+
items: [
73+
{
74+
title: 'Users',
75+
icon: <User />,
76+
url: '/admin/users',
77+
},
78+
],
79+
},
5180
{
5281
title: 'Attack surface',
5382
url: '#',
@@ -121,41 +150,49 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
121150
)}
122151
</SidebarHeader>
123152
<SidebarContent className="gap-1 md:gap-3">
124-
{menu.map((item) => (
125-
<SidebarGroup key={item.title} className="py-0">
126-
<SidebarGroupContent>
127-
<SidebarGroupLabel className="font-bold text-md">
128-
{item.title}
129-
</SidebarGroupLabel>
130-
<SidebarMenu className="gap-0.5">
131-
{item.items.map((item) => {
132-
// Ensure all URLs are absolute for comparison
133-
const toUrl = item.url;
134-
const isActive =
135-
`/${location.pathname.split('/')[1]}` === toUrl;
136-
return (
137-
<SidebarMenuItem key={item.title}>
138-
<SidebarMenuButton
139-
asChild
140-
isActive={isActive}
141-
tooltip={item.title}
142-
className="hover:cursor-pointer"
143-
>
144-
<Link
145-
to={toUrl}
146-
onClick={() => setOpenMobile(false)}
147-
className="flex items-center justify-start w-full h-full text-base"
153+
{menu
154+
.filter(
155+
(item) =>
156+
!item.roles ||
157+
item.roles.length === 0 ||
158+
(data?.user.role != null && item.roles.includes(data.user.role)),
159+
)
160+
.map((item) => (
161+
<SidebarGroup key={item.title} className="py-0">
162+
<SidebarGroupContent>
163+
<SidebarGroupLabel className="font-bold text-md">
164+
{item.title}
165+
</SidebarGroupLabel>
166+
<SidebarMenu className="gap-0.5">
167+
{item.items.map((item) => {
168+
// Ensure all URLs are absolute for comparison
169+
const toUrl = item.url;
170+
const isActive =
171+
`/${location.pathname.split('/')[1]}` === toUrl;
172+
return (
173+
<SidebarMenuItem key={item.title}>
174+
<SidebarMenuButton
175+
asChild
176+
isActive={isActive}
177+
tooltip={item.title}
178+
className="hover:cursor-pointer"
148179
>
149-
{item.icon} {item.title} {item.isNew && <NewBadge />}
150-
</Link>
151-
</SidebarMenuButton>
152-
</SidebarMenuItem>
153-
);
154-
})}
155-
</SidebarMenu>
156-
</SidebarGroupContent>
157-
</SidebarGroup>
158-
))}
180+
<Link
181+
to={toUrl}
182+
onClick={() => setOpenMobile(false)}
183+
className="flex items-center justify-start w-full h-full text-base"
184+
>
185+
{item.icon} {item.title}{' '}
186+
{item.isNew && <NewBadge />}
187+
</Link>
188+
</SidebarMenuButton>
189+
</SidebarMenuItem>
190+
);
191+
})}
192+
</SidebarMenu>
193+
</SidebarGroupContent>
194+
</SidebarGroup>
195+
))}
159196
</SidebarContent>
160197
<SidebarRail />
161198
<SidebarFooter>

console/src/components/common/layout/settings-layout.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Button } from '@/components/ui/button';
22
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
3-
import { settingsTabGroups } from '@/pages/settings/settings';
3+
import { filterTabGroups, settingsTabGroups } from '@/pages/settings/settings';
4+
import { useSession } from '@/utils/authClient';
45
import { ArrowLeft, Menu } from 'lucide-react';
56
import type { JSX, ReactNode } from 'react';
67
import { Link, useLocation } from 'react-router-dom';
@@ -13,6 +14,8 @@ export default function SettingsLayout({
1314
children,
1415
}: SettingsLayoutProps): JSX.Element {
1516
const location = useLocation();
17+
const { data } = useSession();
18+
const visibleGroups = filterTabGroups(settingsTabGroups, data?.user.role);
1619

1720
// Determine if a tab is active based on current path
1821
const isActive = (path: string) => location.pathname.startsWith(path);
@@ -21,7 +24,7 @@ export default function SettingsLayout({
2124
const navMenu = (
2225
<nav className="flex-1 py-3">
2326
<ul className="space-y-1">
24-
{settingsTabGroups.map((group, groupIndex) => (
27+
{visibleGroups.map((group, groupIndex) => (
2528
<div>
2629
<li key={group.name} className="p-2">
2730
{/* Group header */}
@@ -44,7 +47,7 @@ export default function SettingsLayout({
4447
))}
4548
</li>
4649
{/* Group divider - shown after group */}
47-
{groupIndex < settingsTabGroups.length - 1 && (
50+
{groupIndex < visibleGroups.length - 1 && (
4851
<div className="border-b my-2" />
4952
)}
5053
</div>

console/src/hooks/useServerDataTable.ts

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -58,25 +58,29 @@ export function useServerDataTable({
5858
? urlParams.get('filter') || ''
5959
: internalParams.filter;
6060

61-
const setParam = useCallback(
62-
(key: string, value: string | number | undefined) => {
61+
const setParams = useCallback(
62+
(newParams: Partial<typeof internalParams>) => {
6363
if (isUpdateSearchQueryParam) {
6464
setUrlParams(
6565
(prev) => {
6666
const next = new URLSearchParams(prev);
67-
if (!value) {
68-
next.delete(key);
69-
} else {
70-
next.set(key, value.toString());
71-
}
67+
68+
Object.entries(newParams).forEach(([key, value]) => {
69+
if (value === undefined || value === null || value === '') {
70+
next.delete(key);
71+
} else {
72+
next.set(key, String(value));
73+
}
74+
});
75+
7276
return next;
7377
},
7478
{ replace: true },
7579
);
7680
} else {
7781
setInternalParams((prev) => ({
7882
...prev,
79-
[key]: value ?? '',
83+
...newParams,
8084
}));
8185
}
8286
},
@@ -92,36 +96,29 @@ export function useServerDataTable({
9296
filter,
9397
},
9498
tableHandlers: {
95-
setPage: useCallback((v: number) => setParam('page', v), [setParam]),
99+
setParams,
100+
setPage: useCallback((v: number) => setParams({ page: v }), [setParams]),
96101
setPageSize: useCallback(
97-
(v: number) => setParam('pageSize', v),
98-
[setParam],
102+
(v: number) => setParams({ pageSize: v, page: 1 }),
103+
[setParams],
104+
),
105+
setSortBy: useCallback(
106+
(v: string) => setParams({ sortBy: v, page: 1 }),
107+
[setParams],
99108
),
100-
setSortBy: useCallback((v: string) => setParam('sortBy', v), [setParam]),
101109
setSortOrder: useCallback(
102-
(v: 'ASC' | 'DESC') => setParam('sortOrder', v),
103-
[setParam],
110+
(v: 'ASC' | 'DESC') => setParams({ sortOrder: v, page: 1 }),
111+
[setParams],
104112
),
105113
setFilter: useCallback(
106114
(v: string) => {
107-
if (isUpdateSearchQueryParam) {
108-
setUrlParams(
109-
(prev) => {
110-
const next = new URLSearchParams(prev);
111-
if (next.get('filter') === v) return prev;
112-
next.set('page', '1');
113-
next.set('filter', v);
114-
return next;
115-
},
116-
{ replace: true },
117-
);
118-
} else {
119-
if (internalParams.filter === v) return;
120-
setParam('page', 1);
121-
setParam('filter', v);
122-
}
115+
if (filter === v) return;
116+
setParams({
117+
filter: v,
118+
page: 1,
119+
});
123120
},
124-
[isUpdateSearchQueryParam, setUrlParams, setParam, internalParams.filter],
121+
[filter, setParams],
125122
),
126123
},
127124
};

0 commit comments

Comments
 (0)