312 lines
13 KiB
JavaScript
312 lines
13 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
};
|
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
};
|
|
var CoreService_1;
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.CoreService = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const microservices_1 = require("@nestjs/microservices");
|
|
const uuid_1 = require("uuid");
|
|
const fs = __importStar(require("fs"));
|
|
const path = __importStar(require("path"));
|
|
const request_context_interceptor_1 = require("../interceptors/request-context.interceptor");
|
|
let CoreService = CoreService_1 = class CoreService {
|
|
constructor(client) {
|
|
this.client = client;
|
|
this.logger = new common_1.Logger(CoreService_1.name);
|
|
this.serviceName = process.env.SERVICE_NAME || 'plugin';
|
|
this.heartbeatInterval = null;
|
|
}
|
|
/**
|
|
* Спеціальний метод для виконання запитів до Ядра з автоматичним прокиданням авторизації
|
|
*/
|
|
async fetchCore(endpoint, options = {}) {
|
|
const coreUrl = process.env.CORE_BACKEND_URL || 'http://localhost:3001';
|
|
const store = request_context_interceptor_1.requestContext.getStore();
|
|
const authHeader = store?.get('authorization');
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...(authHeader ? { 'Authorization': authHeader } : {}),
|
|
...(options.headers || {}),
|
|
};
|
|
this.logger.log(`[CoreRequest] Sending request to ${endpoint} (Auth: ${!!authHeader})`);
|
|
const response = await fetch(`${coreUrl}${endpoint}`, {
|
|
...options,
|
|
headers,
|
|
});
|
|
if (!response.ok) {
|
|
this.logger.error(`[CoreRequest] Request to ${endpoint} failed with status ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
// ... (решта методів register, emit, request залишаються без змін)
|
|
async register() {
|
|
const coreUrl = process.env.CORE_BACKEND_URL || 'http://localhost:3001';
|
|
const possiblePaths = [
|
|
path.resolve(process.cwd(), '..', 'manifest.json'),
|
|
path.resolve(process.cwd(), 'manifest.json'),
|
|
];
|
|
let manifestPath = '';
|
|
for (const p of possiblePaths) {
|
|
if (fs.existsSync(p)) {
|
|
manifestPath = p;
|
|
break;
|
|
}
|
|
}
|
|
if (!manifestPath)
|
|
return;
|
|
// Dynamically calculate the backendUrl
|
|
const port = process.env.PORT || '3002';
|
|
const serviceName = process.env.SERVICE_NAME || 'plugin';
|
|
const isDocker = coreUrl.includes('core-backend') || (!coreUrl.includes('localhost') && !coreUrl.includes('127.0.0.1'));
|
|
const backendUrl = process.env.PLUGIN_BACKEND_URL || (isDocker ? `http://${serviceName}:${port}` : `http://localhost:${port}`);
|
|
let registered = false;
|
|
while (!registered) {
|
|
try {
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
if (process.env.PUBLIC_URL) {
|
|
let p = '/remoteEntry.js';
|
|
if (manifest.remoteEntry) {
|
|
try {
|
|
if (manifest.remoteEntry.startsWith('http://') || manifest.remoteEntry.startsWith('https://')) {
|
|
p = new URL(manifest.remoteEntry).pathname;
|
|
}
|
|
else {
|
|
p = manifest.remoteEntry;
|
|
}
|
|
}
|
|
catch (e) {
|
|
p = manifest.remoteEntry;
|
|
}
|
|
}
|
|
const publicUrl = process.env.PUBLIC_URL.endsWith('/') ? process.env.PUBLIC_URL.slice(0, -1) : process.env.PUBLIC_URL;
|
|
const pathname = p.startsWith('/') ? p : '/' + p;
|
|
manifest.remoteEntry = `${publicUrl}${pathname}`;
|
|
}
|
|
manifest.backendUrl = backendUrl;
|
|
const response = await fetch(`${coreUrl}/api/plugins/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(manifest),
|
|
});
|
|
if (response.ok) {
|
|
this.logger.log(`Plugin registered successfully!`);
|
|
registered = true;
|
|
this.startHeartbeatLoop(manifest.id, backendUrl);
|
|
}
|
|
else {
|
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
}
|
|
}
|
|
catch (error) {
|
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
}
|
|
}
|
|
}
|
|
startHeartbeatLoop(pluginId, backendUrl) {
|
|
if (this.heartbeatInterval) {
|
|
clearInterval(this.heartbeatInterval);
|
|
}
|
|
this.heartbeatInterval = setInterval(async () => {
|
|
try {
|
|
const coreUrl = process.env.CORE_BACKEND_URL || 'http://localhost:3001';
|
|
this.logger.log(`[Heartbeat] Sending heartbeat for ${pluginId}`);
|
|
const response = await fetch(`${coreUrl}/api/plugins/heartbeat`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: pluginId, backendUrl }),
|
|
});
|
|
if (response.ok) {
|
|
const resBody = await response.json();
|
|
const data = resBody && resBody.data !== undefined ? resBody.data : resBody;
|
|
if (data && data.registered === false) {
|
|
this.logger.warn(`[Heartbeat] Core does not recognize plugin ${pluginId}. Re-registering.`);
|
|
this.register();
|
|
}
|
|
else {
|
|
this.logger.log(`[Heartbeat] Heartbeat acknowledged by core.`);
|
|
}
|
|
}
|
|
else {
|
|
this.logger.error(`[Heartbeat] Failed to send heartbeat. Status: ${response.status}`);
|
|
}
|
|
}
|
|
catch (error) {
|
|
this.logger.error(`[Heartbeat] Error sending heartbeat:`, error.message || error);
|
|
}
|
|
}, 30 * 1000); // 30 seconds
|
|
}
|
|
async emit(pattern, payload) {
|
|
const event = {
|
|
eventId: (0, uuid_1.v4)(),
|
|
pattern,
|
|
payload,
|
|
source: this.serviceName,
|
|
timestamp: Date.now(),
|
|
};
|
|
this.client.emit(pattern, event).subscribe({
|
|
error: (err) => this.logger.error(`Failed to emit event ${pattern}:`, err),
|
|
});
|
|
}
|
|
async auditLog(action, resource, details) {
|
|
const store = request_context_interceptor_1.requestContext.getStore();
|
|
const authHeader = store?.get('authorization');
|
|
const ipAddress = store?.get('ip');
|
|
const userAgent = store?.get('userAgent');
|
|
let userId;
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
try {
|
|
const token = authHeader.split(' ')[1];
|
|
const base64Url = token.split('.')[1];
|
|
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
const jsonPayload = Buffer.from(base64, 'base64').toString();
|
|
const payload = JSON.parse(jsonPayload);
|
|
userId = payload.sub || payload.userId || payload.id;
|
|
}
|
|
catch (e) {
|
|
this.logger.error('Failed to decode JWT for audit log', e);
|
|
}
|
|
}
|
|
await this.emit('system.audit.log', {
|
|
userId,
|
|
action,
|
|
resource,
|
|
details,
|
|
ipAddress,
|
|
userAgent,
|
|
});
|
|
}
|
|
async getGlobalData(key) {
|
|
const response = await this.fetchCore(`/api/store/${key}`, {
|
|
headers: {
|
|
'x-plugin-id': this.serviceName,
|
|
},
|
|
});
|
|
if (response.success) {
|
|
if (response.data === null || response.data === undefined) {
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.parse(response.data);
|
|
}
|
|
catch (e) {
|
|
return response.data;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async setGlobalData(key, value) {
|
|
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
await this.fetchCore(`/api/store/${key}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ value: stringValue }),
|
|
headers: {
|
|
'x-plugin-id': this.serviceName,
|
|
},
|
|
});
|
|
}
|
|
async deleteGlobalData(key) {
|
|
await this.fetchCore(`/api/store/${key}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'x-plugin-id': this.serviceName,
|
|
},
|
|
});
|
|
}
|
|
async sendNotification(payload) {
|
|
await this.emit('system.notification.send', {
|
|
userId: payload.userId,
|
|
title: payload.title,
|
|
message: payload.message,
|
|
icon: payload.icon,
|
|
image: payload.image,
|
|
details: payload.details,
|
|
});
|
|
}
|
|
/**
|
|
* Отримати назву БД, наданої ядром (має сенс після виклику setupDatabase)
|
|
*/
|
|
getDbName() {
|
|
return process.env.PLUGIN_DB_NAME || process.env.DB_DATABASE || 'ultimate_crm';
|
|
}
|
|
/**
|
|
* Зареєструвати БД плагіна в ядрі (створює БД p_<id>, якщо ще не існує).
|
|
* Викликати ДО створення NestJS app, щоб TypeORM міг підхопити PLUGIN_DB_NAME.
|
|
*/
|
|
static async setupDatabase(options) {
|
|
const coreUrl = process.env.CORE_BACKEND_URL || 'http://localhost:3001';
|
|
const response = await fetch(`${coreUrl}/api/db/setup`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ pluginId: options.pluginId }),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Database setup failed: ${response.statusText}`);
|
|
}
|
|
const result = await response.json();
|
|
const data = result.data || result;
|
|
const dbName = data.dbName;
|
|
if (!dbName)
|
|
throw new Error('Database name not returned from core');
|
|
process.env.PLUGIN_DB_NAME = dbName;
|
|
if (data.host)
|
|
process.env.PLUGIN_DB_HOST = data.host;
|
|
if (data.port)
|
|
process.env.PLUGIN_DB_PORT = String(data.port);
|
|
if (data.username)
|
|
process.env.PLUGIN_DB_USER = data.username;
|
|
if (data.password)
|
|
process.env.PLUGIN_DB_PASSWORD = data.password;
|
|
return dbName;
|
|
}
|
|
};
|
|
exports.CoreService = CoreService;
|
|
exports.CoreService = CoreService = CoreService_1 = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__param(0, (0, common_1.Inject)('RABBITMQ_CLIENT')),
|
|
__metadata("design:paramtypes", [microservices_1.ClientProxy])
|
|
], CoreService);
|