feat: control de acceso y visibilidad de funciones de plugins por rol (v1.24.0)
parent
5d206432a5
commit
b6007ecf8a
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -1,5 +1,16 @@
|
|||
# Changelog
|
||||
|
||||
## [1.24.0] - 2026-10-28
|
||||
|
||||
### Added
|
||||
- **Control de Acceso y Visibilidad de Funciones de Plugins por Rol (`PluginService`, `PluginSettingsComponent`, `ExecuteCommandComponent`)**:
|
||||
- Incorporada especificación declarativa de roles autorizados (`roles?: string[]`) en la interfaz `PluginCommand`.
|
||||
- Creada matriz interactiva de permisos en `PluginSettingsComponent` dentro de la nueva subpestaña *"Funciones y Roles"*, permitiendo a los administradores activar o revocar gráficamente el acceso a cada comando expuesto por cualquier plugin para cada rol del sistema.
|
||||
- Los roles disponibles se extraen dinámicamente desde `ogcore` (`GET /user-groups`), con respaldo seguro en los roles canónicos de OpenGnSys (`ROLE_SUPER_ADMIN`, `ROLE_ORGANIZATIONAL_UNIT_ADMIN`, `ROLE_ORGANIZATIONAL_UNIT_OPERATOR`, `ROLE_ORGANIZATIONAL_UNIT_MINIMAL`).
|
||||
- La personalización se persiste en la base de datos dentro de `configData.command_roles` de forma 100% desacoplada sin requerir alteraciones en el esquema backend.
|
||||
- En `PluginService.getPluginGroups()`, los comandos disponibles en los menús de acción (`ExecuteCommandComponent`) se filtran reactivamente en función del rol y permisos del usuario autenticado, ocultando totalmente aquellas opciones que no le correspondan (ej. un conserje con perfil básico solo visualiza Reservar, Liberar y Renovar puesto, permaneciendo invisibles las funciones de instalación o mantenimiento).
|
||||
- Total adhesión a la sintaxis moderna de control de flujo Angular (`@if`, `@for`) y diseño modular de estilos CSS sin atributos inline.
|
||||
|
||||
## [1.23.0] - 2026-10-27
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "og-webconsole",
|
||||
"version": "1.23.0",
|
||||
"version": "1.24.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
|
|
|
|||
|
|
@ -913,3 +913,138 @@
|
|||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
MATRIZ DE CONTROL DE ACCESO A FUNCIONES POR ROL
|
||||
========================================================================= */
|
||||
|
||||
.roles-intro-text {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 0.88rem;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.roles-matrix-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.roles-table-wrapper {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.roles-matrix-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.roles-matrix-table thead tr {
|
||||
background: #f8fafc;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.col-command-header {
|
||||
padding: 14px 18px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #475569;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.col-role-header {
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
min-width: 140px;
|
||||
border-left: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.role-header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.role-header-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.role-header-key {
|
||||
font-size: 0.70rem;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.command-role-row {
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.command-role-row:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.command-identity-cell {
|
||||
padding: 14px 18px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.command-badge-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.command-icon-box {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
color: #0284c7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.command-icon-box mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.command-details-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.command-title-text {
|
||||
font-size: 0.90rem;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.command-slug-subtext {
|
||||
font-size: 0.74rem;
|
||||
color: #64748b;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.role-checkbox-cell {
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
border-left: 1px solid #f1f5f9;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,32 +24,46 @@
|
|||
</div>
|
||||
|
||||
@if (plugin.enabled) {
|
||||
@if (plugin.recordsEndpoint) {
|
||||
@if (plugin.recordsEndpoint || (plugin.commands && plugin.commands.length > 0)) {
|
||||
<div class="subtabs-navigation-bar">
|
||||
<button
|
||||
type="button"
|
||||
class="subtab-nav-item"
|
||||
[class.active]="activeSubTab === 0"
|
||||
(click)="setSubTab(0)">
|
||||
[class.active]="activeSubTab === 'settings'"
|
||||
(click)="setSubTab('settings')">
|
||||
<mat-icon class="subtab-nav-icon">tune</mat-icon>
|
||||
<span>Configuración</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="subtab-nav-item"
|
||||
[class.active]="activeSubTab === 1"
|
||||
(click)="setSubTab(1)">
|
||||
<mat-icon class="subtab-nav-icon">history</mat-icon>
|
||||
<span>Historial y Registros</span>
|
||||
@if (recordsTotal > 0) {
|
||||
<span class="subtab-counter-badge">{{ recordsTotal }}</span>
|
||||
}
|
||||
</button>
|
||||
@if (plugin.commands && plugin.commands.length > 0) {
|
||||
<button
|
||||
type="button"
|
||||
class="subtab-nav-item"
|
||||
[class.active]="activeSubTab === 'permissions'"
|
||||
(click)="setSubTab('permissions')">
|
||||
<mat-icon class="subtab-nav-icon">admin_panel_settings</mat-icon>
|
||||
<span>Funciones y Roles</span>
|
||||
<span class="subtab-counter-badge">{{ plugin.commands.length }}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (plugin.recordsEndpoint) {
|
||||
<button
|
||||
type="button"
|
||||
class="subtab-nav-item"
|
||||
[class.active]="activeSubTab === 'records'"
|
||||
(click)="setSubTab('records')">
|
||||
<mat-icon class="subtab-nav-icon">history</mat-icon>
|
||||
<span>Historial y Registros</span>
|
||||
@if (recordsTotal > 0) {
|
||||
<span class="subtab-counter-badge">{{ recordsTotal }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (activeSubTab === 0) {
|
||||
@if (activeSubTab === 'settings') {
|
||||
<div class="settings-form-wrapper">
|
||||
<div class="section-header-row">
|
||||
<div class="section-title-box">
|
||||
|
|
@ -172,7 +186,80 @@
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else if (activeSubTab === 1 && plugin.recordsEndpoint) {
|
||||
} @else if (activeSubTab === 'permissions' && plugin.commands && plugin.commands.length > 0) {
|
||||
<div class="settings-form-wrapper">
|
||||
<div class="section-header-row">
|
||||
<div class="section-title-box">
|
||||
<mat-icon class="section-icon">admin_panel_settings</mat-icon>
|
||||
<h3 class="section-heading">Control de Acceso a Funciones por Rol</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-actions-bar top-actions">
|
||||
<button mat-stroked-button color="warn" (click)="resetRoles()" class="btn-reset">
|
||||
<mat-icon>restart_alt</mat-icon>
|
||||
Restaurar por defecto
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="saveRoles()" class="btn-save" [disabled]="!hasRoleChanges">
|
||||
<mat-icon>save</mat-icon>
|
||||
Guardar Permisos
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="roles-intro-text">
|
||||
Configura qué roles de usuario tienen autorización para visualizar y ejecutar cada una de las funciones expuestas por este plugin. Los usuarios que no pertenezcan a los roles seleccionados no verán la opción disponible en el menú de acciones de la consola.
|
||||
</p>
|
||||
|
||||
<div class="roles-matrix-card">
|
||||
<div class="roles-table-wrapper">
|
||||
<table class="roles-matrix-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-command-header">Función / Acción del Plugin</th>
|
||||
@for (role of availableRoles; track role.key) {
|
||||
<th class="col-role-header">
|
||||
<div class="role-header-content">
|
||||
<span class="role-header-name">{{ role.name }}</span>
|
||||
<span class="role-header-key">{{ role.shortCode || role.key }}</span>
|
||||
</div>
|
||||
</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (cmd of plugin.commands; track cmd.slug) {
|
||||
<tr class="command-role-row">
|
||||
<td class="command-identity-cell">
|
||||
<div class="command-badge-wrapper">
|
||||
<div class="command-icon-box">
|
||||
<mat-icon>{{ cmd.icon || 'play_circle' }}</mat-icon>
|
||||
</div>
|
||||
<div class="command-details-box">
|
||||
<span class="command-title-text">{{ getCommandLabel(cmd) }}</span>
|
||||
<span class="command-slug-subtext">{{ cmd.slug }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@for (role of availableRoles; track role.key) {
|
||||
<td class="role-checkbox-cell">
|
||||
<mat-checkbox
|
||||
[checked]="isRoleChecked(cmd.slug, role.key)"
|
||||
[disabled]="role.key === 'ROLE_SUPER_ADMIN'"
|
||||
(change)="toggleRole(cmd.slug, role.key, $event.checked)"
|
||||
color="primary"
|
||||
[title]="role.key === 'ROLE_SUPER_ADMIN' ? 'Super Admin siempre tiene acceso total' : ('Permitir a ' + role.name)">
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (activeSubTab === 'records' && plugin.recordsEndpoint) {
|
||||
<div class="records-wrapper">
|
||||
<div class="records-header-row">
|
||||
<div class="records-title-group">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, ChangeDetectorRef, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { PluginManifest, PluginService, SchemaField } from '@services/plugin.service';
|
||||
import { PluginManifest, PluginCommand, PluginService, SchemaField, RoleDefinition } from '@services/plugin.service';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
|
|
@ -18,7 +19,10 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
schema: SchemaField[] = [];
|
||||
configData: Record<string, any> = {};
|
||||
editingScripts: Record<string, boolean> = {};
|
||||
activeSubTab: number = 0; // 0: Configuración, 1: Historial y Registros
|
||||
activeSubTab: string = 'settings'; // 'settings' | 'permissions' | 'records'
|
||||
availableRoles: RoleDefinition[] = [];
|
||||
commandRoles: Record<string, string[]> = {};
|
||||
hasRoleChanges: boolean = false;
|
||||
records: any[] = [];
|
||||
recordsTotal: number = 0;
|
||||
recordsPage: number = 1;
|
||||
|
|
@ -32,6 +36,7 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
private sanitizer = inject(DomSanitizer);
|
||||
private http = inject(HttpClient);
|
||||
private configService = inject(ConfigService);
|
||||
private translate = inject(TranslateService);
|
||||
|
||||
constructor(
|
||||
private pluginService: PluginService,
|
||||
|
|
@ -55,7 +60,8 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.recordsTotal = 0;
|
||||
this.recordsPage = 1;
|
||||
this.recordsSearch = '';
|
||||
this.activeSubTab = 0;
|
||||
this.activeSubTab = 'settings';
|
||||
this.hasRoleChanges = false;
|
||||
this.loadPluginData();
|
||||
}
|
||||
}
|
||||
|
|
@ -66,13 +72,85 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
if (this.plugin) {
|
||||
this.schema = this.plugin.schema;
|
||||
this.configData = this.pluginService.getPluginConfig(this.pluginName);
|
||||
if (this.activeSubTab === 1 && this.plugin.recordsEndpoint) {
|
||||
|
||||
// Cargar catálogo de roles y matriz de permisos de comandos
|
||||
this.pluginService.getUserRoles().subscribe(roles => {
|
||||
this.availableRoles = roles;
|
||||
this.initCommandRoles();
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
|
||||
if (this.activeSubTab === 'records' && this.plugin.recordsEndpoint) {
|
||||
this.loadRecords(this.recordsPage);
|
||||
}
|
||||
}
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
private initCommandRoles(): void {
|
||||
if (!this.plugin || !this.plugin.commands) return;
|
||||
const map: Record<string, string[]> = {};
|
||||
for (const cmd of this.plugin.commands) {
|
||||
map[cmd.slug] = this.pluginService.getCommandRoles(this.pluginName, cmd.slug);
|
||||
}
|
||||
this.commandRoles = map;
|
||||
this.hasRoleChanges = false;
|
||||
}
|
||||
|
||||
getCommandLabel(cmd: PluginCommand): string {
|
||||
const key = cmd.translationKey || cmd.labelKey;
|
||||
if (key) {
|
||||
const trans = this.translate.instant(key);
|
||||
if (trans && trans !== key) {
|
||||
return trans;
|
||||
}
|
||||
}
|
||||
return cmd.label || cmd.slug;
|
||||
}
|
||||
|
||||
isRoleChecked(commandSlug: string, roleKey: string): boolean {
|
||||
const roles = this.commandRoles[commandSlug] || [];
|
||||
return roles.includes(roleKey);
|
||||
}
|
||||
|
||||
toggleRole(commandSlug: string, roleKey: string, checked: boolean): void {
|
||||
const current = [...(this.commandRoles[commandSlug] || [])];
|
||||
if (checked) {
|
||||
if (!current.includes(roleKey)) {
|
||||
current.push(roleKey);
|
||||
}
|
||||
} else {
|
||||
const idx = current.indexOf(roleKey);
|
||||
if (idx !== -1) {
|
||||
current.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
this.commandRoles[commandSlug] = current;
|
||||
this.hasRoleChanges = true;
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
saveRoles(): void {
|
||||
if (!this.pluginName) return;
|
||||
this.pluginService.savePluginCommandRoles(this.pluginName, this.commandRoles);
|
||||
this.hasRoleChanges = false;
|
||||
this.toast.success('Permisos de funciones por rol actualizados correctamente', this.plugin?.title || 'Plugin');
|
||||
}
|
||||
|
||||
resetRoles(): void {
|
||||
if (!this.plugin || !this.plugin.commands) return;
|
||||
const map: Record<string, string[]> = {};
|
||||
for (const cmd of this.plugin.commands) {
|
||||
map[cmd.slug] = cmd.roles && Array.isArray(cmd.roles)
|
||||
? [...cmd.roles]
|
||||
: this.availableRoles.map(r => r.key);
|
||||
}
|
||||
this.commandRoles = map;
|
||||
this.hasRoleChanges = true;
|
||||
this.toast.info('Permisos restaurados a valores por defecto del plugin', this.plugin?.title || 'Plugin');
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
toggleEnabled(checked: boolean): void {
|
||||
if (!this.pluginName) return;
|
||||
if (this.plugin) {
|
||||
|
|
@ -201,9 +279,9 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.toast.info('Configuración restaurada a valores por defecto', this.plugin?.title || 'Plugin');
|
||||
}
|
||||
|
||||
setSubTab(tab: number): void {
|
||||
setSubTab(tab: string): void {
|
||||
this.activeSubTab = tab;
|
||||
if (tab === 1 && this.records.length === 0 && this.plugin?.recordsEndpoint) {
|
||||
if (tab === 'records' && this.records.length === 0 && this.plugin?.recordsEndpoint) {
|
||||
this.loadRecords(1);
|
||||
}
|
||||
this.cdr.markForCheck();
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export class AuthService {
|
|||
}
|
||||
|
||||
/** Roles básicos que aparecen en el token */
|
||||
private get roles(): string[] {
|
||||
public get roles(): string[] {
|
||||
return this.tokenPayload?.roles || [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { BehaviorSubject, Observable, forkJoin, of } from 'rxjs';
|
|||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConfigService } from './config.service';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
export interface CommandParam {
|
||||
key: string;
|
||||
|
|
@ -27,6 +28,13 @@ export interface PluginCommand {
|
|||
params?: CommandParam[];
|
||||
confirm?: string;
|
||||
confirmKey?: string;
|
||||
roles?: string[]; // Roles autorizados por defecto (ej. ['ROLE_SUPER_ADMIN', 'ROLE_ORGANIZATIONAL_UNIT_ADMIN'])
|
||||
}
|
||||
|
||||
export interface RoleDefinition {
|
||||
key: string; // Clave del rol Symfony (ej: 'ROLE_SUPER_ADMIN')
|
||||
name: string; // Nombre descriptivo (ej: 'Super Admin')
|
||||
shortCode?: string;// Código corto usado en frontend (ej: 'super-admin')
|
||||
}
|
||||
|
||||
export interface PluginBadgeRule {
|
||||
|
|
@ -105,6 +113,7 @@ export class PluginService {
|
|||
private http = inject(HttpClient);
|
||||
private configService = inject(ConfigService);
|
||||
private translateService = inject(TranslateService);
|
||||
private authService = inject(AuthService);
|
||||
|
||||
private allPlugins: PluginManifest[] = [];
|
||||
private activePlugins: PluginManifest[] = [];
|
||||
|
|
@ -444,12 +453,9 @@ export class PluginService {
|
|||
.filter(p => p.commands && p.commands.length > 0)
|
||||
.map(p => {
|
||||
const translatedTitle = p.titleKey ? this.translateService.instant(p.titleKey) : p.title;
|
||||
return {
|
||||
plugin: {
|
||||
...p,
|
||||
title: (translatedTitle && translatedTitle !== p.titleKey) ? translatedTitle : p.title
|
||||
},
|
||||
commands: p.commands.map(cmd => {
|
||||
const filteredCommands = p.commands
|
||||
.filter(cmd => this.isCommandAllowedForUser(cmd, p.name))
|
||||
.map(cmd => {
|
||||
const key = cmd.translationKey || cmd.labelKey;
|
||||
if (key) {
|
||||
const trans = this.translateService.instant(key);
|
||||
|
|
@ -458,9 +464,143 @@ export class PluginService {
|
|||
}
|
||||
}
|
||||
return cmd;
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
plugin: {
|
||||
...p,
|
||||
title: (translatedTitle && translatedTitle !== p.titleKey) ? translatedTitle : p.title
|
||||
},
|
||||
commands: filteredCommands
|
||||
};
|
||||
});
|
||||
})
|
||||
.filter(group => group.commands.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprueba si el usuario autenticado tiene permiso para ejecutar un comando específico.
|
||||
* Jerarquía: configData.command_roles[slug] > command.roles > público (todos los roles)
|
||||
*/
|
||||
isCommandAllowedForUser(command: PluginCommand, pluginName: string): boolean {
|
||||
if (this.authService.isSuperAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const config = this.getPluginConfig(pluginName);
|
||||
const commandRolesConfig = config?.['command_roles'] || {};
|
||||
let allowedRoles: string[] | undefined = commandRolesConfig[command.slug];
|
||||
|
||||
if (!allowedRoles || !Array.isArray(allowedRoles)) {
|
||||
allowedRoles = command.roles;
|
||||
}
|
||||
|
||||
// Si ni en configuración ni en el manifiesto se especifican roles, la función es pública
|
||||
if (!allowedRoles || allowedRoles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userRoles = (this.authService.roles || []).map(r => r.toUpperCase());
|
||||
const currentCategory = (this.authService.getCurrentRole() || '').toLowerCase();
|
||||
|
||||
return allowedRoles.some(role => {
|
||||
const rUpper = role.toUpperCase();
|
||||
const rLower = role.toLowerCase();
|
||||
|
||||
return userRoles.includes(rUpper) ||
|
||||
rLower === currentCategory ||
|
||||
(rUpper === 'ROLE_SUPER_ADMIN' && this.authService.isSuperAdmin) ||
|
||||
(rUpper === 'ROLE_ORGANIZATIONAL_UNIT_ADMIN' && this.authService.isOUAdmin) ||
|
||||
(rUpper === 'ROLE_ORGANIZATIONAL_UNIT_OPERATOR' && this.authService.isOUOperator) ||
|
||||
(rUpper === 'ROLE_ORGANIZATIONAL_UNIT_MINIMAL' && this.authService.isOUMinimal);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la lista de roles/grupos disponibles en el sistema consultando a ogCore,
|
||||
* con fallback seguro a los roles canónicos de OpenGnSys.
|
||||
*/
|
||||
getUserRoles(): Observable<RoleDefinition[]> {
|
||||
const apiUrl = this.configService.apiUrl;
|
||||
const defaultRoles: RoleDefinition[] = [
|
||||
{ key: 'ROLE_SUPER_ADMIN', name: 'Super Admin', shortCode: 'super-admin' },
|
||||
{ key: 'ROLE_ORGANIZATIONAL_UNIT_ADMIN', name: 'Admin de aulas', shortCode: 'ou-admin' },
|
||||
{ key: 'ROLE_ORGANIZATIONAL_UNIT_OPERATOR', name: 'Operador de aulas', shortCode: 'ou-operator' },
|
||||
{ key: 'ROLE_ORGANIZATIONAL_UNIT_MINIMAL', name: 'Usuario básico de aulas', shortCode: 'ou-minimal' },
|
||||
];
|
||||
|
||||
if (!apiUrl) {
|
||||
return of(defaultRoles);
|
||||
}
|
||||
|
||||
return this.http.get<any>(`${apiUrl}/user-groups?page=1&itemsPerPage=1000`).pipe(
|
||||
map(res => {
|
||||
const groups = res?.['hydra:member'] || (Array.isArray(res) ? res : []);
|
||||
if (!Array.isArray(groups) || groups.length === 0) {
|
||||
return defaultRoles;
|
||||
}
|
||||
|
||||
const roles: RoleDefinition[] = [];
|
||||
const seenKeys = new Set<string>();
|
||||
|
||||
// Descubrir roles desde los grupos registrados en la base de datos
|
||||
for (const g of groups) {
|
||||
const perms = Array.isArray(g.permissions) ? g.permissions : [];
|
||||
for (const perm of perms) {
|
||||
if (!seenKeys.has(perm)) {
|
||||
seenKeys.add(perm);
|
||||
const matchedDef = defaultRoles.find(dr => dr.key === perm);
|
||||
roles.push({
|
||||
key: perm,
|
||||
name: g.name || matchedDef?.name || perm,
|
||||
shortCode: matchedDef?.shortCode
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Garantizar que todos los roles canónicos estándar estén presentes
|
||||
for (const dr of defaultRoles) {
|
||||
if (!seenKeys.has(dr.key)) {
|
||||
roles.push(dr);
|
||||
}
|
||||
}
|
||||
|
||||
return roles;
|
||||
}),
|
||||
catchError(() => of(defaultRoles))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve los roles actualmente asignados a un comando del plugin.
|
||||
*/
|
||||
getCommandRoles(pluginName: string, commandSlug: string): string[] {
|
||||
const plugin = this.getPlugin(pluginName);
|
||||
const cmd = plugin?.commands?.find(c => c.slug === commandSlug);
|
||||
const config = this.getPluginConfig(pluginName);
|
||||
const commandRolesConfig = config?.['command_roles'] || {};
|
||||
|
||||
if (commandRolesConfig[commandSlug] && Array.isArray(commandRolesConfig[commandSlug])) {
|
||||
return [...commandRolesConfig[commandSlug]];
|
||||
}
|
||||
|
||||
if (cmd?.roles && Array.isArray(cmd.roles)) {
|
||||
return [...cmd.roles];
|
||||
}
|
||||
|
||||
return ['ROLE_SUPER_ADMIN', 'ROLE_ORGANIZATIONAL_UNIT_ADMIN', 'ROLE_ORGANIZATIONAL_UNIT_OPERATOR', 'ROLE_ORGANIZATIONAL_UNIT_MINIMAL'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Persiste la matriz de roles configurados para los comandos de un plugin.
|
||||
*/
|
||||
savePluginCommandRoles(pluginName: string, commandRoles: Record<string, string[]>): void {
|
||||
const currentConfig = this.getPluginConfig(pluginName);
|
||||
const updatedConfig = {
|
||||
...currentConfig,
|
||||
command_roles: commandRoles
|
||||
};
|
||||
this.savePluginConfig(pluginName, updatedConfig);
|
||||
}
|
||||
|
||||
getCommands(): PluginCommand[] {
|
||||
|
|
|
|||
Loading…
Reference in New Issue