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
|
## 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)
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"cronogramas": {
|
"correo": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:application",
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/cronogramas",
|
"outputPath": "dist/correo",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
@ -82,10 +82,10 @@
|
|||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "cronogramas:build:production"
|
"buildTarget": "correo:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "cronogramas:build:development"
|
"buildTarget": "correo:build:development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "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",
|
"version": "0.1.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.x"
|
"node": ">=18.x"
|
||||||
@ -27,6 +27,8 @@
|
|||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"keycloak-angular": "^19.0.2",
|
||||||
|
"keycloak-js": "^26.2.0",
|
||||||
"pdfmake": "^0.2.19",
|
"pdfmake": "^0.2.19",
|
||||||
"primeflex": "^4.0.0",
|
"primeflex": "^4.0.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@ -51,5 +53,5 @@
|
|||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.7.2"
|
"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 { Component, inject, OnInit } from '@angular/core';
|
||||||
import { RouterOutlet } 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 { ToastModule } from 'primeng/toast';
|
||||||
|
import { DirectAuthService } from './services/direct-auth.service';
|
||||||
|
|
||||||
@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,
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit {
|
||||||
title = 'SACG - Sistema Administrador de Cronogramas';
|
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 { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
|
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
|
||||||
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
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 { authInterceptor } from './interceptors/auth.interceptor';
|
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { authInterceptor } from './interceptors/auth.interceptor';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@ -17,7 +17,13 @@ export const appConfig: ApplicationConfig = {
|
|||||||
withPreloading(PreloadAllModules)
|
withPreloading(PreloadAllModules)
|
||||||
),
|
),
|
||||||
provideAnimations(),
|
provideAnimations(),
|
||||||
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
|
|
||||||
|
// Usamos nuestro interceptor personalizado para DirectAuthService
|
||||||
|
provideHttpClient(
|
||||||
|
withFetch(),
|
||||||
|
withInterceptors([authInterceptor])
|
||||||
|
),
|
||||||
|
|
||||||
providePrimeNG({
|
providePrimeNG({
|
||||||
theme: {
|
theme: {
|
||||||
preset: Aura,
|
preset: Aura,
|
||||||
@ -26,6 +32,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
MessageService
|
MessageService
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@ -1,11 +1,18 @@
|
|||||||
import { Routes, PreloadAllModules } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { LoginComponent } from './pages/login/login.component';
|
import { LoginComponent } from './pages/login/login.component';
|
||||||
import { LayoutComponent } from './components/layout/layout.component';
|
import { LayoutComponent } from './components/layout/layout.component';
|
||||||
import { authGuard } from './guards/auth.guard';
|
import { authGuard } from './guards/auth.guard';
|
||||||
|
import { groupGuard } from './guards/group.guard';
|
||||||
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
||||||
|
import { AccessDeniedComponent } from './pages/access-denied/access-denied.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{ path: 'login', component: LoginComponent },
|
{
|
||||||
|
path: 'login',
|
||||||
|
component: LoginComponent,
|
||||||
|
data: { title: 'Login' }
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: LayoutComponent,
|
component: LayoutComponent,
|
||||||
@ -20,35 +27,68 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'unidad-concesiones',
|
path: 'unidad-concesiones',
|
||||||
loadComponent: () => import('./pages/concesiones/concesiones.component').then(m => m.ConcesionesComponent),
|
loadComponent: () => import('./pages/concesiones/concesiones.component').then(m => m.ConcesionesComponent),
|
||||||
data: { title: 'Unidad de Concesiones' }
|
data: {
|
||||||
|
title: 'Unidad de Concesiones',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'ct-actualizacion',
|
path: 'ct-actualizacion',
|
||||||
loadComponent: () => import('./pages/actualizacion-pd/actualizacion-pd.component').then(m => m.ActualizacionPdComponent),
|
loadComponent: () => import('./pages/actualizacion-pd/actualizacion-pd.component').then(m => m.ActualizacionPdComponent),
|
||||||
data: { title: 'Cronograma temporal por actualización de PD' }
|
data: {
|
||||||
|
title: 'Cronograma temporal por actualización de PD',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'ct-ajuste',
|
path: 'ct-ajuste',
|
||||||
loadComponent: () => import('./pages/ajuste-pd/ajuste-pd.component').then(m => m.AjustePdComponent),
|
loadComponent: () => import('./pages/ajuste-pd/ajuste-pd.component').then(m => m.AjustePdComponent),
|
||||||
data: { title: 'Cronograma temporal por ajuste de PD' }
|
data: {
|
||||||
|
title: 'Cronograma temporal por ajuste de PD',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'resumen',
|
path: 'resumen',
|
||||||
loadComponent: () => import('./pages/resumen/resumen.component').then(m => m.ResumenComponent),
|
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',
|
path: 'unidad-informacion',
|
||||||
loadComponent: () => import('./pages/unidad-informacion/unidad-informacion.component').then(m => m.UnidadInformacionComponent),
|
loadComponent: () => import('./pages/unidad-informacion/unidad-informacion.component').then(m => m.UnidadInformacionComponent),
|
||||||
data: { title: 'Unidad de Información' }
|
data: {
|
||||||
|
title: 'Unidad de Información',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '404',
|
path: '404',
|
||||||
component: NotFoundComponent,
|
component: NotFoundComponent,
|
||||||
data: { title: 'Error 404' }
|
data: { title: 'Error 404' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'access-denied',
|
||||||
|
component: AccessDeniedComponent,
|
||||||
|
data: { title: 'Acceso Denegado' }
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ path: '**', redirectTo: '404' }
|
{ path: '**', redirectTo: '404' }
|
||||||
|
|
||||||
];
|
];
|
||||||
@ -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() {
|
||||||
|
|||||||
@ -7,12 +7,9 @@
|
|||||||
<div class="version-badge">1.0</div>
|
<div class="version-badge">1.0</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Subtítulo del logo -->
|
<!-- Subtítulo del logo -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Separador -->
|
<!-- Separador -->
|
||||||
<div class="separator"></div>
|
<div class="separator"></div>
|
||||||
|
|
||||||
<!-- Navigation Menu -->
|
<!-- Navigation Menu -->
|
||||||
<div class="menu-container">
|
<div class="menu-container">
|
||||||
<ul class="sidebar-menu">
|
<ul class="sidebar-menu">
|
||||||
@ -23,7 +20,6 @@
|
|||||||
<span class="active-indicator"></span>
|
<span class="active-indicator"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="menu-item" routerLinkActive="active">
|
<li class="menu-item" routerLinkActive="active">
|
||||||
<a routerLink="/unidad-concesiones" class="menu-link">
|
<a routerLink="/unidad-concesiones" class="menu-link">
|
||||||
<i class="menu-icon pi pi-building"></i>
|
<i class="menu-icon pi pi-building"></i>
|
||||||
@ -31,7 +27,6 @@
|
|||||||
<span class="active-indicator"></span>
|
<span class="active-indicator"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="menu-item" routerLinkActive="active">
|
<li class="menu-item" routerLinkActive="active">
|
||||||
<a routerLink="/ct-actualizacion" class="menu-link">
|
<a routerLink="/ct-actualizacion" class="menu-link">
|
||||||
<i class="menu-icon pi pi-flag"></i>
|
<i class="menu-icon pi pi-flag"></i>
|
||||||
@ -39,7 +34,6 @@
|
|||||||
<span class="active-indicator"></span>
|
<span class="active-indicator"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="menu-item" routerLinkActive="active">
|
<li class="menu-item" routerLinkActive="active">
|
||||||
<a routerLink="/ct-ajuste" class="menu-link">
|
<a routerLink="/ct-ajuste" class="menu-link">
|
||||||
<i class="menu-icon pi pi-sliders-h"></i>
|
<i class="menu-icon pi pi-sliders-h"></i>
|
||||||
@ -47,7 +41,6 @@
|
|||||||
<span class="active-indicator"></span>
|
<span class="active-indicator"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="menu-item" routerLinkActive="active">
|
<li class="menu-item" routerLinkActive="active">
|
||||||
<a routerLink="/resumen" class="menu-link">
|
<a routerLink="/resumen" class="menu-link">
|
||||||
<i class="menu-icon pi pi-list"></i>
|
<i class="menu-icon pi pi-list"></i>
|
||||||
@ -55,7 +48,6 @@
|
|||||||
<span class="active-indicator"></span>
|
<span class="active-indicator"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="menu-item" routerLinkActive="active">
|
<li class="menu-item" routerLinkActive="active">
|
||||||
<a routerLink="/unidad-informacion" class="menu-link">
|
<a routerLink="/unidad-informacion" class="menu-link">
|
||||||
<i class="menu-icon pi pi-box"></i>
|
<i class="menu-icon pi pi-box"></i>
|
||||||
@ -63,6 +55,40 @@
|
|||||||
<span class="active-indicator"></span>
|
<span class="active-indicator"></span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -216,3 +216,114 @@
|
|||||||
opacity: 0.6;
|
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 { Component } from '@angular/core';
|
||||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
import { CommonModule } from '@angular/common';
|
||||||
import { PrimeIcons } from 'primeng/api';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-sidebar',
|
selector: 'app-sidebar',
|
||||||
imports: [RouterLink, RouterLinkActive],
|
standalone: true,
|
||||||
|
imports: [CommonModule, RouterModule],
|
||||||
templateUrl: './sidebar.component.html',
|
templateUrl: './sidebar.component.html',
|
||||||
styleUrl: './sidebar.component.scss',
|
styleUrls: ['./sidebar.component.scss']
|
||||||
standalone: true
|
|
||||||
})
|
})
|
||||||
export class SidebarComponent {
|
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 { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
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);
|
const router = inject(Router);
|
||||||
|
|
||||||
if (authService.isLoggedIn()) {
|
if (authService.isAuthenticated()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to login if not authenticated
|
// Si no está autenticado, redirigir a login con la URL de retorno
|
||||||
router.navigate(['/login']);
|
return router.createUrlTree(['/login'], {
|
||||||
return false;
|
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 { 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 'rxjs';
|
|
||||||
import { catchError } from 'rxjs/operators';
|
|
||||||
import { AuthService } from '../services/auth.service';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
export const authInterceptor: HttpInterceptorFn = (
|
export const authInterceptor: HttpInterceptorFn = (req: HttpRequest<unknown>, next: HttpHandlerFn) => {
|
||||||
request: HttpRequest<unknown>,
|
const authService = inject(DirectAuthService);
|
||||||
next: HttpHandlerFn
|
|
||||||
): Observable<any> => {
|
|
||||||
const authService = inject(AuthService);
|
|
||||||
const router = inject(Router);
|
|
||||||
|
|
||||||
// Get the auth token
|
// No interceptar peticiones al endpoint de token (evitar bucles)
|
||||||
const token = authService.getToken();
|
if (req.url.includes('/protocol/openid-connect/token')) {
|
||||||
|
return next(req);
|
||||||
// Clone the request and add the token if it exists
|
|
||||||
if (token) {
|
|
||||||
const authRequest = request.clone({
|
|
||||||
setHeaders: {
|
|
||||||
Authorization: `Bearer ${token}`
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Handle the authenticated request
|
// Agregar token de autenticación si está disponible
|
||||||
return next(authRequest).pipe(
|
const token = authService.getToken();
|
||||||
catchError((error: HttpErrorResponse) => {
|
if (token) {
|
||||||
// Handle 401 Unauthorized errors by logging out and redirecting to login
|
req = addToken(req, token);
|
||||||
if (error.status === 401) {
|
}
|
||||||
authService.logout();
|
|
||||||
router.navigate(['/login']);
|
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);
|
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
|
// Función para manejar errores 401 (token expirado)
|
||||||
return next(request);
|
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>
|
</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">
|
||||||
@ -23,99 +21,81 @@
|
|||||||
|
|
||||||
<!-- PANEL DE LOGIN -->
|
<!-- PANEL DE LOGIN -->
|
||||||
<div class="panel-container w-full"
|
<div class="panel-container w-full"
|
||||||
[ngClass]="{'animate__animated animate__fadeOut d-none': showRecovery,
|
[ngClass]="{'animate__animated animate__fadeOut d-none': showRecovery(),
|
||||||
'animate__animated animate__fadeIn': !showRecovery && !isInitialLoad}">
|
'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">
|
<form (ngSubmit)="onLogin()" class="p-3">
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div class="field mb-3">
|
<div class="field mb-3">
|
||||||
<input type="email" pInputText [(ngModel)]="email" name="email" placeholder="Email"
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Password -->
|
<!-- Password -->
|
||||||
<div class="field mb-3">
|
<div class="field mb-3">
|
||||||
<input type="password" pInputText [(ngModel)]="password" name="password" placeholder="Password"
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Mensaje de error -->
|
<!-- Mensaje de error -->
|
||||||
<div *ngIf="errorMessage" class="error-message my-2">
|
<div *ngIf="errorMessage()" class="error-message my-2">
|
||||||
<p-message severity="error" [text]="errorMessage"></p-message>
|
<p-message severity="error" [text]="errorMessage() || ''"></p-message>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botón -->
|
<!-- Botón -->
|
||||||
<div class="login-actions">
|
<div class="login-actions">
|
||||||
<button pButton type="submit" [label]="loading ? 'Autenticando...' : 'Autenticar'"
|
<button pButton type="submit" [label]="loading() ? 'Autenticando...' : 'Autenticar'"
|
||||||
class="p-button-primary w-full" [disabled]="loading || !email || !password">
|
class="p-button-primary w-full" [disabled]="loading() || !email || !password">
|
||||||
<i *ngIf="loading" class="pi pi-spin pi-spinner mr-2"></i>
|
<i *ngIf="loading()" class="pi pi-spin pi-spinner mr-2"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PANEL DE RECUPERACIÓN -->
|
<!-- PANEL DE RECUPERACIÓN DE CONTRASEÑA -->
|
||||||
<div class="panel-container w-full"
|
<div class="recovery-panel"
|
||||||
[ngClass]="{'animate__animated animate__fadeIn': showRecovery,
|
[ngClass]="{'animate__animated animate__fadeIn': showRecovery(),
|
||||||
'animate__animated animate__fadeOut d-none': !showRecovery && !isInitialLoad}">
|
'animate__animated animate__fadeOut d-none': !showRecovery()}">
|
||||||
<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>Recuperar Contraseña</h2>
|
<h2>Recuperar Contraseña</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<form (ngSubmit)="onRequestPasswordRecovery()" class="p-3">
|
||||||
<form (ngSubmit)="onRecoverPassword()" class="p-3">
|
<!-- Email de recuperación -->
|
||||||
<!-- Email para recuperación -->
|
|
||||||
<div class="field mb-3">
|
<div class="field mb-3">
|
||||||
<input type="email" pInputText [(ngModel)]="recoveryEmail" name="recoveryEmail"
|
<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>
|
</div>
|
||||||
|
<!-- Mensaje de error -->
|
||||||
<!-- Mensaje informativo -->
|
<div *ngIf="errorMessage()" class="error-message my-2">
|
||||||
<div class="info-message mb-3">
|
<p-message severity="error" [text]="errorMessage() || ''"></p-message>
|
||||||
<p class="text-sm">Te enviaremos un enlace para restablecer tu contraseña.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Botones -->
|
||||||
<!-- 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 -->
|
|
||||||
<div class="login-actions">
|
<div class="login-actions">
|
||||||
<button pButton type="submit" [label]="recoveryLoading ? 'Enviando...' : 'Enviar Enlace'"
|
<button pButton type="submit"
|
||||||
class="p-button-primary w-full" [disabled]="recoveryLoading || !recoveryEmail">
|
[label]="loading() ? 'Enviando...' : 'Enviar Instrucciones'"
|
||||||
<i *ngIf="recoveryLoading" class="pi pi-spin pi-spinner mr-2"></i>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- FOOTER -->
|
<!-- FOOTER -->
|
||||||
<app-footer></app-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 { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { Router } from '@angular/router';
|
import { DirectAuthService } from '../../services/direct-auth.service';
|
||||||
import { CardModule } from 'primeng/card';
|
import { MessageService } from 'primeng/api';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
|
||||||
|
// 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';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
CardModule,
|
|
||||||
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 {
|
||||||
// Login form
|
// Variables de modelo
|
||||||
email: string = '';
|
email: string = '';
|
||||||
password: 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 = '';
|
recoveryEmail: string = '';
|
||||||
recoveryLoading: boolean = false;
|
|
||||||
recoveryMessage: string = '';
|
|
||||||
recoveryStatus: string = 'info';
|
|
||||||
|
|
||||||
// Control de formularios
|
// URL para redirección después del login
|
||||||
showRecovery: boolean = false;
|
private returnUrl: string = '/inicio';
|
||||||
isInitialLoad: boolean = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private authService: DirectAuthService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private authService: AuthService,
|
private route: ActivatedRoute,
|
||||||
private messageService: MessageService
|
private messageService: MessageService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
|
// Obtener URL de retorno de los parámetros de query
|
||||||
|
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/inicio';
|
||||||
|
|
||||||
}
|
// Comprobar si ya hay sesión activa
|
||||||
|
if (this.authService.isAuthenticated()) {
|
||||||
/**
|
this.router.navigate([this.returnUrl]);
|
||||||
* 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';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recoveryLoading = true;
|
// Marcar que ya no es carga inicial (para animaciones)
|
||||||
this.recoveryMessage = '';
|
|
||||||
|
|
||||||
// Simulamos la recuperación (en producción, esto llamaría a un servicio real)
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.recoveryLoading = false;
|
this.isInitialLoad.set(false);
|
||||||
this.recoveryMessage = 'Hemos enviado un enlace de recuperación a tu email';
|
}, 100);
|
||||||
this.recoveryStatus = 'success';
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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({
|
this.messageService.add({
|
||||||
severity: 'success',
|
severity: 'success',
|
||||||
summary: 'Email enviado',
|
summary: 'Bienvenido',
|
||||||
detail: 'Se ha enviado un enlace de recuperación a tu correo'
|
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);
|
}, 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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>cronogramas</title>
|
<title>correo</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<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