keycloack direct funcionando
se implemente el direct access grant para probar el post login
This commit is contained in:
parent
1dd5f1644f
commit
8a1434e553
11
.env.template
Normal file
11
.env.template
Normal 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
387
README.md
@ -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)
|
||||
@ -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"
|
||||
|
||||
@ -1,63 +1,28 @@
|
||||
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() {
|
||||
@ -83,7 +48,7 @@ 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
|
||||
@ -96,7 +61,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
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');
|
||||
|
||||
@ -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
|
||||
]
|
||||
};
|
||||
@ -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() {
|
||||
|
||||
@ -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 } });
|
||||
// If not authenticated, redirect to login
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url !== '/' ? state.url : '/inicio' }
|
||||
});
|
||||
};
|
||||
|
||||
// Optional: Use the createAuthGuard helper if needed
|
||||
export const authGuardWithHelper = createAuthGuard(isAccessAllowed);
|
||||
@ -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');
|
||||
// No interceptar peticiones al endpoint de token (evitar bucles)
|
||||
if (req.url.includes('/protocol/openid-connect/token')) {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
// 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);
|
||||
})
|
||||
);
|
||||
// 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);
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -10,10 +10,8 @@
|
||||
</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">
|
||||
@ -21,43 +19,93 @@
|
||||
<!-- 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@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>
|
||||
@ -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;
|
||||
this.loading.set(true);
|
||||
this.errorMessage.set(null);
|
||||
|
||||
// Construir el redirectUri
|
||||
const redirectUri = window.location.origin + effectiveReturnUrl;
|
||||
console.log('Iniciando login con redirectUri:', redirectUri);
|
||||
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
|
||||
});
|
||||
|
||||
// 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.'
|
||||
// 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
354
src/app/services/direct-auth.service.ts
Normal file
354
src/app/services/direct-auth.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
14
src/environments/environment.prod.ts
Normal file
14
src/environments/environment.prod.ts
Normal 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'
|
||||
}
|
||||
};
|
||||
14
src/environments/environment.ts
Normal file
14
src/environments/environment.ts
Normal 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'
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user