keycloack direct funcionando

se implemente el direct access grant para probar el post login
This commit is contained in:
luis cespedes 2025-05-19 14:18:31 -04:00
parent 1dd5f1644f
commit 8a1434e553
14 changed files with 849 additions and 764 deletions

11
.env.template Normal file
View File

@ -0,0 +1,11 @@
# Keycloak Configuration
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=angular-app
KEYCLOAK_CLIENT_ID=angular-app
KEYCLOAK_CLIENT_SECRET=your-client-secret
# API Configuration
API_BASE_URL=/api
# Environment
NODE_ENV=development

387
README.md
View File

@ -1,14 +1,22 @@
# Cronogramas PrimeNG Application
# SACG - Sistema Administrador de Cronogramas
Este proyecto es una aplicación Angular 19 utilizando PrimeNG para la gestión de cronogramas de concesiones.
Aplicación Angular 19 con PrimeNG para la gestión de cronogramas de concesiones, utilizando autenticación directa con Keycloak.
## Características Principales
- Autenticación DirectAuth con Keycloak (implementación moderna sin usar keycloak-angular)
- Componentes Standalone Angular 19
- Interfaz de usuario con PrimeNG
- Gestión de cronogramas y concesiones
- Exportación de datos a PDF y Excel
## Instalación
### Requisitos Previos
- Node.js (versión 20.x recomendada)
- Node.js (versión 20.x o superior)
- npm (incluido con Node.js)
- Git
- Keycloak Server (para autenticación)
### Pasos de Instalación
@ -16,18 +24,40 @@ Este proyecto es una aplicación Angular 19 utilizando PrimeNG para la gestión
```bash
git clone <URL-del-repositorio>
cd cronogramas-primeng
cd sacg-cronogramas
```
2. **Instalar Dependencias**
2. **Configurar Variables de Entorno**
Copie el archivo de plantilla de entorno y configúrelo para su entorno:
```bash
cp .env.template .env
```
Edite el archivo `.env` con la configuración de su entorno de Keycloak:
```env
# Keycloak Configuration
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=angular-app
KEYCLOAK_CLIENT_ID=angular-app
KEYCLOAK_CLIENT_SECRET=your-client-secret
# API Configuration
API_BASE_URL=/api
# Environment
NODE_ENV=development
```
3. **Instalar Dependencias**
```bash
npm install
```
> **Importante**: Durante la instalación, se ejecutará automáticamente el script `setup-project.js` (definido como `postinstall` en package.json) que solicitará el nombre del nuevo proyecto para personalizar la configuración.
3. **Iniciar el Servidor de Desarrollo**
4. **Iniciar el Servidor de Desarrollo**
```bash
npm start
@ -35,284 +65,127 @@ Este proyecto es una aplicación Angular 19 utilizando PrimeNG para la gestión
La aplicación estará disponible en `http://localhost:4200/`
## Dependencias Principales
## Configuración de Keycloak
El proyecto utiliza las siguientes bibliotecas clave:
### Configuración del Servidor
- **Angular 19**: Framework base
```bash
npm install @angular/core @angular/common @angular/forms @angular/router @angular/compiler
```
1. **Instalar y Ejecutar Keycloak**:
- Descargue Keycloak desde [keycloak.org](https://www.keycloak.org/downloads)
- Inicie el servidor: `bin/kc.[sh|bat] start-dev`
- Acceda a la consola de administración: `http://localhost:8080/admin`
- **PrimeNG 19.1.0**: Biblioteca de componentes UI
```bash
npm install primeng @primeng/themes
```
2. **Crear un Realm**:
- Cree un nuevo realm llamado `angular-app`
- Este nombre debe coincidir con su variable `KEYCLOAK_REALM` en `.env`
- **PrimeFlex 4.0.0**: Sistema de CSS flexible
```bash
npm install primeflex
```
3. **Crear un Cliente**:
- Cree un nuevo cliente con ID `angular-app` (debe coincidir con `KEYCLOAK_CLIENT_ID`)
- Configure:
- Access Type: `confidential` o `public` (según su caso)
- Valid Redirect URIs: `http://localhost:4200/*`
- Web Origins: `http://localhost:4200` (o agregar `+` para permitir cualquier origen)
- **PrimeIcons 7.0.0**: Conjunto de iconos
```bash
npm install primeicons
```
4. **Configurar Usuarios y Roles**:
- Cree usuarios para pruebas
- Configure roles como `admin`, `user`, etc.
- Asigne roles a los usuarios
- **PDFMake 0.2.19**: Para generación de PDFs
```bash
npm install pdfmake @types/pdfmake
```
### Integración con la Aplicación
- **ExcelJS 4.4.0**: Para exportación a Excel
```bash
npm install exceljs
```
La aplicación utiliza una implementación moderna de autenticación directa con Keycloak, sin depender del paquete keycloak-angular que está deprecado. En su lugar:
- **File-Saver 2.5.0**: Para guardar archivos en el cliente
```bash
npm install file-saver @types/file-saver
```
- Utiliza el protocolo OpenID Connect para autenticación directa
- Gestiona tokens manualmente para mayor control
- Implementa renovación automática de tokens
- Provee detección de inactividad de usuario
- **Animate.css 4.1.1**: Para animaciones CSS
```bash
npm install animate.css
```
## Entornos de Ejecución
## Configuración del Proyecto
Los archivos de entorno están configurados en `src/environments/`:
### Script de Configuración (setup-project.js)
- **environment.ts**: Configuración para desarrollo local
- **environment.prod.ts**: Configuración para producción
El proyecto incluye un script de configuración post-instalación (`setup-project.js`) que se ejecuta automáticamente después de `npm install`. Este script realiza las siguientes tareas:
Las variables de entorno relacionadas con Keycloak se definen en estos archivos y se cargan desde los valores configurados en `.env`.
1. Solicita el nombre del nuevo proyecto
2. Actualiza package.json con el nuevo nombre
3. Modifica angular.json para actualizar todas las referencias al nombre del proyecto
4. Actualiza el título en src/index.html
## Flujo de Autenticación
Este script facilita la reutilización del template para crear nuevos proyectos basados en esta estructura.
1. **Login**:
- Usuario ingresa credenciales en la pantalla de login
- La aplicación realiza una solicitud directa al endpoint de token de Keycloak
- Después de una autenticación exitosa, se almacena el token JWT
### Configuración de Estilos en angular.json
2. **Manejo de Tokens**:
- El token se almacena en localStorage para persistencia entre sesiones
- Se configura un temporizador para renovación automática del token
- Se decodifica el token para extraer información del usuario
El proyecto está configurado para utilizar varios estilos externos a través del archivo `angular.json`:
3. **Protección de Rutas**:
- Las rutas protegidas utilizan el guardia `authGuard`
- La verificación se basa en la validez del token almacenado
```json
"styles": [
"src/styles.scss",
{
"input": "node_modules/animate.css/animate.min.css",
"bundleName": "animate",
"inject": true
},
{
"input": "node_modules/primeflex/primeflex.css",
"bundleName": "primeflex",
"inject": true
},
{
"input": "node_modules/primeicons/primeicons.css",
"bundleName": "primeicons",
"inject": true
}
]
```
Para añadir nuevos estilos externos, sigue el mismo patrón en el archivo angular.json.
## Estructura del Proyecto
```
/cronogramas-primeng/
├── src/ # Código fuente
│ ├── app/ # Componentes Angular
│ │ ├── components/ # Componentes compartidos
│ │ │ ├── alert-dialog/ # Diálogo de alertas
│ │ │ ├── footer/ # Pie de página
│ │ │ ├── layout/ # Estructura principal
│ │ │ ├── navbar/ # Barra de navegación
│ │ │ ├── sidebar/ # Barra lateral
│ │ │ └── visor-pdf/ # Visualizador de PDF
│ │ ├── guards/ # Guardias de ruta
│ │ ├── interceptors/ # Interceptores HTTP
│ │ ├── models/ # Interfaces de datos
│ │ ├── pages/ # Componentes de página
│ │ ├── services/ # Servicios
│ │ └── utils/ # Utilidades
│ ├── pipes/ # Pipes personalizados
│ ├── index.html # HTML principal
│ ├── main.ts # Punto de entrada
│ └── styles.scss # Estilos globales
├── public/ # Recursos estáticos
├── angular.json # Configuración de Angular
├── package.json # Dependencias y scripts
├── setup-project.js # Script de configuración post-instalación
├── tsconfig.json # Configuración TypeScript
└── sonar-project.properties # Configuración para SonarQube
```
## Modelos de Datos (Interfaces)
El proyecto define las siguientes interfaces para los modelos de datos:
- **Cronograma**: Modelo base para todos los cronogramas
- **Empresa**: Modelo para empresas
- **TipoCarga**: Modelo para tipos de carga
- **EstadoAprobacion**: Modelo para estados de aprobación
- **ActualizacionPd**: Modelo para actualizaciones de Plan de Desarrollo
- **AjustePd**: Modelo para ajustes de Plan de Desarrollo
- **UnidadInformacion**: Modelo para unidades de información
## Servicios
Los servicios implementados permiten conectarse a un backend mediante HTTP:
- **CronogramaService**: CRUD para cronogramas
- **EmpresaService**: CRUD para empresas
- **ActualizacionPdService**: CRUD para actualizaciones de PD
- **AjustePdService**: CRUD para ajustes de PD
- **UnidadInformacionService**: CRUD para unidades de información
- **TipoCargaService**: Consulta de tipos de carga
- **EstadoAprobacionService**: Consulta de estados de aprobación
- **AuthService**: Autenticación y gestión de tokens
- **PdfService**: Generación y manejo de PDFs
- **AlertService**: Gestión de alertas
## Generación de PDFs con PDFMake
El proyecto utiliza PDFMake para la generación dinámica de PDFs. La configuración básica se realiza así:
1. **Importar pdfMake en el servicio**:
```typescript
import pdfMake from 'pdfmake/build/pdfmake';
import pdfFonts from 'pdfmake/build/vfs_fonts';
pdfMake.vfs = pdfFonts.vfs;
```
2. **Definir el documento PDF** usando la estructura de pdfMake:
```typescript
const docDefinition = {
content: [
{ text: 'Título del Documento', style: 'header' },
// Contenido del documento
],
styles: {
header: {
fontSize: 18,
bold: true
}
}
};
```
3. **Generar o descargar el PDF**:
```typescript
// Generar como URL de datos
const pdfDocGenerator = pdfMake.createPdf(docDefinition);
pdfDocGenerator.getDataUrl((dataUrl) => {
// Usar dataUrl para mostrar el PDF en un iframe
});
// Descargar el PDF
pdfMake.createPdf(docDefinition).download('nombre-archivo.pdf');
```
4. **Visualizar el PDF** usando el componente `visor-pdf`:
```typescript
// En el componente que necesita mostrar el PDF
import { DialogService } from 'primeng/dynamicdialog';
import { VisorPdfComponent } from 'path/to/visor-pdf.component';
constructor(private dialogService: DialogService) {}
mostrarPDF() {
this.dialogService.open(VisorPdfComponent, {
header: 'Cronograma PDF',
width: '80%',
data: {
product: this.item // Datos para el PDF
}
});
}
```
## Seguridad
La aplicación incluye:
- **AuthInterceptor**: Interceptor HTTP para añadir tokens de autenticación a las solicitudes
- **AuthGuard**: Guard para proteger rutas que requieren autenticación
- **Login**: Componente para autenticación de usuarios
## Animaciones de Ruta
El proyecto implementa animaciones de transición entre rutas usando:
- **RouteAnimationsComponent**: Define las animaciones para las transiciones de ruta
- **Animate.css**: Proporciona clases CSS predefinidas para animaciones
## Componentes Principales
### Layout Component
Define la estructura principal de la aplicación, incluyendo:
- Barra de navegación (navbar)
- Barra lateral (sidebar)
- Contenido principal con soporte para animaciones de ruta
- Pie de página (footer)
### VisorPDF Component
Componente standalone que permite:
- Visualizar PDFs generados dinámicamente
- Descargar el PDF visualizado
- Enviar el PDF a través del sistema
4. **Interceptor HTTP**:
- Todas las solicitudes HTTP incluyen automáticamente el token de autenticación
- En caso de error 401, se intenta renovar el token automáticamente
## Comandos Disponibles
| Comando | Descripción |
|---------|-------------|
| `npm start` | Inicia servidor de desarrollo |
| `npm run build` | Compila el proyecto |
| `npm run build:prod` | Compila para producción |
| `npm run watch` | Compila en modo observador |
| `npm start` | Inicia servidor de desarrollo en http://localhost:4200 |
| `npm run build` | Compila el proyecto para desarrollo |
| `npm run build:prod` | Compila para producción con optimizaciones |
| `npm test` | Ejecuta pruebas unitarias |
## Extendiendo el Proyecto
## Estructura del Proyecto
Para extender este proyecto como base para un nuevo desarrollo:
```
/sacg-cronogramas/
├── src/
│ ├── app/
│ │ ├── components/ # Componentes compartidos
│ │ ├── guards/ # Guardias de ruta (authGuard)
│ │ ├── interceptors/ # Interceptores HTTP (authInterceptor)
│ │ ├── models/ # Interfaces de datos
│ │ ├── pages/ # Componentes de página
│ │ ├── services/
│ │ │ ├── direct-auth.service.ts # Implementación DirectAuth
│ │ │ └── ... # Otros servicios
│ │ └── utils/ # Utilidades
│ ├── environments/ # Configuración de entornos
│ │ ├── environment.ts # Desarrollo
│ │ └── environment.prod.ts # Producción
│ ├── pipes/ # Pipes personalizados
│ ├── index.html # HTML principal
│ ├── main.ts # Punto de entrada
│ └── styles.scss # Estilos globales
├── public/ # Recursos estáticos
│ └── keycloak.json # Configuración de Keycloak
├── .env.template # Plantilla para variables de entorno
├── angular.json # Configuración de Angular
├── package.json # Dependencias y scripts
└── README.md # Esta documentación
```
1. Clona el repositorio
2. Ejecuta `npm install` (esto iniciará automáticamente el script setup-project.js)
3. Proporciona el nombre del nuevo proyecto cuando se te solicite
4. El script actualizará automáticamente la configuración con el nuevo nombre
5. Comienza a desarrollar tu aplicación personalizada
## Solución de Problemas
## Buenas Prácticas
### Problemas Comunes de Autenticación
1. **Organización de Componentes**:
- Componentes de página en la carpeta `pages/`
- Componentes reutilizables en `components/`
1. **No se puede iniciar sesión**:
- Verifique que las credenciales sean correctas
- Asegúrese de que el servidor Keycloak esté ejecutándose
- Compruebe que el realm y client ID en `.env` coincidan con su configuración de Keycloak
2. **Modelo de Datos**:
- Definir interfaces para todos los modelos en `models/`
- Exportar todas las interfaces desde `models/index.ts`
2. **Token expirado o inválido**:
- La aplicación debería renovar automáticamente el token
- Si persiste, intente cerrar sesión y volver a iniciar sesión
3. **Servicios**:
- Mantener la responsabilidad única para cada servicio
- Centralizar la lógica de negocio en los servicios
4. **Gestión de Estado**:
- Utilizar servicios para compartir estado entre componentes
3. **Redirecciones en bucle**:
- Limpie el localStorage del navegador
- Verifique la configuración de URLs en el cliente Keycloak
## Recursos Adicionales
- [Documentación de Angular](https://angular.dev/)
- [Documentación de PrimeNG](https://primeng.org/installation)
- [Documentación de PrimeFlex](https://primeflex.org/)
- [Documentación de PDFMake](http://pdfmake.org/)
- [Documentación de ExcelJS](https://github.com/exceljs/exceljs)
- [Documentación oficial de Keycloak](https://www.keycloak.org/documentation)
- [Angular Security Best Practices](https://angular.io/guide/security)
- [Tutorial completo de Keycloak](tutorial-keycloak-completo.md)

View File

@ -1,6 +1,6 @@
{
"realm": "aangular-app",
"auth-server-url": "http://localhost:8080/",
"realm": "angular-app",
"auth-server-url": "http://192.168.1.27:8080/",
"resource": "angular-app",
"credentials": {
"secret": "zYbODELDmLjK9c9gHNbTUe8mSZlcLFZm"

View File

@ -1,65 +1,30 @@
import { Component, inject, OnInit, OnDestroy, effect, Signal } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';
import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { ConfirmationService } from 'primeng/api';
import { ConfirmationService, MessageService } from 'primeng/api';
import { filter, Subscription } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import Keycloak from 'keycloak-js';
import { AuthService } from './services/auth.service';
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType, KeycloakEvent } from 'keycloak-angular';
import { ToastModule } from 'primeng/toast';
import { DirectAuthService } from './services/direct-auth.service';
@Component({
selector: 'app-root',
imports: [RouterOutlet, ConfirmDialogModule],
imports: [RouterOutlet, ConfirmDialogModule, ToastModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
providers: [ConfirmationService],
providers: [ConfirmationService, MessageService],
standalone: true,
})
export class AppComponent implements OnInit, OnDestroy {
export class AppComponent implements OnInit {
title = 'SACG - Sistema Administrador de Cronogramas';
private keycloak = inject(Keycloak);
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
private authService = inject(AuthService);
private authService = inject(DirectAuthService);
private router = inject(Router);
private subscriptions: Subscription[] = [];
constructor() {
// Using effect to handle Keycloak events through Angular's Signal API
effect(() => {
const event = this.keycloakEvents();
if (!event) return;
console.log('Keycloak event received:', event.type);
// Authentication success handling
if (event.type === KeycloakEventType.AuthSuccess) {
console.log('Authentication successful');
// We'll let the guards and login component handle redirections
// No redirect here to avoid conflicts
}
// Authentication error handling
if (event.type === KeycloakEventType.AuthError) {
console.error('Authentication error');
// Only redirect to login if not already there
if (this.router.url !== '/login') {
this.router.navigate(['/login']);
}
}
// Logout handling
if (event.type === KeycloakEventType.AuthLogout) {
console.log('Logged out');
// Only redirect to login if not already there
if (this.router.url !== '/login') {
this.router.navigate(['/login']);
}
}
});
// Ya no es necesario el efecto keycloak
}
ngOnInit() {
// Subscribe to navigation events - using standard unsubscribe pattern
this.subscriptions.push(
@ -71,7 +36,7 @@ export class AppComponent implements OnInit, OnDestroy {
console.log('Navigation completed to:', this.router.url);
})
);
// Check authentication status on load
this.checkAuthenticationStatus();
}
@ -83,20 +48,20 @@ export class AppComponent implements OnInit, OnDestroy {
private async checkAuthenticationStatus(): Promise<void> {
try {
const isLoggedIn = await this.keycloak.authenticated;
const isLoggedIn = this.authService.isAuthenticated();
console.log('Initial authentication status:', isLoggedIn ? 'Authenticated' : 'Not authenticated');
// Let the guards handle the protected routes
// Only do minimal checks here to avoid redirect loops
// If the user is on login page but already authenticated, send to home
if (isLoggedIn && this.router.url === '/login') {
console.log('Already authenticated, redirecting from login to home');
this.router.navigate(['/inicio']);
return;
}
// If not authenticated and on a protected route, go to Keycloak login
// If not authenticated and on a protected route, go to login
if (!isLoggedIn && this.router.url !== '/login') {
// We'll let the auth guard handle this
console.log('Not authenticated on protected route');
@ -105,4 +70,4 @@ export class AppComponent implements OnInit, OnDestroy {
console.error('Error checking authentication status:', error);
}
}
}
}

View File

@ -6,25 +6,8 @@ import { provideAnimations } from '@angular/platform-browser/animations';
import { providePrimeNG } from 'primeng/config';
import Aura from '@primeng/themes/aura';
import { MessageService } from 'primeng/api';
import {
provideKeycloak,
createInterceptorCondition,
IncludeBearerTokenCondition,
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
includeBearerTokenInterceptor
} from 'keycloak-angular';
// Define condition for including the token in requests
const localhostCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: /^(http:\/\/localhost)(\/.*)?$/i, // Match URLs starting with http://localhost
bearerPrefix: 'Bearer'
});
// Define another condition for API URLs
const apiCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
urlPattern: /^(\/api)(\/.*)?$/i, // Match URLs starting with /api
bearerPrefix: 'Bearer'
});
import { authInterceptor } from './interceptors/auth.interceptor';
import { environment } from '../environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
@ -34,11 +17,13 @@ export const appConfig: ApplicationConfig = {
withPreloading(PreloadAllModules)
),
provideAnimations(),
// Use the Keycloak interceptor to attach authentication tokens
// Usamos nuestro interceptor personalizado para DirectAuthService
provideHttpClient(
withFetch(),
withInterceptors([includeBearerTokenInterceptor])
withInterceptors([authInterceptor])
),
providePrimeNG({
theme: {
preset: Aura,
@ -47,27 +32,7 @@ export const appConfig: ApplicationConfig = {
}
}
}),
MessageService,
// Provide Keycloak with initOptions - this automatically creates an APP_INITIALIZER
provideKeycloak({
config: {
url: 'http://localhost:8080',
realm: 'angular-app',
clientId: 'angular-app',
},
initOptions: {
onLoad: 'check-sso', // Cambiado de check-sso a login-required para forzar login
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
checkLoginIframe: false,
pkceMethod: 'S256',
enableLogging: true
},
providers: [
{
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
useValue: [localhostCondition, apiCondition]
}
]
})
MessageService
]
};

View File

@ -7,7 +7,7 @@ import { filter, map, mergeMap } from 'rxjs/operators';
import { CommonModule } from '@angular/common';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { ToastModule } from 'primeng/toast';
import { AuthService } from '../../services/auth.service';
import { DirectAuthService } from '../../services/direct-auth.service';
@Component({
selector: 'app-navbar',
@ -33,7 +33,7 @@ export class NavbarComponent implements OnInit {
private messageService: MessageService,
private router: Router,
private activatedRoute: ActivatedRoute,
private authService: AuthService
private authService: DirectAuthService
) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
@ -52,13 +52,12 @@ export class NavbarComponent implements OnInit {
ngOnInit() {
// Obtener nombre de usuario del usuario logueado
this.authService.user$.subscribe(user => {
if (user) {
this.userName = user.name || user.username || 'Usuario';
} else {
this.userName = 'Usuario';
}
});
const user = this.authService.getCurrentUser();
if (user) {
this.userName = user.name || user.username || 'Usuario';
} else {
this.userName = 'Usuario';
}
}
toggleSidebar() {
@ -92,4 +91,4 @@ export class NavbarComponent implements OnInit {
});
this.router.navigate(['/login']);
}
}
}

View File

@ -1,57 +1,23 @@
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';
import { DirectAuthService } from '../services/direct-auth.service';
// Simple implementation for authentication guard
export const authGuard: CanActivateFn = async (
// Simple implementation for authentication guard using DirectAuthService
export const authGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean | UrlTree> => {
const keycloak = inject(Keycloak);
): boolean | UrlTree => {
const authService = inject(DirectAuthService);
const router = inject(Router);
try {
// Check if user is authenticated
const authenticated = await keycloak.authenticated;
if (authenticated) {
console.log('User is authenticated, allowing access to protected route');
return true;
}
// If not authenticated, redirect to login
console.log('User not authenticated, redirecting to login page');
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url !== '/' ? state.url : '/inicio' }
});
} catch (error) {
console.error('Error checking authentication:', error);
// Fallback to login on error
return router.createUrlTree(['/login']);
}
};
// Alternative implementation using the helper function from keycloak-angular
const isAccessAllowed = async (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
authData: AuthGuardData
): Promise<boolean | UrlTree> => {
const { authenticated } = authData;
if (authenticated) {
// Check if user is authenticated
if (authService.isAuthenticated()) {
return true;
}
// Store the URL the user was trying to access
const returnUrl = state.url;
const router = inject(Router);
// Redirect to login page with return URL
return router.createUrlTree(['/login'], { queryParams: { returnUrl } });
};
// Optional: Use the createAuthGuard helper if needed
export const authGuardWithHelper = createAuthGuard(isAccessAllowed);
// If not authenticated, redirect to login
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url !== '/' ? state.url : '/inicio' }
});
};

View File

@ -1,57 +1,53 @@
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest, HttpErrorResponse } from '@angular/common/http';
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';
import { catchError, switchMap, throwError } from 'rxjs';
import { DirectAuthService } from '../services/direct-auth.service';
import { environment } from '../../environments/environment';
/**
* Note: This interceptor is not strictly necessary when using keycloak-angular's
* built-in includeBearerTokenInterceptor, which is configured in app.config.ts.
* It's included here to provide additional error handling functionality.
*/
export const authInterceptor: HttpInterceptorFn = (
request: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<any> => {
const keycloak = inject(Keycloak);
const router = inject(Router);
export const authInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
const authService = inject(DirectAuthService);
// Handle the request with error handling for auth issues
return next(request).pipe(
catchError((error: HttpErrorResponse) => {
// Handle 401 Unauthorized errors
if (error.status === 401) {
console.log('401 Unauthorized error, refreshing token or redirecting to login');
// Try to refresh the token first
return from(keycloak.updateToken(30)).pipe(
switchMap(refreshed => {
if (refreshed) {
// Token was refreshed, retry the request
return next(request);
} else {
// Token couldn't be refreshed, redirect to login
keycloak.login();
return throwError(() => error);
}
}),
catchError(refreshError => {
console.error('Error refreshing token:', refreshError);
// Redirect to login in case of refresh error
router.navigate(['/login']);
return throwError(() => error);
})
);
// No interceptar peticiones al endpoint de token (evitar bucles)
if (req.url.includes('/protocol/openid-connect/token')) {
return next(req);
}
// Agregar token de autenticación si está disponible
const token = authService.getToken();
if (token) {
req = addToken(req, token);
}
return next(req).pipe(
catchError(error => {
if (error instanceof HttpErrorResponse && error.status === 401) {
// Si es error 401, intentar refrescar token
return handle401Error(req, next, authService);
}
// For other errors, just pass them through
return throwError(() => error);
})
);
};
};
// Función para agregar token a la petición
function addToken(request: HttpRequest<unknown>, token: string): HttpRequest<unknown> {
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
// Función para manejar errores 401 (token expirado)
function handle401Error(req: HttpRequest<unknown>, next: HttpHandlerFn, authService: DirectAuthService) {
return authService.refreshToken().pipe(
switchMap((token: any) => {
return next(addToken(req, token.access_token));
}),
catchError((err) => {
// Si falla la renovación, forzar cierre de sesión
authService.logout();
return throwError(() => err);
})
);
}

View File

@ -10,54 +10,102 @@
</div>
</div>
</div>
<!-- Toast para mensajes -->
<p-toast></p-toast>
<!-- MAIN CONTENT -->
<main class="main-content flex align-items-center justify-content-center">
<div class="login-container">
<div class="login-box">
<!-- Contenedor principal con posición relativa -->
<div class="position-relative overflow-hidden">
<!-- PANEL DE LOGIN CON KEYCLOAK -->
<div class="panel-container w-full animate__animated animate__fadeIn">
<!-- PANEL DE LOGIN -->
<div class="panel-container w-full"
[ngClass]="{'animate__animated animate__fadeOut d-none': showRecovery(),
'animate__animated animate__fadeIn': !showRecovery() && !isInitialLoad()}">
<div class="login-card shadow-2 border-round">
<div class="login-header">
<h2>Iniciar Sesión</h2>
</div>
<div class="p-3">
<!-- Mensaje de información -->
<div class="info-message mb-4 text-center">
<p>Haz clic en el botón para iniciar sesión con la cuenta de usuario registrada en el sistema.</p>
<form (ngSubmit)="onLogin()" class="p-3">
<!-- Email -->
<div class="field mb-3">
<input type="email" pInputText [(ngModel)]="email" name="email" placeholder="Email"
class="input-with-icon w-full" required
(input)="clearErrors()"/>
</div>
<!-- Botón para iniciar sesión con Keycloak -->
<!-- Password -->
<div class="field mb-3">
<input type="password" pInputText [(ngModel)]="password" name="password" placeholder="Password"
class="input-with-lock w-full" required
(input)="clearErrors()"/>
</div>
<!-- Mensaje de error -->
<div *ngIf="errorMessage()" class="error-message my-2">
<p-message severity="error" [text]="errorMessage() || ''"></p-message>
</div>
<!-- Botón -->
<div class="login-actions">
<button pButton type="button" (click)="onLogin()"
[label]="loading ? 'Redirigiendo...' : 'Iniciar Sesión'"
icon="pi pi-sign-in"
class="p-button-primary w-full"
[disabled]="loading">
<i *ngIf="loading" class="pi pi-spin pi-spinner mr-2"></i>
<button pButton type="submit" [label]="loading() ? 'Autenticando...' : 'Autenticar'"
class="p-button-primary w-full" [disabled]="loading() || !email || !password">
<i *ngIf="loading()" class="pi pi-spin pi-spinner mr-2"></i>
</button>
</div>
</form>
<!-- Recuperar contraseña -->
<div class="password-recovery px-3 pb-3">
<a href="#" (click)="toggleRecovery($event)">Recuperar Contraseña</a>
</div>
<!-- Información adicional (opcional) -->
<!-- Credenciales de prueba -->
<div class="test-credentials mx-3 mb-3 p-2 border-round bg-gray-100">
<p class="mb-1 font-bold">Información:</p>
<p class="mb-1">Serás redirigido al sistema de autenticación.</p>
<p>Si tienes problemas, contacta al administrador.</p>
<p class="mb-1 font-bold">Credenciales de prueba:</p>
<p class="mb-1">Email: admin&#64;example.com</p>
<p>Password: admin123</p>
</div>
</div>
</div>
<!-- PANEL DE RECUPERACIÓN DE CONTRASEÑA -->
<div class="recovery-panel"
[ngClass]="{'animate__animated animate__fadeIn': showRecovery(),
'animate__animated animate__fadeOut d-none': !showRecovery()}">
<div class="login-card shadow-2 border-round">
<div class="login-header">
<h2>Recuperar Contraseña</h2>
</div>
<form (ngSubmit)="onRequestPasswordRecovery()" class="p-3">
<!-- Email de recuperación -->
<div class="field mb-3">
<input type="email" pInputText [(ngModel)]="recoveryEmail" name="recoveryEmail"
placeholder="Ingresa tu email" class="input-with-icon w-full" required
(input)="clearErrors()"/>
</div>
<!-- Mensaje de error -->
<div *ngIf="errorMessage()" class="error-message my-2">
<p-message severity="error" [text]="errorMessage() || ''"></p-message>
</div>
<!-- Botones -->
<div class="login-actions">
<button pButton type="submit"
[label]="loading() ? 'Enviando...' : 'Enviar Instrucciones'"
class="p-button-primary w-full mb-2"
[disabled]="loading() || !recoveryEmail">
<i *ngIf="loading()" class="pi pi-spin pi-spinner mr-2"></i>
</button>
<button pButton type="button"
label="Volver al Login"
class="p-button-outlined p-button-secondary w-full"
(click)="toggleRecovery($event)"
[disabled]="loading()">
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- FOOTER -->
<app-footer></app-footer>

View File

@ -1,110 +1,161 @@
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Component, OnInit, signal } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { CardModule } from 'primeng/card';
import { InputTextModule } from 'primeng/inputtext';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { DirectAuthService } from '../../services/direct-auth.service';
import { MessageService } from 'primeng/api';
// PrimeNG Components
import { ButtonModule } from 'primeng/button';
import { PasswordModule } from 'primeng/password';
import { DividerModule } from 'primeng/divider';
import { MessagesModule } from 'primeng/messages';
import { InputTextModule } from 'primeng/inputtext';
import { MessageModule } from 'primeng/message';
import { ToastModule } from 'primeng/toast';
import { MessageService } from 'primeng/api';
import { FooterComponent } from "../../components/footer/footer.component";
import { AuthService } from '../../services/auth.service';
import { take } from 'rxjs/operators';
import { firstValueFrom } from 'rxjs';
import { FooterComponent } from '../../components/footer/footer.component';
@Component({
selector: 'app-login',
standalone: true,
imports: [
CommonModule,
CardModule,
InputTextModule,
FormsModule,
ButtonModule,
PasswordModule,
DividerModule,
MessagesModule,
InputTextModule,
MessageModule,
ToastModule,
FooterComponent
],
providers: [MessageService],
templateUrl: './login.component.html',
styleUrl: './login.component.scss'
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
// Inyectar el servicio de autenticación
private authService = inject(AuthService);
private route = inject(ActivatedRoute);
private router = inject(Router);
// Variables de modelo
email: string = '';
password: string = '';
loading: boolean = false;
returnUrl: string = '';
// Estados con signals
loading = signal<boolean>(false);
errorMessage = signal<string | null>(null);
showRecovery = signal<boolean>(false);
isInitialLoad = signal<boolean>(true);
constructor(private messageService: MessageService) {}
// Credenciales para recuperación
recoveryEmail: string = '';
ngOnInit() {
// Obtener el returnUrl de los query params si existe
// URL para redirección después del login
private returnUrl: string = '/inicio';
constructor(
private authService: DirectAuthService,
private router: Router,
private route: ActivatedRoute,
private messageService: MessageService
) { }
ngOnInit(): void {
// Obtener URL de retorno de los parámetros de query
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/inicio';
// Simplificación - verificar autenticación sin promesas anidadas
this.authService.isLoggedIn().subscribe({
next: (isLoggedIn) => {
if (isLoggedIn) {
console.log('Usuario ya autenticado, redirigiendo a:', this.returnUrl);
// Verificar 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)
});
// Comprobar si ya hay sesión activa
if (this.authService.isAuthenticated()) {
this.router.navigate([this.returnUrl]);
return;
}
// Marcar que ya no es carga inicial (para animaciones)
setTimeout(() => {
this.isInitialLoad.set(false);
}, 100);
}
// Método de login con Keycloak usando nuestro servicio AuthService
async onLogin() {
this.loading = true;
/**
* Maneja el envío del formulario de login
*/
onLogin(): void {
// Validaciones básicas
if (!this.email || !this.password) {
this.errorMessage.set('Por favor ingresa email y contraseña');
return;
}
try {
// Verificar si la URL de retorno es válida
const effectiveReturnUrl = this.returnUrl === '/' ? '/inicio' : this.returnUrl;
// Construir el redirectUri
const redirectUri = window.location.origin + effectiveReturnUrl;
console.log('Iniciando login con redirectUri:', redirectUri);
// Iniciar el flujo de autenticación de Keycloak
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;
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Error al intentar iniciar sesión. Por favor, inténtelo de nuevo.'
this.loading.set(true);
this.errorMessage.set(null);
this.authService.login(this.email, this.password)
.subscribe({
next: () => {
// Mostrar mensaje de éxito
this.messageService.add({
severity: 'success',
summary: 'Bienvenido',
detail: 'Inicio de sesión exitoso',
life: 3000
});
// Redirigir al usuario
setTimeout(() => {
this.router.navigate([this.returnUrl]);
}, 300); // Add small delay to ensure token is properly stored
},
error: (error) => {
console.error('Error de autenticación:', error);
this.loading.set(false);
// Mostrar mensaje de error
this.errorMessage.set('Credenciales incorrectas. Por favor, verifica tu email y contraseña.');
// También mostrar en toast para mejor visibilidad
this.messageService.add({
severity: 'error',
summary: 'Error de autenticación',
detail: 'Credenciales incorrectas. Por favor, verifica tu email y contraseña.',
life: 5000
});
}
});
}
/**
* Alterna entre el panel de login y el de recuperación de contraseña
*/
toggleRecovery(event: Event): void {
event.preventDefault();
this.showRecovery.update(value => !value);
this.errorMessage.set(null); // Limpiar mensajes de error al cambiar de panel
}
/**
* Maneja la solicitud de recuperación de contraseña
*/
onRequestPasswordRecovery(): void {
if (!this.recoveryEmail) {
this.errorMessage.set('Por favor ingresa tu email');
return;
}
this.loading.set(true);
// Simulación de solicitud de recuperación (reemplazar con llamada real al API)
setTimeout(() => {
this.loading.set(false);
this.messageService.add({
severity: 'info',
summary: 'Solicitud enviada',
detail: 'Si el email existe en nuestro sistema, recibirás instrucciones para recuperar tu contraseña.',
life: 5000
});
// Volver al panel de login
this.showRecovery.set(false);
this.recoveryEmail = '';
}, 1500);
}
/**
* Limpia errores cuando el usuario comienza a escribir
*/
clearErrors(): void {
if (this.errorMessage()) {
this.errorMessage.set(null);
}
}
// Mantenemos los métodos de recuperación de contraseña para compatibilidad
toggleRecovery(event: Event) {
event.preventDefault();
this.messageService.add({
severity: 'info',
summary: 'Información',
detail: 'Para recuperar tu contraseña, utiliza la opción en la pantalla de login de Keycloak'
});
}
onRecoverPassword() {
this.messageService.add({
severity: 'info',
summary: 'Información',
detail: 'Esta funcionalidad ahora es manejada por Keycloak'
});
}
}

View File

@ -1,21 +1,17 @@
import { Injectable, inject, effect, signal, computed } from '@angular/core';
// This file is kept for reference only and is not used in the application.
// The application now uses DirectAuthService instead.
import { Injectable, 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';
import { MessageService } from 'primeng/api';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class AuthService {
// Inject Keycloak instance directly
private keycloak = inject(Keycloak);
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
private router = inject(Router);
private messageService = inject(MessageService);
// User state
private userSubject = new BehaviorSubject<any>(null);
public user$ = this.userSubject.asObservable();
@ -23,218 +19,51 @@ export class AuthService {
// Authentication state as a signal
public isAuthenticated = signal<boolean>(false);
// Login error state
public loginError = signal<string | null>(null);
constructor(private http: HttpClient) {
// Check initial state
this.checkInitialAuthState();
// Set up event handlers using Angular effects
effect(() => {
const event = this.keycloakEvents();
if (!event) return;
console.log('Keycloak event:', event.type);
// On successful login
if (event.type === KeycloakEventType.AuthSuccess) {
this.isAuthenticated.set(true);
this.loginError.set(null);
this.loadUserInfo();
}
// On logout
if (event.type === KeycloakEventType.AuthLogout) {
this.isAuthenticated.set(false);
this.userSubject.next(null);
this.router.navigate(['/login']);
}
// On authentication error
if (event.type === KeycloakEventType.AuthError) {
console.error('Authentication error:', event);
this.isAuthenticated.set(false);
this.userSubject.next(null);
// Mostrar mensaje de error
const errorMsg = 'Error de autenticación. Por favor, verifica tus credenciales o inténtalo más tarde.';
this.loginError.set(errorMsg);
this.messageService.add({
severity: 'error',
summary: 'Error de autenticación',
detail: errorMsg,
life: 5000
});
}
// On token expiration
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);
// Mostrar mensaje de error
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudo verificar el estado de autenticación.',
life: 5000
});
}
}
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');
// Get user roles
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);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudo cargar la información del usuario.',
life: 5000
});
}
}
constructor(
private http: HttpClient,
private router: Router,
private messageService: MessageService
) {}
async login(redirectUri?: string): Promise<void> {
try {
this.loginError.set(null);
await this.keycloak.login({
redirectUri: redirectUri || window.location.origin
});
} catch (error) {
console.error('Login error:', error);
this.loginError.set('Error al iniciar sesión. Por favor, inténtalo de nuevo.');
this.messageService.add({
severity: 'error',
summary: 'Error de inicio de sesión',
detail: 'No se pudo iniciar sesión. Por favor, inténtalo de nuevo.',
life: 5000
});
throw error;
}
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return Promise.reject('AuthService is deprecated. Use DirectAuthService instead.');
}
async logout(): Promise<void> {
try {
await this.keycloak.logout({
redirectUri: window.location.origin
});
} catch (error) {
console.error('Logout error:', error);
// Intento manual de navegar a login en caso de error
this.router.navigate(['/login']);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'Error al cerrar sesión.',
life: 5000
});
}
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return Promise.resolve();
}
isLoggedIn(): Observable<boolean> {
try {
// Usar directamente la propiedad authenticated de Keycloak
return from(Promise.resolve(this.keycloak.authenticated || false));
} catch (error) {
console.error('Error al verificar autenticación:', error);
return from(Promise.resolve(false));
}
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return from(Promise.resolve(false));
}
getToken(): Promise<string> {
try {
return Promise.resolve(this.keycloak.token || '');
} catch (error) {
console.error('Error al obtener token:', error);
return Promise.resolve('');
}
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return Promise.resolve('');
}
async updateToken(minValidity = 30): Promise<boolean> {
try {
return await this.keycloak.updateToken(minValidity);
} catch (error) {
console.error('Error refreshing token:', error);
// No redireccionar automáticamente al login, mostrar mensaje primero
this.messageService.add({
severity: 'warn',
summary: 'Sesión expirada',
detail: 'Tu sesión ha expirado. Por favor, inicia sesión nuevamente.',
life: 5000
});
// Esperar un momento para que el usuario vea el mensaje
setTimeout(() => this.login(), 2000);
return false;
}
async updateToken(): Promise<boolean> {
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return false;
}
getCurrentUser(): any {
return this.userSubject.value;
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return null;
}
// Check if user has a specific role
hasRole(role: string): boolean {
return this.keycloak.hasRealmRole(role);
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return false;
}
// Check if user has any of the specified roles
hasAnyRole(roles: string[]): boolean {
for (const role of roles) {
if (this.keycloak.hasRealmRole(role)) {
return true;
}
}
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return false;
}
}

View File

@ -0,0 +1,354 @@
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 { Router } from '@angular/router';
import { MessageService } from 'primeng/api';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class DirectAuthService {
// URLs del servidor Keycloak from environment
private keycloakUrl = environment.keycloak.url;
private realm = environment.keycloak.realm;
private clientId = environment.keycloak.clientId;
private tokenEndpoint = `${this.keycloakUrl}/realms/${this.realm}/protocol/openid-connect/token`;
// Router y MessageService
private router = inject(Router);
private messageService = inject(MessageService);
// Estado de autenticación como signal
private userInfo = signal<any>(null);
// Token y refresh token
private tokenInfo: any = null;
// Temporizador para renovación de token
private refreshTokenTimeout: any;
// Idle detection
private userActivity: any = null;
private userInactive = signal<boolean>(false);
// Percentage for inactivity timeout (90% of token lifetime)
private readonly INACTIVITY_PERCENTAGE = 0.9;
constructor(private http: HttpClient) {
// Intentar cargar el token del almacenamiento local al iniciar
this.loadTokenFromStorage();
// Iniciar monitoreo de actividad
this.setupActivityMonitoring();
}
// Cargar token del almacenamiento local
private loadTokenFromStorage(): void {
const tokenInfo = localStorage.getItem('keycloak_token');
if (tokenInfo) {
try {
this.tokenInfo = JSON.parse(tokenInfo);
// Verificar si el token ha expirado
if (this.isTokenExpired()) {
// Si tiene refresh token, intentar renovar
if (this.tokenInfo.refresh_token) {
this.refreshToken().subscribe();
} else {
this.logout();
}
} else {
// Decodificar info del usuario desde el token
this.setUserFromToken(this.tokenInfo.access_token);
// Configurar temporizador para renovación de token
this.startRefreshTokenTimer();
}
} catch (e) {
console.error('Error al cargar token:', e);
this.logout();
}
}
}
// Login directo con credenciales
public login(username: string, password: string): Observable<any> {
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
const params = new HttpParams()
.set('client_id', this.clientId)
.set('grant_type', 'password')
.set('username', username)
.set('password', password);
return this.http.post<any>(this.tokenEndpoint, params.toString(), { headers })
.pipe(
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);
// Configurar temporizador para renovación de token
this.startRefreshTokenTimer();
// Reiniciar detección de inactividad
this.resetInactivity();
}),
catchError(error => {
console.error('Error de autenticación:', error);
return throwError(() => new Error('Credenciales incorrectas o error de servidor'));
})
);
}
// Cerrar sesión
public logout(): void {
// Detener temporizador de renovación
this.stopRefreshTokenTimer();
// Detener monitoreo de actividad
this.stopActivityMonitoring();
// Limpiar datos de sesión
localStorage.removeItem('keycloak_token');
this.tokenInfo = null;
this.userInfo.set(null);
// Redirigir a la página de login
this.router.navigate(['/login']);
}
// Renovar token usando refresh token
public refreshToken(): Observable<any> {
if (!this.tokenInfo?.refresh_token) {
return throwError(() => new Error('No hay refresh token disponible'));
}
// No refrescar token si el usuario está inactivo
if (this.userInactive()) {
console.log('Usuario inactivo, no se renovará el token');
this.logout();
return throwError(() => new Error('Usuario inactivo'));
}
const headers = new HttpHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
});
const params = new HttpParams()
.set('client_id', this.clientId)
.set('grant_type', 'refresh_token')
.set('refresh_token', this.tokenInfo.refresh_token);
return this.http.post<any>(this.tokenEndpoint, params.toString(), { headers })
.pipe(
tap(newTokenInfo => {
// 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);
// Reiniciar temporizador para renovación de token
this.startRefreshTokenTimer();
// Reiniciar detección de inactividad
this.resetInactivity();
}),
catchError(error => {
console.error('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'));
})
);
}
// Obtener token actual
public getToken(): string {
return this.tokenInfo?.access_token || '';
}
// Verificar si hay usuario autenticado
public isAuthenticated(): boolean {
return !!this.tokenInfo?.access_token && !this.isTokenExpired();
}
// Obtener usuario actual
public getCurrentUser(): any {
return this.userInfo();
}
// Verificar si el token ha expirado
private isTokenExpired(): boolean {
if (!this.tokenInfo?.access_token) {
return true;
}
try {
const tokenParts = this.tokenInfo.access_token.split('.');
if (tokenParts.length !== 3) {
return true;
}
const payload = JSON.parse(atob(tokenParts[1]));
const expirationTime = payload.exp * 1000; // Convertir a milisegundos
const currentTime = new Date().getTime();
return currentTime >= expirationTime;
} catch (e) {
console.error('Error al verificar expiración del token:', e);
return true;
}
}
// Decodificar token y extraer información del usuario
private setUserFromToken(token: string): void {
try {
const tokenParts = token.split('.');
if (tokenParts.length !== 3) {
throw new Error('Formato de token inválido');
}
const payload = JSON.parse(atob(tokenParts[1]));
// Extraer información del usuario del payload
const user = {
id: payload.sub,
username: payload.preferred_username,
name: payload.name,
email: payload.email,
roles: payload.realm_access?.roles || [],
isAdmin: (payload.realm_access?.roles || []).includes('admin')
};
this.userInfo.set(user);
} catch (e) {
console.error('Error al decodificar token:', e);
this.userInfo.set(null);
}
}
// Configurar temporizador para renovación automática del token
private startRefreshTokenTimer(): void {
// Detener cualquier temporizador existente
this.stopRefreshTokenTimer();
// Calcular tiempo de expiración
try {
if (!this.tokenInfo?.access_token) {
return;
}
const tokenParts = this.tokenInfo.access_token.split('.');
const payload = JSON.parse(atob(tokenParts[1]));
const expirationTime = payload.exp * 1000; // Convertir a milisegundos
const currentTime = new Date().getTime();
// Renovar cuando quede el 70% del tiempo de validez
const timeToExpiry = expirationTime - currentTime;
const refreshTime = timeToExpiry * 0.7;
this.refreshTokenTimeout = setTimeout(() => {
this.refreshToken().subscribe();
}, refreshTime);
} catch (e) {
console.error('Error al configurar renovación de token:', e);
}
}
// Detener temporizador de renovación de token
private stopRefreshTokenTimer(): void {
if (this.refreshTokenTimeout) {
clearTimeout(this.refreshTokenTimeout);
this.refreshTokenTimeout = null;
}
}
// Obtener tiempo de expiración del token en milisegundos
private getTokenExpirationTime(): number {
if (!this.tokenInfo?.access_token) {
return 0;
}
try {
// Obtener información del token
const tokenParts = this.tokenInfo.access_token.split('.');
if (tokenParts.length !== 3) {
return 0;
}
// Decodificar la parte de payload del token
const payload = JSON.parse(atob(tokenParts[1]));
// Obtener el tiempo de expiración
if (!payload.exp) {
return 0;
}
// Calcular el tiempo restante en milisegundos
const expirationTime = payload.exp * 1000; // Convertir de segundos a milisegundos
const currentTime = new Date().getTime();
const timeRemaining = expirationTime - currentTime;
return Math.max(0, timeRemaining);
} catch (error) {
console.error('Error al obtener tiempo de expiración del token:', error);
return 0;
}
}
// Configurar monitoreo de actividad del usuario
private setupActivityMonitoring(): void {
if (typeof window !== 'undefined') {
// Lista de eventos para detectar actividad del usuario
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
// Agregar listener para cada evento
events.forEach(eventName => {
window.addEventListener(eventName, () => this.resetInactivity(), true);
});
// Iniciar timer de inactividad
this.resetInactivity();
}
}
// Detener monitoreo de actividad
private stopActivityMonitoring(): void {
if (this.userActivity) {
clearTimeout(this.userActivity);
this.userActivity = null;
}
this.userInactive.set(false);
}
// Reiniciar timer de inactividad basado en un porcentaje de la expiración del token
private resetInactivity(): void {
this.userInactive.set(false);
clearTimeout(this.userActivity);
// Obtener el tiempo de expiración del token
const tokenExpirationTime = this.getTokenExpirationTime();
if (tokenExpirationTime <= 0) {
return; // No configurar timer si el token ya expiró o no está disponible
}
// Calcular el tiempo de inactividad como el porcentaje configurado del tiempo de expiración
const inactivityTime = tokenExpirationTime * this.INACTIVITY_PERCENTAGE;
this.userActivity = setTimeout(() => {
this.userInactive.set(true);
}, inactivityTime);
}
}

View File

@ -0,0 +1,14 @@
export const environment = {
production: true,
keycloak: {
url: 'http://192.168.1.27:8080',
realm: 'angular-app',
clientId: 'angular-app',
credentials: {
secret: 'zYbODELDmLjK9c9gHNbTUe8mSZlcLFZm'
}
},
api: {
baseUrl: '/api'
}
};

View File

@ -0,0 +1,14 @@
export const environment = {
production: false,
keycloak: {
url: 'http://192.168.1.27:8080',
realm: 'angular-app',
clientId: 'angular-app',
credentials: {
secret: 'zYbODELDmLjK9c9gHNbTUe8mSZlcLFZm'
}
},
api: {
baseUrl: '/api'
}
};