diff --git a/CHANGELOG.md b/CHANGELOG.md index b2c2777..79c6d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [1.25.1] - 2026-11-01 + +### Refactored +- **Modularización y Optimización de Arquitectura en `PluginService`**: + - Descompuesto el monolito de ~900 líneas en submódulos especializados bajo `src/app/services/plugin/`: + - `plugin.types.ts`: Modelos, interfaces tipadas y estructuras de datos (`PluginCommand`, `RoleDefinition`, `PluginBadgeRule`, `PluginManifest`, etc.). + - `plugin-state.service.ts`: Manejo reactivo del estado dinámico de plugins en memoria, caché de metadatos contextuales y suscripción a eventos en tiempo real vía Mercure (Server-Sent Events). + - `plugin-roles.service.ts`: Comprobación de roles y permisos granulares de usuarios y comandos, jerarquía de acceso y sincronización con `ogCore`. + - `plugin-badge.service.ts`: Resolución desacoplada de badges visuales e internacionalización de tooltips dinámicos con metadatos. + - Eliminada la duplicación completa (~100 líneas) entre `getClientBadges()` y `getPartitionBadges()`, unificando la lógica de interpolación y renderizado en una función auxiliar común (`buildBadge`). + - `PluginService` actúa ahora como una fachada limpia, liviana y 100% retrocompatible (~280 líneas), preservando sin alteraciones todas las firmas públicas y re-exportando los tipos para los componentes consumidores. + ## [1.25.0] - 2026-11-01 ### Added diff --git a/ogWebconsole/package.json b/ogWebconsole/package.json index 1d84606..b705791 100644 --- a/ogWebconsole/package.json +++ b/ogWebconsole/package.json @@ -1,6 +1,6 @@ { "name": "og-webconsole", - "version": "1.25.0", + "version": "1.25.1", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/ogWebconsole/src/app/services/plugin.service.ts b/ogWebconsole/src/app/services/plugin.service.ts index 5479a05..c1510e8 100644 --- a/ogWebconsole/src/app/services/plugin.service.ts +++ b/ogWebconsole/src/app/services/plugin.service.ts @@ -1,120 +1,23 @@ -import { Injectable, NgZone, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable, Subject, forkJoin, of } from 'rxjs'; +import { BehaviorSubject, forkJoin, Observable, 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'; +import { ConfigService } from '@services/config.service'; -export interface CommandParam { - key: string; - label: string; - labelKey?: string; - type: 'text' | 'number' | 'select' | 'checkbox'; - options?: string[]; - default?: any; - required?: boolean; -} +import { + PluginCommand, + RoleDefinition, + PluginBadge, + PluginManifest, + PluginGroup +} from './plugin/plugin.types'; +import { PluginStateService } from './plugin/plugin-state.service'; +import { PluginBadgeService } from './plugin/plugin-badge.service'; +import { PluginRolesService } from './plugin/plugin-roles.service'; -export interface PluginCommand { - slug: string; - translationKey?: string; - labelKey?: string; - label: string; - icon: string; - disabled: boolean; - endpoint?: string; - os?: string[]; // Ej: ['windows'], ['linux'] - params?: CommandParam[]; - confirm?: string; - confirmKey?: string; - roles?: string[]; // Roles autorizados por defecto (ej. ['ROLE_SUPER_ADMIN', 'ROLE_ORGANIZATIONAL_UNIT_ADMIN']) - requireStatus?: string[]; // Estados requeridos del plugin para habilitar el comando - excludeStatus?: string[]; // Estados del plugin que deshabilitan el comando -} - -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 PluginBadgeField { - key: string; - prefix?: string; - suffix?: string; -} - -export interface PluginBadgeRule { - status: string; - type: string; - icon: string; - label: string; - labelKey?: string; - tooltip: string; - tooltipKey?: string; - tooltipFields?: PluginBadgeField[]; -} - -export interface PluginBadge { - id: string; // Plugin name - type: string; - icon: string; - label: string; - tooltip: string; -} - -export interface FieldAction { - id: string; - label: string; - icon?: string; - endpoint: string; -} - -export interface SchemaField { - key: string; - label: string; - labelKey?: string; - description?: string; - descriptionKey?: string; - type: 'text' | 'number' | 'select' | 'checkbox' | 'textarea'; - language?: string; - options?: string[]; - default: any; - action?: FieldAction; -} - -export interface RecordColumn { - key: string; - label: string; - labelKey?: string; -} - -export interface PluginManifest { - name: string; - title: string; - titleKey?: string; - version: string; - description: string; - descriptionKey?: string; - author: string; - icon: string; - enabled: boolean; - commands: PluginCommand[]; - statusEndpoint?: string; - recordsEndpoint?: string; - recordsColumns?: RecordColumn[]; - badges?: PluginBadgeRule[]; - i18n?: Record>; - schema: SchemaField[]; - defaultConfig?: Record; - config?: Record; -} - -export interface PluginGroup { - plugin: PluginManifest; - commands: PluginCommand[]; -} +// Re-exportar todos los tipos para compatibilidad transparente con componentes existentes +export * from './plugin/plugin.types'; @Injectable({ providedIn: 'root' @@ -123,26 +26,20 @@ export class PluginService { private http = inject(HttpClient); private configService = inject(ConfigService); private translateService = inject(TranslateService); - private authService = inject(AuthService); - private ngZone = inject(NgZone); + private stateService = inject(PluginStateService); + private badgeService = inject(PluginBadgeService); + private rolesService = inject(PluginRolesService); private allPlugins: PluginManifest[] = []; private activePlugins: PluginManifest[] = []; private pluginsSubject = new BehaviorSubject([]); public plugins$ = this.pluginsSubject.asObservable(); - private statusUpdatesSubject = new Subject<{ plugin: string; clientId?: string; status: string; metadata?: any }>(); - public statusUpdates$ = this.statusUpdatesSubject.asObservable(); - - // Dynamic state cache: key is `${pluginName}:${clientIdOrUuid}` -> status string - private clientPluginStatusMap = new Map(); - // Dynamic metadata cache: key is `${pluginName}:${clientIdOrUuid}` -> extra attributes (e.g. dni, remaining_minutes) - private clientPluginMetadataMap = new Map(); - private mercureEventSource: EventSource | null = null; + public statusUpdates$ = this.stateService.statusUpdates$; constructor() { this.refreshPlugins().subscribe(); - this.initMercureSubscription(); + this.stateService.initMercureSubscription(); // Re-aplicar traducciones dinámicas al cambiar de idioma this.translateService.onLangChange.subscribe((event) => { @@ -150,158 +47,50 @@ export class PluginService { }); } - private getStatusKey(pluginName: string, idOrUuid: string | number): string { - return `${pluginName}:${idOrUuid}`; - } - - private getPartitionStatusKey(pluginName: string, idOrUuid: string | number, disk: number, partition: number): string { - return `${pluginName}:${idOrUuid}:${disk}:${partition}`; - } + // --- Gestión de estado delegado a PluginStateService --- public setPluginStatus(pluginName: string, idOrUuid: string | number, status: string): void { - this.clientPluginStatusMap.set(this.getStatusKey(pluginName, idOrUuid), status); + this.stateService.setPluginStatus(pluginName, idOrUuid, status); } public getPluginStatus(pluginName: string, idOrUuid: string | number): string | undefined { - return this.clientPluginStatusMap.get(this.getStatusKey(pluginName, idOrUuid)); + return this.stateService.getPluginStatus(pluginName, idOrUuid); } public setPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number, status: string): void { - this.clientPluginStatusMap.set(this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition), status); + this.stateService.setPartitionPluginStatus(pluginName, idOrUuid, disk, partition, status); } public getPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): string | undefined { - return this.clientPluginStatusMap.get(this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition)); + return this.stateService.getPartitionPluginStatus(pluginName, idOrUuid, disk, partition); } public clearPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): void { - const key = this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition); - this.clientPluginStatusMap.delete(key); - this.clientPluginMetadataMap.delete(key); + this.stateService.clearPartitionPluginStatus(pluginName, idOrUuid, disk, partition); } public clearPluginStatus(pluginName: string, idOrUuid: string | number): void { - const globalKey = this.getStatusKey(pluginName, idOrUuid); - this.clientPluginStatusMap.delete(globalKey); - this.clientPluginMetadataMap.delete(globalKey); - - const prefix = `${pluginName}:${idOrUuid}:`; - for (const key of Array.from(this.clientPluginStatusMap.keys())) { - if (key.startsWith(prefix)) { - this.clientPluginStatusMap.delete(key); - } - } - for (const key of Array.from(this.clientPluginMetadataMap.keys())) { - if (key.startsWith(prefix)) { - this.clientPluginMetadataMap.delete(key); - } - } + this.stateService.clearPluginStatus(pluginName, idOrUuid); } public setPluginMetadata(pluginName: string, idOrUuid: string | number, metadata: any, disk?: number, partition?: number): void { - const key = (disk !== undefined && partition !== undefined) - ? this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition) - : this.getStatusKey(pluginName, idOrUuid); - const existing = this.clientPluginMetadataMap.get(key) || {}; - this.clientPluginMetadataMap.set(key, { ...existing, ...metadata }); + this.stateService.setPluginMetadata(pluginName, idOrUuid, metadata, disk, partition); } public getPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): any { - const key = (disk !== undefined && partition !== undefined) - ? this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition) - : this.getStatusKey(pluginName, idOrUuid); - return this.clientPluginMetadataMap.get(key); + return this.stateService.getPluginMetadata(pluginName, idOrUuid, disk, partition); } public clearPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): void { - const key = (disk !== undefined && partition !== undefined) - ? this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition) - : this.getStatusKey(pluginName, idOrUuid); - this.clientPluginMetadataMap.delete(key); + this.stateService.clearPluginMetadata(pluginName, idOrUuid, disk, partition); } - private initMercureSubscription(): void { - const mercureUrl = this.configService.mercureUrl; - if (!mercureUrl || this.mercureEventSource) { - return; - } - - try { - this.mercureEventSource = new EventSource(`${mercureUrl}?topic=` + encodeURIComponent('plugins')); - this.mercureEventSource.onmessage = (event) => { - this.ngZone.run(() => { - try { - const data = JSON.parse(event.data); - if (data && data.plugin) { - const entityId = data.clientId !== undefined && data.clientId !== null ? String(data.clientId) : null; - const entityUuid = data.clientUuid ? String(data.clientUuid) : null; - const rawDisk = data.disk ?? data.disk_number ?? data.diskNumber; - const rawPart = data.partition ?? data.partition_number ?? data.partitionNumber; - const hasPartition = rawDisk !== undefined && rawDisk !== null && rawPart !== undefined && rawPart !== null; - const disk = hasPartition ? Number(rawDisk) : undefined; - const partition = hasPartition ? Number(rawPart) : undefined; - - // 1. Estado a nivel de partición - if (hasPartition && disk !== undefined && partition !== undefined) { - const partStatus = data.status; - if (partStatus === 'NONE') { - if (entityId) this.clearPartitionPluginStatus(data.plugin, entityId, disk, partition); - if (entityUuid) this.clearPartitionPluginStatus(data.plugin, entityUuid, disk, partition); - } else if (partStatus) { - if (entityId) this.setPartitionPluginStatus(data.plugin, entityId, disk, partition, partStatus); - if (entityUuid) this.setPartitionPluginStatus(data.plugin, entityUuid, disk, partition, partStatus); - } - } - - // 2. Estado general del cliente - const generalStatus = data.clientStatus !== undefined - ? data.clientStatus - : (data.client_status !== undefined ? data.client_status : (!hasPartition ? data.status : null)); - - if (generalStatus !== null) { - if (generalStatus === 'NONE') { - if (entityId) { - this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, entityId)); - this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, entityId)); - } - if (entityUuid) { - this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, entityUuid)); - this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, entityUuid)); - } - } else { - if (entityId) this.setPluginStatus(data.plugin, entityId, generalStatus); - if (entityUuid) this.setPluginStatus(data.plugin, entityUuid, generalStatus); - } - } - - // Metadatos contextuales arbitrarios - const metadata: any = {}; - for (const [k, v] of Object.entries(data)) { - if (!['plugin', 'status', 'clientStatus', 'client_status', 'clientId', 'clientUuid', 'disk', 'disk_number', 'diskNumber', 'partition', 'partition_number', 'partitionNumber', 'action', 'timestamp', 'subtarget'].includes(k) && v !== null && v !== undefined) { - metadata[k] = v; - } - } - const hasMeta = Object.keys(metadata).length > 0; - if (hasMeta) { - if (entityId) this.setPluginMetadata(data.plugin, entityId, metadata, disk, partition); - if (entityUuid) this.setPluginMetadata(data.plugin, entityUuid, metadata, disk, partition); - } - - this.statusUpdatesSubject.next({ - plugin: data.plugin, - clientId: (entityId || entityUuid) ?? undefined, - status: data.status, - metadata - }); - } - } catch { - } - }); - }; - } catch { - } + public loadActivePluginsStatus(): Observable { + return this.stateService.loadActivePluginsStatus(this.activePlugins); } + // --- Registro, ciclo de vida e i18n de Plugins --- + refreshPlugins(): Observable { const apiUrl = this.configService.apiUrl; if (!apiUrl) { @@ -379,84 +168,6 @@ export class PluginService { } } - loadActivePluginsStatus(): Observable { - this.initMercureSubscription(); - const apiUrl = this.configService.apiUrl; - if (!apiUrl || this.activePlugins.length === 0) { - return of(undefined); - } - - const pluginsWithStatus = this.activePlugins.filter(p => !!p.statusEndpoint); - if (pluginsWithStatus.length === 0) { - return of(undefined); - } - - const requests = pluginsWithStatus.map(plugin => { - const endpoint = plugin.statusEndpoint!.startsWith('http') - ? plugin.statusEndpoint! - : `${apiUrl.replace(/\/api\/?$/, '')}${plugin.statusEndpoint!.startsWith('/') ? '' : '/'}${plugin.statusEndpoint}`; - - return this.http.get(endpoint).pipe( - catchError(() => of([])), - tap(rows => { - if (Array.isArray(rows)) { - for (const row of rows) { - const status = row.status; - const id = row.client_id ?? row.clientId ?? row.id; - const uuid = row.client_uuid ?? row.clientUuid ?? row.uuid; - const rawDisk = row.disk_number ?? row.diskNumber ?? row.disk; - const rawPart = row.partition_number ?? row.partitionNumber ?? row.partition; - const hasPartition = rawDisk !== undefined && rawDisk !== null && rawPart !== undefined && rawPart !== null; - const disk = hasPartition ? Number(rawDisk) : undefined; - const partition = hasPartition ? Number(rawPart) : undefined; - const clientStatus = row.client_status ?? row.clientStatus ?? (!hasPartition ? status : null); - - // Extraer metadatos arbitrarios (dni, remaining_minutes, etc.) - const metadata: any = {}; - for (const [k, v] of Object.entries(row)) { - if (!['status', 'client_status', 'clientStatus', 'client_id', 'clientId', 'id', 'client_uuid', 'clientUuid', 'uuid', 'disk_number', 'diskNumber', 'disk', 'partition_number', 'partitionNumber', 'partition', 'subtarget'].includes(k) && v !== null && v !== undefined) { - metadata[k] = v; - } - } - const hasMeta = Object.keys(metadata).length > 0; - - const idStr = id !== undefined && id !== null ? String(id) : null; - if (idStr) { - if (hasPartition && disk !== undefined && partition !== undefined && status && status !== 'NONE') { - this.setPartitionPluginStatus(plugin.name, idStr, disk, partition, status); - } - if (clientStatus && clientStatus !== 'NONE') { - this.setPluginStatus(plugin.name, idStr, clientStatus); - } - if (hasMeta) { - this.setPluginMetadata(plugin.name, idStr, metadata, disk, partition); - } - } - if (uuid) { - if (hasPartition && disk !== undefined && partition !== undefined && status && status !== 'NONE') { - this.setPartitionPluginStatus(plugin.name, uuid, disk, partition, status); - } - if (clientStatus && clientStatus !== 'NONE') { - this.setPluginStatus(plugin.name, uuid, clientStatus); - } - if (hasMeta) { - this.setPluginMetadata(plugin.name, uuid, metadata, disk, partition); - } - } - } - } - }) - ); - }); - - return forkJoin(requests).pipe( - tap(() => { - this.statusUpdatesSubject.next({ plugin: 'all', status: 'REFRESHED' }); - }), - map(() => undefined) - ); - } - getPlugins(): PluginManifest[] { return this.activePlugins.filter(p => p.enabled); } @@ -510,132 +221,6 @@ export class PluginService { .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 { - 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(`${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(); - - // 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): void { - const currentConfig = this.getPluginConfig(pluginName); - const updatedConfig = { - ...currentConfig, - command_roles: commandRoles - }; - this.savePluginConfig(pluginName, updatedConfig); - } - getCommands(): PluginCommand[] { const commands: PluginCommand[] = []; for (const group of this.getPluginGroups()) { @@ -703,155 +288,41 @@ export class PluginService { return true; } + // --- Roles y permisos delegados a PluginRolesService --- + + isCommandAllowedForUser(command: PluginCommand, pluginName: string): boolean { + return this.rolesService.isCommandAllowedForUser(command, this.getPluginConfig(pluginName)); + } + + getUserRoles(): Observable { + return this.rolesService.getUserRoles(); + } + + getCommandRoles(pluginName: string, commandSlug: string): string[] { + return this.rolesService.getCommandRoles(this.getPlugin(pluginName), commandSlug, this.getPluginConfig(pluginName)); + } + + savePluginCommandRoles(pluginName: string, commandRoles: Record): void { + const currentConfig = this.getPluginConfig(pluginName); + const updatedConfig = { + ...currentConfig, + command_roles: commandRoles + }; + this.savePluginConfig(pluginName, updatedConfig); + } + + // --- Badges delegados a PluginBadgeService --- + getClientBadges(client: any): PluginBadge[] { - if (!this.mercureEventSource) { - this.initMercureSubscription(); - } - const badges: PluginBadge[] = []; - if (!client || this.activePlugins.length === 0) return badges; - - const idKey = client.id ? String(client.id) : null; - const uuidKey = client.uuid || client.name; - - for (const plugin of this.activePlugins) { - if (!plugin.badges || plugin.badges.length === 0) { - continue; - } - - // Estado global del cliente definido directamente por el plugin - const currentStatus = (idKey ? this.getPluginStatus(plugin.name, idKey) : null) - || (uuidKey ? this.getPluginStatus(plugin.name, uuidKey) : null); - - if (!currentStatus || currentStatus === 'NONE') { - continue; - } - - const matchingBadge = plugin.badges.find( - b => b.status.toUpperCase() === currentStatus.toUpperCase() - ); - - if (matchingBadge) { - let label = matchingBadge.label; - if (matchingBadge.labelKey) { - const trans = this.translateService.instant(matchingBadge.labelKey); - if (trans && trans !== matchingBadge.labelKey) { - label = trans; - } - } - - let tooltip = matchingBadge.tooltip; - if (matchingBadge.tooltipKey) { - const trans = this.translateService.instant(matchingBadge.tooltipKey); - if (trans && trans !== matchingBadge.tooltipKey) { - tooltip = trans; - } - } - - // Enriquecer tooltip con metadatos contextuales si el badge declara tooltipFields - const meta = (idKey ? this.getPluginMetadata(plugin.name, idKey) : null) - || (uuidKey ? this.getPluginMetadata(plugin.name, uuidKey) : null); - - if (matchingBadge.tooltipFields && matchingBadge.tooltipFields.length > 0 && meta) { - const infoParts: string[] = []; - for (const field of matchingBadge.tooltipFields) { - const val = meta[field.key]; - if (val !== undefined && val !== null && val !== '') { - const prefix = field.prefix || ''; - const suffix = field.suffix || ''; - infoParts.push(`${prefix}${val}${suffix}`); - } - } - if (infoParts.length > 0) { - const detailStr = infoParts.join(' · '); - tooltip = tooltip ? `${tooltip} (${detailStr})` : detailStr; - } - } - - badges.push({ - id: plugin.name, - type: matchingBadge.type, - icon: matchingBadge.icon, - label, - tooltip - }); - } - } - - return badges; + return this.badgeService.getClientBadges(this.activePlugins, client); } getPartitionBadges(client: any, diskNumber: number, partitionNumber: number): PluginBadge[] { - const badges: PluginBadge[] = []; - if (!client || this.activePlugins.length === 0) return badges; - - const idKey = client.id ? String(client.id) : null; - const uuidKey = client.uuid || client.name; - - for (const plugin of this.activePlugins) { - if (!plugin.badges || plugin.badges.length === 0) { - continue; - } - - const currentStatus = (idKey ? this.getPartitionPluginStatus(plugin.name, idKey, diskNumber, partitionNumber) : null) - || (uuidKey ? this.getPartitionPluginStatus(plugin.name, uuidKey, diskNumber, partitionNumber) : null); - - if (!currentStatus || currentStatus === 'NONE') { - continue; - } - - const matchingBadge = plugin.badges.find( - b => b.status.toUpperCase() === currentStatus.toUpperCase() - ); - - if (matchingBadge) { - let label = matchingBadge.label; - if (matchingBadge.labelKey) { - const trans = this.translateService.instant(matchingBadge.labelKey); - if (trans && trans !== matchingBadge.labelKey) { - label = trans; - } - } - - let tooltip = matchingBadge.tooltip; - if (matchingBadge.tooltipKey) { - const trans = this.translateService.instant(matchingBadge.tooltipKey); - if (trans && trans !== matchingBadge.tooltipKey) { - tooltip = trans; - } - } - - const meta = (idKey ? this.getPluginMetadata(plugin.name, idKey, diskNumber, partitionNumber) : null) - || (uuidKey ? this.getPluginMetadata(plugin.name, uuidKey, diskNumber, partitionNumber) : null); - - if (matchingBadge.tooltipFields && matchingBadge.tooltipFields.length > 0 && meta) { - const infoParts: string[] = []; - for (const field of matchingBadge.tooltipFields) { - const val = meta[field.key]; - if (val !== undefined && val !== null && val !== '') { - const prefix = field.prefix || ''; - const suffix = field.suffix || ''; - infoParts.push(`${prefix}${val}${suffix}`); - } - } - if (infoParts.length > 0) { - const detailStr = infoParts.join(' · '); - tooltip = tooltip ? `${tooltip} (${detailStr})` : detailStr; - } - } - - badges.push({ - id: plugin.name, - type: matchingBadge.type, - icon: matchingBadge.icon, - label, - tooltip - }); - } - } - - return badges; + return this.badgeService.getPartitionBadges(this.activePlugins, client, diskNumber, partitionNumber); } + // --- Configuración de Plugins (Local + Backend) --- + getPluginConfig(pluginName: string): Record { const plugin = this.getPlugin(pluginName); const defaults = plugin?.defaultConfig || {}; diff --git a/ogWebconsole/src/app/services/plugin/plugin-badge.service.ts b/ogWebconsole/src/app/services/plugin/plugin-badge.service.ts new file mode 100644 index 0000000..75a393b --- /dev/null +++ b/ogWebconsole/src/app/services/plugin/plugin-badge.service.ts @@ -0,0 +1,126 @@ +import { inject, Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { PluginStateService } from './plugin-state.service'; +import { PluginManifest, PluginBadge } from './plugin.types'; + +@Injectable({ + providedIn: 'root' +}) +export class PluginBadgeService { + private translateService = inject(TranslateService); + private pluginState = inject(PluginStateService); + + public getClientBadges(activePlugins: PluginManifest[], client: any): PluginBadge[] { + if (!this.pluginState.isMercureConnected()) { + this.pluginState.initMercureSubscription(); + } + const badges: PluginBadge[] = []; + if (!client || !activePlugins || activePlugins.length === 0) return badges; + + const idKey = client.id ? String(client.id) : null; + const uuidKey = client.uuid || client.name; + + for (const plugin of activePlugins) { + if (!plugin.badges || plugin.badges.length === 0) { + continue; + } + + // Estado global del cliente definido directamente por el plugin + const currentStatus = (idKey ? this.pluginState.getPluginStatus(plugin.name, idKey) : null) + || (uuidKey ? this.pluginState.getPluginStatus(plugin.name, uuidKey) : null); + + const meta = (idKey ? this.pluginState.getPluginMetadata(plugin.name, idKey) : null) + || (uuidKey ? this.pluginState.getPluginMetadata(plugin.name, uuidKey) : null); + + const badge = this.buildBadge(plugin, currentStatus, meta); + if (badge) { + badges.push(badge); + } + } + + return badges; + } + + public getPartitionBadges(activePlugins: PluginManifest[], client: any, diskNumber: number, partitionNumber: number): PluginBadge[] { + const badges: PluginBadge[] = []; + if (!client || !activePlugins || activePlugins.length === 0) return badges; + + const idKey = client.id ? String(client.id) : null; + const uuidKey = client.uuid || client.name; + + for (const plugin of activePlugins) { + if (!plugin.badges || plugin.badges.length === 0) { + continue; + } + + const currentStatus = (idKey ? this.pluginState.getPartitionPluginStatus(plugin.name, idKey, diskNumber, partitionNumber) : null) + || (uuidKey ? this.pluginState.getPartitionPluginStatus(plugin.name, uuidKey, diskNumber, partitionNumber) : null); + + const meta = (idKey ? this.pluginState.getPluginMetadata(plugin.name, idKey, diskNumber, partitionNumber) : null) + || (uuidKey ? this.pluginState.getPluginMetadata(plugin.name, uuidKey, diskNumber, partitionNumber) : null); + + const badge = this.buildBadge(plugin, currentStatus, meta); + if (badge) { + badges.push(badge); + } + } + + return badges; + } + + private buildBadge(plugin: PluginManifest, currentStatus: string | null | undefined, meta: any): PluginBadge | null { + if (!currentStatus || currentStatus === 'NONE' || !plugin.badges || plugin.badges.length === 0) { + return null; + } + + const matchingBadge = plugin.badges.find( + b => b.status.toUpperCase() === currentStatus.toUpperCase() + ); + + if (!matchingBadge) { + return null; + } + + let label = matchingBadge.label; + if (matchingBadge.labelKey) { + const trans = this.translateService.instant(matchingBadge.labelKey); + if (trans && trans !== matchingBadge.labelKey) { + label = trans; + } + } + + let tooltip = matchingBadge.tooltip; + if (matchingBadge.tooltipKey) { + const trans = this.translateService.instant(matchingBadge.tooltipKey); + if (trans && trans !== matchingBadge.tooltipKey) { + tooltip = trans; + } + } + + // Enriquecer tooltip con metadatos contextuales si el badge declara tooltipFields + if (matchingBadge.tooltipFields && matchingBadge.tooltipFields.length > 0 && meta) { + const infoParts: string[] = []; + for (const field of matchingBadge.tooltipFields) { + const val = meta[field.key]; + if (val !== undefined && val !== null && val !== '') { + const prefix = field.prefix || ''; + const suffix = field.suffix || ''; + infoParts.push(`${prefix}${val}${suffix}`); + } + } + if (infoParts.length > 0) { + const detailStr = infoParts.join(' · '); + tooltip = tooltip ? `${tooltip} (${detailStr})` : detailStr; + } + } + + return { + id: plugin.name, + type: matchingBadge.type, + icon: matchingBadge.icon, + label, + tooltip + }; + } +} + diff --git a/ogWebconsole/src/app/services/plugin/plugin-roles.service.ts b/ogWebconsole/src/app/services/plugin/plugin-roles.service.ts new file mode 100644 index 0000000..d32448d --- /dev/null +++ b/ogWebconsole/src/app/services/plugin/plugin-roles.service.ts @@ -0,0 +1,128 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { AuthService } from '@services/auth.service'; +import { ConfigService } from '@services/config.service'; +import { PluginCommand, RoleDefinition, PluginManifest } from './plugin.types'; + +@Injectable({ + providedIn: 'root' +}) +export class PluginRolesService { + private http = inject(HttpClient); + private authService = inject(AuthService); + private configService = inject(ConfigService); + + /** + * Comprueba si el usuario autenticado tiene permiso para ejecutar un comando específico. + * Jerarquía: config.command_roles[slug] > command.roles > público (todos los roles) + */ + public isCommandAllowedForUser(command: PluginCommand, config?: Record): boolean { + if (this.authService.isSuperAdmin) { + return true; + } + + 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. + */ + public getUserRoles(): Observable { + 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(`${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(); + + // 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. + */ + public getCommandRoles(plugin: PluginManifest | undefined, commandSlug: string, config?: Record): string[] { + const cmd = plugin?.commands?.find(c => c.slug === commandSlug); + 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']; + } +} + diff --git a/ogWebconsole/src/app/services/plugin/plugin-state.service.ts b/ogWebconsole/src/app/services/plugin/plugin-state.service.ts new file mode 100644 index 0000000..9afffa5 --- /dev/null +++ b/ogWebconsole/src/app/services/plugin/plugin-state.service.ts @@ -0,0 +1,259 @@ +import { inject, Injectable, NgZone } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Subject, Observable, of, forkJoin } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; +import { ConfigService } from '@services/config.service'; +import { PluginManifest, PluginStatusUpdate } from './plugin.types'; + +@Injectable({ + providedIn: 'root' +}) +export class PluginStateService { + private http = inject(HttpClient); + private configService = inject(ConfigService); + private ngZone = inject(NgZone); + + private statusUpdatesSubject = new Subject(); + public statusUpdates$ = this.statusUpdatesSubject.asObservable(); + + // Dynamic state cache: key is `${pluginName}:${clientIdOrUuid}` -> status string + private clientPluginStatusMap = new Map(); + // Dynamic metadata cache: key is `${pluginName}:${clientIdOrUuid}` -> extra attributes (e.g. dni, remaining_minutes) + private clientPluginMetadataMap = new Map(); + private mercureEventSource: EventSource | null = null; + + private getStatusKey(pluginName: string, idOrUuid: string | number): string { + return `${pluginName}:${idOrUuid}`; + } + + private getPartitionStatusKey(pluginName: string, idOrUuid: string | number, disk: number, partition: number): string { + return `${pluginName}:${idOrUuid}:${disk}:${partition}`; + } + + public setPluginStatus(pluginName: string, idOrUuid: string | number, status: string): void { + this.clientPluginStatusMap.set(this.getStatusKey(pluginName, idOrUuid), status); + } + + public getPluginStatus(pluginName: string, idOrUuid: string | number): string | undefined { + return this.clientPluginStatusMap.get(this.getStatusKey(pluginName, idOrUuid)); + } + + public setPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number, status: string): void { + this.clientPluginStatusMap.set(this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition), status); + } + + public getPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): string | undefined { + return this.clientPluginStatusMap.get(this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition)); + } + + public clearPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): void { + const key = this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition); + this.clientPluginStatusMap.delete(key); + this.clientPluginMetadataMap.delete(key); + } + + public clearPluginStatus(pluginName: string, idOrUuid: string | number): void { + const globalKey = this.getStatusKey(pluginName, idOrUuid); + this.clientPluginStatusMap.delete(globalKey); + this.clientPluginMetadataMap.delete(globalKey); + + const prefix = `${pluginName}:${idOrUuid}:`; + for (const key of Array.from(this.clientPluginStatusMap.keys())) { + if (key.startsWith(prefix)) { + this.clientPluginStatusMap.delete(key); + } + } + for (const key of Array.from(this.clientPluginMetadataMap.keys())) { + if (key.startsWith(prefix)) { + this.clientPluginMetadataMap.delete(key); + } + } + } + + public setPluginMetadata(pluginName: string, idOrUuid: string | number, metadata: any, disk?: number, partition?: number): void { + const key = (disk !== undefined && partition !== undefined) + ? this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition) + : this.getStatusKey(pluginName, idOrUuid); + const existing = this.clientPluginMetadataMap.get(key) || {}; + this.clientPluginMetadataMap.set(key, { ...existing, ...metadata }); + } + + public getPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): any { + const key = (disk !== undefined && partition !== undefined) + ? this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition) + : this.getStatusKey(pluginName, idOrUuid); + return this.clientPluginMetadataMap.get(key); + } + + public clearPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): void { + const key = (disk !== undefined && partition !== undefined) + ? this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition) + : this.getStatusKey(pluginName, idOrUuid); + this.clientPluginMetadataMap.delete(key); + } + + public isMercureConnected(): boolean { + return this.mercureEventSource !== null; + } + + public initMercureSubscription(): void { + const mercureUrl = this.configService.mercureUrl; + if (!mercureUrl || this.mercureEventSource) { + return; + } + + try { + this.mercureEventSource = new EventSource(`${mercureUrl}?topic=` + encodeURIComponent('plugins')); + this.mercureEventSource.onmessage = (event) => { + this.ngZone.run(() => { + try { + const data = JSON.parse(event.data); + if (data && data.plugin) { + const entityId = data.clientId !== undefined && data.clientId !== null ? String(data.clientId) : null; + const entityUuid = data.clientUuid ? String(data.clientUuid) : null; + const rawDisk = data.disk ?? data.disk_number ?? data.diskNumber; + const rawPart = data.partition ?? data.partition_number ?? data.partitionNumber; + const hasPartition = rawDisk !== undefined && rawDisk !== null && rawPart !== undefined && rawPart !== null; + const disk = hasPartition ? Number(rawDisk) : undefined; + const partition = hasPartition ? Number(rawPart) : undefined; + + // 1. Estado a nivel de partición + if (hasPartition && disk !== undefined && partition !== undefined) { + const partStatus = data.status; + if (partStatus === 'NONE') { + if (entityId) this.clearPartitionPluginStatus(data.plugin, entityId, disk, partition); + if (entityUuid) this.clearPartitionPluginStatus(data.plugin, entityUuid, disk, partition); + } else if (partStatus) { + if (entityId) this.setPartitionPluginStatus(data.plugin, entityId, disk, partition, partStatus); + if (entityUuid) this.setPartitionPluginStatus(data.plugin, entityUuid, disk, partition, partStatus); + } + } + + // 2. Estado general del cliente + const generalStatus = data.clientStatus !== undefined + ? data.clientStatus + : (data.client_status !== undefined ? data.client_status : (!hasPartition ? data.status : null)); + + if (generalStatus !== null) { + if (generalStatus === 'NONE') { + if (entityId) { + this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, entityId)); + this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, entityId)); + } + if (entityUuid) { + this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, entityUuid)); + this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, entityUuid)); + } + } else { + if (entityId) this.setPluginStatus(data.plugin, entityId, generalStatus); + if (entityUuid) this.setPluginStatus(data.plugin, entityUuid, generalStatus); + } + } + + // Metadatos contextuales arbitrarios + const metadata: any = {}; + for (const [k, v] of Object.entries(data)) { + if (!['plugin', 'status', 'clientStatus', 'client_status', 'clientId', 'clientUuid', 'disk', 'disk_number', 'diskNumber', 'partition', 'partition_number', 'partitionNumber', 'action', 'timestamp', 'subtarget'].includes(k) && v !== null && v !== undefined) { + metadata[k] = v; + } + } + const hasMeta = Object.keys(metadata).length > 0; + if (hasMeta) { + if (entityId) this.setPluginMetadata(data.plugin, entityId, metadata, disk, partition); + if (entityUuid) this.setPluginMetadata(data.plugin, entityUuid, metadata, disk, partition); + } + + this.statusUpdatesSubject.next({ + plugin: data.plugin, + clientId: (entityId || entityUuid) ?? undefined, + status: data.status, + metadata + }); + } + } catch { + } + }); + }; + } catch { + } + } + + public loadActivePluginsStatus(activePlugins: PluginManifest[]): Observable { + this.initMercureSubscription(); + const apiUrl = this.configService.apiUrl; + if (!apiUrl || !activePlugins || activePlugins.length === 0) { + return of(undefined); + } + + const pluginsWithStatus = activePlugins.filter(p => !!p.statusEndpoint); + if (pluginsWithStatus.length === 0) { + return of(undefined); + } + + const requests = pluginsWithStatus.map(plugin => { + const endpoint = plugin.statusEndpoint!.startsWith('http') + ? plugin.statusEndpoint! + : `${apiUrl.replace(/\/api\/?$/, '')}${plugin.statusEndpoint!.startsWith('/') ? '' : '/'}${plugin.statusEndpoint}`; + + return this.http.get(endpoint).pipe( + catchError(() => of([])), + tap(rows => { + if (Array.isArray(rows)) { + for (const row of rows) { + const status = row.status; + const id = row.client_id ?? row.clientId ?? row.id; + const uuid = row.client_uuid ?? row.clientUuid ?? row.uuid; + const rawDisk = row.disk_number ?? row.diskNumber ?? row.disk; + const rawPart = row.partition_number ?? row.partitionNumber ?? row.partition; + const hasPartition = rawDisk !== undefined && rawDisk !== null && rawPart !== undefined && rawPart !== null; + const disk = hasPartition ? Number(rawDisk) : undefined; + const partition = hasPartition ? Number(rawPart) : undefined; + const clientStatus = row.client_status ?? row.clientStatus ?? (!hasPartition ? status : null); + + // Extraer metadatos arbitrarios (dni, remaining_minutes, etc.) + const metadata: any = {}; + for (const [k, v] of Object.entries(row)) { + if (!['status', 'client_status', 'clientStatus', 'client_id', 'clientId', 'id', 'client_uuid', 'clientUuid', 'uuid', 'disk_number', 'diskNumber', 'disk', 'partition_number', 'partitionNumber', 'partition', 'subtarget'].includes(k) && v !== null && v !== undefined) { + metadata[k] = v; + } + } + const hasMeta = Object.keys(metadata).length > 0; + + const idStr = id !== undefined && id !== null ? String(id) : null; + if (idStr) { + if (hasPartition && disk !== undefined && partition !== undefined && status && status !== 'NONE') { + this.setPartitionPluginStatus(plugin.name, idStr, disk, partition, status); + } + if (clientStatus && clientStatus !== 'NONE') { + this.setPluginStatus(plugin.name, idStr, clientStatus); + } + if (hasMeta) { + this.setPluginMetadata(plugin.name, idStr, metadata, disk, partition); + } + } + if (uuid) { + if (hasPartition && disk !== undefined && partition !== undefined && status && status !== 'NONE') { + this.setPartitionPluginStatus(plugin.name, uuid, disk, partition, status); + } + if (clientStatus && clientStatus !== 'NONE') { + this.setPluginStatus(plugin.name, uuid, clientStatus); + } + if (hasMeta) { + this.setPluginMetadata(plugin.name, uuid, metadata, disk, partition); + } + } + } + } + }) + ); + }); + + return forkJoin(requests).pipe( + tap(() => { + this.statusUpdatesSubject.next({ plugin: 'all', status: 'REFRESHED' }); + }), + map(() => undefined) + ); + } +} + diff --git a/ogWebconsole/src/app/services/plugin/plugin.types.ts b/ogWebconsole/src/app/services/plugin/plugin.types.ts new file mode 100644 index 0000000..d0e59e1 --- /dev/null +++ b/ogWebconsole/src/app/services/plugin/plugin.types.ts @@ -0,0 +1,117 @@ +export interface CommandParam { + key: string; + label: string; + labelKey?: string; + type: 'text' | 'number' | 'select' | 'checkbox'; + options?: string[]; + default?: any; + required?: boolean; +} + +export interface PluginCommand { + slug: string; + translationKey?: string; + labelKey?: string; + label: string; + icon: string; + disabled: boolean; + endpoint?: string; + os?: string[]; // Ej: ['windows'], ['linux'] + params?: CommandParam[]; + confirm?: string; + confirmKey?: string; + roles?: string[]; // Roles autorizados por defecto (ej. ['ROLE_SUPER_ADMIN', 'ROLE_ORGANIZATIONAL_UNIT_ADMIN']) + requireStatus?: string[]; // Estados requeridos del plugin para habilitar el comando + excludeStatus?: string[]; // Estados del plugin que deshabilitan el comando +} + +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 PluginBadgeField { + key: string; + prefix?: string; + suffix?: string; +} + +export interface PluginBadgeRule { + status: string; + type: string; + icon: string; + label: string; + labelKey?: string; + tooltip: string; + tooltipKey?: string; + tooltipFields?: PluginBadgeField[]; +} + +export interface PluginBadge { + id: string; // Plugin name + type: string; + icon: string; + label: string; + tooltip: string; +} + +export interface FieldAction { + id: string; + label: string; + icon?: string; + endpoint: string; +} + +export interface SchemaField { + key: string; + label: string; + labelKey?: string; + description?: string; + descriptionKey?: string; + type: 'text' | 'number' | 'select' | 'checkbox' | 'textarea'; + language?: string; + options?: string[]; + default: any; + action?: FieldAction; +} + +export interface RecordColumn { + key: string; + label: string; + labelKey?: string; +} + +export interface PluginManifest { + name: string; + title: string; + titleKey?: string; + version: string; + description: string; + descriptionKey?: string; + author: string; + icon: string; + enabled: boolean; + commands: PluginCommand[]; + statusEndpoint?: string; + recordsEndpoint?: string; + recordsColumns?: RecordColumn[]; + badges?: PluginBadgeRule[]; + i18n?: Record>; + schema: SchemaField[]; + defaultConfig?: Record; + config?: Record; +} + +export interface PluginGroup { + plugin: PluginManifest; + commands: PluginCommand[]; +} + +export interface PluginStatusUpdate { + plugin: string; + clientId?: string; + status: string; + metadata?: any; +} +