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
|
## Instalación
|
||||||
|
|
||||||
### Requisitos Previos
|
### Requisitos Previos
|
||||||
|
|
||||||
- Node.js (versión 20.x recomendada)
|
- Node.js (versión 20.x o superior)
|
||||||
- npm (incluido con Node.js)
|
- npm (incluido con Node.js)
|
||||||
- Git
|
- Keycloak Server (para autenticación)
|
||||||
|
|
||||||
### Pasos de Instalación
|
### Pasos de Instalación
|
||||||
|
|
||||||
@ -16,18 +24,40 @@ Este proyecto es una aplicación Angular 19 utilizando PrimeNG para la gestión
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <URL-del-repositorio>
|
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
|
```bash
|
||||||
npm install
|
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.
|
4. **Iniciar el Servidor de Desarrollo**
|
||||||
|
|
||||||
3. **Iniciar el Servidor de Desarrollo**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
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/`
|
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
|
1. **Instalar y Ejecutar Keycloak**:
|
||||||
```bash
|
- Descargue Keycloak desde [keycloak.org](https://www.keycloak.org/downloads)
|
||||||
npm install @angular/core @angular/common @angular/forms @angular/router @angular/compiler
|
- 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
|
2. **Crear un Realm**:
|
||||||
```bash
|
- Cree un nuevo realm llamado `angular-app`
|
||||||
npm install primeng @primeng/themes
|
- Este nombre debe coincidir con su variable `KEYCLOAK_REALM` en `.env`
|
||||||
```
|
|
||||||
|
|
||||||
- **PrimeFlex 4.0.0**: Sistema de CSS flexible
|
3. **Crear un Cliente**:
|
||||||
```bash
|
- Cree un nuevo cliente con ID `angular-app` (debe coincidir con `KEYCLOAK_CLIENT_ID`)
|
||||||
npm install primeflex
|
- 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
|
4. **Configurar Usuarios y Roles**:
|
||||||
```bash
|
- Cree usuarios para pruebas
|
||||||
npm install primeicons
|
- Configure roles como `admin`, `user`, etc.
|
||||||
```
|
- Asigne roles a los usuarios
|
||||||
|
|
||||||
- **PDFMake 0.2.19**: Para generación de PDFs
|
### Integración con la Aplicación
|
||||||
```bash
|
|
||||||
npm install pdfmake @types/pdfmake
|
|
||||||
```
|
|
||||||
|
|
||||||
- **ExcelJS 4.4.0**: Para exportación a Excel
|
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:
|
||||||
```bash
|
|
||||||
npm install exceljs
|
|
||||||
```
|
|
||||||
|
|
||||||
- **File-Saver 2.5.0**: Para guardar archivos en el cliente
|
- Utiliza el protocolo OpenID Connect para autenticación directa
|
||||||
```bash
|
- Gestiona tokens manualmente para mayor control
|
||||||
npm install file-saver @types/file-saver
|
- Implementa renovación automática de tokens
|
||||||
```
|
- Provee detección de inactividad de usuario
|
||||||
|
|
||||||
- **Animate.css 4.1.1**: Para animaciones CSS
|
## Entornos de Ejecución
|
||||||
```bash
|
|
||||||
npm install animate.css
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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
|
## Flujo de Autenticación
|
||||||
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
|
|
||||||
|
|
||||||
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
|
4. **Interceptor HTTP**:
|
||||||
"styles": [
|
- Todas las solicitudes HTTP incluyen automáticamente el token de autenticación
|
||||||
"src/styles.scss",
|
- En caso de error 401, se intenta renovar el token automáticamente
|
||||||
{
|
|
||||||
"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
|
|
||||||
|
|
||||||
## Comandos Disponibles
|
## Comandos Disponibles
|
||||||
|
|
||||||
| Comando | Descripción |
|
| Comando | Descripción |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `npm start` | Inicia servidor de desarrollo |
|
| `npm start` | Inicia servidor de desarrollo en http://localhost:4200 |
|
||||||
| `npm run build` | Compila el proyecto |
|
| `npm run build` | Compila el proyecto para desarrollo |
|
||||||
| `npm run build:prod` | Compila para producción |
|
| `npm run build:prod` | Compila para producción con optimizaciones |
|
||||||
| `npm run watch` | Compila en modo observador |
|
|
||||||
| `npm test` | Ejecuta pruebas unitarias |
|
| `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
|
## Solución de Problemas
|
||||||
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
|
|
||||||
|
|
||||||
## Buenas Prácticas
|
### Problemas Comunes de Autenticación
|
||||||
|
|
||||||
1. **Organización de Componentes**:
|
1. **No se puede iniciar sesión**:
|
||||||
- Componentes de página en la carpeta `pages/`
|
- Verifique que las credenciales sean correctas
|
||||||
- Componentes reutilizables en `components/`
|
- 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**:
|
2. **Token expirado o inválido**:
|
||||||
- Definir interfaces para todos los modelos en `models/`
|
- La aplicación debería renovar automáticamente el token
|
||||||
- Exportar todas las interfaces desde `models/index.ts`
|
- Si persiste, intente cerrar sesión y volver a iniciar sesión
|
||||||
|
|
||||||
3. **Servicios**:
|
3. **Redirecciones en bucle**:
|
||||||
- Mantener la responsabilidad única para cada servicio
|
- Limpie el localStorage del navegador
|
||||||
- Centralizar la lógica de negocio en los servicios
|
- Verifique la configuración de URLs en el cliente Keycloak
|
||||||
|
|
||||||
4. **Gestión de Estado**:
|
|
||||||
- Utilizar servicios para compartir estado entre componentes
|
|
||||||
|
|
||||||
## Recursos Adicionales
|
## Recursos Adicionales
|
||||||
|
|
||||||
- [Documentación de Angular](https://angular.dev/)
|
- [Documentación oficial de Keycloak](https://www.keycloak.org/documentation)
|
||||||
- [Documentación de PrimeNG](https://primeng.org/installation)
|
- [Angular Security Best Practices](https://angular.io/guide/security)
|
||||||
- [Documentación de PrimeFlex](https://primeflex.org/)
|
- [Tutorial completo de Keycloak](tutorial-keycloak-completo.md)
|
||||||
- [Documentación de PDFMake](http://pdfmake.org/)
|
|
||||||
- [Documentación de ExcelJS](https://github.com/exceljs/exceljs)
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"realm": "aangular-app",
|
"realm": "angular-app",
|
||||||
"auth-server-url": "http://localhost:8080/",
|
"auth-server-url": "http://192.168.1.27:8080/",
|
||||||
"resource": "angular-app",
|
"resource": "angular-app",
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"secret": "zYbODELDmLjK9c9gHNbTUe8mSZlcLFZm"
|
"secret": "zYbODELDmLjK9c9gHNbTUe8mSZlcLFZm"
|
||||||
|
|||||||
@ -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 { Router, RouterOutlet, NavigationEnd } from '@angular/router';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
import { ConfirmationService } from 'primeng/api';
|
import { ConfirmationService, MessageService } from 'primeng/api';
|
||||||
import { filter, Subscription } from 'rxjs';
|
import { filter, Subscription } from 'rxjs';
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import Keycloak from 'keycloak-js';
|
import { DirectAuthService } from './services/direct-auth.service';
|
||||||
import { AuthService } from './services/auth.service';
|
|
||||||
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType, KeycloakEvent } from 'keycloak-angular';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet, ConfirmDialogModule],
|
imports: [RouterOutlet, ConfirmDialogModule, ToastModule],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
providers: [ConfirmationService],
|
providers: [ConfirmationService, MessageService],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent implements OnInit {
|
||||||
title = 'SACG - Sistema Administrador de Cronogramas';
|
title = 'SACG - Sistema Administrador de Cronogramas';
|
||||||
private keycloak = inject(Keycloak);
|
private authService = inject(DirectAuthService);
|
||||||
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
|
|
||||||
private authService = inject(AuthService);
|
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
|
||||||
private subscriptions: Subscription[] = [];
|
private subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Using effect to handle Keycloak events through Angular's Signal API
|
// Ya no es necesario el efecto keycloak
|
||||||
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']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Subscribe to navigation events - using standard unsubscribe pattern
|
// Subscribe to navigation events - using standard unsubscribe pattern
|
||||||
this.subscriptions.push(
|
this.subscriptions.push(
|
||||||
@ -71,7 +36,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
console.log('Navigation completed to:', this.router.url);
|
console.log('Navigation completed to:', this.router.url);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check authentication status on load
|
// Check authentication status on load
|
||||||
this.checkAuthenticationStatus();
|
this.checkAuthenticationStatus();
|
||||||
}
|
}
|
||||||
@ -83,20 +48,20 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private async checkAuthenticationStatus(): Promise<void> {
|
private async checkAuthenticationStatus(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const isLoggedIn = await this.keycloak.authenticated;
|
const isLoggedIn = this.authService.isAuthenticated();
|
||||||
console.log('Initial authentication status:', isLoggedIn ? 'Authenticated' : 'Not authenticated');
|
console.log('Initial authentication status:', isLoggedIn ? 'Authenticated' : 'Not authenticated');
|
||||||
|
|
||||||
// Let the guards handle the protected routes
|
// Let the guards handle the protected routes
|
||||||
// Only do minimal checks here to avoid redirect loops
|
// Only do minimal checks here to avoid redirect loops
|
||||||
|
|
||||||
// If the user is on login page but already authenticated, send to home
|
// If the user is on login page but already authenticated, send to home
|
||||||
if (isLoggedIn && this.router.url === '/login') {
|
if (isLoggedIn && this.router.url === '/login') {
|
||||||
console.log('Already authenticated, redirecting from login to home');
|
console.log('Already authenticated, redirecting from login to home');
|
||||||
this.router.navigate(['/inicio']);
|
this.router.navigate(['/inicio']);
|
||||||
return;
|
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') {
|
if (!isLoggedIn && this.router.url !== '/login') {
|
||||||
// We'll let the auth guard handle this
|
// We'll let the auth guard handle this
|
||||||
console.log('Not authenticated on protected route');
|
console.log('Not authenticated on protected route');
|
||||||
@ -105,4 +70,4 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
console.error('Error checking authentication status:', error);
|
console.error('Error checking authentication status:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,25 +6,8 @@ import { provideAnimations } from '@angular/platform-browser/animations';
|
|||||||
import { providePrimeNG } from 'primeng/config';
|
import { providePrimeNG } from 'primeng/config';
|
||||||
import Aura from '@primeng/themes/aura';
|
import Aura from '@primeng/themes/aura';
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
import {
|
import { authInterceptor } from './interceptors/auth.interceptor';
|
||||||
provideKeycloak,
|
import { environment } from '../environments/environment';
|
||||||
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'
|
|
||||||
});
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@ -34,11 +17,13 @@ export const appConfig: ApplicationConfig = {
|
|||||||
withPreloading(PreloadAllModules)
|
withPreloading(PreloadAllModules)
|
||||||
),
|
),
|
||||||
provideAnimations(),
|
provideAnimations(),
|
||||||
// Use the Keycloak interceptor to attach authentication tokens
|
|
||||||
|
// Usamos nuestro interceptor personalizado para DirectAuthService
|
||||||
provideHttpClient(
|
provideHttpClient(
|
||||||
withFetch(),
|
withFetch(),
|
||||||
withInterceptors([includeBearerTokenInterceptor])
|
withInterceptors([authInterceptor])
|
||||||
),
|
),
|
||||||
|
|
||||||
providePrimeNG({
|
providePrimeNG({
|
||||||
theme: {
|
theme: {
|
||||||
preset: Aura,
|
preset: Aura,
|
||||||
@ -47,27 +32,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
MessageService,
|
|
||||||
// Provide Keycloak with initOptions - this automatically creates an APP_INITIALIZER
|
MessageService
|
||||||
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]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@ -7,7 +7,7 @@ import { filter, map, mergeMap } from 'rxjs/operators';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-navbar',
|
selector: 'app-navbar',
|
||||||
@ -33,7 +33,7 @@ export class NavbarComponent implements OnInit {
|
|||||||
private messageService: MessageService,
|
private messageService: MessageService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private authService: AuthService
|
private authService: DirectAuthService
|
||||||
) {
|
) {
|
||||||
this.router.events.pipe(
|
this.router.events.pipe(
|
||||||
filter(event => event instanceof NavigationEnd),
|
filter(event => event instanceof NavigationEnd),
|
||||||
@ -52,13 +52,12 @@ export class NavbarComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Obtener nombre de usuario del usuario logueado
|
// Obtener nombre de usuario del usuario logueado
|
||||||
this.authService.user$.subscribe(user => {
|
const user = this.authService.getCurrentUser();
|
||||||
if (user) {
|
if (user) {
|
||||||
this.userName = user.name || user.username || 'Usuario';
|
this.userName = user.name || user.username || 'Usuario';
|
||||||
} else {
|
} else {
|
||||||
this.userName = 'Usuario';
|
this.userName = 'Usuario';
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
@ -92,4 +91,4 @@ export class NavbarComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
this.router.navigate(['/login']);
|
this.router.navigate(['/login']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,57 +1,23 @@
|
|||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
import Keycloak from 'keycloak-js';
|
|
||||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
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
|
// Simple implementation for authentication guard using DirectAuthService
|
||||||
export const authGuard: CanActivateFn = async (
|
export const authGuard: CanActivateFn = (
|
||||||
route: ActivatedRouteSnapshot,
|
route: ActivatedRouteSnapshot,
|
||||||
state: RouterStateSnapshot
|
state: RouterStateSnapshot
|
||||||
): Promise<boolean | UrlTree> => {
|
): boolean | UrlTree => {
|
||||||
const keycloak = inject(Keycloak);
|
const authService = inject(DirectAuthService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
try {
|
// Check if user is authenticated
|
||||||
// Check if user is authenticated
|
if (authService.isAuthenticated()) {
|
||||||
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) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the URL the user was trying to access
|
// If not authenticated, redirect to login
|
||||||
const returnUrl = state.url;
|
return router.createUrlTree(['/login'], {
|
||||||
const router = inject(Router);
|
queryParams: { returnUrl: state.url !== '/' ? state.url : '/inicio' }
|
||||||
|
});
|
||||||
// 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);
|
|
||||||
@ -1,57 +1,53 @@
|
|||||||
|
import { HttpHandlerFn, HttpInterceptorFn, HttpRequest, HttpErrorResponse } from '@angular/common/http';
|
||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import {
|
import { catchError, switchMap, throwError } from 'rxjs';
|
||||||
HttpRequest,
|
import { DirectAuthService } from '../services/direct-auth.service';
|
||||||
HttpHandlerFn,
|
import { environment } from '../../environments/environment';
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
export const authInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||||
* Note: This interceptor is not strictly necessary when using keycloak-angular's
|
const authService = inject(DirectAuthService);
|
||||||
* 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);
|
|
||||||
|
|
||||||
// Handle the request with error handling for auth issues
|
// No interceptar peticiones al endpoint de token (evitar bucles)
|
||||||
return next(request).pipe(
|
if (req.url.includes('/protocol/openid-connect/token')) {
|
||||||
catchError((error: HttpErrorResponse) => {
|
return next(req);
|
||||||
// Handle 401 Unauthorized errors
|
}
|
||||||
if (error.status === 401) {
|
|
||||||
console.log('401 Unauthorized error, refreshing token or redirecting to login');
|
// Agregar token de autenticación si está disponible
|
||||||
|
const token = authService.getToken();
|
||||||
// Try to refresh the token first
|
if (token) {
|
||||||
return from(keycloak.updateToken(30)).pipe(
|
req = addToken(req, token);
|
||||||
switchMap(refreshed => {
|
}
|
||||||
if (refreshed) {
|
|
||||||
// Token was refreshed, retry the request
|
return next(req).pipe(
|
||||||
return next(request);
|
catchError(error => {
|
||||||
} else {
|
if (error instanceof HttpErrorResponse && error.status === 401) {
|
||||||
// Token couldn't be refreshed, redirect to login
|
// Si es error 401, intentar refrescar token
|
||||||
keycloak.login();
|
return handle401Error(req, next, authService);
|
||||||
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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other errors, just pass them through
|
|
||||||
return throwError(() => error);
|
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,54 +10,102 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast para mensajes -->
|
<!-- Toast para mensajes -->
|
||||||
<p-toast></p-toast>
|
<p-toast></p-toast>
|
||||||
|
|
||||||
<!-- MAIN CONTENT -->
|
<!-- MAIN CONTENT -->
|
||||||
<main class="main-content flex align-items-center justify-content-center">
|
<main class="main-content flex align-items-center justify-content-center">
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<div class="login-box">
|
<div class="login-box">
|
||||||
<!-- Contenedor principal con posición relativa -->
|
<!-- Contenedor principal con posición relativa -->
|
||||||
<div class="position-relative overflow-hidden">
|
<div class="position-relative overflow-hidden">
|
||||||
|
|
||||||
<!-- PANEL DE LOGIN CON KEYCLOAK -->
|
<!-- PANEL DE LOGIN -->
|
||||||
<div class="panel-container w-full animate__animated animate__fadeIn">
|
<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-card shadow-2 border-round">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<h2>Iniciar Sesión</h2>
|
<h2>Iniciar Sesión</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<form (ngSubmit)="onLogin()" class="p-3">
|
||||||
<div class="p-3">
|
<!-- Email -->
|
||||||
<!-- Mensaje de información -->
|
<div class="field mb-3">
|
||||||
<div class="info-message mb-4 text-center">
|
<input type="email" pInputText [(ngModel)]="email" name="email" placeholder="Email"
|
||||||
<p>Haz clic en el botón para iniciar sesión con la cuenta de usuario registrada en el sistema.</p>
|
class="input-with-icon w-full" required
|
||||||
|
(input)="clearErrors()"/>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Password -->
|
||||||
<!-- Botón para iniciar sesión con Keycloak -->
|
<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">
|
<div class="login-actions">
|
||||||
<button pButton type="button" (click)="onLogin()"
|
<button pButton type="submit" [label]="loading() ? 'Autenticando...' : 'Autenticar'"
|
||||||
[label]="loading ? 'Redirigiendo...' : 'Iniciar Sesión'"
|
class="p-button-primary w-full" [disabled]="loading() || !email || !password">
|
||||||
icon="pi pi-sign-in"
|
<i *ngIf="loading()" class="pi pi-spin pi-spinner mr-2"></i>
|
||||||
class="p-button-primary w-full"
|
|
||||||
[disabled]="loading">
|
|
||||||
<i *ngIf="loading" class="pi pi-spin pi-spinner mr-2"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
<!-- Recuperar contraseña -->
|
||||||
|
<div class="password-recovery px-3 pb-3">
|
||||||
|
<a href="#" (click)="toggleRecovery($event)">Recuperar Contraseña</a>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Credenciales de prueba -->
|
||||||
<!-- Información adicional (opcional) -->
|
|
||||||
<div class="test-credentials mx-3 mb-3 p-2 border-round bg-gray-100">
|
<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 font-bold">Credenciales de prueba:</p>
|
||||||
<p class="mb-1">Serás redirigido al sistema de autenticación.</p>
|
<p class="mb-1">Email: admin@example.com</p>
|
||||||
<p>Si tienes problemas, contacta al administrador.</p>
|
<p>Password: admin123</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<app-footer></app-footer>
|
<app-footer></app-footer>
|
||||||
@ -1,110 +1,161 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, signal } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { CardModule } from 'primeng/card';
|
import { CommonModule } from '@angular/common';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
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 { ButtonModule } from 'primeng/button';
|
||||||
import { PasswordModule } from 'primeng/password';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { DividerModule } from 'primeng/divider';
|
|
||||||
import { MessagesModule } from 'primeng/messages';
|
|
||||||
import { MessageModule } from 'primeng/message';
|
import { MessageModule } from 'primeng/message';
|
||||||
import { ToastModule } from 'primeng/toast';
|
import { ToastModule } from 'primeng/toast';
|
||||||
import { MessageService } from 'primeng/api';
|
import { FooterComponent } from '../../components/footer/footer.component';
|
||||||
import { FooterComponent } from "../../components/footer/footer.component";
|
|
||||||
import { AuthService } from '../../services/auth.service';
|
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
CardModule,
|
FormsModule,
|
||||||
InputTextModule,
|
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
PasswordModule,
|
InputTextModule,
|
||||||
DividerModule,
|
|
||||||
MessagesModule,
|
|
||||||
MessageModule,
|
MessageModule,
|
||||||
ToastModule,
|
ToastModule,
|
||||||
FooterComponent
|
FooterComponent
|
||||||
],
|
],
|
||||||
providers: [MessageService],
|
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrl: './login.component.scss'
|
styleUrls: ['./login.component.scss']
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent implements OnInit {
|
||||||
// Inyectar el servicio de autenticación
|
// Variables de modelo
|
||||||
private authService = inject(AuthService);
|
email: string = '';
|
||||||
private route = inject(ActivatedRoute);
|
password: string = '';
|
||||||
private router = inject(Router);
|
|
||||||
|
|
||||||
loading: boolean = false;
|
// Estados con signals
|
||||||
returnUrl: string = '';
|
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() {
|
// URL para redirección después del login
|
||||||
// Obtener el returnUrl de los query params si existe
|
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';
|
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/inicio';
|
||||||
|
|
||||||
// Simplificación - verificar autenticación sin promesas anidadas
|
// Comprobar si ya hay sesión activa
|
||||||
this.authService.isLoggedIn().subscribe({
|
if (this.authService.isAuthenticated()) {
|
||||||
next: (isLoggedIn) => {
|
this.router.navigate([this.returnUrl]);
|
||||||
if (isLoggedIn) {
|
return;
|
||||||
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;
|
// Marcar que ya no es carga inicial (para animaciones)
|
||||||
// Redirigir
|
setTimeout(() => {
|
||||||
this.router.navigate([effectiveReturnUrl], { replaceUrl: true });
|
this.isInitialLoad.set(false);
|
||||||
}
|
}, 100);
|
||||||
},
|
|
||||||
error: (error) => console.error('Error al verificar autenticación:', error)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Método de login con Keycloak usando nuestro servicio AuthService
|
/**
|
||||||
async onLogin() {
|
* Maneja el envío del formulario de login
|
||||||
this.loading = true;
|
*/
|
||||||
|
onLogin(): void {
|
||||||
|
// Validaciones básicas
|
||||||
|
if (!this.email || !this.password) {
|
||||||
|
this.errorMessage.set('Por favor ingresa email y contraseña');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
this.loading.set(true);
|
||||||
// Verificar si la URL de retorno es válida
|
this.errorMessage.set(null);
|
||||||
const effectiveReturnUrl = this.returnUrl === '/' ? '/inicio' : this.returnUrl;
|
|
||||||
|
this.authService.login(this.email, this.password)
|
||||||
// Construir el redirectUri
|
.subscribe({
|
||||||
const redirectUri = window.location.origin + effectiveReturnUrl;
|
next: () => {
|
||||||
console.log('Iniciando login con redirectUri:', redirectUri);
|
// Mostrar mensaje de éxito
|
||||||
|
this.messageService.add({
|
||||||
// Iniciar el flujo de autenticación de Keycloak
|
severity: 'success',
|
||||||
await this.authService.login(redirectUri);
|
summary: 'Bienvenido',
|
||||||
// Keycloak se encargará de la redirección
|
detail: 'Inicio de sesión exitoso',
|
||||||
} catch (error) {
|
life: 3000
|
||||||
console.error('Error al iniciar sesión:', error);
|
});
|
||||||
this.loading = false;
|
|
||||||
this.messageService.add({
|
// Redirigir al usuario
|
||||||
severity: 'error',
|
setTimeout(() => {
|
||||||
summary: 'Error',
|
this.router.navigate([this.returnUrl]);
|
||||||
detail: 'Error al intentar iniciar sesión. Por favor, inténtelo de nuevo.'
|
}, 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 { HttpClient } from '@angular/common/http';
|
||||||
import { BehaviorSubject, Observable, from } from 'rxjs';
|
import { BehaviorSubject, Observable, from } from 'rxjs';
|
||||||
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType } from 'keycloak-angular';
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import Keycloak from 'keycloak-js';
|
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AuthService {
|
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
|
// User state
|
||||||
private userSubject = new BehaviorSubject<any>(null);
|
private userSubject = new BehaviorSubject<any>(null);
|
||||||
public user$ = this.userSubject.asObservable();
|
public user$ = this.userSubject.asObservable();
|
||||||
@ -23,218 +19,51 @@ export class AuthService {
|
|||||||
// Authentication state as a signal
|
// Authentication state as a signal
|
||||||
public isAuthenticated = signal<boolean>(false);
|
public isAuthenticated = signal<boolean>(false);
|
||||||
|
|
||||||
// Login error state
|
constructor(
|
||||||
public loginError = signal<string | null>(null);
|
private http: HttpClient,
|
||||||
|
private router: Router,
|
||||||
constructor(private http: HttpClient) {
|
private messageService: MessageService
|
||||||
// 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async login(redirectUri?: string): Promise<void> {
|
async login(redirectUri?: string): Promise<void> {
|
||||||
try {
|
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
|
||||||
this.loginError.set(null);
|
return Promise.reject('AuthService is deprecated. Use DirectAuthService instead.');
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
try {
|
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
|
||||||
await this.keycloak.logout({
|
return Promise.resolve();
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn(): Observable<boolean> {
|
isLoggedIn(): Observable<boolean> {
|
||||||
try {
|
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
|
||||||
// Usar directamente la propiedad authenticated de Keycloak
|
return from(Promise.resolve(false));
|
||||||
return from(Promise.resolve(this.keycloak.authenticated || false));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al verificar autenticación:', error);
|
|
||||||
return from(Promise.resolve(false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): Promise<string> {
|
getToken(): Promise<string> {
|
||||||
try {
|
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
|
||||||
return Promise.resolve(this.keycloak.token || '');
|
return Promise.resolve('');
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al obtener token:', error);
|
|
||||||
return Promise.resolve('');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateToken(minValidity = 30): Promise<boolean> {
|
async updateToken(): Promise<boolean> {
|
||||||
try {
|
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
|
||||||
return await this.keycloak.updateToken(minValidity);
|
return false;
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentUser(): any {
|
getCurrentUser(): any {
|
||||||
return this.userSubject.value;
|
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has a specific role
|
// Check if user has a specific role
|
||||||
hasRole(role: string): boolean {
|
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
|
// Check if user has any of the specified roles
|
||||||
hasAnyRole(roles: string[]): boolean {
|
hasAnyRole(roles: string[]): boolean {
|
||||||
for (const role of roles) {
|
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
|
||||||
if (this.keycloak.hasRealmRole(role)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
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