Control de acceso basado en funciones
Añade a la aplicación el control de acceso basado en roles mediante guardias. Introduce guardias de acceso, administración, grupo y rol para la protección de rutas. Actualiza la barra lateral para incluir elementos de menú basados en roles. Configura el cliente Keycloak para incluir información de grupo en el token de acceso, habilitando la autorización basada en grupos. Añade la página de acceso denegado.
This commit is contained in:
parent
55fc1f6278
commit
6b351ff5b3
@ -1,59 +1,94 @@
|
||||
import { Routes, PreloadAllModules } from '@angular/router';
|
||||
import { Routes } from '@angular/router';
|
||||
import { LoginComponent } from './pages/login/login.component';
|
||||
import { LayoutComponent } from './components/layout/layout.component';
|
||||
import { authGuard } from './guards/auth.guard';
|
||||
import { groupGuard } from './guards/group.guard';
|
||||
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
||||
import { AccessDeniedComponent } from './pages/access-denied/access-denied.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
// Public routes that don't require authentication
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginComponent,
|
||||
data: { title: 'Login' }
|
||||
},
|
||||
// Protected routes that require authentication
|
||||
|
||||
{
|
||||
path: '',
|
||||
component: LayoutComponent,
|
||||
canActivate: [authGuard],
|
||||
children: [
|
||||
{ path: '', redirectTo: 'inicio', pathMatch: 'full' },
|
||||
{
|
||||
path: 'inicio',
|
||||
{
|
||||
path: 'inicio',
|
||||
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent),
|
||||
data: { title: 'Inicio' }
|
||||
data: { title: 'Inicio' }
|
||||
},
|
||||
{
|
||||
path: 'unidad-concesiones',
|
||||
{
|
||||
path: 'unidad-concesiones',
|
||||
loadComponent: () => import('./pages/concesiones/concesiones.component').then(m => m.ConcesionesComponent),
|
||||
data: { title: 'Unidad de Concesiones' }
|
||||
data: {
|
||||
title: 'Unidad de Concesiones',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ct-actualizacion',
|
||||
{
|
||||
path: 'ct-actualizacion',
|
||||
loadComponent: () => import('./pages/actualizacion-pd/actualizacion-pd.component').then(m => m.ActualizacionPdComponent),
|
||||
data: { title: 'Cronograma temporal por actualización de PD' }
|
||||
data: {
|
||||
title: 'Cronograma temporal por actualización de PD',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ct-ajuste',
|
||||
{
|
||||
path: 'ct-ajuste',
|
||||
loadComponent: () => import('./pages/ajuste-pd/ajuste-pd.component').then(m => m.AjustePdComponent),
|
||||
data: { title: 'Cronograma temporal por ajuste de PD' }
|
||||
data: {
|
||||
title: 'Cronograma temporal por ajuste de PD',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'resumen',
|
||||
{
|
||||
path: 'resumen',
|
||||
loadComponent: () => import('./pages/resumen/resumen.component').then(m => m.ResumenComponent),
|
||||
data: { title: 'Resumen' }
|
||||
data: {
|
||||
title: 'Resumen',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'unidad-informacion',
|
||||
// Rutas específicas por grupos
|
||||
{
|
||||
path: 'grupos/admin',
|
||||
loadComponent: () => import('./pages/admin-area/admin-area.component').then(m => m.AdminAreaComponent),
|
||||
canActivate: [groupGuard],
|
||||
data: {
|
||||
title: 'Área de Administrador',
|
||||
groups: ['administradores'] // Solo administradores pueden acceder
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'grupos/user',
|
||||
loadComponent: () => import('./pages/user-area/user-area.component').then(m => m.UserAreaComponent),
|
||||
canActivate: [groupGuard],
|
||||
data: {
|
||||
title: 'Área de Usuario',
|
||||
groups: ['administradores', 'usuarios'] // Administradores y usuarios pueden acceder
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'unidad-informacion',
|
||||
loadComponent: () => import('./pages/unidad-informacion/unidad-informacion.component').then(m => m.UnidadInformacionComponent),
|
||||
data: { title: 'Unidad de Información' }
|
||||
data: {
|
||||
title: 'Unidad de Información',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
{
|
||||
path: '404',
|
||||
component: NotFoundComponent,
|
||||
data: { title: 'Error 404' }
|
||||
data: { title: 'Error 404' }
|
||||
},
|
||||
{
|
||||
path: 'access-denied',
|
||||
component: AccessDeniedComponent,
|
||||
data: { title: 'Acceso Denegado' }
|
||||
},
|
||||
]
|
||||
},
|
||||
{ path: '**', redirectTo: '404' }
|
||||
];
|
||||
];
|
||||
@ -7,12 +7,9 @@
|
||||
<div class="version-badge">1.0</div>
|
||||
</div>
|
||||
<!-- Subtítulo del logo -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Separador -->
|
||||
<div class="separator"></div>
|
||||
|
||||
<!-- Navigation Menu -->
|
||||
<div class="menu-container">
|
||||
<ul class="sidebar-menu">
|
||||
@ -23,7 +20,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/unidad-concesiones" class="menu-link">
|
||||
<i class="menu-icon pi pi-building"></i>
|
||||
@ -31,7 +27,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/ct-actualizacion" class="menu-link">
|
||||
<i class="menu-icon pi pi-flag"></i>
|
||||
@ -39,7 +34,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/ct-ajuste" class="menu-link">
|
||||
<i class="menu-icon pi pi-sliders-h"></i>
|
||||
@ -47,7 +41,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/resumen" class="menu-link">
|
||||
<i class="menu-icon pi pi-list"></i>
|
||||
@ -55,7 +48,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/unidad-informacion" class="menu-link">
|
||||
<i class="menu-icon pi pi-box"></i>
|
||||
@ -63,6 +55,40 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Menú desplegable para Prueba de Roles -->
|
||||
<li class="menu-item" [class.active]="isRolesMenuOpen">
|
||||
<div class="menu-link" (click)="toggleRolesMenu()">
|
||||
<i class="menu-icon pi pi-shield"></i>
|
||||
<span class="menu-text">Prueba de Roles</span>
|
||||
<i class="pi" [class.pi-chevron-down]="isRolesMenuOpen" [class.pi-chevron-right]="!isRolesMenuOpen"
|
||||
style="margin-left: auto;"></i>
|
||||
<span class="active-indicator"></span>
|
||||
</div>
|
||||
</li>
|
||||
<!-- Submenú de Roles (visible cuando isRolesMenuOpen es true) -->
|
||||
<div *ngIf="isRolesMenuOpen" class="submenu">
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/grupos/admin" class="menu-link" style="padding-left: 2.5rem;">
|
||||
<i class="menu-icon pi pi-user-plus"></i>
|
||||
<span class="menu-text">Solo Admin</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/grupos/user" class="menu-link" style="padding-left: 2.5rem;">
|
||||
<i class="menu-icon pi pi-user"></i>
|
||||
<span class="menu-text">Solo Usuario</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/roles/public" class="menu-link" style="padding-left: 2.5rem;">
|
||||
<i class="menu-icon pi pi-users"></i>
|
||||
<span class="menu-text">Acceso Público</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -215,4 +215,115 @@
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// Estilos adicionales para el submenu de roles
|
||||
/* Estilos para el submenú */
|
||||
.submenu {
|
||||
margin-left: 0.5rem;
|
||||
animation: slideDown 0.3s ease-in-out;
|
||||
overflow: hidden;
|
||||
border-left: 1px dashed rgba(163, 197, 230, 0.7);
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
max-height: 200px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Para hacer el elemento del menú principal como un botón */
|
||||
.menu-item .menu-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Estilo especial para el elemento del menú desplegable */
|
||||
.menu-item .menu-link:has(i.pi-chevron-right),
|
||||
.menu-item .menu-link:has(i.pi-chevron-down) {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Efecto de onda al hacer clic en el menú desplegable */
|
||||
.menu-item .menu-link:has(i.pi-chevron-right)::after,
|
||||
.menu-item .menu-link:has(i.pi-chevron-down)::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
background-image: radial-gradient(circle, rgba(255,255,255,0.4) 0%, transparent 10.5%);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
transform: scale(10,10);
|
||||
opacity: 0;
|
||||
transition: transform .3s, opacity 0.8s;
|
||||
}
|
||||
|
||||
.menu-item .menu-link:active:has(i.pi-chevron-right)::after,
|
||||
.menu-item .menu-link:active:has(i.pi-chevron-down)::after {
|
||||
transform: scale(0,0);
|
||||
opacity: .3;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
/* Estilos para elementos del submenú */
|
||||
.submenu .menu-item .menu-link {
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.submenu .menu-item .menu-link::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 1rem;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: #a3c5e6;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.submenu .menu-item .menu-link:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.submenu .menu-item.active .menu-link::before {
|
||||
opacity: 1;
|
||||
background-color: white;
|
||||
box-shadow: 0 0 4px rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* Estilos especiales para el submenú activo */
|
||||
.menu-item.active + .submenu {
|
||||
border-left: 1px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
/* Animación para el icono de chevron */
|
||||
.menu-item .pi-chevron-right,
|
||||
.menu-item .pi-chevron-down {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item .pi-chevron-down {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.menu-item .pi-chevron-right {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
@ -1,14 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { PrimeIcons } from 'primeng/api';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
imports: [RouterLink, RouterLinkActive],
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule],
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrl: './sidebar.component.scss',
|
||||
standalone: true
|
||||
styleUrls: ['./sidebar.component.scss']
|
||||
})
|
||||
export class SidebarComponent {
|
||||
|
||||
}
|
||||
isRolesMenuOpen = false;
|
||||
|
||||
constructor(private authService: DirectAuthService) {}
|
||||
|
||||
toggleRolesMenu() {
|
||||
this.isRolesMenuOpen = !this.isRolesMenuOpen;
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
return this.authService.getCurrentUser();
|
||||
}
|
||||
}
|
||||
65
src/app/guards/access.guard.ts
Normal file
65
src/app/guards/access.guard.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
/**
|
||||
* Guard combinado que verifica si el usuario tiene los roles y/o pertenece a los grupos requeridos
|
||||
* Espera un array de roles en route.data['roles'] y/o un array de grupos en route.data['groups']
|
||||
* El parámetro route.data['requireAll'] determina si se requieren todos los roles/grupos o solo alguno
|
||||
* Si no cumple con los requisitos, redirige a la página de acceso denegado
|
||||
*/
|
||||
export const accessGuard: CanActivateFn = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Primero verificar si está autenticado
|
||||
if (!authService.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener los roles y grupos requeridos del data de la ruta
|
||||
const requiredRoles = route.data['roles'] as Array<string> || [];
|
||||
const requiredGroups = route.data['groups'] as Array<string> || [];
|
||||
const requireAll = route.data['requireAll'] as boolean || false;
|
||||
|
||||
// Si no hay roles ni grupos requeridos, permitir acceso
|
||||
if (requiredRoles.length === 0 && requiredGroups.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar acceso según la configuración
|
||||
let hasAccess = false;
|
||||
|
||||
if (requireAll) {
|
||||
// Necesita cumplir TODOS los requisitos
|
||||
let hasRequiredRoles = true;
|
||||
let hasRequiredGroups = true;
|
||||
|
||||
if (requiredRoles.length > 0) {
|
||||
hasRequiredRoles = authService.hasAllRoles(requiredRoles);
|
||||
}
|
||||
|
||||
if (requiredGroups.length > 0) {
|
||||
hasRequiredGroups = authService.inAllGroups(requiredGroups);
|
||||
}
|
||||
|
||||
hasAccess = hasRequiredRoles && hasRequiredGroups;
|
||||
} else {
|
||||
// Necesita cumplir ALGUNO de los requisitos
|
||||
hasAccess = (requiredRoles.length > 0 && authService.hasAnyRole(requiredRoles)) ||
|
||||
(requiredGroups.length > 0 && authService.inAnyGroup(requiredGroups));
|
||||
}
|
||||
|
||||
if (hasAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no cumple los requisitos, redirigir a página de acceso denegado
|
||||
return router.createUrlTree(['/access-denied']);
|
||||
};
|
||||
31
src/app/guards/admin.guard.ts
Normal file
31
src/app/guards/admin.guard.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
/**
|
||||
* Guard que verifica si el usuario es administrador
|
||||
* Si no es administrador, redirige a la página de acceso denegado
|
||||
*/
|
||||
export const adminGuard: CanActivateFn = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Primero verificar si está autenticado
|
||||
if (!authService.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
}
|
||||
|
||||
// Verificar si es administrador
|
||||
if (authService.isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no es administrador, redirigir a página de acceso denegado
|
||||
return router.createUrlTree(['/access-denied']);
|
||||
};
|
||||
@ -3,21 +3,23 @@ import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
// Simple implementation for authentication guard using DirectAuthService
|
||||
/**
|
||||
* Guard que verifica si el usuario está autenticado
|
||||
* Si el usuario no está autenticado, redirige al login con la URL de retorno
|
||||
*/
|
||||
export const authGuard: CanActivateFn = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Check if user is authenticated
|
||||
|
||||
if (authService.isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not authenticated, redirect to login
|
||||
|
||||
// Si no está autenticado, redirigir a login con la URL de retorno
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url !== '/' ? state.url : '/inicio' }
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
};
|
||||
40
src/app/guards/group.guard.ts
Normal file
40
src/app/guards/group.guard.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
/**
|
||||
* Guard que verifica si el usuario pertenece a los grupos requeridos
|
||||
* Espera un array de grupos en route.data['groups']
|
||||
* Si no pertenece a los grupos necesarios, redirige a la página de acceso denegado
|
||||
*/
|
||||
export const groupGuard: CanActivateFn = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Primero verificar si está autenticado
|
||||
if (!authService.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener los grupos requeridos del data de la ruta
|
||||
const requiredGroups = route.data['groups'] as Array<string>;
|
||||
|
||||
// Si no hay grupos requeridos, permitir acceso
|
||||
if (!requiredGroups || requiredGroups.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Usar la función de servicio para verificar grupos
|
||||
if (authService.inAnyGroup(requiredGroups)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no pertenece a los grupos necesarios, redirigir a página de acceso denegado
|
||||
return router.createUrlTree(['/access-denied']);
|
||||
};
|
||||
9
src/app/guards/index.ts
Normal file
9
src/app/guards/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// Archivo de barril (index.ts) para exportar todos los guards
|
||||
export * from './auth.guard';
|
||||
export * from './role.guard';
|
||||
export * from './group.guard';
|
||||
export * from './access.guard';
|
||||
export * from './admin.guard';
|
||||
|
||||
// Esto permite importar todos los guards de una vez:
|
||||
// import { authGuard, roleGuard, groupGuard, accessGuard, adminGuard } from './guards';
|
||||
40
src/app/guards/role.guard.ts
Normal file
40
src/app/guards/role.guard.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
/**
|
||||
* Guard que verifica si el usuario tiene los roles requeridos
|
||||
* Espera un array de roles en route.data['roles']
|
||||
* Si no tiene los roles necesarios, redirige a la página de acceso denegado
|
||||
*/
|
||||
export const roleGuard: CanActivateFn = (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Primero verificar si está autenticado
|
||||
if (!authService.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener los roles requeridos del data de la ruta
|
||||
const requiredRoles = route.data['roles'] as Array<string>;
|
||||
|
||||
// Si no hay roles requeridos, permitir acceso
|
||||
if (!requiredRoles || requiredRoles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Usar la función de servicio para verificar roles
|
||||
if (authService.hasAnyRole(requiredRoles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no tiene los roles necesarios, redirigir a página de acceso denegado
|
||||
return router.createUrlTree(['/access-denied']);
|
||||
};
|
||||
12
src/app/pages/access-denied/access-denied.component.html
Normal file
12
src/app/pages/access-denied/access-denied.component.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="p-4 flex justify-content-center align-items-center" style="min-height: 70vh;">
|
||||
<div class="p-card p-shadow-4 p-4 text-center" style="max-width: 500px;">
|
||||
<i class="pi pi-lock text-danger" style="font-size: 4rem;"></i>
|
||||
<h2 class="mt-3 text-2xl font-bold">Acceso Denegado</h2>
|
||||
<p class="mt-2 mb-4">No tienes los permisos necesarios para acceder a esta página.</p>
|
||||
<p class="mb-4 text-sm">Tu rol actual no te permite ver este contenido.</p>
|
||||
<button class="p-button p-button-primary mt-3" (click)="goHome()">
|
||||
<i class="pi pi-home mr-2"></i>
|
||||
Volver al inicio
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
23
src/app/pages/access-denied/access-denied.component.spec.ts
Normal file
23
src/app/pages/access-denied/access-denied.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccessDeniedComponent } from './access-denied.component';
|
||||
|
||||
describe('AccessDeniedComponent', () => {
|
||||
let component: AccessDeniedComponent;
|
||||
let fixture: ComponentFixture<AccessDeniedComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AccessDeniedComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AccessDeniedComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
17
src/app/pages/access-denied/access-denied.component.ts
Normal file
17
src/app/pages/access-denied/access-denied.component.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-access-denied',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './access-denied.component.html',
|
||||
styleUrl: './access-denied.component.scss'
|
||||
})
|
||||
export class AccessDeniedComponent {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
goHome() {
|
||||
this.router.navigate(['/inicio']);
|
||||
}
|
||||
}
|
||||
15
src/app/pages/admin-area/admin-area.component.html
Normal file
15
src/app/pages/admin-area/admin-area.component.html
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="p-4">
|
||||
<div class="p-card p-shadow-4 p-4">
|
||||
<h2 class="text-2xl font-bold mb-4">Área de Administrador</h2>
|
||||
<div class="bg-blue-50 p-3 rounded border-left-3 border-blue-500 mb-3">
|
||||
<i class="pi pi-info-circle text-blue-500 mr-2"></i>
|
||||
Esta página solo es accesible para usuarios con rol de administrador.
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h3 class="text-xl font-medium mb-2">Información del usuario:</h3>
|
||||
<pre class="p-2 border-1 border-gray-300 rounded bg-gray-100 overflow-auto">{{ userInfo | json }}</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
0
src/app/pages/admin-area/admin-area.component.scss
Normal file
0
src/app/pages/admin-area/admin-area.component.scss
Normal file
23
src/app/pages/admin-area/admin-area.component.spec.ts
Normal file
23
src/app/pages/admin-area/admin-area.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminAreaComponent } from './admin-area.component';
|
||||
|
||||
describe('AdminAreaComponent', () => {
|
||||
let component: AdminAreaComponent;
|
||||
let fixture: ComponentFixture<AdminAreaComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AdminAreaComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminAreaComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
20
src/app/pages/admin-area/admin-area.component.ts
Normal file
20
src/app/pages/admin-area/admin-area.component.ts
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-area',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './admin-area.component.html',
|
||||
styleUrl: './admin-area.component.scss'
|
||||
})
|
||||
export class AdminAreaComponent implements OnInit {
|
||||
userInfo: any;
|
||||
|
||||
constructor(private authService: DirectAuthService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.userInfo = this.authService.getCurrentUser();
|
||||
}
|
||||
}
|
||||
14
src/app/pages/user-area/user-area.component.html
Normal file
14
src/app/pages/user-area/user-area.component.html
Normal file
@ -0,0 +1,14 @@
|
||||
<div class="p-4">
|
||||
<div class="p-card p-shadow-4 p-4">
|
||||
<h2 class="text-2xl font-bold mb-4">Área de Usuario</h2>
|
||||
<div class="bg-green-50 p-3 rounded border-left-3 border-green-500 mb-3">
|
||||
<i class="pi pi-info-circle text-green-500 mr-2"></i>
|
||||
Esta página es accesible para usuarios con roles estándar.
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h3 class="text-xl font-medium mb-2">Información del usuario:</h3>
|
||||
<pre class="p-2 border-1 border-gray-300 rounded bg-gray-100 overflow-auto">{{ userInfo | json }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
0
src/app/pages/user-area/user-area.component.scss
Normal file
0
src/app/pages/user-area/user-area.component.scss
Normal file
23
src/app/pages/user-area/user-area.component.spec.ts
Normal file
23
src/app/pages/user-area/user-area.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserAreaComponent } from './user-area.component';
|
||||
|
||||
describe('UserAreaComponent', () => {
|
||||
let component: UserAreaComponent;
|
||||
let fixture: ComponentFixture<UserAreaComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserAreaComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserAreaComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
18
src/app/pages/user-area/user-area.component.ts
Normal file
18
src/app/pages/user-area/user-area.component.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||
@Component({
|
||||
selector: 'app-user-area',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './user-area.component.html',
|
||||
styleUrl: './user-area.component.scss'
|
||||
})
|
||||
export class UserAreaComponent implements OnInit {
|
||||
userInfo: any;
|
||||
|
||||
constructor(private authService: DirectAuthService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.userInfo = this.authService.getCurrentUser();
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable, signal, inject } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { Observable, throwError, of } from 'rxjs';
|
||||
import { catchError, tap, map } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { environment } from '../../environments/environment';
|
||||
@ -15,13 +15,25 @@ export class DirectAuthService {
|
||||
private realm = environment.keycloak.realm;
|
||||
private clientId = environment.keycloak.clientId;
|
||||
private tokenEndpoint = `${this.keycloakUrl}/realms/${this.realm}/protocol/openid-connect/token`;
|
||||
private userEndpoint = `${this.keycloakUrl}/realms/${this.realm}/protocol/openid-connect/userinfo`;
|
||||
|
||||
// Storage keys
|
||||
private readonly STORAGE_TOKEN_KEY = 'keycloak_token';
|
||||
private readonly STORAGE_USER_KEY = 'keycloak_user';
|
||||
private readonly STORAGE_ROLES_KEY = 'keycloak_roles';
|
||||
private readonly STORAGE_GROUPS_KEY = 'keycloak_groups';
|
||||
private readonly STORAGE_PERMISSIONS_KEY = 'keycloak_permissions';
|
||||
private readonly STORAGE_LAST_ACTIVITY_KEY = 'keycloak_last_activity';
|
||||
|
||||
// Router y MessageService
|
||||
private router = inject(Router);
|
||||
private messageService = inject(MessageService);
|
||||
|
||||
// Estado de autenticación como signal
|
||||
// Estado de autenticación como signals
|
||||
private userInfo = signal<any>(null);
|
||||
private userRoles = signal<string[]>([]);
|
||||
private userGroups = signal<string[]>([]);
|
||||
private userPermissions = signal<string[]>([]);
|
||||
|
||||
// Token y refresh token
|
||||
private tokenInfo: any = null;
|
||||
@ -44,13 +56,18 @@ export class DirectAuthService {
|
||||
// Temporizador para logs de actividad
|
||||
private activityLogTimer: any = null;
|
||||
|
||||
// Nombre de grupos específicos
|
||||
private readonly GROUP_ADMINISTRADORES = 'administradores';
|
||||
private readonly GROUP_DESARROLLADORES = 'desarrolladores';
|
||||
private readonly GROUP_USUARIOS = 'usuarios';
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
// Inicializar configuraciones desde environment
|
||||
this.INACTIVITY_PERCENTAGE = environment.auth.inactivityPercentage;
|
||||
this.MIN_INACTIVITY_TIME = environment.auth.minInactivityTime;
|
||||
this.STRICT_TOKEN_RENEWAL = environment.auth.strictTokenRenewal;
|
||||
this.ENABLE_ACTIVITY_LOGS = environment.auth.enableActivityLogs;
|
||||
this.LOG_INTERVAL = environment.auth.logInterval;
|
||||
this.INACTIVITY_PERCENTAGE = environment.auth.inactivityPercentage || 0.5;
|
||||
this.MIN_INACTIVITY_TIME = environment.auth.minInactivityTime || 300000; // 5 minutos por defecto
|
||||
this.STRICT_TOKEN_RENEWAL = environment.auth.strictTokenRenewal || false;
|
||||
this.ENABLE_ACTIVITY_LOGS = environment.auth.enableActivityLogs || false;
|
||||
this.LOG_INTERVAL = environment.auth.logInterval || 60000; // 1 minuto por defecto
|
||||
|
||||
// Intentar cargar el token del almacenamiento local al iniciar
|
||||
this.loadTokenFromStorage();
|
||||
@ -66,25 +83,68 @@ export class DirectAuthService {
|
||||
|
||||
// Cargar token del almacenamiento local
|
||||
private loadTokenFromStorage(): void {
|
||||
const tokenInfo = localStorage.getItem('keycloak_token');
|
||||
const tokenInfo = localStorage.getItem(this.STORAGE_TOKEN_KEY);
|
||||
const userInfo = localStorage.getItem(this.STORAGE_USER_KEY);
|
||||
const rolesInfo = localStorage.getItem(this.STORAGE_ROLES_KEY);
|
||||
const groupsInfo = localStorage.getItem(this.STORAGE_GROUPS_KEY);
|
||||
const permissionsInfo = localStorage.getItem(this.STORAGE_PERMISSIONS_KEY);
|
||||
const lastActivity = localStorage.getItem(this.STORAGE_LAST_ACTIVITY_KEY);
|
||||
|
||||
// Cargar última actividad si existe
|
||||
if (lastActivity) {
|
||||
this.lastActivityTime = parseInt(lastActivity, 10);
|
||||
}
|
||||
|
||||
if (tokenInfo) {
|
||||
try {
|
||||
this.tokenInfo = JSON.parse(tokenInfo);
|
||||
|
||||
// Cargar información del usuario
|
||||
if (userInfo) {
|
||||
this.userInfo.set(JSON.parse(userInfo));
|
||||
}
|
||||
|
||||
// Cargar roles
|
||||
if (rolesInfo) {
|
||||
this.userRoles.set(JSON.parse(rolesInfo));
|
||||
}
|
||||
|
||||
// Cargar grupos
|
||||
if (groupsInfo) {
|
||||
this.userGroups.set(JSON.parse(groupsInfo));
|
||||
}
|
||||
|
||||
// Cargar permisos
|
||||
if (permissionsInfo) {
|
||||
this.userPermissions.set(JSON.parse(permissionsInfo));
|
||||
}
|
||||
|
||||
// Verificar si el token ha expirado
|
||||
if (this.isTokenExpired()) {
|
||||
// Token expirado al cargar
|
||||
// Si tiene refresh token y (no está en modo estricto o el usuario ha estado activo), intentar renovar
|
||||
if (this.tokenInfo.refresh_token && (!this.STRICT_TOKEN_RENEWAL || this.isUserActive())) {
|
||||
// Usuario activo o no en modo estricto, intentando renovar token
|
||||
this.refreshToken().subscribe();
|
||||
this.refreshToken().subscribe({
|
||||
next: () => {
|
||||
console.log('[Auth] Token renovado con éxito al iniciar');
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('[Auth] Error al renovar token al iniciar:', err);
|
||||
this.logout();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Usuario inactivo o sin refresh token, cerrando sesión
|
||||
this.logout();
|
||||
}
|
||||
} else {
|
||||
// Decodificar info del usuario desde el token
|
||||
this.setUserFromToken(this.tokenInfo.access_token);
|
||||
// Decodificar info del usuario desde el token si no fue cargado del storage
|
||||
if (!userInfo) {
|
||||
this.setUserFromToken(this.tokenInfo.access_token);
|
||||
// Obtener información adicional del usuario desde el endpoint userinfo
|
||||
}
|
||||
|
||||
// Configurar temporizador para renovación de token
|
||||
// Solo si el usuario está activo (en modo estricto) o siempre (en modo no estricto)
|
||||
if (!this.STRICT_TOKEN_RENEWAL || this.isUserActive()) {
|
||||
@ -99,11 +159,49 @@ export class DirectAuthService {
|
||||
}
|
||||
} catch (e) {
|
||||
// Error al cargar token
|
||||
console.error('[Auth] Error al cargar datos del storage:', e);
|
||||
this.clearAllStorageData();
|
||||
this.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guardar toda la información en el localStorage
|
||||
private saveAllToStorage(): void {
|
||||
if (this.tokenInfo) {
|
||||
localStorage.setItem(this.STORAGE_TOKEN_KEY, JSON.stringify(this.tokenInfo));
|
||||
}
|
||||
|
||||
if (this.userInfo()) {
|
||||
localStorage.setItem(this.STORAGE_USER_KEY, JSON.stringify(this.userInfo()));
|
||||
}
|
||||
|
||||
if (this.userRoles()) {
|
||||
localStorage.setItem(this.STORAGE_ROLES_KEY, JSON.stringify(this.userRoles()));
|
||||
}
|
||||
|
||||
if (this.userGroups()) {
|
||||
localStorage.setItem(this.STORAGE_GROUPS_KEY, JSON.stringify(this.userGroups()));
|
||||
}
|
||||
|
||||
if (this.userPermissions()) {
|
||||
localStorage.setItem(this.STORAGE_PERMISSIONS_KEY, JSON.stringify(this.userPermissions()));
|
||||
}
|
||||
|
||||
// Guardar tiempo de última actividad
|
||||
localStorage.setItem(this.STORAGE_LAST_ACTIVITY_KEY, this.lastActivityTime.toString());
|
||||
}
|
||||
|
||||
// Limpiar todos los datos del storage
|
||||
private clearAllStorageData(): void {
|
||||
localStorage.removeItem(this.STORAGE_TOKEN_KEY);
|
||||
localStorage.removeItem(this.STORAGE_USER_KEY);
|
||||
localStorage.removeItem(this.STORAGE_ROLES_KEY);
|
||||
localStorage.removeItem(this.STORAGE_GROUPS_KEY);
|
||||
localStorage.removeItem(this.STORAGE_PERMISSIONS_KEY);
|
||||
localStorage.removeItem(this.STORAGE_LAST_ACTIVITY_KEY);
|
||||
}
|
||||
|
||||
// Comprueba si el usuario ha estado activo dentro del período de inactividad
|
||||
private isUserActive(): boolean {
|
||||
const currentTime = Date.now();
|
||||
@ -131,11 +229,15 @@ export class DirectAuthService {
|
||||
tap(tokenInfo => {
|
||||
// Guardar información del token
|
||||
this.tokenInfo = tokenInfo;
|
||||
localStorage.setItem('keycloak_token', JSON.stringify(tokenInfo));
|
||||
|
||||
// Decodificar info del usuario
|
||||
this.setUserFromToken(tokenInfo.access_token);
|
||||
|
||||
// Obtener información adicional del usuario desde el endpoint userinfo
|
||||
|
||||
// Guardar toda la información en localStorage
|
||||
this.saveAllToStorage();
|
||||
|
||||
// Configurar temporizador para renovación de token
|
||||
this.startRefreshTokenTimer();
|
||||
|
||||
@ -153,11 +255,18 @@ export class DirectAuthService {
|
||||
}),
|
||||
catchError(error => {
|
||||
// Error de autenticación
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Error de autenticación',
|
||||
detail: 'Credenciales incorrectas o error de servidor'
|
||||
});
|
||||
return throwError(() => new Error('Credenciales incorrectas o error de servidor'));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Cerrar sesión
|
||||
public logout(): void {
|
||||
// Detener temporizador de renovación
|
||||
@ -170,9 +279,12 @@ export class DirectAuthService {
|
||||
this.stopActivityLogging();
|
||||
|
||||
// Limpiar datos de sesión
|
||||
localStorage.removeItem('keycloak_token');
|
||||
this.clearAllStorageData();
|
||||
this.tokenInfo = null;
|
||||
this.userInfo.set(null);
|
||||
this.userRoles.set([]);
|
||||
this.userGroups.set([]);
|
||||
this.userPermissions.set([]);
|
||||
|
||||
// Redirigir a la página de login
|
||||
this.router.navigate(['/login']);
|
||||
@ -228,19 +340,19 @@ export class DirectAuthService {
|
||||
.set('grant_type', 'refresh_token')
|
||||
.set('refresh_token', this.tokenInfo.refresh_token);
|
||||
|
||||
// Intentando renovar el token
|
||||
|
||||
return this.http.post<any>(this.tokenEndpoint, params.toString(), { headers })
|
||||
.pipe(
|
||||
tap(newTokenInfo => {
|
||||
// Token renovado exitosamente
|
||||
// Actualizar información del token
|
||||
this.tokenInfo = newTokenInfo;
|
||||
localStorage.setItem('keycloak_token', JSON.stringify(newTokenInfo));
|
||||
|
||||
// Actualizar información del usuario si es necesario
|
||||
this.setUserFromToken(newTokenInfo.access_token);
|
||||
|
||||
// Guardar toda la información actualizada en localStorage
|
||||
this.saveAllToStorage();
|
||||
|
||||
// Reiniciar temporizador para renovación de token
|
||||
this.startRefreshTokenTimer();
|
||||
|
||||
@ -255,6 +367,7 @@ export class DirectAuthService {
|
||||
}),
|
||||
catchError(error => {
|
||||
// Error al renovar token
|
||||
console.error('[Auth] Error al renovar token:', error);
|
||||
// Si falla la renovación, forzar cierre de sesión
|
||||
this.logout();
|
||||
return throwError(() => new Error('Error al renovar la sesión'));
|
||||
@ -277,6 +390,98 @@ export class DirectAuthService {
|
||||
return this.userInfo();
|
||||
}
|
||||
|
||||
// Obtener el ID del usuario
|
||||
public getUserId(): string | null {
|
||||
return this.userInfo()?.id || null;
|
||||
}
|
||||
|
||||
// Obtener username del usuario
|
||||
public getUsername(): string | null {
|
||||
return this.userInfo()?.username || null;
|
||||
}
|
||||
|
||||
// Obtener email del usuario
|
||||
public getUserEmail(): string | null {
|
||||
return this.userInfo()?.email || null;
|
||||
}
|
||||
|
||||
// Obtener nombre completo del usuario
|
||||
public getUserFullName(): string | null {
|
||||
return this.userInfo()?.name || null;
|
||||
}
|
||||
|
||||
// Obtener todos los roles del usuario
|
||||
public getUserRoles(): string[] {
|
||||
return this.userRoles() || [];
|
||||
}
|
||||
|
||||
// Obtener todos los grupos del usuario
|
||||
public getUserGroups(): string[] {
|
||||
return this.userGroups() || [];
|
||||
}
|
||||
|
||||
// Obtener todos los permisos del usuario
|
||||
public getUserPermissions(): string[] {
|
||||
return this.userPermissions() || [];
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene un rol específico
|
||||
public hasRole(role: string): boolean {
|
||||
return this.userRoles().includes(role);
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene cualquiera de los roles especificados
|
||||
public hasAnyRole(roles: string[]): boolean {
|
||||
return roles.some(role => this.userRoles().includes(role));
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene todos los roles especificados
|
||||
public hasAllRoles(roles: string[]): boolean {
|
||||
return roles.every(role => this.userRoles().includes(role));
|
||||
}
|
||||
|
||||
// Verificar si el usuario pertenece a un grupo específico
|
||||
public inGroup(group: string): boolean {
|
||||
return this.userGroups().includes(group);
|
||||
}
|
||||
|
||||
// Verificar si el usuario pertenece a cualquiera de los grupos especificados
|
||||
public inAnyGroup(groups: string[]): boolean {
|
||||
return groups.some(group => this.userGroups().includes(group));
|
||||
}
|
||||
|
||||
// Verificar si el usuario pertenece a todos los grupos especificados
|
||||
public inAllGroups(groups: string[]): boolean {
|
||||
return groups.every(group => this.userGroups().includes(group));
|
||||
}
|
||||
|
||||
// Verificar si el usuario es administrador (por rol o grupo)
|
||||
public isAdmin(): boolean {
|
||||
return this.hasRole('admin') ||
|
||||
this.inGroup(this.GROUP_ADMINISTRADORES);
|
||||
}
|
||||
|
||||
// Verificar si el usuario es desarrollador (por rol o grupo)
|
||||
public isDeveloper(): boolean {
|
||||
return this.hasRole('developer') ||
|
||||
this.inGroup(this.GROUP_DESARROLLADORES);
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene un permiso específico
|
||||
public hasPermission(permission: string): boolean {
|
||||
return this.userPermissions().includes(permission);
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene cualquiera de los permisos especificados
|
||||
public hasAnyPermission(permissions: string[]): boolean {
|
||||
return permissions.some(perm => this.userPermissions().includes(perm));
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene todos los permisos especificados
|
||||
public hasAllPermissions(permissions: string[]): boolean {
|
||||
return permissions.every(perm => this.userPermissions().includes(perm));
|
||||
}
|
||||
|
||||
// Verificar si el token ha expirado
|
||||
private isTokenExpired(): boolean {
|
||||
if (!this.tokenInfo?.access_token) {
|
||||
@ -300,6 +505,30 @@ export class DirectAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
// Tiempo restante de validez del token en segundos
|
||||
public getTokenRemainingTimeInSeconds(): number {
|
||||
return Math.floor(this.getTokenExpirationTime() / 1000);
|
||||
}
|
||||
|
||||
// Obtiene el payload completo del token decodificado
|
||||
public getDecodedToken(): any {
|
||||
if (!this.tokenInfo?.access_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenParts = this.tokenInfo.access_token.split('.');
|
||||
if (tokenParts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(atob(tokenParts[1]));
|
||||
} catch (e) {
|
||||
console.error('[Auth] Error al decodificar token:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Decodificar token y extraer información del usuario
|
||||
private setUserFromToken(token: string): void {
|
||||
try {
|
||||
@ -309,21 +538,71 @@ export class DirectAuthService {
|
||||
}
|
||||
|
||||
const payload = JSON.parse(atob(tokenParts[1]));
|
||||
console.log('[Auth] Token payload:', payload);
|
||||
|
||||
// Extraer información del usuario del payload
|
||||
const user = {
|
||||
id: payload.sub,
|
||||
username: payload.preferred_username,
|
||||
name: payload.name,
|
||||
firstName: payload.given_name,
|
||||
lastName: payload.family_name,
|
||||
email: payload.email,
|
||||
emailVerified: payload.email_verified,
|
||||
locale: payload.locale,
|
||||
roles: payload.realm_access?.roles || [],
|
||||
isAdmin: (payload.realm_access?.roles || []).includes('admin')
|
||||
clientRoles: payload.resource_access?.[this.clientId]?.roles || [],
|
||||
isAdmin: (payload.realm_access?.roles || []).includes('admin'),
|
||||
groups: payload.groups || [], // Puede estar vacío inicialmente
|
||||
attributes: payload.attributes || {}
|
||||
};
|
||||
|
||||
// Actualizar señales
|
||||
this.userInfo.set(user);
|
||||
this.userRoles.set([
|
||||
...(payload.realm_access?.roles || []),
|
||||
...(payload.resource_access?.[this.clientId]?.roles || [])
|
||||
]);
|
||||
|
||||
// Los grupos podrían estar vacíos en el token, esperaremos a obtenerlos del userinfo
|
||||
if (payload.groups && Array.isArray(payload.groups)) {
|
||||
this.userGroups.set(payload.groups);
|
||||
}
|
||||
|
||||
// Extraer permisos si están disponibles en el token
|
||||
const permissions = [];
|
||||
|
||||
// Si hay scope, intentar extraer permisos
|
||||
if (payload.scope) {
|
||||
const scopes = payload.scope.split(' ');
|
||||
scopes.forEach((scope: string) => {
|
||||
if (scope.startsWith('permission:')) {
|
||||
permissions.push(scope.substring(11));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Si hay resource_access con permisos
|
||||
if (payload.resource_access && payload.resource_access[this.clientId]) {
|
||||
const clientPerms = payload.resource_access[this.clientId].permissions;
|
||||
if (clientPerms && Array.isArray(clientPerms)) {
|
||||
permissions.push(...clientPerms);
|
||||
}
|
||||
}
|
||||
|
||||
this.userPermissions.set(permissions);
|
||||
|
||||
// Loguear información para depuración
|
||||
console.log('[Auth] Usuario decodificado:', user);
|
||||
console.log('[Auth] Roles:', this.userRoles());
|
||||
console.log('[Auth] Grupos iniciales:', this.userGroups());
|
||||
} catch (e) {
|
||||
// Error al decodificar token
|
||||
console.error('[Auth] Error al decodificar token:', e);
|
||||
this.userInfo.set(null);
|
||||
this.userRoles.set([]);
|
||||
this.userGroups.set([]);
|
||||
this.userPermissions.set([]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -347,19 +626,28 @@ export class DirectAuthService {
|
||||
const timeToExpiry = expirationTime - currentTime;
|
||||
const refreshTime = timeToExpiry * 0.7;
|
||||
|
||||
// Calcular tiempos para la renovación del token
|
||||
console.log(`[Auth] Token configurado para renovarse en ${Math.round(refreshTime/1000)} segundos`);
|
||||
|
||||
this.refreshTokenTimeout = setTimeout(() => {
|
||||
// Si STRICT_TOKEN_RENEWAL está activado, verificar la actividad del usuario
|
||||
if (!this.STRICT_TOKEN_RENEWAL || this.isUserActive()) {
|
||||
this.refreshToken().subscribe();
|
||||
console.log('[Auth] Intentando renovar token automáticamente');
|
||||
this.refreshToken().subscribe({
|
||||
next: () => console.log('[Auth] Token renovado con éxito'),
|
||||
error: (err) => {
|
||||
console.error('[Auth] Error al renovar token:', err);
|
||||
this.logout();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// En modo estricto, si el usuario está inactivo, cerrar sesión
|
||||
console.log('[Auth] Usuario inactivo, cerrando sesión por inactividad');
|
||||
this.logout();
|
||||
}
|
||||
}, refreshTime);
|
||||
} catch (e) {
|
||||
// Error al configurar renovación de token
|
||||
console.error('[Auth] Error al configurar renovación de token:', e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,6 +692,87 @@ export class DirectAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar si el token necesita ser renovado pronto
|
||||
public tokenNeedsRenewal(): boolean {
|
||||
const expirationTime = this.getTokenExpirationTime();
|
||||
// Considerar renovación si queda menos del 30% del tiempo de vida del token
|
||||
return expirationTime > 0 && expirationTime < this.getTokenLifespan() * 0.3;
|
||||
}
|
||||
|
||||
// Obtener el tiempo de vida total del token en milisegundos
|
||||
private getTokenLifespan(): number {
|
||||
if (!this.tokenInfo?.access_token) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenParts = this.tokenInfo.access_token.split('.');
|
||||
const payload = JSON.parse(atob(tokenParts[1]));
|
||||
|
||||
// Calcular tiempo total de vida (exp - iat)
|
||||
if (payload.exp && payload.iat) {
|
||||
return (payload.exp - payload.iat) * 1000;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar acceso por roles para usar en Guards
|
||||
public checkRoleAccess(requiredRoles: string[]): Observable<boolean> {
|
||||
if (!this.isAuthenticated()) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
// Si no hay roles requeridos, permitir acceso
|
||||
if (!requiredRoles || requiredRoles.length === 0) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene alguno de los roles requeridos
|
||||
return of(this.hasAnyRole(requiredRoles));
|
||||
}
|
||||
|
||||
// Verificar acceso por grupos para usar en Guards
|
||||
public checkGroupAccess(requiredGroups: string[]): Observable<boolean> {
|
||||
if (!this.isAuthenticated()) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
// Si no hay grupos requeridos, permitir acceso
|
||||
if (!requiredGroups || requiredGroups.length === 0) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
// Verificar si el usuario pertenece a alguno de los grupos requeridos
|
||||
return of(this.inAnyGroup(requiredGroups));
|
||||
}
|
||||
|
||||
// Verificar acceso por permisos para usar en Guards
|
||||
public checkPermissionAccess(requiredPermissions: string[]): Observable<boolean> {
|
||||
if (!this.isAuthenticated()) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
// Si no hay permisos requeridos, permitir acceso
|
||||
if (!requiredPermissions || requiredPermissions.length === 0) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene todos los permisos requeridos
|
||||
return of(this.hasAllPermissions(requiredPermissions));
|
||||
}
|
||||
|
||||
// Forzar renovación del token manualmente
|
||||
public forceTokenRenewal(): Observable<any> {
|
||||
if (this.isAuthenticated() && this.tokenInfo?.refresh_token) {
|
||||
return this.refreshToken();
|
||||
}
|
||||
return throwError(() => new Error('No hay sesión activa para renovar'));
|
||||
}
|
||||
|
||||
// Configurar monitoreo de actividad del usuario
|
||||
private setupActivityMonitoring(): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
@ -435,6 +804,9 @@ export class DirectAuthService {
|
||||
// Registrar que el usuario está activo
|
||||
this.lastActivityTime = Date.now();
|
||||
|
||||
// Guardar tiempo de última actividad en localStorage
|
||||
localStorage.setItem(this.STORAGE_LAST_ACTIVITY_KEY, this.lastActivityTime.toString());
|
||||
|
||||
// Marca el usuario como activo
|
||||
this.userInactive.set(false);
|
||||
|
||||
@ -455,10 +827,195 @@ export class DirectAuthService {
|
||||
);
|
||||
|
||||
// Configurar timer de inactividad
|
||||
|
||||
this.userActivity = setTimeout(() => {
|
||||
// Umbral de inactividad alcanzado
|
||||
this.userInactive.set(true);
|
||||
}, inactivityTime);
|
||||
}
|
||||
|
||||
// Obtener el estado de inactividad del usuario
|
||||
public isUserInactive(): boolean {
|
||||
return this.userInactive();
|
||||
}
|
||||
|
||||
// Obtener tiempo de inactividad en segundos
|
||||
public getInactivityTime(): number {
|
||||
return Math.floor((Date.now() - this.lastActivityTime) / 1000);
|
||||
}
|
||||
|
||||
// ======== FUNCIONES ESPECÍFICAS PARA LOS GUARDS ========
|
||||
|
||||
// Guard para comprobar si el usuario es administrador
|
||||
public isAdminGuard(): Observable<boolean> {
|
||||
if (!this.isAuthenticated()) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return of(this.isAdmin());
|
||||
}
|
||||
|
||||
// Guard para comprobar si el usuario es desarrollador
|
||||
public isDeveloperGuard(): Observable<boolean> {
|
||||
if (!this.isAuthenticated()) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return of(this.isDeveloper());
|
||||
}
|
||||
|
||||
// Guard para comprobar si el usuario pertenece al grupo de usuarios normales
|
||||
public isRegularUserGuard(): Observable<boolean> {
|
||||
if (!this.isAuthenticated()) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return of(this.inGroup(this.GROUP_USUARIOS));
|
||||
}
|
||||
|
||||
// Guard combinado para roles y grupos
|
||||
public checkAccess(requiredRoles: string[] = [], requiredGroups: string[] = []): Observable<boolean> {
|
||||
if (!this.isAuthenticated()) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
// Si no hay requisitos, permitir acceso
|
||||
if ((!requiredRoles || requiredRoles.length === 0) &&
|
||||
(!requiredGroups || requiredGroups.length === 0)) {
|
||||
return of(true);
|
||||
}
|
||||
|
||||
// Verificar roles si hay alguno especificado
|
||||
const hasRequiredRole = requiredRoles.length === 0 || this.hasAnyRole(requiredRoles);
|
||||
|
||||
// Verificar grupos si hay alguno especificado
|
||||
const hasRequiredGroup = requiredGroups.length === 0 || this.inAnyGroup(requiredGroups);
|
||||
|
||||
// Combinar verificaciones según si necesitamos ambas o cualquiera
|
||||
const hasAccess = (requiredRoles.length > 0 && requiredGroups.length > 0)
|
||||
? (hasRequiredRole && hasRequiredGroup) // Necesita ambos
|
||||
: (hasRequiredRole || hasRequiredGroup); // Necesita cualquiera
|
||||
|
||||
return of(hasAccess);
|
||||
}
|
||||
|
||||
// ======== FUNCIONES PARA DEPURACIÓN ========
|
||||
|
||||
// Obtener información completa del estado de autenticación para depuración
|
||||
public getAuthDebugInfo(): any {
|
||||
return {
|
||||
isAuthenticated: this.isAuthenticated(),
|
||||
tokenExpired: this.isTokenExpired(),
|
||||
user: this.userInfo(),
|
||||
roles: this.userRoles(),
|
||||
groups: this.userGroups(),
|
||||
permissions: this.userPermissions(),
|
||||
tokenRemainingTime: this.getTokenRemainingTimeInSeconds(),
|
||||
isAdmin: this.isAdmin(),
|
||||
isDeveloper: this.isDeveloper(),
|
||||
isInactivityTimerActive: !!this.userActivity,
|
||||
inactivityTimeSeconds: this.getInactivityTime(),
|
||||
isUserInactive: this.userInactive()
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar si existe el token en localStorage (útil para depuración)
|
||||
public hasLocalStorageToken(): boolean {
|
||||
return !!localStorage.getItem(this.STORAGE_TOKEN_KEY);
|
||||
}
|
||||
|
||||
// Imprimir información completa del token para depuración
|
||||
public logTokenInfo(): void {
|
||||
if (!this.isAuthenticated()) {
|
||||
console.log('[Auth] No hay sesión activa');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('====== INFORMACIÓN DEL TOKEN ======');
|
||||
console.log('Token:', this.getToken());
|
||||
console.log('Token Decodificado:', this.getDecodedToken());
|
||||
console.log('Tiempo restante:', this.getTokenRemainingTimeInSeconds(), 'segundos');
|
||||
console.log('Usuario:', this.userInfo());
|
||||
console.log('Roles:', this.userRoles());
|
||||
console.log('Grupos:', this.userGroups());
|
||||
console.log('Permisos:', this.userPermissions());
|
||||
console.log('Es Admin:', this.isAdmin());
|
||||
console.log('Es Developer:', this.isDeveloper());
|
||||
console.log('================================');
|
||||
}
|
||||
|
||||
// ======== FUNCIONES AUXILIARES PARA MAPEO DE RECURSOS ========
|
||||
|
||||
// Obtener recursos permitidos según roles y grupos
|
||||
public getAuthorizedResources(resourceMap: any): string[] {
|
||||
if (!this.isAuthenticated()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const authorizedResources: string[] = [];
|
||||
|
||||
// Si el usuario es admin, tiene acceso a todo
|
||||
if (this.isAdmin()) {
|
||||
return Object.keys(resourceMap);
|
||||
}
|
||||
|
||||
// Recorrer el mapa de recursos
|
||||
for (const resource in resourceMap) {
|
||||
const access = resourceMap[resource];
|
||||
|
||||
// Verificar acceso por roles
|
||||
if (access.roles && this.hasAnyRole(access.roles)) {
|
||||
authorizedResources.push(resource);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verificar acceso por grupos
|
||||
if (access.groups && this.inAnyGroup(access.groups)) {
|
||||
authorizedResources.push(resource);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verificar acceso por permisos
|
||||
if (access.permissions && this.hasAllPermissions(access.permissions)) {
|
||||
authorizedResources.push(resource);
|
||||
}
|
||||
}
|
||||
|
||||
return authorizedResources;
|
||||
}
|
||||
|
||||
// Verificar si el usuario puede acceder a un recurso específico
|
||||
public canAccessResource(resource: string, resourceMap: any): boolean {
|
||||
if (!this.isAuthenticated()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si el usuario es admin, tiene acceso a todo
|
||||
if (this.isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar si el recurso existe en el mapa
|
||||
if (!resourceMap[resource]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const access = resourceMap[resource];
|
||||
|
||||
// Verificar acceso por roles
|
||||
if (access.roles && this.hasAnyRole(access.roles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar acceso por grupos
|
||||
if (access.groups && this.inAnyGroup(access.groups)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar acceso por permisos
|
||||
if (access.permissions && this.hasAllPermissions(access.permissions)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -717,778 +717,34 @@ Ahora puedes acceder a la consola de administración de Keycloak en http://192.1
|
||||
> **Nota**: Para aplicaciones que no son SPA, puedes habilitar "Client authentication" y obtener un client secret que deberás usar en la configuración.
|
||||
> **Nota**: Puedes descargar un ejemplo de una maquina virtual de virtual box en el siguiente [enlace](https://valposystemscom-my.sharepoint.com/:u:/g/personal/luis_cespedes_valposystems_com/EepH0pHrk8ZEgeU-RmVkopgBiN6LWoPm6P8MJEzt-qdxXw?e=IZFMMg) con todo ya configurado , solo importar y usar
|
||||
|
||||
## 6. Integración con Angular: Enfoque básico
|
||||
## Configurar cliente para incluir grupos
|
||||
**Añadir un Protocol Mapper en el cliente de Keycloak:** a. Inicia sesión en la consola de administración de Keycloak b. Navega a "Clients" > selecciona tu cliente "angular-app" c. Ve a la pestaña "Client Scopes" > Selecciona el scope por defecto (normalmente es el nombre del cliente) d. Ve a la pestaña "Mappers" e. Haz clic en "Create" o "Add Mapper" y selecciona "Group Membership" f. Configura el mapper con los siguientes valores:
|
||||
|
||||
Para las versiones más antiguas de Angular, vamos a utilizar el enfoque tradicional de integración con Keycloak.
|
||||
- Name: groups
|
||||
- Token Claim Name: groups
|
||||
- Add to ID token: ON
|
||||
- Add to access token: ON
|
||||
- Add to userinfo: ON
|
||||
- Full group path: OFF (a menos que necesites la ruta completa) g. Guarda la configuración
|
||||
|
||||
### Crear una aplicación Angular
|
||||
Alternativamente, puedes crear un mapper de tipo "User Attribute" si prefieres almacenar los grupos como atributos de usuario.
|
||||
|
||||
```bash
|
||||
# Crear una nueva aplicación
|
||||
ng new angular-keycloak-app
|
||||
cd angular-keycloak-app
|
||||

|
||||
|
||||
# Instalar la biblioteca para integrar Keycloak
|
||||
npm install keycloak-angular keycloak-js
|
||||
```
|
||||

|
||||
|
||||
### Configurar Keycloak en Angular
|
||||
|
||||
Crea un archivo `src/assets/silent-check-sso.html`:
|
||||

|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.href, location.origin);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Configurar el módulo principal (Angular tradicional)
|
||||

|
||||
|
||||
Modifica el archivo `src/app/app.module.ts`:
|
||||

|
||||
|
||||
```typescript
|
||||
import { NgModule, APP_INITIALIZER } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
Que logramos con esto ? facil , que en el accestoken aparezcan los grupos que pertenece el usuario , tal cual lo configuramos en ldap, esto puede ayudar para crear politicas de acceso a paginas o rutas protegidas para algun grupo en especifico mas adelante
|
||||

|
||||
|
||||
function initializeKeycloak(keycloak: KeycloakService) {
|
||||
return () =>
|
||||
keycloak.init({
|
||||
config: {
|
||||
url: 'http://localhost:8080',
|
||||
realm: 'angular-app',
|
||||
clientId: 'angular-app'
|
||||
},
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri:
|
||||
window.location.origin + '/assets/silent-check-sso.html'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
AppRoutingModule,
|
||||
KeycloakAngularModule
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeKeycloak,
|
||||
multi: true,
|
||||
deps: [KeycloakService]
|
||||
}
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
```
|
||||
|
||||
### Crear un servicio de autenticación básico
|
||||
|
||||
```bash
|
||||
ng generate service auth
|
||||
```
|
||||
|
||||
Edita `src/app/auth.service.ts`:
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { KeycloakProfile } from 'keycloak-js';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
|
||||
constructor(private keycloak: KeycloakService) { }
|
||||
|
||||
public getLoggedUser(): Promise<KeycloakProfile | null> {
|
||||
return this.keycloak.loadUserProfile();
|
||||
}
|
||||
|
||||
public login(): void {
|
||||
this.keycloak.login();
|
||||
}
|
||||
|
||||
public logout(): void {
|
||||
this.keycloak.logout();
|
||||
}
|
||||
|
||||
public isLoggedIn(): Promise<boolean> {
|
||||
return this.keycloak.isLoggedIn();
|
||||
}
|
||||
|
||||
public getRoles(): string[] {
|
||||
return this.keycloak.getUserRoles();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Crear un guardia para rutas protegidas (Angular tradicional)
|
||||
|
||||
```bash
|
||||
ng generate guard auth
|
||||
```
|
||||
|
||||
Edita `src/app/auth.guard.ts`:
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
|
||||
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard extends KeycloakAuthGuard {
|
||||
|
||||
constructor(
|
||||
protected override readonly router: Router,
|
||||
protected readonly keycloak: KeycloakService
|
||||
) {
|
||||
super(router, keycloak);
|
||||
}
|
||||
|
||||
public async isAccessAllowed(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Promise<boolean | UrlTree> {
|
||||
|
||||
// Verifica si el usuario está autenticado
|
||||
if (!this.authenticated) {
|
||||
await this.keycloak.login({
|
||||
redirectUri: window.location.origin + state.url,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Obtiene los roles requeridos desde la ruta
|
||||
const requiredRoles = route.data['roles'];
|
||||
|
||||
// Permite el acceso si no hay roles requeridos
|
||||
if (!requiredRoles || requiredRoles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verifica si el usuario tiene al menos uno de los roles requeridos
|
||||
return requiredRoles.some((role: string) => this.roles.includes(role));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configurar las rutas con protección
|
||||
|
||||
Modifica `src/app/app-routing.module.ts`:
|
||||
|
||||
```typescript
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from './auth.guard';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'admin',
|
||||
component: AppComponent,
|
||||
canActivate: [AuthGuard],
|
||||
data: { roles: ['administradores'] }
|
||||
},
|
||||
{
|
||||
path: 'developer',
|
||||
component: AppComponent,
|
||||
canActivate: [AuthGuard],
|
||||
data: { roles: ['desarrolladores'] }
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
component: AppComponent,
|
||||
canActivate: [AuthGuard],
|
||||
data: { roles: ['usuarios'] }
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: AppComponent
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
```
|
||||
|
||||
## 7. Integración con Angular 19: Enfoque moderno
|
||||
|
||||
Para Angular 19 y versiones más recientes, utilizaremos el enfoque moderno que aprovecha características como componentes independientes y el sistema de inyección de dependencias mejorado.
|
||||
|
||||
### Configuración en app.config.ts (Angular 19)
|
||||
|
||||
Configura Keycloak en el archivo `src/app/app.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
|
||||
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
||||
import { routes } from './app.routes';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import {
|
||||
provideKeycloak,
|
||||
createInterceptorCondition,
|
||||
IncludeBearerTokenCondition,
|
||||
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
||||
includeBearerTokenInterceptor
|
||||
} from 'keycloak-angular';
|
||||
|
||||
// Define condiciones para incluir el token en las peticiones
|
||||
const localhostCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
|
||||
urlPattern: /^(http:\/\/localhost)(\/.*)?$/i, // URLs que comienzan con http://localhost
|
||||
bearerPrefix: 'Bearer'
|
||||
});
|
||||
|
||||
// Condición para APIs
|
||||
const apiCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
|
||||
urlPattern: /^(\/api)(\/.*)?$/i, // URLs que comienzan con /api
|
||||
bearerPrefix: 'Bearer'
|
||||
});
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(
|
||||
routes,
|
||||
withPreloading(PreloadAllModules)
|
||||
),
|
||||
provideAnimations(),
|
||||
// Usar el interceptor de Keycloak para adjuntar tokens de autenticación
|
||||
provideHttpClient(
|
||||
withFetch(),
|
||||
withInterceptors([includeBearerTokenInterceptor])
|
||||
),
|
||||
// Configuración para Keycloak
|
||||
provideKeycloak({
|
||||
config: {
|
||||
url: 'http://localhost:8080', // URL del servidor Keycloak
|
||||
realm: 'angular-app', // Nombre del realm
|
||||
clientId: 'angular-app' // ID del cliente
|
||||
},
|
||||
initOptions: {
|
||||
onLoad: 'check-sso', // Opciones: 'login-required' o 'check-sso'
|
||||
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
|
||||
checkLoginIframe: false,
|
||||
pkceMethod: 'S256' // Mejora la seguridad
|
||||
},
|
||||
// Configurar el interceptor especificando las URLs donde incluir el token
|
||||
providers: [
|
||||
{
|
||||
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
||||
useValue: [localhostCondition, apiCondition]
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
## 8. Servicios de autenticación avanzados
|
||||
|
||||
Con Angular 19, podemos crear un servicio de autenticación más avanzado utilizando signals y effects.
|
||||
|
||||
```typescript
|
||||
import { Injectable, inject, effect, signal } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, from } from 'rxjs';
|
||||
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType } from 'keycloak-angular';
|
||||
import { Router } from '@angular/router';
|
||||
import Keycloak from 'keycloak-js';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
// Inyectar Keycloak directamente
|
||||
private keycloak = inject(Keycloak);
|
||||
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
|
||||
private router = inject(Router);
|
||||
|
||||
// Estado del usuario
|
||||
private userSubject = new BehaviorSubject<any>(null);
|
||||
public user$ = this.userSubject.asObservable();
|
||||
|
||||
// Estado de autenticación como signal
|
||||
public isAuthenticated = signal<boolean>(false);
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
// Verificar estado inicial
|
||||
this.checkInitialAuthState();
|
||||
|
||||
// Configurar manejadores de eventos usando Angular effects
|
||||
effect(() => {
|
||||
const event = this.keycloakEvents();
|
||||
if (!event) return;
|
||||
|
||||
console.log('Keycloak event:', event.type);
|
||||
|
||||
// Autenticación exitosa
|
||||
if (event.type === KeycloakEventType.AuthSuccess) {
|
||||
this.isAuthenticated.set(true);
|
||||
this.loadUserInfo();
|
||||
}
|
||||
|
||||
// Cierre de sesión
|
||||
if (event.type === KeycloakEventType.AuthLogout) {
|
||||
this.isAuthenticated.set(false);
|
||||
this.userSubject.next(null);
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
// Error de autenticación
|
||||
if (event.type === KeycloakEventType.AuthError) {
|
||||
console.error('Authentication error:', event);
|
||||
this.isAuthenticated.set(false);
|
||||
this.userSubject.next(null);
|
||||
}
|
||||
|
||||
// Expiración del token
|
||||
if (event.type === KeycloakEventType.TokenExpired) {
|
||||
console.log('Token expired, refreshing...');
|
||||
this.updateToken();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async checkInitialAuthState(): Promise<void> {
|
||||
try {
|
||||
const isLoggedIn = await this.keycloak.authenticated;
|
||||
this.isAuthenticated.set(isLoggedIn);
|
||||
|
||||
if (isLoggedIn) {
|
||||
await this.loadUserInfo();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking initial auth state:', error);
|
||||
this.isAuthenticated.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadUserInfo(): Promise<void> {
|
||||
try {
|
||||
const isLoggedIn = await this.keycloak.authenticated;
|
||||
|
||||
if (!isLoggedIn) {
|
||||
this.userSubject.next(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const userProfile = await this.keycloak.loadUserProfile();
|
||||
const isAdmin = this.keycloak.hasRealmRole('admin');
|
||||
|
||||
// Obtener roles del usuario
|
||||
const realmRoles = this.keycloak.realmAccess?.roles || [];
|
||||
const resourceRoles = this.keycloak.resourceAccess || {};
|
||||
|
||||
const user = {
|
||||
id: userProfile.id,
|
||||
username: userProfile.username,
|
||||
name: `${userProfile.firstName || ''} ${userProfile.lastName || ''}`.trim(),
|
||||
email: userProfile.email,
|
||||
role: isAdmin ? 'admin' : 'user',
|
||||
roles: {
|
||||
realm: realmRoles,
|
||||
resource: resourceRoles
|
||||
},
|
||||
isAdmin: isAdmin
|
||||
};
|
||||
|
||||
this.userSubject.next(user);
|
||||
} catch (error) {
|
||||
console.error('Error loading user profile:', error);
|
||||
this.userSubject.next(null);
|
||||
}
|
||||
}
|
||||
|
||||
login(redirectUri?: string): Promise<void> {
|
||||
return this.keycloak.login({
|
||||
redirectUri: redirectUri || window.location.origin
|
||||
});
|
||||
}
|
||||
|
||||
logout(): Promise<void> {
|
||||
return this.keycloak.logout({
|
||||
redirectUri: window.location.origin
|
||||
});
|
||||
}
|
||||
|
||||
isLoggedIn(): Observable<boolean> {
|
||||
try {
|
||||
return from(Promise.resolve(this.keycloak.authenticated || false));
|
||||
} catch (error) {
|
||||
console.error('Error checking authentication:', error);
|
||||
return from(Promise.resolve(false));
|
||||
}
|
||||
}
|
||||
|
||||
getToken(): Promise<string> {
|
||||
try {
|
||||
return Promise.resolve(this.keycloak.token || '');
|
||||
} catch (error) {
|
||||
console.error('Error getting token:', error);
|
||||
return Promise.resolve('');
|
||||
}
|
||||
}
|
||||
|
||||
async updateToken(minValidity = 30): Promise<boolean> {
|
||||
try {
|
||||
return await this.keycloak.updateToken(minValidity);
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error);
|
||||
await this.login();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentUser(): any {
|
||||
return this.userSubject.value;
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene un rol específico
|
||||
hasRole(role: string): boolean {
|
||||
return this.keycloak.hasRealmRole(role);
|
||||
}
|
||||
|
||||
// Verificar si el usuario tiene alguno de los roles especificados
|
||||
hasAnyRole(roles: string[]): boolean {
|
||||
for (const role of roles) {
|
||||
if (this.keycloak.hasRealmRole(role)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Guardias de ruta e interceptores HTTP
|
||||
|
||||
### Guardia de ruta funcional (Angular 19)
|
||||
|
||||
```typescript
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import Keycloak from 'keycloak-js';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { createAuthGuard, AuthGuardData } from 'keycloak-angular';
|
||||
|
||||
// Implementación directa
|
||||
export const authGuard: CanActivateFn = async (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Promise<boolean | UrlTree> => {
|
||||
const keycloak = inject(Keycloak);
|
||||
const router = inject(Router);
|
||||
|
||||
try {
|
||||
// Verificar si el usuario está autenticado
|
||||
const authenticated = await keycloak.authenticated;
|
||||
|
||||
if (authenticated) {
|
||||
// Verificar roles si están especificados en la ruta
|
||||
const requiredRoles = route.data['roles'] as string[];
|
||||
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
// Comprobar si el usuario tiene los roles requeridos
|
||||
const hasRequiredRole = requiredRoles.some(role =>
|
||||
keycloak.hasRealmRole(role)
|
||||
);
|
||||
|
||||
if (!hasRequiredRole) {
|
||||
console.log('Usuario no tiene los roles requeridos');
|
||||
return router.createUrlTree(['/unauthorized']);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no está autenticado, redirigir a la página de login
|
||||
console.log('Usuario no autenticado, redirigiendo a login');
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url !== '/' ? state.url : '/inicio' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error al verificar autenticación:', error);
|
||||
return router.createUrlTree(['/login']);
|
||||
}
|
||||
};
|
||||
|
||||
// Alternativa usando el helper de keycloak-angular
|
||||
const isAccessAllowed = async (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
authData: AuthGuardData
|
||||
): Promise<boolean | UrlTree> => {
|
||||
const { authenticated, grantedRoles } = authData;
|
||||
const router = inject(Router);
|
||||
|
||||
if (authenticated) {
|
||||
const requiredRoles = route.data['roles'] as string[];
|
||||
|
||||
if (!requiredRoles || requiredRoles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasRequiredRole = requiredRoles.some(role =>
|
||||
grantedRoles.realmRoles.includes(role)
|
||||
);
|
||||
|
||||
if (hasRequiredRole) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return router.createUrlTree(['/unauthorized']);
|
||||
}
|
||||
|
||||
return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
|
||||
};
|
||||
|
||||
// Helper para crear el guardia (opcional)
|
||||
export const authGuardWithHelper = createAuthGuard(isAccessAllowed);
|
||||
```
|
||||
|
||||
### Interceptor HTTP para manejo de errores
|
||||
|
||||
```typescript
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpHandlerFn,
|
||||
HttpInterceptorFn,
|
||||
HttpErrorResponse
|
||||
} from '@angular/common/http';
|
||||
import { Observable, throwError, from, switchMap } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import Keycloak from 'keycloak-js';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Este interceptor complementa al incluido en app.config.ts
|
||||
* Proporciona funcionalidad adicional para manejo de errores
|
||||
*/
|
||||
export const authInterceptor: HttpInterceptorFn = (
|
||||
request: HttpRequest<unknown>,
|
||||
next: HttpHandlerFn
|
||||
): Observable<any> => {
|
||||
const keycloak = inject(Keycloak);
|
||||
const router = inject(Router);
|
||||
|
||||
// Manejar la petición con gestión de errores de autenticación
|
||||
return next(request).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
// Manejar errores 401 Unauthorized
|
||||
if (error.status === 401) {
|
||||
console.log('Error 401, refrescando token o redirigiendo a login');
|
||||
|
||||
// Intentar refrescar el token primero
|
||||
return from(keycloak.updateToken(30)).pipe(
|
||||
switchMap(refreshed => {
|
||||
if (refreshed) {
|
||||
// Token refrescado, reintentar la petición
|
||||
return next(request);
|
||||
} else {
|
||||
// No se pudo refrescar el token, redirigir a login
|
||||
keycloak.login();
|
||||
return throwError(() => error);
|
||||
}
|
||||
}),
|
||||
catchError(refreshError => {
|
||||
console.error('Error al refrescar token:', refreshError);
|
||||
// Redirigir a login en caso de error
|
||||
router.navigate(['/login']);
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Para otros errores, simplemente pasarlos
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
## 10. Componentes de UI para login/logout
|
||||
|
||||
### Componente de login (Angular 19)
|
||||
|
||||
```typescript
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './login.component.html',
|
||||
styleUrl: './login.component.scss'
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
private authService = inject(AuthService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
|
||||
loading: boolean = false;
|
||||
returnUrl: string = '';
|
||||
|
||||
ngOnInit() {
|
||||
// Obtener el returnUrl de los query params
|
||||
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/inicio';
|
||||
|
||||
// Verificar si ya está autenticado y redirigir
|
||||
this.authService.isLoggedIn().subscribe({
|
||||
next: (isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
console.log('Usuario ya autenticado, redirigiendo a:', this.returnUrl);
|
||||
// Comprobar si la URL de retorno es válida
|
||||
const effectiveReturnUrl = this.returnUrl === '/' ? '/inicio' : this.returnUrl;
|
||||
// Redirigir
|
||||
this.router.navigate([effectiveReturnUrl], { replaceUrl: true });
|
||||
}
|
||||
},
|
||||
error: (error) => console.error('Error al verificar autenticación:', error)
|
||||
});
|
||||
}
|
||||
|
||||
async onLogin() {
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
// Comprobar si la URL de retorno es válida
|
||||
const effectiveReturnUrl = this.returnUrl === '/' ? '/inicio' : this.returnUrl;
|
||||
|
||||
// Construir la redirectUri
|
||||
const redirectUri = window.location.origin + effectiveReturnUrl;
|
||||
console.log('Iniciando login con redirectUri:', redirectUri);
|
||||
|
||||
// Iniciar el flujo de autenticación
|
||||
await this.authService.login(redirectUri);
|
||||
// Keycloak se encargará de la redirección
|
||||
} catch (error) {
|
||||
console.error('Error al iniciar sesión:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Plantilla HTML para el componente de login
|
||||
|
||||
```html
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h2>Iniciar Sesión</h2>
|
||||
<p>Por favor, inicia sesión para acceder al sistema.</p>
|
||||
|
||||
<button
|
||||
[disabled]="loading"
|
||||
(click)="onLogin()"
|
||||
class="login-button">
|
||||
{{ loading ? 'Cargando...' : 'Iniciar Sesión con Keycloak' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Componente principal con información de usuario (común para ambos enfoques)
|
||||
|
||||
Edita `src/app/app.component.ts`:
|
||||
|
||||
```typescript
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
import { KeycloakProfile } from 'keycloak-js';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
title = 'angular-keycloak-app';
|
||||
isLoggedIn = false;
|
||||
userProfile: KeycloakProfile | null = null;
|
||||
userRoles: string[] = [];
|
||||
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isLoggedIn = await this.authService.isLoggedIn();
|
||||
|
||||
if (this.isLoggedIn) {
|
||||
this.userProfile = await this.authService.getLoggedUser();
|
||||
this.userRoles = this.authService.getRoles();
|
||||
}
|
||||
}
|
||||
|
||||
login() {
|
||||
this.authService.login();
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authService.logout();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Edita `src/app/app.component.html`:
|
||||
|
||||
```html
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Demo Keycloak con LDAP - Correos.com</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div *ngIf="isLoggedIn; else loginButton">
|
||||
<h3>Bienvenido, {{ userProfile?.firstName }} {{ userProfile?.lastName }}</h3>
|
||||
<p>Email: {{ userProfile?.email }}</p>
|
||||
<p>Nombre de Usuario: {{ userProfile?.username }}</p>
|
||||
|
||||
<h4>Tus Roles:</h4>
|
||||
<ul>
|
||||
<li *ngFor="let role of userRoles">{{ role }}</li>
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-danger" (click)="logout()">Cerrar Sesión</button>
|
||||
</div>
|
||||
|
||||
<ng-template #loginButton>
|
||||
<p>Por favor inicia sesión para acceder al sistema</p>
|
||||
<button class="btn btn-primary" (click)="login()">Iniciar Sesión</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## 11. Arquitectura del sistema
|
||||
|
||||
@ -1523,8 +779,6 @@ El flujo de autenticación funciona de la siguiente manera:
|
||||
6. Los interceptores HTTP añaden automáticamente el token a las peticiones API
|
||||
7. Las rutas protegidas verifican los roles del usuario antes de permitir el acceso
|
||||
|
||||

|
||||

|
||||
|
||||
## 12. Consideraciones de seguridad
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user