Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e2b387f
feat(console, admin): add list users for administration
mizhm Jan 30, 2026
8b72e3e
feat(console): redesign user detail sheet
mizhm Jan 30, 2026
2115160
Merge branch 'main' into feat/user-management
l1ttps Mar 2, 2026
d442101
fix(console): fix sorting for server datatable
mizhm Mar 4, 2026
af5c8ad
refactor(console): restyle user-detail-sheet
mizhm Mar 7, 2026
668207b
feat(console): add confirm dialog for action ban
mizhm Mar 7, 2026
c078b23
feat(console): add user detail section
mizhm Mar 7, 2026
4eccfad
fix(console): fix small typo in tls tab (#290)
mizhm Mar 2, 2026
f5793a0
chore(deps): bump multer from 2.0.2 to 2.1.0 (#292)
dependabot[bot] Mar 4, 2026
d58fb59
feat(targets): create multiple targets (#291)
l1ttps Mar 5, 2026
ea3e0f0
fix(console): move tabList into component to avoid out of context (#293)
mizhm Mar 5, 2026
67d6060
fix(assets): select asset relations in query (#297)
l1ttps Mar 5, 2026
4498bc5
refactor(ui): improve flow onboarding with first workspace creation a…
l1ttps Mar 7, 2026
6fdf3e5
feat(auth): add session retry with exponential backoff
l1ttps Mar 7, 2026
197d979
chore(agent): migrate ai agent
l1ttps Mar 7, 2026
781115d
Merge branch 'main' into feat/user-management
l1ttps Mar 7, 2026
5b671e5
feat(router): add admin users route
l1ttps Mar 7, 2026
85166f5
feat(console): implement create user
mizhm Mar 8, 2026
de66cdb
feat(console): add change name, email and reset password in user detail
mizhm Mar 8, 2026
72fa0f8
Merge branch 'main' into feat/user-management
l1ttps Mar 10, 2026
5f09752
Merge branch 'main' into feat/user-management
mizhm Mar 14, 2026
c7807d2
fix(console): fix duplicate tlsHosts in context
mizhm Mar 14, 2026
641d61d
fix(console): use loading state of data table and improve client user…
mizhm Mar 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions console/src/components/common/layout/menu-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
LayoutDashboard,
SquareTerminal,
Target,
User,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Imported User icon but admin route lacks proper protection

The Admin section with Users route is added to the sidebar menu, but there's no indication that this route is protected by authentication or authorization middleware. Anyone could potentially access /admin/users without proper credentials.

} from 'lucide-react';
import { NavUser } from '../../ui/nav-user';
import { NewBadge } from '../new-badge';
Expand All @@ -48,6 +49,17 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Admin section route /admin/users missing authentication protection

The new Admin > Users menu item links to /admin/users but there's no evidence this route is protected by authentication middleware. This could allow unauthenticated access to user management functionality.

],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Admin section route /admin/users missing authorization check

Even if authenticated, there's no indication that only admin users can access the /admin/users route. Regular users could potentially access user management functionality.

},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Admin section route /admin/users missing role-based validation

The route should validate that the user has the appropriate role (admin) before granting access to user management features.

{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Admin section route /admin/users missing permission check

Beyond role validation, there should be specific permission checks for user management actions (view, create, edit, delete users).

title: 'Admin',
url: '#',
items: [
{
title: 'Users',
icon: <User />,
url: '/admin/users',
},
],
},
Comment on lines +52 to +62
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sidebar always shows the new "Admin → Users" navigation item without checking the current user's role. This exposes admin functionality to non-admin users (leading to confusing UX at best, and unnecessary surface area). Consider conditionally rendering this section based on the session user's role or a dedicated permissions check.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The 'Admin' menu section is rendered for all authenticated users without checking if they have the 'admin' role. This exposes administrative functionality in the UI to unauthorized users. While backend enforcement may prevent unauthorized actions, exposing these menu items to non-admin users violates the principle of least privilege and increases the attack surface. It is recommended to conditionally render this section only for users with administrative privileges by checking the user's role from the session.

{
title: 'Attack surface',
url: '#',
Expand Down
59 changes: 28 additions & 31 deletions console/src/hooks/useServerDataTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,29 @@ export function useServerDataTable({
? urlParams.get('filter') || ''
: internalParams.filter;

const setParam = useCallback(
(key: string, value: string | number | undefined) => {
const setParams = useCallback(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: setParams function signature change may break existing consumers

Changed from setParam(key, value) to setParams(newParams) which is a breaking change for any components that still call the old signature. Consider maintaining backward compatibility or updating all callers.

(newParams: Partial<typeof internalParams>) => {
if (isUpdateSearchQueryParam) {
setUrlParams(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: setParams implementation may cause infinite loops

The function calls setUrlParams which triggers a re-render, which could cause setParams to be called again if not properly memoized. The dependency array looks correct, but double-check that this doesn't create update loops.

(prev) => {
const next = new URLSearchParams(prev);
if (!value) {
next.delete(key);
} else {
next.set(key, value.toString());
}

Object.entries(newParams).forEach(([key, value]) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Consider adding validation for parameter values

Before setting URL parameters, consider validating that values are appropriate for each parameter type (e.g., page should be positive integer, sortOrder should be 'ASC' or 'DESC').

if (value === undefined || value === null || value === '') {
next.delete(key);
} else {
next.set(key, String(value));
}
});

return next;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Add error handling for URL parameter updates

The setUrlParams call could fail (e.g., if URL exceeds length limits). Consider wrapping in try/catch and falling back to internal state only.

},
{ replace: true },
);
} else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Consider preserving unspecificed parameters when using internal state

When isUpdateSearchQueryParam is false, the function spreads ...newParams over prev, which is good, but consider if there are any parameters that should be preserved regardless.

setInternalParams((prev) => ({
...prev,
[key]: value ?? '',
...newParams,
}));
}
},
Expand All @@ -92,36 +96,29 @@ export function useServerDataTable({
filter,
},
tableHandlers: {
setPage: useCallback((v: number) => setParam('page', v), [setParam]),
setParams,
setPage: useCallback((v: number) => setParams({ page: v }), [setParams]),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: setPage handler should preserve other table state

Currently setPage only updates the page parameter. Consider if it should preserve other state like sortBy, sortOrder, etc. when just changing page.

setPageSize: useCallback(
(v: number) => setParam('pageSize', v),
[setParam],
(v: number) => setParams({ pageSize: v }),
[setParams],
),
setSortBy: useCallback(
(v: string) => setParams({ sortBy: v }),
[setParams],
),
setSortBy: useCallback((v: string) => setParam('sortBy', v), [setParam]),
setSortOrder: useCallback(
(v: 'ASC' | 'DESC') => setParam('sortOrder', v),
[setParam],
(v: 'ASC' | 'DESC') => setParams({ sortOrder: v }),
[setParams],
),
setFilter: useCallback(
(v: string) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: setFilter should implement debouncing

For search/filter functionality, consider adding debounce to prevent excessive API calls while user is typing.

if (isUpdateSearchQueryParam) {
setUrlParams(
(prev) => {
const next = new URLSearchParams(prev);
if (next.get('filter') === v) return prev;
next.set('page', '1');
next.set('filter', v);
return next;
},
{ replace: true },
);
} else {
if (internalParams.filter === v) return;
setParam('page', 1);
setParam('filter', v);
}
if (filter === v) return;
setParams({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: Consider resetting page on filter change

When filter changes, it's common practice to reset to page 1 since the filtered results may have different pagination. This is already implemented, which is good.

filter: v,
page: 1,
});
},
[isUpdateSearchQueryParam, setUrlParams, setParam, internalParams.filter],
[filter, setParams],
),
},
};
Expand Down
93 changes: 93 additions & 0 deletions console/src/pages/admin/list-users.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Badge } from '@/components/ui/badge';
import { type ColumnDef } from '@tanstack/react-table';
import { DataTable } from '@/components/ui/data-table';
import { useServerDataTable } from '@/hooks/useServerDataTable';
import { authClient, type User } from '@/utils/authClient';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { UserDetailSheet } from './user-detail-sheet';

const userColumns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
enableSorting: true,
},
{
accessorKey: 'email',
header: 'Email',
enableSorting: true,
},
{
accessorKey: 'role',
header: 'Role',
cell: ({ row }) => <Badge>{row.original.role}</Badge>,
},
{
accessorKey: 'banned',
header: 'Status',
cell: ({ row }) =>
row.original.banned ? (
<Badge variant="destructive">Banned</Badge>
) : (
<Badge variant="secondary">Active</Badge>
),
},
];

export function ListUsers() {
const {
tableParams: { page, pageSize, sortBy, sortOrder, filter },
tableHandlers: { setPage, setPageSize, setParams, setFilter },
} = useServerDataTable();
const [selectedUser, setSelectedUser] = useState<User | null>(null);

const { data, isLoading } = useQuery({
queryKey: ['users', { page, pageSize, sortBy, sortOrder, filter }],
queryFn: () =>
authClient.admin.listUsers({
query: {
limit: pageSize,
offset: (page - 1) * pageSize,
searchField: 'name',
searchValue: filter,
filterField: 'role',
filterOperator: 'ne',
filterValue: 'bot',
sortBy: sortBy,
sortDirection: sortOrder.toLowerCase() as 'asc' | 'desc',
},
}),
});

if (!data) return null;

return (
<>
<DataTable
data={(data.data?.users as User[]) || []}
columns={userColumns}
isLoading={isLoading}
page={page}
pageSize={pageSize}
sortBy={sortBy}
sortOrder={sortOrder}
onPageChange={setPage}
onPageSizeChange={setPageSize}
onSortChange={(col, order) => {
setParams({ sortBy: col, sortOrder: order });
}}
filterColumnKey="value"
filterValue={filter}
onFilterChange={setFilter}
totalItems={data.data?.total}
onRowClick={setSelectedUser}
rowClassName="cursor-pointer hover:bg-muted/50 transition-colors"
/>
<UserDetailSheet
user={selectedUser}
onOpenChange={(open) => !open && setSelectedUser(null)}
/>
</>
);
}
Loading