feat(web): router wiring completo + nav link usuarios + MustChangePasswordGate integration [UDT-008]

- Agrega ProtectedPage helper que combina ProtectedRoute + MustChangePasswordGate + ProtectedLayout
- Rutas nuevas: /usuarios, /usuarios/:id, /usuarios/:id/editar con permisos RBAC
- /perfil/contrasena sin MustChangePasswordGate (evita redirect loop)
- Sidebar: sección "Mi cuenta" con cambio de contraseña; link Usuarios en sección admin
This commit is contained in:
2026-04-15 18:12:54 -03:00
parent 25ed0f6452
commit 2e2d4543ad
2 changed files with 117 additions and 27 deletions

View File

@@ -6,8 +6,10 @@ import {
Zap, Zap,
Settings, Settings,
UserPlus, UserPlus,
Users,
ShieldCheck, ShieldCheck,
KeyRound, KeyRound,
Lock,
} from 'lucide-react' } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -86,6 +88,25 @@ export function SidebarNav() {
) )
})} })}
{/* Profile / account section — visible for all authenticated users */}
<div className="pt-2 pb-1 px-3">
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/60">
Mi cuenta
</span>
</div>
<Link
to="/perfil/contrasena"
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
pathname === '/perfil/contrasena'
? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground',
)}
>
<Lock className="h-4 w-4 shrink-0" />
<span>Cambiar contraseña</span>
</Link>
{/* Admin-only section */} {/* Admin-only section */}
{isAdmin && ( {isAdmin && (
<> <>
@@ -94,6 +115,18 @@ export function SidebarNav() {
Administración Administración
</span> </span>
</div> </div>
<Link
to="/usuarios"
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
pathname.startsWith('/usuarios') && pathname !== '/usuarios/nuevo'
? 'bg-accent text-accent-foreground font-medium'
: 'text-muted-foreground',
)}
>
<Users className="h-4 w-4 shrink-0" />
<span>Usuarios</span>
</Link>
<Link <Link
to="/usuarios/nuevo" to="/usuarios/nuevo"
className={cn( className={cn(

View File

@@ -1,8 +1,13 @@
import { Navigate, Route, Routes } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { useAuthStore } from './stores/authStore' import { useAuthStore } from './stores/authStore'
import { ProtectedRoute } from './components/routing/ProtectedRoute' import { ProtectedRoute } from './components/routing/ProtectedRoute'
import { MustChangePasswordGate } from './components/routing/MustChangePasswordGate'
import { LoginPage } from './features/auth/pages/LoginPage' import { LoginPage } from './features/auth/pages/LoginPage'
import { CreateUserPage } from './features/users/pages/CreateUserPage' import { CreateUserPage } from './features/users/pages/CreateUserPage'
import { UsersListPage } from './features/users/pages/UsersListPage'
import { UserDetailPage } from './features/users/pages/UserDetailPage'
import { UserEditPage } from './features/users/pages/UserEditPage'
import { ChangeMyPasswordPage } from './features/profile/pages/ChangeMyPasswordPage'
import { RolesPage } from './features/roles/pages/RolesPage' import { RolesPage } from './features/roles/pages/RolesPage'
import { NewRolPage } from './features/roles/pages/NewRolPage' import { NewRolPage } from './features/roles/pages/NewRolPage'
import { EditRolPage } from './features/roles/pages/EditRolPage' import { EditRolPage } from './features/roles/pages/EditRolPage'
@@ -19,9 +24,30 @@ function PublicRoute({ children }: { children: React.ReactNode }) {
return <>{children}</> return <>{children}</>
} }
/**
* Wraps a protected route with ProtectedLayout + MustChangePasswordGate.
* The gate forces users with mustChangePassword=true to /perfil/contrasena.
*/
function ProtectedPage({
children,
requiredPermissions,
}: {
children: React.ReactNode
requiredPermissions?: string[]
}) {
return (
<ProtectedRoute requiredPermissions={requiredPermissions}>
<MustChangePasswordGate>
<ProtectedLayout>{children}</ProtectedLayout>
</MustChangePasswordGate>
</ProtectedRoute>
)
}
export function AppRoutes() { export function AppRoutes() {
return ( return (
<Routes> <Routes>
{/* Public routes */}
<Route <Route
path="/login" path="/login"
element={ element={
@@ -32,71 +58,102 @@ export function AppRoutes() {
</PublicRoute> </PublicRoute>
} }
/> />
{/* Change password — protected but NO MustChangePasswordGate (avoids redirect loop) */}
<Route <Route
path="/" path="/perfil/contrasena"
element={ element={
<ProtectedRoute> <ProtectedRoute>
<ProtectedLayout> <ProtectedLayout>
<HomePage /> <ChangeMyPasswordPage />
</ProtectedLayout> </ProtectedLayout>
</ProtectedRoute> </ProtectedRoute>
} }
/> />
{/* Protected routes — all wrapped with MustChangePasswordGate */}
<Route
path="/"
element={<ProtectedPage><HomePage /></ProtectedPage>}
/>
<Route
path="/usuarios"
element={
<ProtectedPage requiredPermissions={['administracion:usuarios:gestionar']}>
<UsersListPage />
</ProtectedPage>
}
/>
<Route <Route
path="/usuarios/nuevo" path="/usuarios/nuevo"
element={ element={
<ProtectedRoute requiredPermissions={['administracion:usuarios:gestionar']}> <ProtectedPage requiredPermissions={['administracion:usuarios:gestionar']}>
<ProtectedLayout> <CreateUserPage />
<CreateUserPage /> </ProtectedPage>
</ProtectedLayout>
</ProtectedRoute>
} }
/> />
<Route
path="/usuarios/:id"
element={
<ProtectedPage requiredPermissions={['administracion:usuarios:gestionar']}>
<UserDetailPage />
</ProtectedPage>
}
/>
<Route
path="/usuarios/:id/editar"
element={
<ProtectedPage requiredPermissions={['administracion:usuarios:gestionar']}>
<UserEditPage />
</ProtectedPage>
}
/>
<Route <Route
path="/admin/roles" path="/admin/roles"
element={ element={
<ProtectedRoute requiredPermissions={['administracion:roles:gestionar']}> <ProtectedPage requiredPermissions={['administracion:roles:gestionar']}>
<ProtectedLayout> <RolesPage />
<RolesPage /> </ProtectedPage>
</ProtectedLayout>
</ProtectedRoute>
} }
/> />
<Route <Route
path="/admin/roles/nuevo" path="/admin/roles/nuevo"
element={ element={
<ProtectedRoute requiredPermissions={['administracion:roles:gestionar']}> <ProtectedPage requiredPermissions={['administracion:roles:gestionar']}>
<ProtectedLayout> <NewRolPage />
<NewRolPage /> </ProtectedPage>
</ProtectedLayout>
</ProtectedRoute>
} }
/> />
<Route <Route
path="/admin/roles/:codigo/editar" path="/admin/roles/:codigo/editar"
element={ element={
<ProtectedRoute requiredPermissions={['administracion:roles:gestionar']}> <ProtectedPage requiredPermissions={['administracion:roles:gestionar']}>
<ProtectedLayout> <EditRolPage />
<EditRolPage /> </ProtectedPage>
</ProtectedLayout>
</ProtectedRoute>
} }
/> />
<Route <Route
path="/admin/permisos" path="/admin/permisos"
element={ element={
<ProtectedRoute <ProtectedPage
requiredPermissions={[ requiredPermissions={[
'administracion:roles_permisos:gestionar', 'administracion:roles_permisos:gestionar',
'administracion:permisos:ver', 'administracion:permisos:ver',
]} ]}
> >
<ProtectedLayout> <RolPermisosPage />
<RolPermisosPage /> </ProtectedPage>
</ProtectedLayout>
</ProtectedRoute>
} }
/> />
<Route path="*" element={<Navigate to="/" replace />} /> <Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
) )