diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index 0dd0f6c..def1694 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -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' }
-];
+];
\ No newline at end of file
diff --git a/src/app/components/sidebar/sidebar.component.html b/src/app/components/sidebar/sidebar.component.html
index 8811b3e..16f18a6 100644
--- a/src/app/components/sidebar/sidebar.component.html
+++ b/src/app/components/sidebar/sidebar.component.html
@@ -7,12 +7,9 @@
1.0
-
-
-
\ No newline at end of file
diff --git a/src/app/components/sidebar/sidebar.component.scss b/src/app/components/sidebar/sidebar.component.scss
index cd8a21a..6e33a5b 100644
--- a/src/app/components/sidebar/sidebar.component.scss
+++ b/src/app/components/sidebar/sidebar.component.scss
@@ -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);
}
\ No newline at end of file
diff --git a/src/app/components/sidebar/sidebar.component.ts b/src/app/components/sidebar/sidebar.component.ts
index de38495..fa49e7a 100644
--- a/src/app/components/sidebar/sidebar.component.ts
+++ b/src/app/components/sidebar/sidebar.component.ts
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/app/guards/access.guard.ts b/src/app/guards/access.guard.ts
new file mode 100644
index 0000000..170e7c1
--- /dev/null
+++ b/src/app/guards/access.guard.ts
@@ -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 || [];
+ const requiredGroups = route.data['groups'] as Array || [];
+ 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']);
+};
\ No newline at end of file
diff --git a/src/app/guards/admin.guard.ts b/src/app/guards/admin.guard.ts
new file mode 100644
index 0000000..4d30112
--- /dev/null
+++ b/src/app/guards/admin.guard.ts
@@ -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']);
+};
\ No newline at end of file
diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts
index 9843946..574b71d 100644
--- a/src/app/guards/auth.guard.ts
+++ b/src/app/guards/auth.guard.ts
@@ -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 }
});
};
\ No newline at end of file
diff --git a/src/app/guards/group.guard.ts b/src/app/guards/group.guard.ts
new file mode 100644
index 0000000..2439cbe
--- /dev/null
+++ b/src/app/guards/group.guard.ts
@@ -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;
+
+ // 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']);
+};
\ No newline at end of file
diff --git a/src/app/guards/index.ts b/src/app/guards/index.ts
new file mode 100644
index 0000000..2dc5142
--- /dev/null
+++ b/src/app/guards/index.ts
@@ -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';
\ No newline at end of file
diff --git a/src/app/guards/role.guard.ts b/src/app/guards/role.guard.ts
new file mode 100644
index 0000000..a11a4ca
--- /dev/null
+++ b/src/app/guards/role.guard.ts
@@ -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;
+
+ // 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']);
+};
\ No newline at end of file
diff --git a/src/app/pages/access-denied/access-denied.component.html b/src/app/pages/access-denied/access-denied.component.html
new file mode 100644
index 0000000..c9479ef
--- /dev/null
+++ b/src/app/pages/access-denied/access-denied.component.html
@@ -0,0 +1,12 @@
+
+
+
+
Acceso Denegado
+
No tienes los permisos necesarios para acceder a esta página.
+
Tu rol actual no te permite ver este contenido.
+
+
+ Volver al inicio
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/access-denied/access-denied.component.scss b/src/app/pages/access-denied/access-denied.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/pages/access-denied/access-denied.component.spec.ts b/src/app/pages/access-denied/access-denied.component.spec.ts
new file mode 100644
index 0000000..fc50ec6
--- /dev/null
+++ b/src/app/pages/access-denied/access-denied.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AccessDeniedComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AccessDeniedComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/access-denied/access-denied.component.ts b/src/app/pages/access-denied/access-denied.component.ts
new file mode 100644
index 0000000..2f0bdf5
--- /dev/null
+++ b/src/app/pages/access-denied/access-denied.component.ts
@@ -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']);
+ }
+}
diff --git a/src/app/pages/admin-area/admin-area.component.html b/src/app/pages/admin-area/admin-area.component.html
new file mode 100644
index 0000000..4f792b2
--- /dev/null
+++ b/src/app/pages/admin-area/admin-area.component.html
@@ -0,0 +1,15 @@
+
+
+
Área de Administrador
+
+
+ Esta página solo es accesible para usuarios con rol de administrador.
+
+
+
+
Información del usuario:
+
{{ userInfo | json }}
+
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/admin-area/admin-area.component.scss b/src/app/pages/admin-area/admin-area.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/pages/admin-area/admin-area.component.spec.ts b/src/app/pages/admin-area/admin-area.component.spec.ts
new file mode 100644
index 0000000..7d89a84
--- /dev/null
+++ b/src/app/pages/admin-area/admin-area.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [AdminAreaComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AdminAreaComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/admin-area/admin-area.component.ts b/src/app/pages/admin-area/admin-area.component.ts
new file mode 100644
index 0000000..495c53a
--- /dev/null
+++ b/src/app/pages/admin-area/admin-area.component.ts
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/app/pages/user-area/user-area.component.html b/src/app/pages/user-area/user-area.component.html
new file mode 100644
index 0000000..8fa6c02
--- /dev/null
+++ b/src/app/pages/user-area/user-area.component.html
@@ -0,0 +1,14 @@
+
+
+
Área de Usuario
+
+
+ Esta página es accesible para usuarios con roles estándar.
+
+
+
+
Información del usuario:
+
{{ userInfo | json }}
+
+
+
\ No newline at end of file
diff --git a/src/app/pages/user-area/user-area.component.scss b/src/app/pages/user-area/user-area.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/pages/user-area/user-area.component.spec.ts b/src/app/pages/user-area/user-area.component.spec.ts
new file mode 100644
index 0000000..859eaba
--- /dev/null
+++ b/src/app/pages/user-area/user-area.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [UserAreaComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(UserAreaComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/pages/user-area/user-area.component.ts b/src/app/pages/user-area/user-area.component.ts
new file mode 100644
index 0000000..6646e96
--- /dev/null
+++ b/src/app/pages/user-area/user-area.component.ts
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/app/services/direct-auth.service.ts b/src/app/services/direct-auth.service.ts
index 76d1e3d..07ae4c3 100644
--- a/src/app/services/direct-auth.service.ts
+++ b/src/app/services/direct-auth.service.ts
@@ -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(null);
+ private userRoles = signal([]);
+ private userGroups = signal([]);
+ private userPermissions = signal([]);
// 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(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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ if (!this.isAuthenticated()) {
+ return of(false);
+ }
+
+ return of(this.isAdmin());
+ }
+
+ // Guard para comprobar si el usuario es desarrollador
+ public isDeveloperGuard(): Observable {
+ 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 {
+ 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 {
+ 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;
+ }
}
\ No newline at end of file
diff --git a/tutorial-keycloak-completo.md b/tutorial-keycloak-completo.md
index 93c0422..a0af57f 100644
--- a/tutorial-keycloak-completo.md
+++ b/tutorial-keycloak-completo.md
@@ -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
-
-
-
-
-
-```
-### 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 {
- return this.keycloak.loadUserProfile();
- }
-
- public login(): void {
- this.keycloak.login();
- }
-
- public logout(): void {
- this.keycloak.logout();
- }
-
- public isLoggedIn(): Promise {
- 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 {
-
- // 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({
- urlPattern: /^(http:\/\/localhost)(\/.*)?$/i, // URLs que comienzan con http://localhost
- bearerPrefix: 'Bearer'
-});
-
-// Condición para APIs
-const apiCondition = createInterceptorCondition({
- 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(null);
- public user$ = this.userSubject.asObservable();
-
- // Estado de autenticación como signal
- public isAuthenticated = signal(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 {
- 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 {
- 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 {
- return this.keycloak.login({
- redirectUri: redirectUri || window.location.origin
- });
- }
-
- logout(): Promise {
- return this.keycloak.logout({
- redirectUri: window.location.origin
- });
- }
-
- isLoggedIn(): Observable {
- try {
- return from(Promise.resolve(this.keycloak.authenticated || false));
- } catch (error) {
- console.error('Error checking authentication:', error);
- return from(Promise.resolve(false));
- }
- }
-
- getToken(): Promise {
- try {
- return Promise.resolve(this.keycloak.token || '');
- } catch (error) {
- console.error('Error getting token:', error);
- return Promise.resolve('');
- }
- }
-
- async updateToken(minValidity = 30): Promise {
- 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 => {
- 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 => {
- 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,
- next: HttpHandlerFn
-): Observable => {
- 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
-
-
-
Iniciar Sesión
-
Por favor, inicia sesión para acceder al sistema.
-
-
- {{ loading ? 'Cargando...' : 'Iniciar Sesión con Keycloak' }}
-
-
-
-```
-
-### 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
-
-
-
-
-
-
-
-
Bienvenido, {{ userProfile?.firstName }} {{ userProfile?.lastName }}
-
Email: {{ userProfile?.email }}
-
Nombre de Usuario: {{ userProfile?.username }}
-
-
Tus Roles:
-
-
-
Cerrar Sesión
-
-
-
- Por favor inicia sesión para acceder al sistema
- Iniciar Sesión
-
-
-
-
-
-
-```
## 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