829 lines
30 KiB
TypeScript
829 lines
30 KiB
TypeScript
import { Injectable, inject } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
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;
|
|
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'])
|
|
}
|
|
|
|
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 {
|
|
status: string;
|
|
type: string;
|
|
icon: string;
|
|
label: string;
|
|
labelKey?: string;
|
|
tooltip: string;
|
|
tooltipKey?: string;
|
|
}
|
|
|
|
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';
|
|
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<string, Record<string, any>>;
|
|
schema: SchemaField[];
|
|
defaultConfig?: Record<string, any>;
|
|
config?: Record<string, any>;
|
|
}
|
|
|
|
export interface PluginGroup {
|
|
plugin: PluginManifest;
|
|
commands: PluginCommand[];
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
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[] = [];
|
|
private pluginsSubject = new BehaviorSubject<PluginManifest[]>([]);
|
|
public plugins$ = this.pluginsSubject.asObservable();
|
|
|
|
// Dynamic state cache: key is `${pluginName}:${clientIdOrUuid}` -> status string
|
|
private clientPluginStatusMap = new Map<string, string>();
|
|
// Dynamic metadata cache: key is `${pluginName}:${clientIdOrUuid}` -> extra attributes (e.g. dni, remaining_minutes)
|
|
private clientPluginMetadataMap = new Map<string, any>();
|
|
private mercureEventSource: EventSource | null = null;
|
|
|
|
constructor() {
|
|
this.refreshPlugins().subscribe();
|
|
this.initMercureSubscription();
|
|
|
|
// Re-aplicar traducciones dinámicas al cambiar de idioma
|
|
this.translateService.onLangChange.subscribe((event) => {
|
|
this.loadPluginTranslations(event.lang);
|
|
});
|
|
}
|
|
|
|
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 setPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number, status: string): void {
|
|
this.clientPluginStatusMap.set(this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition), status);
|
|
}
|
|
|
|
public getPluginStatus(pluginName: string, idOrUuid: string | number): string | undefined {
|
|
return this.clientPluginStatusMap.get(this.getStatusKey(pluginName, idOrUuid));
|
|
}
|
|
|
|
public getPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): string | undefined {
|
|
return this.clientPluginStatusMap.get(this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition));
|
|
}
|
|
|
|
public setPluginMetadata(pluginName: string, idOrUuid: string | number, metadata: any): void {
|
|
const key = this.getStatusKey(pluginName, idOrUuid);
|
|
const existing = this.clientPluginMetadataMap.get(key) || {};
|
|
this.clientPluginMetadataMap.set(key, { ...existing, ...metadata });
|
|
}
|
|
|
|
public getPluginMetadata(pluginName: string, idOrUuid: string | number): any {
|
|
return this.clientPluginMetadataMap.get(this.getStatusKey(pluginName, idOrUuid));
|
|
}
|
|
|
|
public clearPluginMetadata(pluginName: string, idOrUuid: string | number): void {
|
|
this.clientPluginMetadataMap.delete(this.getStatusKey(pluginName, idOrUuid));
|
|
}
|
|
|
|
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) => {
|
|
try {
|
|
const data = JSON.parse(event.data);
|
|
if (data && data.plugin && data.status) {
|
|
const disk = data.disk ?? data.disk_number ?? data.diskNumber;
|
|
const part = data.partition ?? data.partition_number ?? data.partitionNumber;
|
|
|
|
const clearEntriesForPrefix = (prefix: string) => {
|
|
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);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (data.status === 'NONE') {
|
|
if (data.clientId !== undefined && data.clientId !== null) {
|
|
const idStr = String(data.clientId);
|
|
this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, idStr));
|
|
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
|
this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, idStr, Number(disk), Number(part)));
|
|
const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, idStr));
|
|
if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) {
|
|
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, idStr));
|
|
}
|
|
} else if (disk !== undefined && disk !== null) {
|
|
clearEntriesForPrefix(`${data.plugin}:${idStr}:${disk}:`);
|
|
} else {
|
|
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, idStr));
|
|
clearEntriesForPrefix(`${data.plugin}:${idStr}:`);
|
|
}
|
|
}
|
|
if (data.clientUuid) {
|
|
const uuidStr = data.clientUuid;
|
|
this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, uuidStr));
|
|
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
|
this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, uuidStr, Number(disk), Number(part)));
|
|
const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, uuidStr));
|
|
if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) {
|
|
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, uuidStr));
|
|
}
|
|
} else if (disk !== undefined && disk !== null) {
|
|
clearEntriesForPrefix(`${data.plugin}:${uuidStr}:${disk}:`);
|
|
} else {
|
|
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, uuidStr));
|
|
clearEntriesForPrefix(`${data.plugin}:${uuidStr}:`);
|
|
}
|
|
}
|
|
} else {
|
|
// Extraer metadatos contextuales (p. ej. dni, remainingMinutes, remaining_minutes)
|
|
const metadata: any = {};
|
|
for (const [k, v] of Object.entries(data)) {
|
|
if (!['plugin', 'status', 'clientId', 'clientUuid', 'disk', 'disk_number', 'diskNumber', 'partition', 'partition_number', 'partitionNumber', 'action', 'timestamp'].includes(k) && v !== null && v !== undefined) {
|
|
metadata[k] = v;
|
|
}
|
|
}
|
|
const hasMeta = Object.keys(metadata).length > 0;
|
|
|
|
if (data.clientId !== undefined && data.clientId !== null) {
|
|
const idStr = String(data.clientId);
|
|
this.setPluginStatus(data.plugin, idStr, data.status);
|
|
if (hasMeta && data.status !== 'FREE') {
|
|
this.setPluginMetadata(data.plugin, idStr, metadata);
|
|
} else if (data.status === 'FREE') {
|
|
this.clearPluginMetadata(data.plugin, idStr);
|
|
}
|
|
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
|
this.setPartitionPluginStatus(data.plugin, idStr, Number(disk), Number(part), data.status);
|
|
}
|
|
}
|
|
if (data.clientUuid) {
|
|
this.setPluginStatus(data.plugin, data.clientUuid, data.status);
|
|
if (hasMeta && data.status !== 'FREE') {
|
|
this.setPluginMetadata(data.plugin, data.clientUuid, metadata);
|
|
} else if (data.status === 'FREE') {
|
|
this.clearPluginMetadata(data.plugin, data.clientUuid);
|
|
}
|
|
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
|
this.setPartitionPluginStatus(data.plugin, data.clientUuid, Number(disk), Number(part), data.status);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
}
|
|
};
|
|
} catch {
|
|
}
|
|
}
|
|
|
|
refreshPlugins(): Observable<PluginManifest[]> {
|
|
const apiUrl = this.configService.apiUrl;
|
|
if (!apiUrl) {
|
|
return of([]);
|
|
}
|
|
|
|
return this.http.get<PluginManifest[]>(`${apiUrl}/plugins?all=true`).pipe(
|
|
catchError(() => of([])),
|
|
switchMap((plugins) => {
|
|
if (!plugins || plugins.length === 0) {
|
|
this.allPlugins = [];
|
|
this.activePlugins = [];
|
|
this.pluginsSubject.next([]);
|
|
return of([]);
|
|
}
|
|
|
|
// Descubrir y fusionar manifiestos extendidos de cada plugin
|
|
const manifestRequests = plugins.map(p =>
|
|
this.http.get<Partial<PluginManifest>>(`${apiUrl}/plugins/${p.name}/manifest`).pipe(
|
|
catchError(() => of({})),
|
|
map(extra => ({
|
|
...p,
|
|
...extra,
|
|
enabled: Boolean(p.enabled) // La base de datos es la única fuente de verdad para el estado de activación
|
|
}))
|
|
)
|
|
);
|
|
|
|
return forkJoin(manifestRequests).pipe(
|
|
tap((fullPlugins) => {
|
|
this.allPlugins = fullPlugins;
|
|
this.activePlugins = fullPlugins.filter(p => p.enabled);
|
|
this.pluginsSubject.next(this.allPlugins);
|
|
this.loadPluginTranslations();
|
|
this.loadActivePluginsStatus().subscribe();
|
|
})
|
|
);
|
|
})
|
|
);
|
|
}
|
|
|
|
togglePlugin(name: string, enabled?: boolean): Observable<any> {
|
|
const apiUrl = this.configService.apiUrl;
|
|
if (!apiUrl) return of(null);
|
|
return this.http.post(`${apiUrl}/plugins/${name}/toggle`, enabled !== undefined ? { enabled } : {}).pipe(
|
|
switchMap(() => this.refreshPlugins())
|
|
);
|
|
}
|
|
|
|
public loadPluginTranslations(lang?: string): void {
|
|
const currentLang = lang || this.translateService.currentLang || this.translateService.defaultLang || 'es';
|
|
const apiUrl = this.configService.apiUrl;
|
|
|
|
for (const plugin of this.allPlugins) {
|
|
// 1. Si el plugin declara i18n directamente en el manifest, lo inyectamos
|
|
if (plugin.i18n && typeof plugin.i18n === 'object') {
|
|
for (const [l, dict] of Object.entries(plugin.i18n)) {
|
|
if (dict && typeof dict === 'object') {
|
|
this.translateService.setTranslation(l, dict, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Si el plugin tiene endpoint /i18n/{lang} (carpeta i18n/ estandarizada), lo descargamos bajo demanda
|
|
if (apiUrl) {
|
|
this.http.get<Record<string, any>>(`${apiUrl}/plugins/${plugin.name}/i18n/${currentLang}`).pipe(
|
|
catchError(() => of(null)),
|
|
tap((catalog) => {
|
|
if (catalog && typeof catalog === 'object' && Object.keys(catalog).length > 0) {
|
|
this.translateService.setTranslation(currentLang, catalog, true);
|
|
}
|
|
})
|
|
).subscribe();
|
|
}
|
|
}
|
|
}
|
|
|
|
loadActivePluginsStatus(): Observable<void> {
|
|
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<any[]>(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 disk = row.disk_number ?? row.diskNumber ?? row.disk;
|
|
const part = row.partition_number ?? row.partitionNumber ?? row.partition;
|
|
|
|
// Extraer metadatos arbitrarios (dni, remaining_minutes, etc.)
|
|
const metadata: any = {};
|
|
for (const [k, v] of Object.entries(row)) {
|
|
if (!['status', 'client_id', 'clientId', 'id', 'client_uuid', 'clientUuid', 'uuid', 'disk_number', 'diskNumber', 'disk', 'partition_number', 'partitionNumber', 'partition'].includes(k) && v !== null && v !== undefined) {
|
|
metadata[k] = v;
|
|
}
|
|
}
|
|
const hasMeta = Object.keys(metadata).length > 0;
|
|
|
|
if (status) {
|
|
if (id !== undefined && id !== null) {
|
|
const idStr = String(id);
|
|
this.setPluginStatus(plugin.name, idStr, status);
|
|
if (hasMeta && status !== 'FREE') {
|
|
this.setPluginMetadata(plugin.name, idStr, metadata);
|
|
} else if (status === 'FREE') {
|
|
this.clearPluginMetadata(plugin.name, idStr);
|
|
}
|
|
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
|
this.setPartitionPluginStatus(plugin.name, idStr, Number(disk), Number(part), status);
|
|
}
|
|
}
|
|
if (uuid) {
|
|
this.setPluginStatus(plugin.name, uuid, status);
|
|
if (hasMeta && status !== 'FREE') {
|
|
this.setPluginMetadata(plugin.name, uuid, metadata);
|
|
} else if (status === 'FREE') {
|
|
this.clearPluginMetadata(plugin.name, uuid);
|
|
}
|
|
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
|
this.setPartitionPluginStatus(plugin.name, uuid, Number(disk), Number(part), status);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
return forkJoin(requests).pipe(map(() => undefined));
|
|
}
|
|
|
|
getPlugins(): PluginManifest[] {
|
|
return this.activePlugins.filter(p => p.enabled);
|
|
}
|
|
|
|
getAllPlugins(): PluginManifest[] {
|
|
return this.allPlugins;
|
|
}
|
|
|
|
hasPlugins(): boolean {
|
|
return this.allPlugins.length > 0;
|
|
}
|
|
|
|
hasActivePlugins(): boolean {
|
|
return this.getPlugins().length > 0;
|
|
}
|
|
|
|
isPluginActive(name: string): boolean {
|
|
return this.activePlugins.some(p => p.name === name && p.enabled);
|
|
}
|
|
|
|
getPlugin(name: string): PluginManifest | undefined {
|
|
return this.allPlugins.find(p => p.name === name);
|
|
}
|
|
|
|
getPluginGroups(): PluginGroup[] {
|
|
return this.getPlugins()
|
|
.filter(p => p.commands && p.commands.length > 0)
|
|
.map(p => {
|
|
const translatedTitle = p.titleKey ? this.translateService.instant(p.titleKey) : p.title;
|
|
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);
|
|
if (trans && trans !== key) {
|
|
return { ...cmd, label: trans };
|
|
}
|
|
}
|
|
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[] {
|
|
const commands: PluginCommand[] = [];
|
|
for (const group of this.getPluginGroups()) {
|
|
if (group.commands && Array.isArray(group.commands)) {
|
|
commands.push(...group.commands);
|
|
}
|
|
}
|
|
return commands;
|
|
}
|
|
|
|
findCommand(slug: string): PluginCommand | undefined {
|
|
return this.getCommands().find(c => c.slug === slug);
|
|
}
|
|
|
|
isCommandSupportedForClients(command: PluginCommand, clients: any[]): boolean {
|
|
if (!command.os || command.os.length === 0) {
|
|
return true;
|
|
}
|
|
if (!clients || clients.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const requiredOsList = command.os.map(os => os.toLowerCase());
|
|
|
|
return clients.every(client => {
|
|
const statusStr = (client.status || '').toLowerCase();
|
|
const matchesStatus = requiredOsList.some(req => statusStr.includes(req));
|
|
|
|
const hasMatchingPartition = (client.partitions || []).some((partition: any) => {
|
|
const osName = (partition.operativeSystem?.name || partition.operativeSystem || '').toLowerCase();
|
|
return requiredOsList.some(req => osName.includes(req));
|
|
});
|
|
|
|
return matchesStatus || hasMatchingPartition;
|
|
});
|
|
}
|
|
|
|
getClientBadges(client: any): 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;
|
|
}
|
|
|
|
// 1. Estado global del cliente
|
|
let currentStatus = (idKey ? this.getPluginStatus(plugin.name, idKey) : null)
|
|
|| (uuidKey ? this.getPluginStatus(plugin.name, uuidKey) : null);
|
|
|
|
// 2. Si no hay estado global explícito, buscar si alguna partición del equipo tiene un estado activo
|
|
if (!currentStatus) {
|
|
for (const [key, val] of this.clientPluginStatusMap.entries()) {
|
|
const isMatch = (idKey && key.startsWith(`${plugin.name}:${idKey}:`)) ||
|
|
(uuidKey && key.startsWith(`${plugin.name}:${uuidKey}:`));
|
|
if (isMatch && val && val !== 'NONE' && val !== 'UNFROZEN') {
|
|
currentStatus = val;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!currentStatus) {
|
|
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 están disponibles (p. ej. DNI del alumno, tiempo restante)
|
|
const meta = (idKey ? this.getPluginMetadata(plugin.name, idKey) : null)
|
|
|| (uuidKey ? this.getPluginMetadata(plugin.name, uuidKey) : null);
|
|
|
|
if (meta) {
|
|
const dni = meta.dni || meta.user || meta.student;
|
|
const mins = meta.remaining_minutes !== undefined ? meta.remaining_minutes : meta.remainingMinutes;
|
|
const infoParts: string[] = [];
|
|
if (dni) {
|
|
infoParts.push(`DNI: ${dni}`);
|
|
}
|
|
if (mins !== undefined && mins !== null && mins !== '') {
|
|
infoParts.push(`${mins} min`);
|
|
}
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
badges.push({
|
|
id: plugin.name,
|
|
type: matchingBadge.type,
|
|
icon: matchingBadge.icon,
|
|
label,
|
|
tooltip
|
|
});
|
|
}
|
|
}
|
|
|
|
return badges;
|
|
}
|
|
|
|
getPluginConfig(pluginName: string): Record<string, any> {
|
|
const plugin = this.getPlugin(pluginName);
|
|
const defaults = plugin?.defaultConfig || {};
|
|
|
|
const storageKey = `og_plugin_config_${pluginName}`;
|
|
const saved = localStorage.getItem(storageKey);
|
|
if (saved) {
|
|
try {
|
|
return { ...defaults, ...(plugin?.config || {}), ...JSON.parse(saved) };
|
|
} catch {
|
|
return { ...defaults, ...(plugin?.config || {}) };
|
|
}
|
|
}
|
|
|
|
return { ...defaults, ...(plugin?.config || {}) };
|
|
}
|
|
|
|
savePluginConfig(pluginName: string, config: Record<string, any>): void {
|
|
const storageKey = `og_plugin_config_${pluginName}`;
|
|
localStorage.setItem(storageKey, JSON.stringify(config));
|
|
|
|
const plugin = this.getPlugin(pluginName);
|
|
if (plugin) {
|
|
plugin.config = config;
|
|
}
|
|
|
|
const apiUrl = this.configService.apiUrl;
|
|
if (apiUrl) {
|
|
this.http.post(`${apiUrl}/plugins/${pluginName}/config`, config).pipe(
|
|
catchError((err) => {
|
|
console.warn(`[PluginService] Could not persist config for ${pluginName} to backend`, err);
|
|
return of(null);
|
|
})
|
|
).subscribe();
|
|
}
|
|
}
|
|
|
|
resetPluginConfig(pluginName: string): Record<string, any> {
|
|
const plugin = this.getPlugin(pluginName);
|
|
const defaults = plugin?.defaultConfig || {};
|
|
const storageKey = `og_plugin_config_${pluginName}`;
|
|
localStorage.removeItem(storageKey);
|
|
|
|
this.savePluginConfig(pluginName, defaults);
|
|
return defaults;
|
|
}
|
|
}
|