Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89e97a3634 | |||
| 04b7840341 | |||
| 6b351ff5b3 | |||
| 55fc1f6278 | |||
| 56eee3fb97 | |||
| 68b466bd3c | |||
| f2ce7327d8 | |||
| 8a1434e553 | |||
| 1dd5f1644f | |||
| db0815f3ed | |||
| 1ca16b0e94 | |||
| c84c9a95c8 | |||
| 01b93eca37 | |||
| ea91f1c8f0 |
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)
|
||||
@ -3,7 +3,7 @@
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"cronogramas": {
|
||||
"correo": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
@ -17,7 +17,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/cronogramas",
|
||||
"outputPath": "dist/correo",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
@ -82,10 +82,10 @@
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "cronogramas:build:production"
|
||||
"buildTarget": "correo:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "cronogramas:build:development"
|
||||
"buildTarget": "correo:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
|
||||
1658
package-lock.json
generated
1658
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "cronogramas",
|
||||
"name": "correo",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=18.x"
|
||||
@ -27,6 +27,8 @@
|
||||
"animate.css": "^4.1.1",
|
||||
"exceljs": "^4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"keycloak-angular": "^19.0.2",
|
||||
"keycloak-js": "^26.2.0",
|
||||
"pdfmake": "^0.2.19",
|
||||
"primeflex": "^4.0.0",
|
||||
"primeicons": "^7.0.0",
|
||||
@ -51,5 +53,5 @@
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
},
|
||||
"description": "cronogramas - Proyecto generado desde template"
|
||||
"description": "correo - Proyecto generado desde template"
|
||||
}
|
||||
8
public/keycloak.json
Normal file
8
public/keycloak.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"realm": "angular-app",
|
||||
"auth-server-url": "http://192.168.1.27:8080/",
|
||||
"resource": "angular-app",
|
||||
"credentials": {
|
||||
"secret": "zYbODELDmLjK9c9gHNbTUe8mSZlcLFZm"
|
||||
}
|
||||
}
|
||||
7
public/silent-check-sso.html
Normal file
7
public/silent-check-sso.html
Normal file
@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.href, location.origin);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,15 +1,73 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
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 { 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 {
|
||||
export class AppComponent implements OnInit {
|
||||
title = 'SACG - Sistema Administrador de Cronogramas';
|
||||
private authService = inject(DirectAuthService);
|
||||
private router = inject(Router);
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
|
||||
constructor() {
|
||||
// Ya no es necesario el efecto keycloak
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Subscribe to navigation events - using standard unsubscribe pattern
|
||||
this.subscriptions.push(
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter(event => event instanceof NavigationEnd)
|
||||
)
|
||||
.subscribe(() => {
|
||||
console.log('Navigation completed to:', this.router.url);
|
||||
})
|
||||
);
|
||||
|
||||
// Check authentication status on load
|
||||
this.checkAuthenticationStatus();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Unsubscribe from any remaining subscriptions
|
||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
private async checkAuthenticationStatus(): Promise<void> {
|
||||
try {
|
||||
const isLoggedIn = this.authService.isAuthenticated();
|
||||
console.log('Initial authentication status:', isLoggedIn ? 'Authenticated' : 'Not authenticated');
|
||||
|
||||
// Let the guards handle the protected routes
|
||||
// Only do minimal checks here to avoid redirect loops
|
||||
|
||||
// If the user is on login page but already authenticated, send to home
|
||||
if (isLoggedIn && this.router.url === '/login') {
|
||||
console.log('Already authenticated, redirecting from login to home');
|
||||
this.router.navigate(['/inicio']);
|
||||
return;
|
||||
}
|
||||
|
||||
// If not authenticated and on a protected route, go to login
|
||||
if (!isLoggedIn && this.router.url !== '/login') {
|
||||
// We'll let the auth guard handle this
|
||||
console.log('Not authenticated on protected route');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking authentication status:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,13 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
|
||||
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { providePrimeNG } from 'primeng/config';
|
||||
import Aura from '@primeng/themes/aura';
|
||||
import { authInterceptor } from './interceptors/auth.interceptor';
|
||||
import { MessageService } from 'primeng/api';
|
||||
import { authInterceptor } from './interceptors/auth.interceptor';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
@ -17,7 +17,13 @@ export const appConfig: ApplicationConfig = {
|
||||
withPreloading(PreloadAllModules)
|
||||
),
|
||||
provideAnimations(),
|
||||
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
|
||||
|
||||
// Usamos nuestro interceptor personalizado para DirectAuthService
|
||||
provideHttpClient(
|
||||
withFetch(),
|
||||
withInterceptors([authInterceptor])
|
||||
),
|
||||
|
||||
providePrimeNG({
|
||||
theme: {
|
||||
preset: Aura,
|
||||
@ -26,6 +32,7 @@ export const appConfig: ApplicationConfig = {
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
MessageService
|
||||
]
|
||||
};
|
||||
@ -1,11 +1,18 @@
|
||||
import { Routes, PreloadAllModules } from '@angular/router';
|
||||
import { Routes } from '@angular/router';
|
||||
import { LoginComponent } from './pages/login/login.component';
|
||||
import { LayoutComponent } from './components/layout/layout.component';
|
||||
import { authGuard } from './guards/auth.guard';
|
||||
import { groupGuard } from './guards/group.guard';
|
||||
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
||||
import { AccessDeniedComponent } from './pages/access-denied/access-denied.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{
|
||||
path: 'login',
|
||||
component: LoginComponent,
|
||||
data: { title: 'Login' }
|
||||
},
|
||||
|
||||
{
|
||||
path: '',
|
||||
component: LayoutComponent,
|
||||
@ -20,35 +27,68 @@ export const routes: Routes = [
|
||||
{
|
||||
path: 'unidad-concesiones',
|
||||
loadComponent: () => import('./pages/concesiones/concesiones.component').then(m => m.ConcesionesComponent),
|
||||
data: { title: 'Unidad de Concesiones' }
|
||||
data: {
|
||||
title: 'Unidad de Concesiones',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ct-actualizacion',
|
||||
loadComponent: () => import('./pages/actualizacion-pd/actualizacion-pd.component').then(m => m.ActualizacionPdComponent),
|
||||
data: { title: 'Cronograma temporal por actualización de PD' }
|
||||
data: {
|
||||
title: 'Cronograma temporal por actualización de PD',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'ct-ajuste',
|
||||
loadComponent: () => import('./pages/ajuste-pd/ajuste-pd.component').then(m => m.AjustePdComponent),
|
||||
data: { title: 'Cronograma temporal por ajuste de PD' }
|
||||
data: {
|
||||
title: 'Cronograma temporal por ajuste de PD',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'resumen',
|
||||
loadComponent: () => import('./pages/resumen/resumen.component').then(m => m.ResumenComponent),
|
||||
data: { title: 'Resumen' }
|
||||
data: {
|
||||
title: 'Resumen',
|
||||
}
|
||||
},
|
||||
// Rutas específicas por grupos
|
||||
{
|
||||
path: 'grupos/admin',
|
||||
loadComponent: () => import('./pages/admin-area/admin-area.component').then(m => m.AdminAreaComponent),
|
||||
canActivate: [groupGuard],
|
||||
data: {
|
||||
title: 'Área de Administrador',
|
||||
groups: ['administradores'] // Solo administradores pueden acceder
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'grupos/user',
|
||||
loadComponent: () => import('./pages/user-area/user-area.component').then(m => m.UserAreaComponent),
|
||||
canActivate: [groupGuard],
|
||||
data: {
|
||||
title: 'Área de Usuario',
|
||||
groups: ['administradores', 'usuarios'] // Administradores y usuarios pueden acceder
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'unidad-informacion',
|
||||
loadComponent: () => import('./pages/unidad-informacion/unidad-informacion.component').then(m => m.UnidadInformacionComponent),
|
||||
data: { title: 'Unidad de Información' }
|
||||
data: {
|
||||
title: 'Unidad de Información',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '404',
|
||||
component: NotFoundComponent,
|
||||
data: { title: 'Error 404' }
|
||||
},
|
||||
{
|
||||
path: 'access-denied',
|
||||
component: AccessDeniedComponent,
|
||||
data: { title: 'Acceso Denegado' }
|
||||
},
|
||||
]
|
||||
},
|
||||
{ path: '**', redirectTo: '404' }
|
||||
|
||||
];
|
||||
@ -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 => {
|
||||
const user = this.authService.getCurrentUser();
|
||||
if (user) {
|
||||
this.userName = user.name || user.username || 'Usuario';
|
||||
} else {
|
||||
this.userName = 'Usuario';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
|
||||
@ -7,12 +7,9 @@
|
||||
<div class="version-badge">1.0</div>
|
||||
</div>
|
||||
<!-- Subtítulo del logo -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Separador -->
|
||||
<div class="separator"></div>
|
||||
|
||||
<!-- Navigation Menu -->
|
||||
<div class="menu-container">
|
||||
<ul class="sidebar-menu">
|
||||
@ -23,7 +20,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/unidad-concesiones" class="menu-link">
|
||||
<i class="menu-icon pi pi-building"></i>
|
||||
@ -31,7 +27,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/ct-actualizacion" class="menu-link">
|
||||
<i class="menu-icon pi pi-flag"></i>
|
||||
@ -39,7 +34,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/ct-ajuste" class="menu-link">
|
||||
<i class="menu-icon pi pi-sliders-h"></i>
|
||||
@ -47,7 +41,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/resumen" class="menu-link">
|
||||
<i class="menu-icon pi pi-list"></i>
|
||||
@ -55,7 +48,6 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/unidad-informacion" class="menu-link">
|
||||
<i class="menu-icon pi pi-box"></i>
|
||||
@ -63,6 +55,40 @@
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Menú desplegable para Prueba de Roles -->
|
||||
<li class="menu-item" [class.active]="isRolesMenuOpen">
|
||||
<div class="menu-link" (click)="toggleRolesMenu()">
|
||||
<i class="menu-icon pi pi-shield"></i>
|
||||
<span class="menu-text">Prueba de Roles</span>
|
||||
<i class="pi" [class.pi-chevron-down]="isRolesMenuOpen" [class.pi-chevron-right]="!isRolesMenuOpen"
|
||||
style="margin-left: auto;"></i>
|
||||
<span class="active-indicator"></span>
|
||||
</div>
|
||||
</li>
|
||||
<!-- Submenú de Roles (visible cuando isRolesMenuOpen es true) -->
|
||||
<div *ngIf="isRolesMenuOpen" class="submenu">
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/grupos/admin" class="menu-link" style="padding-left: 2.5rem;">
|
||||
<i class="menu-icon pi pi-user-plus"></i>
|
||||
<span class="menu-text">Solo Admin</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/grupos/user" class="menu-link" style="padding-left: 2.5rem;">
|
||||
<i class="menu-icon pi pi-user"></i>
|
||||
<span class="menu-text">Solo Usuario</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item" routerLinkActive="active">
|
||||
<a routerLink="/roles/public" class="menu-link" style="padding-left: 2.5rem;">
|
||||
<i class="menu-icon pi pi-users"></i>
|
||||
<span class="menu-text">Acceso Público</span>
|
||||
<span class="active-indicator"></span>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -216,3 +216,114 @@
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// Estilos adicionales para el submenu de roles
|
||||
/* Estilos para el submenú */
|
||||
.submenu {
|
||||
margin-left: 0.5rem;
|
||||
animation: slideDown 0.3s ease-in-out;
|
||||
overflow: hidden;
|
||||
border-left: 1px dashed rgba(163, 197, 230, 0.7);
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
max-height: 200px;
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Para hacer el elemento del menú principal como un botón */
|
||||
.menu-item .menu-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Estilo especial para el elemento del menú desplegable */
|
||||
.menu-item .menu-link:has(i.pi-chevron-right),
|
||||
.menu-item .menu-link:has(i.pi-chevron-down) {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Efecto de onda al hacer clic en el menú desplegable */
|
||||
.menu-item .menu-link:has(i.pi-chevron-right)::after,
|
||||
.menu-item .menu-link:has(i.pi-chevron-down)::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
background-image: radial-gradient(circle, rgba(255,255,255,0.4) 0%, transparent 10.5%);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
transform: scale(10,10);
|
||||
opacity: 0;
|
||||
transition: transform .3s, opacity 0.8s;
|
||||
}
|
||||
|
||||
.menu-item .menu-link:active:has(i.pi-chevron-right)::after,
|
||||
.menu-item .menu-link:active:has(i.pi-chevron-down)::after {
|
||||
transform: scale(0,0);
|
||||
opacity: .3;
|
||||
transition: 0s;
|
||||
}
|
||||
|
||||
/* Estilos para elementos del submenú */
|
||||
.submenu .menu-item .menu-link {
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.submenu .menu-item .menu-link::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 1rem;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: #a3c5e6;
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.submenu .menu-item .menu-link:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.submenu .menu-item.active .menu-link::before {
|
||||
opacity: 1;
|
||||
background-color: white;
|
||||
box-shadow: 0 0 4px rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
/* Estilos especiales para el submenú activo */
|
||||
.menu-item.active + .submenu {
|
||||
border-left: 1px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
/* Animación para el icono de chevron */
|
||||
.menu-item .pi-chevron-right,
|
||||
.menu-item .pi-chevron-down {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item .pi-chevron-down {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
.menu-item .pi-chevron-right {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
@ -1,14 +1,25 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { PrimeIcons } from 'primeng/api';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
imports: [RouterLink, RouterLinkActive],
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterModule],
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrl: './sidebar.component.scss',
|
||||
standalone: true
|
||||
styleUrls: ['./sidebar.component.scss']
|
||||
})
|
||||
export class SidebarComponent {
|
||||
isRolesMenuOpen = false;
|
||||
|
||||
constructor(private authService: DirectAuthService) {}
|
||||
|
||||
toggleRolesMenu() {
|
||||
this.isRolesMenuOpen = !this.isRolesMenuOpen;
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
return this.authService.getCurrentUser();
|
||||
}
|
||||
}
|
||||
62
src/app/guards/access.guard.ts
Normal file
62
src/app/guards/access.guard.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
/**
|
||||
* Guard combinado que verifica si el usuario tiene los roles y/o pertenece a los grupos requeridos
|
||||
* Espera un array de roles en route.data['roles'] y/o un array de grupos en route.data['groups']
|
||||
* El parámetro route.data['requireAll'] determina si se requieren todos los roles/grupos o solo alguno
|
||||
* Si no cumple con los requisitos, redirige a la página de acceso denegado
|
||||
*/
|
||||
export const accessGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Primero verificar si está autenticado
|
||||
if (!authService.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener los roles y grupos requeridos del data de la ruta
|
||||
const requiredRoles = route.data['roles'] as Array<string> || [];
|
||||
const requiredGroups = route.data['groups'] as Array<string> || [];
|
||||
const requireAll = route.data['requireAll'] as boolean || false;
|
||||
|
||||
// Si no hay roles ni grupos requeridos, permitir acceso
|
||||
if (requiredRoles.length === 0 && requiredGroups.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Verificar acceso según la configuración
|
||||
let hasAccess = false;
|
||||
|
||||
if (requireAll) {
|
||||
// Necesita cumplir TODOS los requisitos
|
||||
let hasRequiredRoles = true;
|
||||
let hasRequiredGroups = true;
|
||||
|
||||
if (requiredRoles.length > 0) {
|
||||
hasRequiredRoles = authService.hasAllRoles(requiredRoles);
|
||||
}
|
||||
|
||||
if (requiredGroups.length > 0) {
|
||||
hasRequiredGroups = authService.inAllGroups(requiredGroups);
|
||||
}
|
||||
|
||||
hasAccess = hasRequiredRoles && hasRequiredGroups;
|
||||
} else {
|
||||
// Necesita cumplir ALGUNO de los requisitos
|
||||
hasAccess = (requiredRoles.length > 0 && authService.hasAnyRole(requiredRoles)) ||
|
||||
(requiredGroups.length > 0 && authService.inAnyGroup(requiredGroups));
|
||||
}
|
||||
|
||||
if (hasAccess) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no cumple los requisitos, redirigir a página de acceso denegado
|
||||
return router.createUrlTree(['/access-denied']);
|
||||
};
|
||||
28
src/app/guards/admin.guard.ts
Normal file
28
src/app/guards/admin.guard.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
/**
|
||||
* Guard que verifica si el usuario es administrador
|
||||
* Si no es administrador, redirige a la página de acceso denegado
|
||||
*/
|
||||
export const adminGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Primero verificar si está autenticado
|
||||
if (!authService.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
}
|
||||
|
||||
// Verificar si es administrador
|
||||
if (authService.isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no es administrador, redirigir a página de acceso denegado
|
||||
return router.createUrlTree(['/access-denied']);
|
||||
};
|
||||
@ -1,16 +1,22 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
export const authGuard: CanActivateFn = () => {
|
||||
const authService = inject(AuthService);
|
||||
/**
|
||||
* Guard que verifica si el usuario está autenticado
|
||||
* Si el usuario no está autenticado, redirige al login con la URL de retorno
|
||||
*/
|
||||
export const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
if (authService.isLoggedIn()) {
|
||||
if (authService.isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
router.navigate(['/login']);
|
||||
return false;
|
||||
// Si no está autenticado, redirigir a login con la URL de retorno
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
};
|
||||
37
src/app/guards/group.guard.ts
Normal file
37
src/app/guards/group.guard.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
/**
|
||||
* Guard que verifica si el usuario pertenece a los grupos requeridos
|
||||
* Espera un array de grupos en route.data['groups']
|
||||
* Si no pertenece a los grupos necesarios, redirige a la página de acceso denegado
|
||||
*/
|
||||
export const groupGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Primero verificar si está autenticado
|
||||
if (!authService.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener los grupos requeridos del data de la ruta
|
||||
const requiredGroups = route.data['groups'] as Array<string>;
|
||||
|
||||
// Si no hay grupos requeridos, permitir acceso
|
||||
if (!requiredGroups || requiredGroups.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Usar la función de servicio para verificar grupos
|
||||
if (authService.inAnyGroup(requiredGroups)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no pertenece a los grupos necesarios, redirigir a página de acceso denegado
|
||||
return router.createUrlTree(['/access-denied']);
|
||||
};
|
||||
9
src/app/guards/index.ts
Normal file
9
src/app/guards/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// Archivo de barril (index.ts) para exportar todos los guards
|
||||
export * from './auth.guard';
|
||||
export * from './role.guard';
|
||||
export * from './group.guard';
|
||||
export * from './access.guard';
|
||||
export * from './admin.guard';
|
||||
|
||||
// Esto permite importar todos los guards de una vez:
|
||||
// import { authGuard, roleGuard, groupGuard, accessGuard, adminGuard } from './guards';
|
||||
37
src/app/guards/role.guard.ts
Normal file
37
src/app/guards/role.guard.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
|
||||
/**
|
||||
* Guard que verifica si el usuario tiene los roles requeridos
|
||||
* Espera un array de roles en route.data['roles']
|
||||
* Si no tiene los roles necesarios, redirige a la página de acceso denegado
|
||||
*/
|
||||
export const roleGuard: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree => {
|
||||
const authService = inject(DirectAuthService);
|
||||
const router = inject(Router);
|
||||
|
||||
// Primero verificar si está autenticado
|
||||
if (!authService.isAuthenticated()) {
|
||||
return router.createUrlTree(['/login'], {
|
||||
queryParams: { returnUrl: state.url }
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener los roles requeridos del data de la ruta
|
||||
const requiredRoles = route.data['roles'] as Array<string>;
|
||||
|
||||
// Si no hay roles requeridos, permitir acceso
|
||||
if (!requiredRoles || requiredRoles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Usar la función de servicio para verificar roles
|
||||
if (authService.hasAnyRole(requiredRoles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Si no tiene los roles necesarios, redirigir a página de acceso denegado
|
||||
return router.createUrlTree(['/access-denied']);
|
||||
};
|
||||
@ -1,46 +1,55 @@
|
||||
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 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { catchError, switchMap, throwError } from 'rxjs';
|
||||
import { DirectAuthService } from '../services/direct-auth.service';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (
|
||||
request: HttpRequest<unknown>,
|
||||
next: HttpHandlerFn
|
||||
): Observable<any> => {
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
export const authInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||
const authService = inject(DirectAuthService);
|
||||
|
||||
// Get the auth token
|
||||
const token = authService.getToken();
|
||||
|
||||
// Clone the request and add the token if it exists
|
||||
if (token) {
|
||||
const authRequest = request.clone({
|
||||
setHeaders: {
|
||||
Authorization: `Bearer ${token}`
|
||||
// No interceptar peticiones al endpoint de token (evitar bucles)
|
||||
if (req.url.includes('/protocol/openid-connect/token')) {
|
||||
return next(req);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the authenticated request
|
||||
return next(authRequest).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
// Handle 401 Unauthorized errors by logging out and redirecting to login
|
||||
if (error.status === 401) {
|
||||
authService.logout();
|
||||
router.navigate(['/login']);
|
||||
// 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);
|
||||
}
|
||||
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}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If no token, just pass the request through
|
||||
return next(request);
|
||||
};
|
||||
// Función para manejar errores 401 (token expirado)
|
||||
function handle401Error(req: HttpRequest<unknown>, next: HttpHandlerFn, authService: DirectAuthService) {
|
||||
// Intentar refrescar el token solo si el usuario está activo
|
||||
// El método refreshToken() ya maneja el caso de inactividad internamente
|
||||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
12
src/app/pages/access-denied/access-denied.component.html
Normal file
12
src/app/pages/access-denied/access-denied.component.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="p-4 flex justify-content-center align-items-center" style="min-height: 70vh;">
|
||||
<div class="p-card p-shadow-4 p-4 text-center" style="max-width: 500px;">
|
||||
<i class="pi pi-lock text-danger" style="font-size: 4rem;"></i>
|
||||
<h2 class="mt-3 text-2xl font-bold">Acceso Denegado</h2>
|
||||
<p class="mt-2 mb-4">No tienes los permisos necesarios para acceder a esta página.</p>
|
||||
<p class="mb-4 text-sm">Tu rol actual no te permite ver este contenido.</p>
|
||||
<button class="p-button p-button-primary mt-3" (click)="goHome()">
|
||||
<i class="pi pi-home mr-2"></i>
|
||||
Volver al inicio
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
23
src/app/pages/access-denied/access-denied.component.spec.ts
Normal file
23
src/app/pages/access-denied/access-denied.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AccessDeniedComponent } from './access-denied.component';
|
||||
|
||||
describe('AccessDeniedComponent', () => {
|
||||
let component: AccessDeniedComponent;
|
||||
let fixture: ComponentFixture<AccessDeniedComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AccessDeniedComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AccessDeniedComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
17
src/app/pages/access-denied/access-denied.component.ts
Normal file
17
src/app/pages/access-denied/access-denied.component.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-access-denied',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './access-denied.component.html',
|
||||
styleUrl: './access-denied.component.scss'
|
||||
})
|
||||
export class AccessDeniedComponent {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
goHome() {
|
||||
this.router.navigate(['/inicio']);
|
||||
}
|
||||
}
|
||||
15
src/app/pages/admin-area/admin-area.component.html
Normal file
15
src/app/pages/admin-area/admin-area.component.html
Normal file
@ -0,0 +1,15 @@
|
||||
<div class="p-4">
|
||||
<div class="p-card p-shadow-4 p-4">
|
||||
<h2 class="text-2xl font-bold mb-4">Área de Administrador</h2>
|
||||
<div class="bg-blue-50 p-3 rounded border-left-3 border-blue-500 mb-3">
|
||||
<i class="pi pi-info-circle text-blue-500 mr-2"></i>
|
||||
Esta página solo es accesible para usuarios con rol de administrador.
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h3 class="text-xl font-medium mb-2">Información del usuario:</h3>
|
||||
<pre class="p-2 border-1 border-gray-300 rounded bg-gray-100 overflow-auto">{{ userInfo | json }}</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
0
src/app/pages/admin-area/admin-area.component.scss
Normal file
0
src/app/pages/admin-area/admin-area.component.scss
Normal file
23
src/app/pages/admin-area/admin-area.component.spec.ts
Normal file
23
src/app/pages/admin-area/admin-area.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminAreaComponent } from './admin-area.component';
|
||||
|
||||
describe('AdminAreaComponent', () => {
|
||||
let component: AdminAreaComponent;
|
||||
let fixture: ComponentFixture<AdminAreaComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AdminAreaComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminAreaComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
20
src/app/pages/admin-area/admin-area.component.ts
Normal file
20
src/app/pages/admin-area/admin-area.component.ts
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-area',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './admin-area.component.html',
|
||||
styleUrl: './admin-area.component.scss'
|
||||
})
|
||||
export class AdminAreaComponent implements OnInit {
|
||||
userInfo: any;
|
||||
|
||||
constructor(private authService: DirectAuthService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.userInfo = this.authService.getCurrentUser();
|
||||
}
|
||||
}
|
||||
@ -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">
|
||||
@ -23,99 +21,81 @@
|
||||
|
||||
<!-- PANEL DE LOGIN -->
|
||||
<div class="panel-container w-full"
|
||||
[ngClass]="{'animate__animated animate__fadeOut d-none': showRecovery,
|
||||
'animate__animated animate__fadeIn': !showRecovery && !isInitialLoad}">
|
||||
[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>
|
||||
|
||||
<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 />
|
||||
class="input-with-icon w-full" required
|
||||
(input)="clearErrors()"/>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="field mb-3">
|
||||
<input type="password" pInputText [(ngModel)]="password" name="password" placeholder="Password"
|
||||
class="input-with-lock w-full" required />
|
||||
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 *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="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 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>
|
||||
|
||||
<!-- Credenciales de prueba -->
|
||||
<div class="test-credentials mx-3 mb-3 p-2 border-round bg-gray-100">
|
||||
<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 -->
|
||||
<div class="panel-container w-full"
|
||||
[ngClass]="{'animate__animated animate__fadeIn': showRecovery,
|
||||
'animate__animated animate__fadeOut d-none': !showRecovery && !isInitialLoad}">
|
||||
<!-- 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)="onRecoverPassword()" class="p-3">
|
||||
<!-- Email para recuperación -->
|
||||
<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 />
|
||||
placeholder="Ingresa tu email" class="input-with-icon w-full" required
|
||||
(input)="clearErrors()"/>
|
||||
</div>
|
||||
|
||||
<!-- Mensaje informativo -->
|
||||
<div class="info-message mb-3">
|
||||
<p class="text-sm">Te enviaremos un enlace para restablecer tu contraseña.</p>
|
||||
<!-- Mensaje de error -->
|
||||
<div *ngIf="errorMessage()" class="error-message my-2">
|
||||
<p-message severity="error" [text]="errorMessage() || ''"></p-message>
|
||||
</div>
|
||||
|
||||
<!-- Mensaje de estado de recuperación -->
|
||||
<div *ngIf="recoveryMessage" class="recovery-message my-2">
|
||||
<p-message [severity]="recoveryStatus" [text]="recoveryMessage"></p-message>
|
||||
</div>
|
||||
|
||||
<!-- Botón de enviar -->
|
||||
<!-- Botones -->
|
||||
<div class="login-actions">
|
||||
<button pButton type="submit" [label]="recoveryLoading ? 'Enviando...' : 'Enviar Enlace'"
|
||||
class="p-button-primary w-full" [disabled]="recoveryLoading || !recoveryEmail">
|
||||
<i *ngIf="recoveryLoading" class="pi pi-spin pi-spinner mr-2"></i>
|
||||
<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>
|
||||
|
||||
<!-- Volver al login -->
|
||||
<div class="password-recovery px-3 pb-3">
|
||||
<a href="#" (click)="toggleRecovery($event)">Volver al Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<app-footer></app-footer>
|
||||
@ -1,136 +1,161 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { CardModule } from 'primeng/card';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
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 { FooterComponent } from '../../components/footer/footer.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
CardModule,
|
||||
InputTextModule,
|
||||
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 {
|
||||
// Login form
|
||||
// Variables de modelo
|
||||
email: string = '';
|
||||
password: string = '';
|
||||
loading: boolean = false;
|
||||
errorMessage: string = '';
|
||||
|
||||
// Password recovery form
|
||||
// Estados con signals
|
||||
loading = signal<boolean>(false);
|
||||
errorMessage = signal<string | null>(null);
|
||||
showRecovery = signal<boolean>(false);
|
||||
isInitialLoad = signal<boolean>(true);
|
||||
|
||||
// Credenciales para recuperación
|
||||
recoveryEmail: string = '';
|
||||
recoveryLoading: boolean = false;
|
||||
recoveryMessage: string = '';
|
||||
recoveryStatus: string = 'info';
|
||||
|
||||
// Control de formularios
|
||||
showRecovery: boolean = false;
|
||||
isInitialLoad: boolean = false;
|
||||
// URL para redirección después del login
|
||||
private returnUrl: string = '/inicio';
|
||||
|
||||
constructor(
|
||||
private authService: DirectAuthService,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
private route: ActivatedRoute,
|
||||
private messageService: MessageService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
// Obtener URL de retorno de los parámetros de query
|
||||
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/inicio';
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambia entre el formulario de login y recuperación
|
||||
*/
|
||||
toggleRecovery(event: Event) {
|
||||
event.preventDefault();
|
||||
this.showRecovery = !this.showRecovery;
|
||||
this.errorMessage = '';
|
||||
this.recoveryMessage = '';
|
||||
|
||||
// Si estamos cambiando al formulario de recuperación, copiar el email actual
|
||||
if (this.showRecovery && this.email) {
|
||||
this.recoveryEmail = this.email;
|
||||
}
|
||||
|
||||
// Forzar actualización del DOM con un pequeño retraso
|
||||
setTimeout(() => {
|
||||
// Este timeout ayuda a que Angular aplique los cambios de clase completamente
|
||||
}, 50); // Aumentado para asegurar la transición suave
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceso de login
|
||||
*/
|
||||
onLogin() {
|
||||
this.loading = true;
|
||||
this.errorMessage = '';
|
||||
|
||||
// Llamar al servicio auth
|
||||
this.authService.login({ email: this.email, password: this.password })
|
||||
.subscribe({
|
||||
next: () => {
|
||||
console.log('Login exitoso');
|
||||
this.loading = false;
|
||||
this.router.navigate(['/inicio']);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error en login:', error);
|
||||
this.loading = false;
|
||||
this.errorMessage = 'Credenciales incorrectas';
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Credenciales incorrectas'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Proceso de recuperación de contraseña
|
||||
*/
|
||||
onRecoverPassword() {
|
||||
if (!this.recoveryEmail) {
|
||||
this.recoveryMessage = 'Debes ingresar un email';
|
||||
this.recoveryStatus = 'error';
|
||||
// Comprobar si ya hay sesión activa
|
||||
if (this.authService.isAuthenticated()) {
|
||||
this.router.navigate([this.returnUrl]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.recoveryLoading = true;
|
||||
this.recoveryMessage = '';
|
||||
|
||||
// Simulamos la recuperación (en producción, esto llamaría a un servicio real)
|
||||
// Marcar que ya no es carga inicial (para animaciones)
|
||||
setTimeout(() => {
|
||||
this.recoveryLoading = false;
|
||||
this.recoveryMessage = 'Hemos enviado un enlace de recuperación a tu email';
|
||||
this.recoveryStatus = 'success';
|
||||
this.isInitialLoad.set(false);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
this.loading.set(true);
|
||||
this.errorMessage.set(null);
|
||||
|
||||
this.authService.login(this.email, this.password)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
// Mostrar mensaje de éxito
|
||||
this.messageService.add({
|
||||
severity: 'success',
|
||||
summary: 'Email enviado',
|
||||
detail: 'Se ha enviado un enlace de recuperación a tu correo'
|
||||
summary: 'Bienvenido',
|
||||
detail: 'Inicio de sesión exitoso',
|
||||
life: 3000
|
||||
});
|
||||
|
||||
// Redirigir al usuario
|
||||
setTimeout(() => {
|
||||
this.router.navigate([this.returnUrl]);
|
||||
}, 300); // Add small delay to ensure token is properly stored
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error de autenticación:', error);
|
||||
this.loading.set(false);
|
||||
|
||||
// Mostrar mensaje de error
|
||||
this.errorMessage.set('Credenciales incorrectas. Por favor, verifica tu email y contraseña.');
|
||||
|
||||
// También mostrar en toast para mejor visibilidad
|
||||
this.messageService.add({
|
||||
severity: 'error',
|
||||
summary: 'Error de autenticación',
|
||||
detail: 'Credenciales incorrectas. Por favor, verifica tu email y contraseña.',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Alterna entre el panel de login y el de recuperación de contraseña
|
||||
*/
|
||||
toggleRecovery(event: Event): void {
|
||||
event.preventDefault();
|
||||
this.showRecovery.update(value => !value);
|
||||
this.errorMessage.set(null); // Limpiar mensajes de error al cambiar de panel
|
||||
}
|
||||
|
||||
/**
|
||||
* Maneja la solicitud de recuperación de contraseña
|
||||
*/
|
||||
onRequestPasswordRecovery(): void {
|
||||
if (!this.recoveryEmail) {
|
||||
this.errorMessage.set('Por favor ingresa tu email');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading.set(true);
|
||||
|
||||
// Simulación de solicitud de recuperación (reemplazar con llamada real al API)
|
||||
setTimeout(() => {
|
||||
this.loading.set(false);
|
||||
this.messageService.add({
|
||||
severity: 'info',
|
||||
summary: 'Solicitud enviada',
|
||||
detail: 'Si el email existe en nuestro sistema, recibirás instrucciones para recuperar tu contraseña.',
|
||||
life: 5000
|
||||
});
|
||||
|
||||
// Volver al panel de login
|
||||
this.showRecovery.set(false);
|
||||
this.recoveryEmail = '';
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia errores cuando el usuario comienza a escribir
|
||||
*/
|
||||
clearErrors(): void {
|
||||
if (this.errorMessage()) {
|
||||
this.errorMessage.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/app/pages/user-area/user-area.component.html
Normal file
14
src/app/pages/user-area/user-area.component.html
Normal file
@ -0,0 +1,14 @@
|
||||
<div class="p-4">
|
||||
<div class="p-card p-shadow-4 p-4">
|
||||
<h2 class="text-2xl font-bold mb-4">Área de Usuario</h2>
|
||||
<div class="bg-green-50 p-3 rounded border-left-3 border-green-500 mb-3">
|
||||
<i class="pi pi-info-circle text-green-500 mr-2"></i>
|
||||
Esta página es accesible para usuarios con roles estándar.
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h3 class="text-xl font-medium mb-2">Información del usuario:</h3>
|
||||
<pre class="p-2 border-1 border-gray-300 rounded bg-gray-100 overflow-auto">{{ userInfo | json }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
0
src/app/pages/user-area/user-area.component.scss
Normal file
0
src/app/pages/user-area/user-area.component.scss
Normal file
23
src/app/pages/user-area/user-area.component.spec.ts
Normal file
23
src/app/pages/user-area/user-area.component.spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserAreaComponent } from './user-area.component';
|
||||
|
||||
describe('UserAreaComponent', () => {
|
||||
let component: UserAreaComponent;
|
||||
let fixture: ComponentFixture<UserAreaComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserAreaComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserAreaComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
18
src/app/pages/user-area/user-area.component.ts
Normal file
18
src/app/pages/user-area/user-area.component.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||
@Component({
|
||||
selector: 'app-user-area',
|
||||
imports: [CommonModule],
|
||||
templateUrl: './user-area.component.html',
|
||||
styleUrl: './user-area.component.scss'
|
||||
})
|
||||
export class UserAreaComponent implements OnInit {
|
||||
userInfo: any;
|
||||
|
||||
constructor(private authService: DirectAuthService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.userInfo = this.authService.getCurrentUser();
|
||||
}
|
||||
}
|
||||
@ -1,111 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, BehaviorSubject, of, throwError } from 'rxjs';
|
||||
import { tap, delay } from 'rxjs/operators';
|
||||
|
||||
interface LoginCredentials {
|
||||
username?: string;
|
||||
password?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
token: string;
|
||||
user: {
|
||||
id: number;
|
||||
username: string;
|
||||
name: string;
|
||||
role: string;
|
||||
email?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private apiUrl = 'api/auth';
|
||||
private userSubject = new BehaviorSubject<any>(null);
|
||||
public user$ = this.userSubject.asObservable();
|
||||
|
||||
// Usuarios de prueba para simular login
|
||||
private mockUsers = [
|
||||
{
|
||||
email: 'admin@example.com',
|
||||
password: 'admin123',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
name: 'Administrador',
|
||||
role: 'admin',
|
||||
email: 'admin@example.com'
|
||||
}
|
||||
},
|
||||
{
|
||||
email: 'user@example.com',
|
||||
password: 'user123',
|
||||
user: {
|
||||
id: 2,
|
||||
username: 'user',
|
||||
name: 'Usuario',
|
||||
role: 'user',
|
||||
email: 'user@example.com'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
// Check if user is already logged in on service initialization
|
||||
const user = localStorage.getItem('user');
|
||||
if (user) {
|
||||
this.userSubject.next(JSON.parse(user));
|
||||
}
|
||||
}
|
||||
|
||||
login(credentials: LoginCredentials): Observable<AuthResponse> {
|
||||
// Simular login con usuarios de prueba
|
||||
const user = this.mockUsers.find(u =>
|
||||
u.email === credentials.email && u.password === credentials.password);
|
||||
|
||||
if (user) {
|
||||
// Generar token falso
|
||||
const mockResponse: AuthResponse = {
|
||||
token: 'mock-jwt-token-' + Math.random().toString(36).substring(2, 15),
|
||||
user: user.user
|
||||
};
|
||||
|
||||
return of(mockResponse).pipe(
|
||||
// Simular retraso de red
|
||||
delay(800),
|
||||
tap(response => {
|
||||
// Store token and user info
|
||||
localStorage.setItem('token', response.token);
|
||||
localStorage.setItem('user', JSON.stringify(response.user));
|
||||
this.userSubject.next(response.user);
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Simular error de credenciales inválidas
|
||||
return throwError(() => new Error('Credenciales incorrectas'));
|
||||
}
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
// Clear storage and update subject
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
this.userSubject.next(null);
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return !!localStorage.getItem('token');
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
return localStorage.getItem('token');
|
||||
}
|
||||
|
||||
getCurrentUser(): any {
|
||||
return this.userSubject.value;
|
||||
}
|
||||
}
|
||||
1021
src/app/services/direct-auth.service.ts
Normal file
1021
src/app/services/direct-auth.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
26
src/environments/environment.prod.ts
Normal file
26
src/environments/environment.prod.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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'
|
||||
},
|
||||
auth: {
|
||||
// Tiempo mínimo de inactividad en milisegundos (5 minutos para producción)
|
||||
minInactivityTime: 5 * 60 * 1000,
|
||||
// Porcentaje del tiempo de vida del token para considerar inactividad (90%)
|
||||
inactivityPercentage: 0.9,
|
||||
// Estricto: true = no renovar si inactivo, false = renovar siempre
|
||||
strictTokenRenewal: true,
|
||||
// Intervalo de log en milisegundos (desactivado en producción)
|
||||
logInterval: 0,
|
||||
// Deshabilitar logs de inactividad en producción
|
||||
enableActivityLogs: false
|
||||
}
|
||||
};
|
||||
26
src/environments/environment.ts
Normal file
26
src/environments/environment.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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'
|
||||
},
|
||||
auth: {
|
||||
// Tiempo mínimo de inactividad en milisegundos (30 segundos para desarrollo)
|
||||
minInactivityTime: 30 * 1000,
|
||||
// Porcentaje del tiempo de vida del token para considerar inactividad (90%)
|
||||
inactivityPercentage: 0.9,
|
||||
// Estricto: true = no renovar si inactivo, false = renovar siempre
|
||||
strictTokenRenewal: true,
|
||||
// Intervalo de log en milisegundos (1 segundo)
|
||||
logInterval: 2000,
|
||||
// Habilitar logs de inactividad
|
||||
enableActivityLogs: false
|
||||
}
|
||||
};
|
||||
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>cronogramas</title>
|
||||
<title>correo</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
|
||||
1061
tutorial-keycloak-completo.md
Normal file
1061
tutorial-keycloak-completo.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user