Add paginated users/roles, user status, frontend permissions, profile pictures, identifier file storage
- Users page: paginated, searchable, sortable with inline roles (no N+1) - Roles page: paginated, searchable, sortable + /roles/all for dropdowns - User is_active field with migration, PATCH toggle, auth check (disabled=401) - Frontend permission checks: auth store loads permissions, sidebar/buttons conditional - Profile pictures via file storage for users and members, avatar component - Identifier images use file storage API instead of base64 - Fix TypeScript errors across admin UI - 64 API tests passing (10 new)
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect } from 'react'
|
||||
import { useAuthStore } from '@/stores/auth.store'
|
||||
import { myPermissionsOptions } from '@/api/rbac'
|
||||
import { Avatar } from '@/components/shared/avatar-upload'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User } from 'lucide-react'
|
||||
|
||||
@@ -13,16 +17,48 @@ export const Route = createFileRoute('/_authenticated')({
|
||||
component: AuthenticatedLayout,
|
||||
})
|
||||
|
||||
function NavLink({ to, icon, label }: { to: string; icon: React.ReactNode; label: string }) {
|
||||
return (
|
||||
<Link
|
||||
to={to as '/accounts'}
|
||||
search={{} as any}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function AuthenticatedLayout() {
|
||||
const router = useRouter()
|
||||
const user = useAuthStore((s) => s.user)
|
||||
const logout = useAuthStore((s) => s.logout)
|
||||
const hasPermission = useAuthStore((s) => s.hasPermission)
|
||||
const setPermissions = useAuthStore((s) => s.setPermissions)
|
||||
const permissionsLoaded = useAuthStore((s) => s.permissionsLoaded)
|
||||
|
||||
// Fetch permissions on mount
|
||||
const { data: permData } = useQuery({
|
||||
...myPermissionsOptions(),
|
||||
enabled: !!useAuthStore.getState().token,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (permData?.permissions) {
|
||||
setPermissions(permData.permissions)
|
||||
}
|
||||
}, [permData, setPermissions])
|
||||
|
||||
function handleLogout() {
|
||||
logout()
|
||||
router.navigate({ to: '/login', replace: true })
|
||||
}
|
||||
|
||||
const canViewAccounts = !permissionsLoaded || hasPermission('accounts.view')
|
||||
const canViewUsers = !permissionsLoaded || hasPermission('users.view')
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground">
|
||||
<div className="flex">
|
||||
@@ -31,59 +67,37 @@ function AuthenticatedLayout() {
|
||||
<h2 className="text-lg font-semibold text-sidebar-foreground">Forte</h2>
|
||||
</div>
|
||||
|
||||
{/* Sidebar links use `as any` on search because TanStack Router
|
||||
requires the full validated search shape, but these links just
|
||||
navigate to the page with default params. */}
|
||||
<div className="flex-1 px-3 space-y-1">
|
||||
<Link
|
||||
to="/accounts"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||
>
|
||||
<Users className="h-4 w-4" />
|
||||
Accounts
|
||||
</Link>
|
||||
<Link
|
||||
to="/members"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||
>
|
||||
<UserRound className="h-4 w-4" />
|
||||
Members
|
||||
</Link>
|
||||
<div className="mt-4 mb-1 px-3">
|
||||
<span className="text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wide">Admin</span>
|
||||
</div>
|
||||
<Link
|
||||
to="/users"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||
>
|
||||
<UserCog className="h-4 w-4" />
|
||||
Users
|
||||
</Link>
|
||||
<Link
|
||||
to="/roles"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
Roles
|
||||
</Link>
|
||||
<Link
|
||||
to="/help"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground' }}
|
||||
>
|
||||
<HelpCircle className="h-4 w-4" />
|
||||
Help
|
||||
</Link>
|
||||
{canViewAccounts && (
|
||||
<>
|
||||
<NavLink to="/accounts" icon={<Users className="h-4 w-4" />} label="Accounts" />
|
||||
<NavLink to="/members" icon={<UserRound className="h-4 w-4" />} label="Members" />
|
||||
</>
|
||||
)}
|
||||
{canViewUsers && (
|
||||
<div className="mt-4 mb-1 px-3">
|
||||
<span className="text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wide">Admin</span>
|
||||
</div>
|
||||
)}
|
||||
{canViewUsers && (
|
||||
<>
|
||||
<NavLink to="/users" icon={<UserCog className="h-4 w-4" />} label="Users" />
|
||||
<NavLink to="/roles" icon={<Shield className="h-4 w-4" />} label="Roles" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 border-t border-sidebar-border space-y-1">
|
||||
<NavLink to="/help" icon={<HelpCircle className="h-4 w-4" />} label="Help" />
|
||||
<Link
|
||||
to="/profile"
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-md text-sm text-sidebar-foreground hover:bg-sidebar-accent w-full"
|
||||
activeProps={{ className: 'flex items-center gap-2 px-3 py-2 rounded-md text-sm bg-sidebar-accent text-sidebar-accent-foreground w-full' }}
|
||||
>
|
||||
<User className="h-4 w-4" />
|
||||
{user?.id ? <Avatar entityType="user" entityId={user.id} size="sm" /> : <User className="h-4 w-4" />}
|
||||
<span className="truncate">{user?.firstName} {user?.lastName}</span>
|
||||
</Link>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user