373 lines
13 KiB
TypeScript
373 lines
13 KiB
TypeScript
import { inject, Injectable } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs';
|
|
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { ConfigService } from '@services/config.service';
|
|
|
|
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';
|
|
|
|
// Re-exportar todos los tipos para compatibilidad transparente con componentes existentes
|
|
export * from './plugin/plugin.types';
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class PluginService {
|
|
private http = inject(HttpClient);
|
|
private configService = inject(ConfigService);
|
|
private translateService = inject(TranslateService);
|
|
private stateService = inject(PluginStateService);
|
|
private badgeService = inject(PluginBadgeService);
|
|
private rolesService = inject(PluginRolesService);
|
|
|
|
private allPlugins: PluginManifest[] = [];
|
|
private activePlugins: PluginManifest[] = [];
|
|
private pluginsSubject = new BehaviorSubject<PluginManifest[]>([]);
|
|
public plugins$ = this.pluginsSubject.asObservable();
|
|
|
|
public statusUpdates$ = this.stateService.statusUpdates$;
|
|
|
|
constructor() {
|
|
this.refreshPlugins().subscribe();
|
|
this.stateService.initMercureSubscription();
|
|
|
|
// Re-aplicar traducciones dinámicas al cambiar de idioma
|
|
this.translateService.onLangChange.subscribe((event) => {
|
|
this.loadPluginTranslations(event.lang);
|
|
});
|
|
}
|
|
|
|
// --- Gestión de estado delegado a PluginStateService ---
|
|
|
|
public setPluginStatus(pluginName: string, idOrUuid: string | number, status: string): void {
|
|
this.stateService.setPluginStatus(pluginName, idOrUuid, status);
|
|
}
|
|
|
|
public getPluginStatus(pluginName: string, idOrUuid: string | number): string | undefined {
|
|
return this.stateService.getPluginStatus(pluginName, idOrUuid);
|
|
}
|
|
|
|
public setPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number, status: string): void {
|
|
this.stateService.setPartitionPluginStatus(pluginName, idOrUuid, disk, partition, status);
|
|
}
|
|
|
|
public getPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): string | undefined {
|
|
return this.stateService.getPartitionPluginStatus(pluginName, idOrUuid, disk, partition);
|
|
}
|
|
|
|
public clearPartitionPluginStatus(pluginName: string, idOrUuid: string | number, disk: number, partition: number): void {
|
|
this.stateService.clearPartitionPluginStatus(pluginName, idOrUuid, disk, partition);
|
|
}
|
|
|
|
public clearPluginStatus(pluginName: string, idOrUuid: string | number): void {
|
|
this.stateService.clearPluginStatus(pluginName, idOrUuid);
|
|
}
|
|
|
|
public setPluginMetadata(pluginName: string, idOrUuid: string | number, metadata: any, disk?: number, partition?: number): void {
|
|
this.stateService.setPluginMetadata(pluginName, idOrUuid, metadata, disk, partition);
|
|
}
|
|
|
|
public getPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): any {
|
|
return this.stateService.getPluginMetadata(pluginName, idOrUuid, disk, partition);
|
|
}
|
|
|
|
public clearPluginMetadata(pluginName: string, idOrUuid: string | number, disk?: number, partition?: number): void {
|
|
this.stateService.clearPluginMetadata(pluginName, idOrUuid, disk, partition);
|
|
}
|
|
|
|
public loadActivePluginsStatus(): Observable<void> {
|
|
return this.stateService.loadActivePluginsStatus(this.activePlugins);
|
|
}
|
|
|
|
// --- Registro, ciclo de vida e i18n de Plugins ---
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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[], pluginName?: string): boolean {
|
|
if (!clients || clients.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (command.os && command.os.length > 0) {
|
|
const requiredOsList = command.os.map(os => os.toLowerCase());
|
|
const osSupported = 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;
|
|
});
|
|
|
|
if (!osSupported) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (pluginName && (command.requireStatus || command.excludeStatus)) {
|
|
return clients.every(client => {
|
|
const idKey = client.id ? String(client.id) : null;
|
|
const uuidKey = client.uuid || client.name;
|
|
const currentStatus = (idKey ? this.getPluginStatus(pluginName, idKey) : null)
|
|
|| (uuidKey ? this.getPluginStatus(pluginName, uuidKey) : null)
|
|
|| 'DISABLED';
|
|
|
|
if (command.excludeStatus && command.excludeStatus.length > 0) {
|
|
const excludedUpper = command.excludeStatus.map(s => s.toUpperCase());
|
|
if (excludedUpper.includes(currentStatus.toUpperCase())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (command.requireStatus && command.requireStatus.length > 0) {
|
|
const requiredUpper = command.requireStatus.map(s => s.toUpperCase());
|
|
if (!requiredUpper.includes(currentStatus.toUpperCase())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// --- Roles y permisos delegados a PluginRolesService ---
|
|
|
|
isCommandAllowedForUser(command: PluginCommand, pluginName: string): boolean {
|
|
return this.rolesService.isCommandAllowedForUser(command, this.getPluginConfig(pluginName));
|
|
}
|
|
|
|
getUserRoles(): Observable<RoleDefinition[]> {
|
|
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<string, string[]>): void {
|
|
const currentConfig = this.getPluginConfig(pluginName);
|
|
const updatedConfig = {
|
|
...currentConfig,
|
|
command_roles: commandRoles
|
|
};
|
|
this.savePluginConfig(pluginName, updatedConfig);
|
|
}
|
|
|
|
// --- Badges delegados a PluginBadgeService ---
|
|
|
|
getClientBadges(client: any): PluginBadge[] {
|
|
return this.badgeService.getClientBadges(this.activePlugins, client);
|
|
}
|
|
|
|
getPartitionBadges(client: any, diskNumber: number, partitionNumber: number): PluginBadge[] {
|
|
return this.badgeService.getPartitionBadges(this.activePlugins, client, diskNumber, partitionNumber);
|
|
}
|
|
|
|
// --- Configuración de Plugins (Local + Backend) ---
|
|
|
|
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;
|
|
}
|
|
}
|