utn-cli 2.0.56 → 2.0.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/templates/backend/rutas/misc.js +17 -0
- package/templates/backend/servicios/Nucleo/Miscelaneas.js +6 -0
- package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.css +62 -15
- package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.html +34 -24
- package/templates/frontend/src/app/Componentes/Nucleo/gestion-actividad/gestion-actividad.component.ts +26 -12
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.component.css +237 -2
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.component.html +56 -18
- package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.ts +72 -51
- package/templates/frontend/src/app/app.config.ts +4 -1
package/package.json
CHANGED
|
@@ -353,6 +353,23 @@ Router.get('/cerrarSesion', async (solicitud, respuesta, next) => {
|
|
|
353
353
|
}
|
|
354
354
|
});
|
|
355
355
|
|
|
356
|
+
Router.delete('/cerrarSesionPorToken/:Token', async (solicitud, respuesta, next) => {
|
|
357
|
+
try {
|
|
358
|
+
if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
|
|
359
|
+
try {
|
|
360
|
+
return respuesta.json({ body: await Miscelaneo.cerrarSesionPorToken(solicitud.params.Token), error: undefined });
|
|
361
|
+
} catch (error) {
|
|
362
|
+
const MensajeDeError = 'No fue posible cerrar la sesión';
|
|
363
|
+
console.error(new ManejadorDeErrores(MensajeDeError, ManejadorDeErrores.obtenerNumeroDeLinea(), true, `Dirección IP: ${solicitud.ip}`));
|
|
364
|
+
return respuesta.status(500).json({ body: undefined, error: MensajeDeError });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return respuesta.status(401).json({ body: undefined, error: ManejadorDeErrores.mensajeDeError401() });
|
|
368
|
+
} catch (error) {
|
|
369
|
+
next(error);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
356
373
|
Router.get('/obtenerEnlaceDePerfil', async (solicitud, respuesta, next) => {
|
|
357
374
|
try {
|
|
358
375
|
if (await Miscelaneo.validarTokenV2(solicitud.headers.authorization) && await Miscelaneo.validarAccesoDelOrigen(solicitud)) {
|
|
@@ -66,6 +66,7 @@ class Miscelaneo {
|
|
|
66
66
|
JSON_VALUE(Solicitud, '$.method') AS Metodo,
|
|
67
67
|
JSON_VALUE(Solicitud, '$.originalUrl') AS URL,
|
|
68
68
|
IFNULL(JSON_VALUE(Solicitud, '$.country'), 'Costa Rica') AS Pais,
|
|
69
|
+
SUBSTRING_INDEX(JSON_VALUE(Solicitud, '$.headers.authorization'), ' ', -1) AS Token,
|
|
69
70
|
CASE
|
|
70
71
|
WHEN JSON_VALUE(Solicitud, '$.userAgent') LIKE '%Mobile%' THEN 'Móvil'
|
|
71
72
|
WHEN JSON_VALUE(Solicitud, '$.userAgent') LIKE '%Android%' AND JSON_VALUE(Solicitud, '$.userAgent') NOT LIKE '%Mobile%' THEN 'Tablet'
|
|
@@ -85,6 +86,11 @@ class Miscelaneo {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
async cerrarSesionPorToken(Token) {
|
|
90
|
+
await ejecutarConsultaSIGU("DELETE FROM `SIGU`.`SIGU_Sesiones` WHERE `Token` = ?", [Token]);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
async UsuariosActuales() {
|
|
89
95
|
const ConexionSigu = await crearObjetoConexionSIGU();
|
|
90
96
|
const Actuales = await ConexionSigu.query("SELECT COUNT(DISTINCT `Identificador`) AS `Total` FROM `SIGU`.`SIGU_Sesiones` WHERE `LastUpdate` >= NOW() - INTERVAL 2 HOUR");
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
border-radius: 12px;
|
|
11
11
|
margin-bottom: 20px;
|
|
12
12
|
border: 1px solid #e0e6ed;
|
|
13
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
13
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
14
14
|
text-align: left;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
border: 1px solid #e0e6ed;
|
|
81
81
|
border-radius: 12px;
|
|
82
82
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
83
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
83
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
84
84
|
text-align: left;
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -110,17 +110,37 @@
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/* Colores por método */
|
|
113
|
-
.metodo-get {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
.metodo-get {
|
|
114
|
+
color: #0069B4;
|
|
115
|
+
background-color: #e3f2fd;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.metodo-post {
|
|
119
|
+
color: #518a5f;
|
|
120
|
+
background-color: #e8f5e9;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.metodo-put {
|
|
124
|
+
color: #FFA757;
|
|
125
|
+
background-color: #fff3e0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.metodo-delete {
|
|
129
|
+
color: #F82617;
|
|
130
|
+
background-color: #ffebee;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.metodo-default {
|
|
134
|
+
color: #7d7d7d;
|
|
135
|
+
background-color: #f5f5f5;
|
|
136
|
+
}
|
|
118
137
|
|
|
119
138
|
.item-contenido {
|
|
120
139
|
flex: 1;
|
|
121
140
|
display: flex;
|
|
122
141
|
flex-direction: column;
|
|
123
|
-
min-width: 0;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
/* Evita que el flex desborde */
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
.item-titulo {
|
|
@@ -129,6 +149,22 @@
|
|
|
129
149
|
font-weight: 600;
|
|
130
150
|
word-break: break-all;
|
|
131
151
|
margin-bottom: 2px;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
flex-wrap: wrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.badge-sesion-actual {
|
|
159
|
+
background-color: #e8f5e9;
|
|
160
|
+
color: #2e7d32;
|
|
161
|
+
font-size: 10px;
|
|
162
|
+
padding: 2px 8px;
|
|
163
|
+
border-radius: 12px;
|
|
164
|
+
border: 1px solid #c8e6c9;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
white-space: nowrap;
|
|
132
168
|
}
|
|
133
169
|
|
|
134
170
|
.item-detalles {
|
|
@@ -152,10 +188,21 @@
|
|
|
152
188
|
height: 14px;
|
|
153
189
|
}
|
|
154
190
|
|
|
155
|
-
.detalle-dispositivo mat-icon {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
191
|
+
.detalle-dispositivo mat-icon {
|
|
192
|
+
color: #673ab7;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.detalle-browser mat-icon {
|
|
196
|
+
color: #5f6368;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.detalle-ip mat-icon {
|
|
200
|
+
color: #0069B4;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.detalle-pais mat-icon {
|
|
204
|
+
color: #F87600;
|
|
205
|
+
}
|
|
159
206
|
|
|
160
207
|
.item-fecha {
|
|
161
208
|
font-size: 11px;
|
|
@@ -180,14 +227,14 @@ mat-progress-bar {
|
|
|
180
227
|
flex-direction: column;
|
|
181
228
|
align-items: flex-start;
|
|
182
229
|
}
|
|
183
|
-
|
|
230
|
+
|
|
184
231
|
.item-icono {
|
|
185
232
|
margin-bottom: 12px;
|
|
186
233
|
}
|
|
187
|
-
|
|
234
|
+
|
|
188
235
|
.item-fecha {
|
|
189
236
|
margin-left: 0;
|
|
190
237
|
margin-top: 12px;
|
|
191
238
|
align-items: flex-start;
|
|
192
239
|
}
|
|
193
|
-
}
|
|
240
|
+
}
|
|
@@ -2,49 +2,59 @@
|
|
|
2
2
|
<div class="header-actividad">
|
|
3
3
|
<h2><mat-icon>history</mat-icon> Actividad reciente de la cuenta</h2>
|
|
4
4
|
<p>Historial de acciones realizadas en el sistema</p>
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
<div class="mensaje-informativo">
|
|
7
7
|
<mat-icon>info</mat-icon>
|
|
8
|
-
<p>Si tiene dudas sobre la información que ve en pantalla, por favor comparta una captura de pantalla al correo
|
|
8
|
+
<p>Si tiene dudas sobre la información que ve en pantalla, por favor comparta una captura de pantalla al correo
|
|
9
|
+
<strong>soporte@utn.ac.cr</strong>.</p>
|
|
9
10
|
</div>
|
|
10
11
|
</div>
|
|
11
12
|
|
|
12
13
|
@if(cargando){
|
|
13
|
-
|
|
14
|
+
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
<div class="lista-actividad">
|
|
17
18
|
@if(actividad.length === 0 && !cargando) {
|
|
18
|
-
|
|
19
|
+
<p class="sin-datos">No hay actividad reciente.</p>
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
@for (item of actividad; track item.Consecutivo) {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
<div class="actividad-item">
|
|
24
|
+
<div class="item-icono" [ngClass]="{
|
|
24
25
|
'metodo-get': item.Metodo === 'GET',
|
|
25
26
|
'metodo-post': item.Metodo === 'POST',
|
|
26
27
|
'metodo-put': item.Metodo === 'PUT',
|
|
27
28
|
'metodo-delete': item.Metodo === 'DELETE'
|
|
28
29
|
}">
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
</span>
|
|
38
|
-
<span class="detalle-browser"><mat-icon>public</mat-icon> {{ item.Navegador }}</span>
|
|
39
|
-
<span class="detalle-ip"><mat-icon>lan</mat-icon> {{ item.IP }}</span>
|
|
40
|
-
<span class="detalle-pais"><mat-icon>location_on</mat-icon> {{ item.Pais }}</span>
|
|
41
|
-
</div>
|
|
30
|
+
<mat-icon [matTooltip]="item.Metodo">{{ getIcon(item.Metodo) }}</mat-icon>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="item-contenido">
|
|
33
|
+
<div class="item-titulo">
|
|
34
|
+
{{ item.URL }}
|
|
35
|
+
@if(esSesionActual(item.Token)){
|
|
36
|
+
<span class="badge-sesion-actual">Sesión actual</span>
|
|
37
|
+
}
|
|
42
38
|
</div>
|
|
43
|
-
<div class="item-
|
|
44
|
-
<span
|
|
45
|
-
|
|
39
|
+
<div class="item-detalles">
|
|
40
|
+
<span class="detalle-dispositivo">
|
|
41
|
+
<mat-icon>{{ item.Dispositivo === 'Móvil' ? 'smartphone' : (item.Dispositivo === 'Tablet' ? 'tablet' :
|
|
42
|
+
'laptop') }}</mat-icon>
|
|
43
|
+
{{ item.Dispositivo }}
|
|
44
|
+
</span>
|
|
45
|
+
<span class="detalle-browser"><mat-icon>public</mat-icon> {{ item.Navegador }}</span>
|
|
46
|
+
<span class="detalle-ip"><mat-icon>lan</mat-icon> {{ item.IP }}</span>
|
|
47
|
+
<span class="detalle-pais"><mat-icon>location_on</mat-icon> {{ item.Pais }}</span>
|
|
46
48
|
</div>
|
|
47
49
|
</div>
|
|
50
|
+
<div class="item-fecha">
|
|
51
|
+
<span>{{ item.Fecha | date: 'dd/MM/yyyy' }}</span>
|
|
52
|
+
<strong>{{ item.Fecha | date: 'HH:mm:ss' }}</strong>
|
|
53
|
+
<button mat-icon-button color="warn" matTooltip="Cerrar esta sesión" (click)="cerrarSesion(item.Token)">
|
|
54
|
+
<mat-icon>logout</mat-icon>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
48
58
|
}
|
|
49
59
|
</div>
|
|
50
|
-
</div>
|
|
60
|
+
</div>
|
|
@@ -4,26 +4,30 @@ import { HttpClient } from '@angular/common/http';
|
|
|
4
4
|
import { MatIconModule } from '@angular/material/icon';
|
|
5
5
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
6
6
|
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
7
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
7
8
|
import { DatosGlobalesService } from '../../../datos-globales.service';
|
|
8
9
|
|
|
9
10
|
@Component({
|
|
10
11
|
selector: 'app-gestion-actividad',
|
|
11
12
|
standalone: true,
|
|
12
|
-
imports: [CommonModule, MatIconModule, MatTooltipModule, MatProgressBarModule],
|
|
13
|
+
imports: [CommonModule, MatIconModule, MatTooltipModule, MatProgressBarModule, MatButtonModule],
|
|
13
14
|
templateUrl: './gestion-actividad.component.html',
|
|
14
15
|
styleUrls: ['./gestion-actividad.component.css']
|
|
15
16
|
})
|
|
16
17
|
export class GestionActividadComponent implements OnInit {
|
|
17
18
|
public actividad: any[] = [];
|
|
18
19
|
public cargando = false;
|
|
20
|
+
private tokenActual = '';
|
|
19
21
|
|
|
20
22
|
constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService) { }
|
|
21
23
|
|
|
22
24
|
ngOnInit(): void {
|
|
25
|
+
this.tokenActual = this.datosGlobalesService.ObtenerToken().split(' ')[1] || '';
|
|
23
26
|
this.obtenerActividad();
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
obtenerActividad(): void {
|
|
30
|
+
this.cargando = true;
|
|
27
31
|
this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/ListarActividades`).subscribe({
|
|
28
32
|
next: (datos: any) => {
|
|
29
33
|
this.actividad = datos.body;
|
|
@@ -36,17 +40,27 @@ export class GestionActividadComponent implements OnInit {
|
|
|
36
40
|
this.cargando = false;
|
|
37
41
|
}
|
|
38
42
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
cerrarSesion(token: string): void {
|
|
46
|
+
if (confirm('¿Está seguro de que desea cerrar esta sesión?')) {
|
|
47
|
+
this.http.delete(`${this.datosGlobalesService.ObtenerURL()}misc/cerrarSesionPorToken/${token}`).subscribe({
|
|
48
|
+
next: () => {
|
|
49
|
+
if (token === this.tokenActual) {
|
|
50
|
+
this.datosGlobalesService.RedirigirALogin();
|
|
51
|
+
} else {
|
|
52
|
+
this.obtenerActividad();
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
error: (error) => {
|
|
56
|
+
console.error('Error al cerrar sesión:', error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
esSesionActual(token: string): boolean {
|
|
63
|
+
return token === this.tokenActual;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
getIcon(metodo: string): string {
|
|
@@ -1,5 +1,240 @@
|
|
|
1
|
-
.
|
|
2
|
-
|
|
1
|
+
.contenedor-actividad {
|
|
2
|
+
background-color: transparent;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 10px;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.header-actividad {
|
|
8
|
+
background-color: white;
|
|
9
|
+
padding: 20px 24px;
|
|
10
|
+
border-radius: 12px;
|
|
11
|
+
margin-bottom: 20px;
|
|
12
|
+
border: 1px solid #e0e6ed;
|
|
13
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
14
|
+
text-align: left;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.header-actividad h2 {
|
|
18
|
+
margin: 0;
|
|
19
|
+
color: #002f6b;
|
|
20
|
+
font-size: 1.5rem;
|
|
21
|
+
font-weight: 700;
|
|
3
22
|
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 12px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.header-actividad h2 mat-icon {
|
|
28
|
+
font-size: 28px;
|
|
29
|
+
width: 28px;
|
|
30
|
+
height: 28px;
|
|
31
|
+
color: #0b4fce;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.header-actividad p {
|
|
35
|
+
margin: 8px 0 0 0;
|
|
36
|
+
color: #5f6368;
|
|
37
|
+
font-size: 0.95rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.mensaje-informativo {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 12px;
|
|
44
|
+
background-color: #e3f2fd;
|
|
45
|
+
border-left: 4px solid #1976d2;
|
|
46
|
+
padding: 12px 16px;
|
|
47
|
+
margin-top: 20px;
|
|
48
|
+
border-radius: 4px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.mensaje-informativo mat-icon {
|
|
52
|
+
color: #1976d2;
|
|
53
|
+
font-size: 24px;
|
|
54
|
+
width: 24px;
|
|
55
|
+
height: 24px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.mensaje-informativo p {
|
|
59
|
+
margin: 0 !important;
|
|
60
|
+
font-size: 13.5px !important;
|
|
61
|
+
color: #0d47a1 !important;
|
|
62
|
+
line-height: 1.4;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.mensaje-informativo strong {
|
|
66
|
+
color: #d32f2f;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.lista-actividad {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
gap: 12px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.actividad-item {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
padding: 16px;
|
|
79
|
+
background-color: white;
|
|
80
|
+
border: 1px solid #e0e6ed;
|
|
81
|
+
border-radius: 12px;
|
|
82
|
+
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
83
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
84
|
+
text-align: left;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.actividad-item:hover {
|
|
88
|
+
transform: translateY(-2px);
|
|
89
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
90
|
+
border-color: #0b4fce;
|
|
91
|
+
background-color: #f0f7ff;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.item-icono {
|
|
95
|
+
margin-right: 12px;
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
4
98
|
justify-content: center;
|
|
99
|
+
width: 40px;
|
|
100
|
+
height: 40px;
|
|
101
|
+
border-radius: 50%;
|
|
102
|
+
background-color: #f1f3f4;
|
|
103
|
+
flex-shrink: 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.item-icono mat-icon {
|
|
107
|
+
font-size: 20px;
|
|
108
|
+
width: 20px;
|
|
109
|
+
height: 20px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Colores por método */
|
|
113
|
+
.metodo-get {
|
|
114
|
+
color: #0069B4;
|
|
115
|
+
background-color: #e3f2fd;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.metodo-post {
|
|
119
|
+
color: #518a5f;
|
|
120
|
+
background-color: #e8f5e9;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.metodo-put {
|
|
124
|
+
color: #FFA757;
|
|
125
|
+
background-color: #fff3e0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.metodo-delete {
|
|
129
|
+
color: #F82617;
|
|
130
|
+
background-color: #ffebee;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.metodo-default {
|
|
134
|
+
color: #7d7d7d;
|
|
135
|
+
background-color: #f5f5f5;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.item-contenido {
|
|
139
|
+
flex: 1;
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
/* Evita que el flex desborde */
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.item-titulo {
|
|
147
|
+
font-size: 14px;
|
|
148
|
+
color: #002f6b;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
word-break: break-all;
|
|
151
|
+
margin-bottom: 2px;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
flex-wrap: wrap;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.badge-sesion-actual {
|
|
159
|
+
background-color: #e8f5e9;
|
|
160
|
+
color: #2e7d32;
|
|
161
|
+
font-size: 10px;
|
|
162
|
+
padding: 2px 8px;
|
|
163
|
+
border-radius: 12px;
|
|
164
|
+
border: 1px solid #c8e6c9;
|
|
165
|
+
text-transform: uppercase;
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.item-detalles {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-wrap: wrap;
|
|
173
|
+
gap: 8px;
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
color: #5f6368;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.item-detalles span {
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
gap: 3px;
|
|
182
|
+
white-space: nowrap;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.item-detalles mat-icon {
|
|
186
|
+
font-size: 14px;
|
|
187
|
+
width: 14px;
|
|
188
|
+
height: 14px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.detalle-dispositivo mat-icon {
|
|
192
|
+
color: #673ab7;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.detalle-browser mat-icon {
|
|
196
|
+
color: #5f6368;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.detalle-ip mat-icon {
|
|
200
|
+
color: #0069B4;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.detalle-pais mat-icon {
|
|
204
|
+
color: #F87600;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.item-fecha {
|
|
208
|
+
font-size: 11px;
|
|
209
|
+
color: #9aa0a6;
|
|
210
|
+
white-space: nowrap;
|
|
211
|
+
margin-left: 12px;
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
align-items: flex-end;
|
|
215
|
+
font-family: 'Roboto Mono', monospace;
|
|
216
|
+
flex-shrink: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
mat-progress-bar {
|
|
220
|
+
height: 4px;
|
|
221
|
+
border-radius: 2px;
|
|
222
|
+
margin-bottom: 10px;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@media (max-width: 600px) {
|
|
226
|
+
.actividad-item {
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
align-items: flex-start;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.item-icono {
|
|
232
|
+
margin-bottom: 12px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.item-fecha {
|
|
236
|
+
margin-left: 0;
|
|
237
|
+
margin-top: 12px;
|
|
238
|
+
align-items: flex-start;
|
|
239
|
+
}
|
|
5
240
|
}
|
|
@@ -1,22 +1,60 @@
|
|
|
1
|
-
<
|
|
1
|
+
<div class="contenedor-actividad">
|
|
2
|
+
<div class="header-actividad">
|
|
3
|
+
<h2><mat-icon>history</mat-icon> Actividad reciente de la cuenta</h2>
|
|
4
|
+
<p>Historial de acciones realizadas en el sistema</p>
|
|
2
5
|
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
</mat-form-field>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
</mat-dialog-content>
|
|
6
|
+
<div class="mensaje-informativo">
|
|
7
|
+
<mat-icon>info</mat-icon>
|
|
8
|
+
<p>Si tiene dudas sobre la información que ve en pantalla, por favor comparta una captura de pantalla al correo
|
|
9
|
+
<strong>soporte@utn.ac.cr</strong>.</p>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<button mat-button (click)="onClose()">{{ data.textoCerrar }}</button>
|
|
13
|
+
@if(cargando){
|
|
14
|
+
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
<div class="lista-actividad">
|
|
18
|
+
@if(actividad.length === 0 && !cargando) {
|
|
19
|
+
<p class="sin-datos">No hay actividad reciente.</p>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@for (item of actividad; track item.Consecutivo) {
|
|
23
|
+
<div class="actividad-item">
|
|
24
|
+
<div class="item-icono" [ngClass]="{
|
|
25
|
+
'metodo-get': item.Metodo === 'GET',
|
|
26
|
+
'metodo-post': item.Metodo === 'POST',
|
|
27
|
+
'metodo-put': item.Metodo === 'PUT',
|
|
28
|
+
'metodo-delete': item.Metodo === 'DELETE'
|
|
29
|
+
}">
|
|
30
|
+
<mat-icon [matTooltip]="item.Metodo">{{ getIcon(item.Metodo) }}</mat-icon>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="item-contenido">
|
|
33
|
+
<div class="item-titulo">
|
|
34
|
+
{{ item.URL }}
|
|
35
|
+
@if(esSesionActual(item.Token)){
|
|
36
|
+
<span class="badge-sesion-actual">Sesión actual</span>
|
|
37
|
+
}
|
|
38
|
+
</div>
|
|
39
|
+
<div class="item-detalles">
|
|
40
|
+
<span class="detalle-dispositivo">
|
|
41
|
+
<mat-icon>{{ item.Dispositivo === 'Móvil' ? 'smartphone' : (item.Dispositivo === 'Tablet' ? 'tablet' :
|
|
42
|
+
'laptop') }}</mat-icon>
|
|
43
|
+
{{ item.Dispositivo }}
|
|
44
|
+
</span>
|
|
45
|
+
<span class="detalle-browser"><mat-icon>public</mat-icon> {{ item.Navegador }}</span>
|
|
46
|
+
<span class="detalle-ip"><mat-icon>lan</mat-icon> {{ item.IP }}</span>
|
|
47
|
+
<span class="detalle-pais"><mat-icon>location_on</mat-icon> {{ item.Pais }}</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="item-fecha">
|
|
51
|
+
<span>{{ item.Fecha | date: 'dd/MM/yyyy' }}</span>
|
|
52
|
+
<strong>{{ item.Fecha | date: 'HH:mm:ss' }}</strong>
|
|
53
|
+
<button mat-icon-button color="warn" matTooltip="Cerrar esta sesión" (click)="cerrarSesion(item.Token)">
|
|
54
|
+
<mat-icon>logout</mat-icon>
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
package/templates/frontend/src/app/Componentes/Nucleo/mensaje-confirmacion/mensaje-confirmacion.ts
CHANGED
|
@@ -1,65 +1,86 @@
|
|
|
1
|
-
import { Component,
|
|
1
|
+
import { Component, OnInit } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { HttpClient } from '@angular/common/http';
|
|
4
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
5
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
6
|
+
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
|
2
7
|
import { MatButtonModule } from '@angular/material/button';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
MatDialogContent,
|
|
7
|
-
MatDialogRef,
|
|
8
|
-
MatDialogTitle
|
|
9
|
-
} from '@angular/material/dialog';
|
|
10
|
-
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
11
|
-
import { MatInputModule } from '@angular/material/input';
|
|
12
|
-
import { FormsModule } from '@angular/forms';
|
|
13
|
-
|
|
14
|
-
export interface DialogData {
|
|
15
|
-
titulo: string;
|
|
16
|
-
mensaje: string;
|
|
17
|
-
textoCerrar?: string;
|
|
18
|
-
textoAceptar?: string;
|
|
19
|
-
SolicitaMensaje?: string;
|
|
20
|
-
onClose?: () => void;
|
|
21
|
-
onAccept?: (MensajeBrindado: any) => void;
|
|
22
|
-
}
|
|
8
|
+
import { MatDialog } from '@angular/material/dialog';
|
|
9
|
+
import { MensajeConfirmacionComponent } from '../mensaje-confirmacion/mensaje-confirmacion';
|
|
10
|
+
import { DatosGlobalesService } from '../../../datos-globales.service';
|
|
23
11
|
|
|
24
12
|
@Component({
|
|
25
|
-
selector: 'app-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
MatDialogTitle,
|
|
31
|
-
MatDialogContent,
|
|
32
|
-
MatDialogActions,
|
|
33
|
-
MatFormFieldModule,
|
|
34
|
-
MatInputModule,
|
|
35
|
-
FormsModule
|
|
36
|
-
]
|
|
13
|
+
selector: 'app-gestion-actividad',
|
|
14
|
+
standalone: true,
|
|
15
|
+
imports: [CommonModule, MatIconModule, MatTooltipModule, MatProgressBarModule, MatButtonModule],
|
|
16
|
+
templateUrl: './gestion-actividad.component.html',
|
|
17
|
+
styleUrls: ['./gestion-actividad.component.css']
|
|
37
18
|
})
|
|
38
|
-
export class
|
|
19
|
+
export class GestionActividadComponent implements OnInit {
|
|
20
|
+
public actividad: any[] = [];
|
|
21
|
+
public cargando = false;
|
|
22
|
+
private tokenActual = '';
|
|
23
|
+
|
|
24
|
+
constructor(private http: HttpClient, private datosGlobalesService: DatosGlobalesService, private dialog: MatDialog) { }
|
|
39
25
|
|
|
40
|
-
|
|
26
|
+
ngOnInit(): void {
|
|
27
|
+
this.tokenActual = this.datosGlobalesService.ObtenerToken().split(' ')[1] || '';
|
|
28
|
+
this.obtenerActividad();
|
|
29
|
+
}
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
obtenerActividad(): void {
|
|
32
|
+
this.cargando = true;
|
|
33
|
+
this.http.get(`${this.datosGlobalesService.ObtenerURL()}misc/ListarActividades`).subscribe({
|
|
34
|
+
next: (datos: any) => {
|
|
35
|
+
this.actividad = datos.body;
|
|
36
|
+
},
|
|
37
|
+
error: (error) => {
|
|
38
|
+
console.error('Error al obtener actividad:', error);
|
|
39
|
+
this.cargando = false;
|
|
40
|
+
},
|
|
41
|
+
complete: () => {
|
|
42
|
+
this.cargando = false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
cerrarSesion(token: string): void {
|
|
48
|
+
this.dialog.open(MensajeConfirmacionComponent, {
|
|
49
|
+
data: {
|
|
50
|
+
titulo: 'Confirmación',
|
|
51
|
+
mensaje: '¿Está seguro de que desea cerrar esta sesión?',
|
|
52
|
+
textoCerrar: 'Cancelar',
|
|
53
|
+
textoAceptar: 'Cerrar sesión',
|
|
54
|
+
onClose: () => { },
|
|
55
|
+
onAccept: () => {
|
|
56
|
+
this.http.delete(`${this.datosGlobalesService.ObtenerURL()}misc/cerrarSesionPorToken/${token}`).subscribe({
|
|
57
|
+
next: () => {
|
|
58
|
+
if (token === this.tokenActual) {
|
|
59
|
+
this.datosGlobalesService.RedirigirALogin();
|
|
60
|
+
} else {
|
|
61
|
+
this.obtenerActividad();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
error: (error) => {
|
|
65
|
+
console.error('Error al cerrar sesión:', error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
50
71
|
}
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.data.onClose();
|
|
55
|
-
}
|
|
56
|
-
this.dialogRef.close();
|
|
73
|
+
esSesionActual(token: string): boolean {
|
|
74
|
+
return token === this.tokenActual;
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
getIcon(metodo: string): string {
|
|
78
|
+
switch (metodo) {
|
|
79
|
+
case 'GET': return 'search';
|
|
80
|
+
case 'POST': return 'add_circle';
|
|
81
|
+
case 'PUT': return 'edit';
|
|
82
|
+
case 'DELETE': return 'delete';
|
|
83
|
+
default: return 'help_outline';
|
|
62
84
|
}
|
|
63
|
-
this.dialogRef.close(this.MensajeBrindado);
|
|
64
85
|
}
|
|
65
86
|
}
|
|
@@ -3,6 +3,7 @@ import { provideRouter } from '@angular/router';
|
|
|
3
3
|
|
|
4
4
|
import { routes } from './app.routes';
|
|
5
5
|
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
|
|
6
|
+
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
|
6
7
|
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
|
7
8
|
import { MatPaginatorIntl } from '@angular/material/paginator';
|
|
8
9
|
import { PaginadorPersonalizado } from './Componentes/Nucleo/tabla/paginador-personalizado';
|
|
@@ -15,8 +16,10 @@ export const appConfig: ApplicationConfig = {
|
|
|
15
16
|
{ provide: MatPaginatorIntl, useClass: PaginadorPersonalizado },
|
|
16
17
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
|
17
18
|
provideRouter(routes),
|
|
18
|
-
provideHttpClient(withFetch()),
|
|
19
19
|
provideClientHydration(withEventReplay()),
|
|
20
|
+
provideAnimationsAsync(),
|
|
21
|
+
provideHttpClient(withFetch()),
|
|
22
|
+
provideClientHydration(),
|
|
20
23
|
provideCharts(withDefaultRegisterables()),
|
|
21
24
|
...AnalyticsModule.forRoot().providers!,
|
|
22
25
|
provideHttpClient(withInterceptors([AuthInterceptor]))
|