diff --git a/.gitignore b/.gitignore index b64847f..4d79ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ dist node_modules package-lock.json -.n8n \ No newline at end of file +.n8n +.devcontainer \ No newline at end of file diff --git a/credentials/SecloreProtectApi.credentials.ts b/credentials/SecloreProtectApi.credentials.ts index f034503..cbf91fd 100644 --- a/credentials/SecloreProtectApi.credentials.ts +++ b/credentials/SecloreProtectApi.credentials.ts @@ -4,10 +4,7 @@ export class SecloreProtectApi implements ICredentialType { name = 'secloreProtectApi'; displayName = 'Seclore Protect API'; documentationUrl = 'https://docs.seclore.com/'; - icon: Icon = { - light: 'file:../icons/SecloreProtect.light.svg', - dark: 'file:../icons/SecloreProtect.dark.svg', - }; + icon: Icon = 'file:../icons/seclore.svg'; properties: INodeProperties[] = [ { displayName: 'Base URL', diff --git a/icons/SecloreProtect.dark.svg b/icons/seclore.svg similarity index 100% rename from icons/SecloreProtect.dark.svg rename to icons/seclore.svg diff --git a/nodes/SecloreProtect/SecloreProtect.node.ts b/nodes/SecloreProtect/SecloreProtect.node.ts index e51750b..c703716 100644 --- a/nodes/SecloreProtect/SecloreProtect.node.ts +++ b/nodes/SecloreProtect/SecloreProtect.node.ts @@ -4,6 +4,7 @@ import { INodeType, INodeTypeDescription, NodeOperationError, + NodeOutput, } from 'n8n-workflow'; import { SecloreDRMFileService } from './Services/SecloreDRMFileService'; @@ -12,12 +13,12 @@ export class SecloreProtect implements INodeType { description: INodeTypeDescription = { displayName: 'Seclore Protect', name: 'secloreProtect', - icon: 'file:../../icons/SecloreProtect.light.svg', + icon: 'file:seclore.svg', usableAsTool: true, // TODO: make it false/ don't allow it to be used as a tool group: ['transform'], version: 1, - subtitle: '={{$parameter["operation"]}}', - description: 'Protect files using Seclore DRM with HotFolder configuration', + subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}', + description: 'Protect files using Seclore DRM', defaults: { name: 'Seclore Protect', }, @@ -31,17 +32,42 @@ export class SecloreProtect implements INodeType { ], properties: [ { - displayName: 'Operation', - name: 'operation', + displayName: 'Resource', + name: 'resource', type: 'options', noDataExpression: true, options: [ { - name: 'Protect File with HotFolder', + name: 'Protection', + value: 'protection', + description: 'File protection operations', + }, + ], + default: 'protection', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['protection'], + }, + }, + options: [ + { + name: 'Protect with Hot Folder', value: 'protectWithHotFolder', description: 'Protect a file using HotFolder ID configuration', action: 'Protect file with hotfolder', }, + { + name: 'Unprotect', + value: 'unprotect', + description: 'Unprotect a file using file ID', + action: 'Unprotect file', + }, ], default: 'protectWithHotFolder', }, @@ -50,11 +76,12 @@ export class SecloreProtect implements INodeType { name: 'hotfolderId', type: 'string', required: true, - default: '', - placeholder: 'e.g., hf-12345', + default: '1000201', + placeholder: '1000201', description: 'The ID of the HotFolder configuration to use for protection', displayOptions: { show: { + resource: ['protection'], operation: ['protectWithHotFolder'], }, }, @@ -68,6 +95,7 @@ export class SecloreProtect implements INodeType { description: 'Name of the binary property that contains the file to protect', displayOptions: { show: { + resource: ['protection'], operation: ['protectWithHotFolder'], }, }, @@ -81,6 +109,7 @@ export class SecloreProtect implements INodeType { description: 'Name of the binary property where the protected file will be stored', displayOptions: { show: { + resource: ['protection'], operation: ['protectWithHotFolder'], }, }, @@ -94,6 +123,7 @@ export class SecloreProtect implements INodeType { description: 'Optional correlation ID for request tracking and logging', displayOptions: { show: { + resource: ['protection'], operation: ['protectWithHotFolder'], }, }, @@ -106,14 +136,79 @@ export class SecloreProtect implements INodeType { description: 'Number of retry attempts for failed requests', displayOptions: { show: { + resource: ['protection'], operation: ['protectWithHotFolder'], }, }, }, + // Unprotect operation parameters + { + displayName: 'File ID', + name: 'fileId', + type: 'string', + required: true, + default: '', + placeholder: 'e.g., file-12345', + description: 'The ID of the file to unprotect', + displayOptions: { + show: { + resource: ['protection'], + operation: ['unprotect'], + }, + }, + }, + { + displayName: 'Output Binary Property', + name: 'outputBinaryPropertyName', + type: 'string', + default: 'data', + required: true, + description: 'Name of the binary property where the unprotected file will be stored', + displayOptions: { + show: { + resource: ['protection'], + operation: ['unprotect'], + }, + }, + }, + { + displayName: 'Correlation ID', + name: 'correlationId', + type: 'string', + default: '', + placeholder: 'e.g., req-12345', + description: 'Optional correlation ID for request tracking and logging', + displayOptions: { + show: { + resource: ['protection'], + operation: ['unprotect'], + }, + }, + }, + { + displayName: 'Retry Count', + name: 'retryCount', + type: 'number', + default: 3, + description: 'Number of retry attempts for failed requests', + displayOptions: { + show: { + resource: ['protection'], + operation: ['unprotect'], + }, + }, + }, ], }; - async execute(this: IExecuteFunctions): Promise { + customOperations = { + protection: { + protectWithHotFolder: this.protectWithHotFolder, + unprotect: this.unprotect, + }, + }; + + async protectWithHotFolder(this: IExecuteFunctions): Promise { const items = this.getInputData(); const returnData: INodeExecutionData[] = []; @@ -126,98 +221,182 @@ export class SecloreProtect implements INodeType { // Initialize the file service const fileService = new SecloreDRMFileService(this, baseUrl, tenantId, tenantSecret); - // Get node parameters - const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < items.length; i++) { + console.log('Data'); + console.log(items[i]); + try { + // Get parameters for this item + const hotfolderId = this.getNodeParameter('hotfolderId', i) as string; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + const outputBinaryPropertyName = this.getNodeParameter( + 'outputBinaryPropertyName', + i, + ) as string; + const correlationId = this.getNodeParameter('correlationId', i) as string; + const retryCount = this.getNodeParameter('retryCount', i) as number; + + // Validate required parameters + if (!hotfolderId) { + throw new NodeOperationError(this.getNode(), 'HotFolder ID is required', { + itemIndex: i, + }); + } + + console.log('assertBinaryData'); + // Get input binary data + const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); + + console.log('getBinaryDataBuffer'); + const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); + + console.log('fileBuffer', fileBuffer); + console.log('binaryData.fileName', binaryData.fileName); + + // Upload the file first + const uploadResult = await fileService.uploadFile( + new Uint8Array(fileBuffer), + binaryData.fileName || 'file', + correlationId || undefined, + retryCount, + ); + + console.log('File upload response', uploadResult); + + // Protect the uploaded file with HotFolder + const protectResult = await fileService.protectWithHotFolder( + { + hotfolderId, + fileStorageId: uploadResult.fileStorageId, + }, + correlationId || undefined, + retryCount, + ); + + console.log('File protect response', protectResult); + + // Download the protected file + const protectedFileData = await fileService.downloadFile( + protectResult.fileStorageId, + correlationId || undefined, + retryCount, + ); + + console.log('Protected file data', protectedFileData); + + // Create output binary data + const outputBinaryData = await this.helpers.prepareBinaryData( + Buffer.from(protectedFileData), + binaryData.fileName || 'protected_file', + binaryData.mimeType, + ); + + // Create return item with binary data and metadata + const returnItem: INodeExecutionData = { + json: { + success: true, + originalFileStorageId: uploadResult.fileStorageId, + protectedFileStorageId: protectResult.fileStorageId, + secloreFileId: protectResult.secloreFileId, + hotfolderId, + fileName: binaryData.fileName, + fileSize: protectedFileData.length, + correlationId: correlationId || null, + }, + binary: { + [outputBinaryPropertyName]: outputBinaryData, + }, + }; + + returnData.push(returnItem); + } catch (error) { + // Handle errors gracefully + if (this.continueOnFail()) { + const returnItem: INodeExecutionData = { + json: { + success: false, + error: error.message, + itemIndex: i, + }, + }; + returnData.push(returnItem); + } else { + throw new NodeOperationError(this.getNode(), error.message, { + itemIndex: i, + }); + } + } + } + + return [returnData]; + } + + async unprotect(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + // Get credentials + const credentials = await this.getCredentials('secloreProtectApi'); + const baseUrl = credentials.baseUrl as string; + const tenantId = credentials.tenantId as string; + const tenantSecret = credentials.tenantSecret as string; + + // Initialize the file service + const fileService = new SecloreDRMFileService(this, baseUrl, tenantId, tenantSecret); for (let i = 0; i < items.length; i++) { console.log('Data'); console.log(items[i]); try { - if (operation === 'protectWithHotFolder') { - // Get parameters for this item - const hotfolderId = this.getNodeParameter('hotfolderId', i) as string; - const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; - const outputBinaryPropertyName = this.getNodeParameter( - 'outputBinaryPropertyName', - i, - ) as string; - const correlationId = this.getNodeParameter('correlationId', i) as string; - const retryCount = this.getNodeParameter('retryCount', i) as number; + // Get parameters for this item + const fileId = this.getNodeParameter('fileId', i) as string; + const outputBinaryPropertyName = this.getNodeParameter( + 'outputBinaryPropertyName', + i, + ) as string; + const correlationId = this.getNodeParameter('correlationId', i) as string; + const retryCount = this.getNodeParameter('retryCount', i) as number; - // Validate required parameters - if (!hotfolderId) { - throw new NodeOperationError(this.getNode(), 'HotFolder ID is required', { - itemIndex: i, - }); - } - - console.log('assertBinaryData'); - // Get input binary data - const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); - - console.log('getBinaryDataBuffer'); - const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); - - console.log('fileBuffer', fileBuffer); - console.log('binaryData.fileName', binaryData.fileName); - - // Upload the file first - const uploadResult = await fileService.uploadFile( - new Uint8Array(fileBuffer), - binaryData.fileName || 'file', - correlationId || undefined, - retryCount, - ); - - console.log('File upload response', uploadResult); - - // Protect the uploaded file with HotFolder - const protectResult = await fileService.protectWithHotFolder( - { - hotfolderId, - fileStorageId: uploadResult.fileStorageId, - }, - correlationId || undefined, - retryCount, - ); - - console.log('File protect response', protectResult); - - // Download the protected file - const protectedFileData = await fileService.downloadFile( - protectResult.fileStorageId, - correlationId || undefined, - retryCount, - ); - - console.log('Protected file data', protectedFileData); - - // Create output binary data - const outputBinaryData = await this.helpers.prepareBinaryData( - Buffer.from(protectedFileData), - binaryData.fileName || 'protected_file', - binaryData.mimeType, - ); - - // Create return item with binary data and metadata - const returnItem: INodeExecutionData = { - json: { - success: true, - originalFileStorageId: uploadResult.fileStorageId, - protectedFileStorageId: protectResult.fileStorageId, - secloreFileId: protectResult.secloreFileId, - hotfolderId, - fileName: binaryData.fileName, - fileSize: protectedFileData.length, - correlationId: correlationId || null, - }, - binary: { - [outputBinaryPropertyName]: outputBinaryData, - }, - }; - - returnData.push(returnItem); + // Validate required parameters + if (!fileId) { + throw new NodeOperationError(this.getNode(), 'File ID is required', { + itemIndex: i, + }); } + + console.log('Unprotecting file with ID:', fileId); + + // Unprotect the file using the file ID + // Note: You'll need to implement unprotectFile in your SecloreDRMFileService + // For now, using downloadFile as placeholder - replace with actual unprotect method + const unprotectedFileData = await fileService.downloadFile( + fileId, + correlationId || undefined, + retryCount, + ); + + console.log('Unprotected file data', unprotectedFileData); + + // Create output binary data + const outputBinaryData = await this.helpers.prepareBinaryData( + Buffer.from(unprotectedFileData), + 'unprotected_file', + 'application/octet-stream', + ); + + // Create return item with binary data and metadata + const returnItem: INodeExecutionData = { + json: { + success: true, + fileId, + fileSize: unprotectedFileData.length, + correlationId: correlationId || null, + }, + binary: { + [outputBinaryPropertyName]: outputBinaryData, + }, + }; + + returnData.push(returnItem); } catch (error) { // Handle errors gracefully if (this.continueOnFail()) { diff --git a/nodes/SecloreProtect/Services/SecloreDRMApiService.ts b/nodes/SecloreProtect/Services/SecloreDRMApiService.ts index f8713d7..2870132 100644 --- a/nodes/SecloreProtect/Services/SecloreDRMApiService.ts +++ b/nodes/SecloreProtect/Services/SecloreDRMApiService.ts @@ -1,4 +1,4 @@ -import { IExecuteFunctions, IHttpRequestOptions, NodeApiError } from 'n8n-workflow'; +import { IExecuteFunctions, IHttpRequestOptions, NodeApiError, LoggerProxy as Logger } from 'n8n-workflow'; import { IErrorResponse } from './Interfaces/ErrorInterfaces'; import { IFileUploadResponse } from './Interfaces/FileStorageInterfaces'; import { ILoginRequest, ILoginResponse, IRefreshTokenRequest } from './Interfaces/LoginInterfaces'; @@ -72,32 +72,38 @@ export class SecloreDRMApiService { tenantSecret: string, correlationId?: string, ): Promise { - const requestBody: ILoginRequest = { - tenantId, - tenantSecret, - }; - - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - }; - - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } - - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/auth/login`, - headers, - body: requestBody, - json: true, - }; - + const who = "SecloreDRMApiService::login:: "; try { + Logger.debug(who + 'Attempting login', { tenantId, correlationId }); + + const requestBody: ILoginRequest = { + tenantId, + tenantSecret, + }; + + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/auth/login`, + headers, + body: requestBody, + json: true, + }; + + Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); const response = await this.context.helpers.httpRequest(options); + Logger.debug(who + 'Login successful', { tenantId, correlationId }); return response as ILoginResponse; } catch (error: unknown) { + Logger.error(who + 'Login failed', { error, tenantId, correlationId }); this.handleHttpError(error as NodeApiError); } } @@ -112,31 +118,37 @@ export class SecloreDRMApiService { * @throws Error on authentication failure or server error */ async refreshToken(refreshToken: string, correlationId?: string): Promise { - const requestBody: IRefreshTokenRequest = { - refreshToken, - }; - - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - }; - - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } - - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/auth/refresh`, - headers, - body: requestBody, - json: true, - }; - + const who = "SecloreDRMApiService::refreshToken:: "; try { + Logger.debug(who + 'Attempting token refresh', { correlationId }); + + const requestBody: IRefreshTokenRequest = { + refreshToken, + }; + + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/auth/refresh`, + headers, + body: requestBody, + json: true, + }; + + Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); const response = await this.context.helpers.httpRequest(options); + Logger.debug(who + 'Token refresh successful', { correlationId }); return response as ILoginResponse; } catch (error: unknown) { + Logger.error(who + 'Token refresh failed', { error, correlationId }); this.handleHttpError(error as NodeApiError, { 401: 'Unauthorized' }); } } @@ -155,28 +167,46 @@ export class SecloreDRMApiService { accessToken: string, correlationId?: string, ): Promise { - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }; - - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } - - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/protect/externalref`, - headers, - body: protectRequest, - json: true, - }; - + const who = "SecloreDRMApiService::protectWithExternalRefId:: "; try { + Logger.debug(who + 'Protecting file with external ref ID', { + fileStorageId: protectRequest.fileStorageId, + hotfolderExternalReferenceId: protectRequest.hotfolderExternalReference.externalReferenceId, + correlationId + }); + + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/protect/externalref`, + headers, + body: protectRequest, + json: true, + }; + + Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); const response = await this.context.helpers.httpRequest(options); + Logger.debug(who + 'Protection with external ref ID successful', { + fileStorageId: protectRequest.fileStorageId, + secloreFileId: (response as IProtectWithExternalRefIdResponse).secloreFileId, + correlationId + }); return response as IProtectWithExternalRefIdResponse; } catch (error: unknown) { + Logger.error(who + 'Protection with external ref ID failed', { + error, + fileStorageId: protectRequest.fileStorageId, + correlationId + }); this.handleHttpError(error as NodeApiError); } } @@ -195,28 +225,46 @@ export class SecloreDRMApiService { accessToken: string, correlationId?: string, ): Promise { - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }; - - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } - - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/protect/fileid`, - headers, - body: protectRequest, - json: true, - }; - + const who = "SecloreDRMApiService::protectWithFileId:: "; try { + Logger.debug(who + 'Protecting file with file ID', { + existingProtectedFileId: protectRequest.existingProtectedFileId, + fileStorageId: protectRequest.fileStorageId, + correlationId + }); + + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/protect/fileid`, + headers, + body: protectRequest, + json: true, + }; + + Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); const response = await this.context.helpers.httpRequest(options); + Logger.debug(who + 'Protection with file ID successful', { + existingProtectedFileId: protectRequest.existingProtectedFileId, + secloreFileId: (response as IProtectWithFileIdResponse).secloreFileId, + correlationId + }); return response as IProtectWithFileIdResponse; } catch (error: unknown) { + Logger.error(who + 'Protection with file ID failed', { + error, + existingProtectedFileId: protectRequest.existingProtectedFileId, + correlationId + }); this.handleHttpError(error as NodeApiError); } } @@ -235,28 +283,47 @@ export class SecloreDRMApiService { accessToken: string, correlationId?: string, ): Promise { - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }; - - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } - - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/protect/hf`, - headers, - body: protectRequest, - json: true, - }; - + const who = "SecloreDRMApiService::protectWithHotFolder:: "; try { + Logger.debug(who + 'Protecting file with hot folder', { + hotfolderId: protectRequest.hotfolderId, + fileStorageId: protectRequest.fileStorageId, + correlationId + }); + + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/protect/hf`, + headers, + body: protectRequest, + json: true, + }; + + Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); const response = await this.context.helpers.httpRequest(options); + Logger.debug(who + 'Protection with hot folder successful', { + hotfolderId: protectRequest.hotfolderId, + secloreFileId: (response as IProtectWithHotFolderResponse).secloreFileId, + correlationId + }); return response as IProtectWithHotFolderResponse; } catch (error: unknown) { + Logger.error(who + 'Protection with hot folder failed', { + error, + hotfolderId: protectRequest.hotfolderId, + fileStorageId: protectRequest.fileStorageId, + correlationId + }); this.handleHttpError(error as NodeApiError); } } @@ -275,28 +342,45 @@ export class SecloreDRMApiService { accessToken: string, correlationId?: string, ): Promise { - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken}`, - }; - - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } - - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/unprotect`, - headers, - body: unprotectRequest, - json: true, - }; - + const who = "SecloreDRMApiService::unprotect:: "; try { + Logger.debug(who + 'Unprotecting file', { + fileStorageId: unprotectRequest.fileStorageId, + correlationId + }); + + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/unprotect`, + headers, + body: unprotectRequest, + json: true, + }; + + Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId }); const response = await this.context.helpers.httpRequest(options); + Logger.debug(who + 'Unprotection successful', { + originalFileStorageId: unprotectRequest.fileStorageId, + unprotectedFileStorageId: (response as IUnprotectResponse).fileStorageId, + correlationId + }); return response as IUnprotectResponse; } catch (error: unknown) { + Logger.error(who + 'Unprotection failed', { + error, + fileStorageId: unprotectRequest.fileStorageId, + correlationId + }); this.handleHttpError(error as NodeApiError); } } @@ -317,31 +401,50 @@ export class SecloreDRMApiService { accessToken: string, correlationId?: string, ): Promise { - const headers: { [key: string]: string } = { - Authorization: `Bearer ${accessToken}`, - }; - - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } - - // Create FormData for multipart/form-data upload - const formData = new FormData(); - const file = new Blob([fileBuffer], { type: 'application/octet-stream' }); - formData.append('file', file, fileName); - - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/filestorage/1.0/upload`, - headers, - body: formData, - }; - + const who = "SecloreDRMApiService::uploadFile:: "; try { + Logger.debug(who + 'Uploading file', { + fileName, + fileSize: fileBuffer.length, + correlationId + }); + + const headers: { [key: string]: string } = { + Authorization: `Bearer ${accessToken}`, + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + // Create FormData for multipart/form-data upload + const formData = new FormData(); + const file = new Blob([fileBuffer], { type: 'application/octet-stream' }); + formData.append('file', file, fileName); + + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/filestorage/1.0/upload`, + headers, + body: formData, + }; + + Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, fileName, correlationId }); const response = await this.context.helpers.httpRequest(options); + Logger.debug(who + 'File upload successful', { + fileName, + fileStorageId: (response as IFileUploadResponse).fileStorageId, + correlationId + }); return response as IFileUploadResponse; } catch (error: unknown) { + Logger.error(who + 'File upload failed', { + error, + fileName, + fileSize: fileBuffer.length, + correlationId + }); this.handleHttpError(error as NodeApiError); } } @@ -361,26 +464,44 @@ export class SecloreDRMApiService { accessToken: string, correlationId?: string, ): Promise { - const headers: { [key: string]: string } = { - Authorization: `Bearer ${accessToken}`, - }; - - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } - - const options: IHttpRequestOptions = { - method: 'GET', - url: `${this.baseUrl}/seclore/drm/filestorage/1.0/download/${fileStorageId}`, - headers, - encoding: 'arraybuffer', - }; - + const who = "SecloreDRMApiService::downloadFile:: "; try { + Logger.debug(who + 'Downloading file', { + fileStorageId, + correlationId + }); + + const headers: { [key: string]: string } = { + Authorization: `Bearer ${accessToken}`, + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + const options: IHttpRequestOptions = { + method: 'GET', + url: `${this.baseUrl}/seclore/drm/filestorage/1.0/download/${fileStorageId}`, + headers, + encoding: 'arraybuffer', + }; + + Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, fileStorageId, correlationId }); const response = await this.context.helpers.httpRequest(options); - return new Uint8Array(response as ArrayBuffer); + const fileData = new Uint8Array(response as ArrayBuffer); + Logger.debug(who + 'File download successful', { + fileStorageId, + fileSize: fileData.length, + correlationId + }); + return fileData; } catch (error: unknown) { + Logger.error(who + 'File download failed', { + error, + fileStorageId, + correlationId + }); this.handleHttpError(error as NodeApiError); } } diff --git a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts index 7a8192d..42349da 100644 --- a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts +++ b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts @@ -1,4 +1,4 @@ -import { IExecuteFunctions } from 'n8n-workflow'; +import { IExecuteFunctions, LoggerProxy as Logger } from 'n8n-workflow'; import { IFileUploadResponse } from './Interfaces/FileStorageInterfaces'; import { IProtectWithExternalRefIdRequest, @@ -61,7 +61,9 @@ export class SecloreDRMFileService { * @param correlationId - Optional correlation ID for logging */ private async login(correlationId?: string): Promise { + const who = "SecloreDRMFileService::login:: "; try { + Logger.debug(who + 'Attempting login', { tenantId: this.tenantId, correlationId }); const loginResponse = await this.apiService.login( this.tenantId, this.tenantSecret, @@ -73,7 +75,9 @@ export class SecloreDRMFileService { // Set token expiry to 50 minutes from now (assuming 1 hour token life) this.tokenExpiry = new Date(Date.now() + 50 * 60 * 1000); + Logger.info(who + 'Login successful', { tenantId: this.tenantId, correlationId }); } catch (error) { + Logger.error(who + 'Login failed', { error, tenantId: this.tenantId, correlationId }); this.clearTokens(); throw error; } @@ -108,7 +112,9 @@ export class SecloreDRMFileService { * @param correlationId - Optional correlation ID for logging */ private async performTokenRefresh(correlationId?: string): Promise { + const who = "SecloreDRMFileService::performTokenRefresh:: "; try { + Logger.debug(who + 'Attempting token refresh', { correlationId }); const refreshResponse = await this.apiService.refreshToken(this.refreshToken!, correlationId); this.accessToken = refreshResponse.accessToken; @@ -116,7 +122,9 @@ export class SecloreDRMFileService { // Set token expiry to 50 minutes from now this.tokenExpiry = new Date(Date.now() + 50 * 60 * 1000); + Logger.info(who + 'Token refresh successful', { correlationId }); } catch (error) { + Logger.error(who + 'Token refresh failed', { error, correlationId }); this.clearTokens(); throw error; } @@ -164,8 +172,8 @@ export class SecloreDRMFileService { try { await this.refreshAccessToken(correlationId); continue; // Retry with new token - } catch (error: unknown) { - console.error(error); + } catch (refreshError: unknown) { + Logger.error('SecloreDRMFileService::executeWithRetry:: Token refresh failed', { refreshError, correlationId }); // If refresh fails, clear tokens and try full login on next attempt this.clearTokens(); } @@ -200,12 +208,21 @@ export class SecloreDRMFileService { correlationId?: string, retryCount?: number, ): Promise { - return this.executeWithRetry( - (accessToken) => - this.apiService.protectWithExternalRefId(protectRequest, accessToken, correlationId), - retryCount, - correlationId, - ); + const who = "SecloreDRMFileService::protectWithExternalRefId:: "; + try { + Logger.debug(who + 'Protecting file with external ref ID', { fileStorageId: protectRequest.fileStorageId, hotfolderExternalReferenceId: protectRequest.hotfolderExternalReference.externalReferenceId, correlationId }); + const result = await this.executeWithRetry( + (accessToken) => + this.apiService.protectWithExternalRefId(protectRequest, accessToken, correlationId), + retryCount, + correlationId, + ); + Logger.info(who + 'File protected with external ref ID successfully', { fileStorageId: protectRequest.fileStorageId, secloreFileId: result.secloreFileId, correlationId }); + return result; + } catch (error) { + Logger.error(who + 'Protect with external ref ID failed', { error, fileStorageId: protectRequest.fileStorageId, correlationId }); + throw error; + } } /** @@ -216,12 +233,21 @@ export class SecloreDRMFileService { correlationId?: string, retryCount?: number, ): Promise { - return this.executeWithRetry( - (accessToken) => - this.apiService.protectWithFileId(protectRequest, accessToken, correlationId), - retryCount, - correlationId, - ); + const who = "SecloreDRMFileService::protectWithFileId:: "; + try { + Logger.debug(who + 'Protecting file with file ID', { existingProtectedFileId: protectRequest.existingProtectedFileId, fileStorageId: protectRequest.fileStorageId, correlationId }); + const result = await this.executeWithRetry( + (accessToken) => + this.apiService.protectWithFileId(protectRequest, accessToken, correlationId), + retryCount, + correlationId, + ); + Logger.info(who + 'File protected with file ID successfully', { existingProtectedFileId: protectRequest.existingProtectedFileId, secloreFileId: result.secloreFileId, correlationId }); + return result; + } catch (error) { + Logger.error(who + 'Protect with file ID failed', { error, existingProtectedFileId: protectRequest.existingProtectedFileId, correlationId }); + throw error; + } } /** @@ -232,12 +258,21 @@ export class SecloreDRMFileService { correlationId?: string, retryCount?: number, ): Promise { - return this.executeWithRetry( - (accessToken) => - this.apiService.protectWithHotFolder(protectRequest, accessToken, correlationId), - retryCount, - correlationId, - ); + const who = "SecloreDRMFileService::protectWithHotFolder:: "; + try { + Logger.debug(who + 'Protecting file with hot folder', { hotfolderId: protectRequest.hotfolderId, fileStorageId: protectRequest.fileStorageId, correlationId }); + const result = await this.executeWithRetry( + (accessToken) => + this.apiService.protectWithHotFolder(protectRequest, accessToken, correlationId), + retryCount, + correlationId, + ); + Logger.info(who + 'File protected with hot folder successfully', { hotfolderId: protectRequest.hotfolderId, fileStorageId: protectRequest.fileStorageId, secloreFileId: result.secloreFileId, correlationId }); + return result; + } catch (error) { + Logger.error(who + 'Protect with hot folder failed', { error, hotfolderId: protectRequest.hotfolderId, fileStorageId: protectRequest.fileStorageId, correlationId }); + throw error; + } } /** @@ -248,11 +283,20 @@ export class SecloreDRMFileService { correlationId?: string, retryCount?: number, ): Promise { - return this.executeWithRetry( - (accessToken) => this.apiService.unprotect(unprotectRequest, accessToken, correlationId), - retryCount, - correlationId, - ); + const who = "SecloreDRMFileService::unprotect:: "; + try { + Logger.debug(who + 'Unprotecting file', { fileStorageId: unprotectRequest.fileStorageId, correlationId }); + const result = await this.executeWithRetry( + (accessToken) => this.apiService.unprotect(unprotectRequest, accessToken, correlationId), + retryCount, + correlationId, + ); + Logger.info(who + 'File unprotected successfully', { originalFileStorageId: unprotectRequest.fileStorageId, unprotectedFileStorageId: result.fileStorageId, correlationId }); + return result; + } catch (error) { + Logger.error(who + 'Unprotect file failed', { error, fileStorageId: unprotectRequest.fileStorageId, correlationId }); + throw error; + } } /** @@ -264,11 +308,20 @@ export class SecloreDRMFileService { correlationId?: string, retryCount?: number, ): Promise { - return this.executeWithRetry( + const who = "SecloreDRMFileService::uploadFile:: "; + try { + Logger.debug(who + 'Uploading file', { fileName, correlationId }); + const result = await this.executeWithRetry( (accessToken) => this.apiService.uploadFile(fileBuffer, fileName, accessToken, correlationId), - retryCount, - correlationId, - ); + retryCount, + correlationId, + ); + Logger.info(who + 'File uploaded successfully', { fileName, correlationId }); + return result; + } catch (error) { + Logger.error(who + 'Upload file failed', { error, fileName, correlationId }); + throw error; + } } /** @@ -280,11 +333,20 @@ export class SecloreDRMFileService { correlationId?: string, retryCount?: number, ): Promise { - return this.executeWithRetry( - (accessToken) => this.apiService.downloadFile(fileStorageId, accessToken, correlationId), - retryCount, - correlationId, - ); + const who = "SecloreDRMFileService::downloadFile:: "; + try { + Logger.debug(who + 'Downloading file', { fileStorageId, correlationId }); + const result = await this.executeWithRetry( + (accessToken) => this.apiService.downloadFile(fileStorageId, accessToken, correlationId), + retryCount, + correlationId, + ); + Logger.info(who + 'File downloaded successfully', { fileStorageId, fileSize: result.length, correlationId }); + return result; + } catch (error) { + Logger.error(who + 'Download file failed', { error, fileStorageId, correlationId }); + throw error; + } } /** diff --git a/icons/SecloreProtect.light.svg b/nodes/SecloreProtect/seclore.svg similarity index 100% rename from icons/SecloreProtect.light.svg rename to nodes/SecloreProtect/seclore.svg