299 lines
8.9 KiB
TypeScript
299 lines
8.9 KiB
TypeScript
import { IExecuteFunctions } from 'n8n-workflow';
|
|
import { SecloreDRMApiService } from './SecloreDRMApiService';
|
|
import { IProtectWithExternalRefIdRequest, IProtectWithExternalRefIdResponse, IProtectWithFileIdRequest, IProtectWithFileIdResponse, IProtectWithHotFolderRequest, IProtectWithHotFolderResponse } from './Interfaces/ProtectInterfaces';
|
|
import { IUnprotectRequest, IUnprotectResponse } from './Interfaces/UnprotectInterfaces';
|
|
import { IFileUploadResponse } from './Interfaces/FileStorageInterfaces';
|
|
|
|
export class SecloreDRMFileService {
|
|
private apiService: SecloreDRMApiService;
|
|
private tenantId: string;
|
|
private tenantSecret: string;
|
|
private accessToken?: string;
|
|
private refreshToken?: string;
|
|
private tokenExpiry?: Date;
|
|
private refreshPromise?: Promise<void>;
|
|
private loginPromise?: Promise<void>;
|
|
|
|
constructor(
|
|
context: IExecuteFunctions,
|
|
baseUrl: string,
|
|
tenantId: string,
|
|
tenantSecret: string,
|
|
private defaultRetryCount: number = 3
|
|
) {
|
|
this.apiService = new SecloreDRMApiService(context, baseUrl);
|
|
this.tenantId = tenantId;
|
|
this.tenantSecret = tenantSecret;
|
|
}
|
|
|
|
/**
|
|
* Ensures we have a valid access token, logging in if necessary
|
|
* @param correlationId - Optional correlation ID for logging
|
|
*/
|
|
private async ensureAuthenticated(correlationId?: string): Promise<void> {
|
|
// If we don't have a token or it's expired, login
|
|
if (!this.accessToken || (this.tokenExpiry && new Date() >= this.tokenExpiry)) {
|
|
// If there's already a login in progress, wait for it
|
|
if (this.loginPromise) {
|
|
await this.loginPromise;
|
|
return;
|
|
}
|
|
|
|
// Start login and store the promise
|
|
this.loginPromise = this.login(correlationId);
|
|
try {
|
|
await this.loginPromise;
|
|
} finally {
|
|
this.loginPromise = undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs login and stores tokens
|
|
* @param correlationId - Optional correlation ID for logging
|
|
*/
|
|
private async login(correlationId?: string): Promise<void> {
|
|
try {
|
|
const loginResponse = await this.apiService.login(
|
|
this.tenantId,
|
|
this.tenantSecret,
|
|
correlationId
|
|
);
|
|
|
|
this.accessToken = loginResponse.accessToken;
|
|
this.refreshToken = loginResponse.refreshToken;
|
|
|
|
// Set token expiry to 50 minutes from now (assuming 1 hour token life)
|
|
this.tokenExpiry = new Date(Date.now() + 50 * 60 * 1000);
|
|
} catch (error) {
|
|
this.clearTokens();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempts to refresh the access token with concurrency protection
|
|
* @param correlationId - Optional correlation ID for logging
|
|
*/
|
|
private async refreshAccessToken(correlationId?: string): Promise<void> {
|
|
// If there's already a refresh in progress, wait for it
|
|
if (this.refreshPromise) {
|
|
await this.refreshPromise;
|
|
return;
|
|
}
|
|
|
|
if (!this.refreshToken) {
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
// Start refresh and store the promise
|
|
this.refreshPromise = this.performTokenRefresh(correlationId);
|
|
try {
|
|
await this.refreshPromise;
|
|
} finally {
|
|
this.refreshPromise = undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Performs the actual token refresh
|
|
* @param correlationId - Optional correlation ID for logging
|
|
*/
|
|
private async performTokenRefresh(correlationId?: string): Promise<void> {
|
|
try {
|
|
const refreshResponse = await this.apiService.refreshToken(
|
|
this.refreshToken!,
|
|
correlationId
|
|
);
|
|
|
|
this.accessToken = refreshResponse.accessToken;
|
|
this.refreshToken = refreshResponse.refreshToken;
|
|
|
|
// Set token expiry to 50 minutes from now
|
|
this.tokenExpiry = new Date(Date.now() + 50 * 60 * 1000);
|
|
} catch (error) {
|
|
this.clearTokens();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears stored tokens and pending promises
|
|
*/
|
|
private clearTokens(): void {
|
|
this.accessToken = undefined;
|
|
this.refreshToken = undefined;
|
|
this.tokenExpiry = undefined;
|
|
this.refreshPromise = undefined;
|
|
this.loginPromise = undefined;
|
|
}
|
|
|
|
/**
|
|
* Executes an API call with automatic authentication and retry logic
|
|
* @param apiCall - The API call function to execute
|
|
* @param retryCount - Number of retries (defaults to class default)
|
|
* @param correlationId - Optional correlation ID for logging
|
|
*/
|
|
private async executeWithRetry<T>(
|
|
apiCall: (accessToken: string) => Promise<T>,
|
|
retryCount: number = this.defaultRetryCount,
|
|
correlationId?: string
|
|
): Promise<T> {
|
|
let lastError: Error;
|
|
|
|
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
try {
|
|
// Ensure we have a valid token
|
|
await this.ensureAuthenticated(correlationId);
|
|
|
|
if (!this.accessToken) {
|
|
throw new Error('Failed to obtain access token');
|
|
}
|
|
|
|
// Execute the API call
|
|
return await apiCall(this.accessToken);
|
|
|
|
} catch (error: any) {
|
|
lastError = error;
|
|
|
|
// If it's an authentication error and we have retries left, try to refresh token
|
|
if (error.message.includes('Unauthorized') && attempt < retryCount) {
|
|
try {
|
|
await this.refreshAccessToken(correlationId);
|
|
continue; // Retry with new token
|
|
} catch (refreshError) {
|
|
// If refresh fails, clear tokens and try full login on next attempt
|
|
this.clearTokens();
|
|
}
|
|
}
|
|
|
|
// If it's the last attempt or not an auth error, throw
|
|
if (attempt === retryCount) {
|
|
throw lastError;
|
|
}
|
|
|
|
// Wait before retry (exponential backoff)
|
|
await new Promise(resolve => {
|
|
const delay = Math.pow(2, attempt) * 1000;
|
|
// Use a simple delay implementation
|
|
const start = Date.now();
|
|
while (Date.now() - start < delay) {
|
|
// Busy wait for delay
|
|
}
|
|
resolve(undefined);
|
|
});
|
|
}
|
|
}
|
|
|
|
throw lastError!;
|
|
}
|
|
|
|
/**
|
|
* Protect file using external identifier with automatic authentication and retry
|
|
*/
|
|
async protectWithExternalRefId(
|
|
protectRequest: IProtectWithExternalRefIdRequest,
|
|
correlationId?: string,
|
|
retryCount?: number
|
|
): Promise<IProtectWithExternalRefIdResponse> {
|
|
return this.executeWithRetry(
|
|
(accessToken) => this.apiService.protectWithExternalRefId(protectRequest, accessToken, correlationId),
|
|
retryCount,
|
|
correlationId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Protect file using existing protected file ID with automatic authentication and retry
|
|
*/
|
|
async protectWithFileId(
|
|
protectRequest: IProtectWithFileIdRequest,
|
|
correlationId?: string,
|
|
retryCount?: number
|
|
): Promise<IProtectWithFileIdResponse> {
|
|
return this.executeWithRetry(
|
|
(accessToken) => this.apiService.protectWithFileId(protectRequest, accessToken, correlationId),
|
|
retryCount,
|
|
correlationId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Protect file using HotFolder ID with automatic authentication and retry
|
|
*/
|
|
async protectWithHotFolder(
|
|
protectRequest: IProtectWithHotFolderRequest,
|
|
correlationId?: string,
|
|
retryCount?: number
|
|
): Promise<IProtectWithHotFolderResponse> {
|
|
return this.executeWithRetry(
|
|
(accessToken) => this.apiService.protectWithHotFolder(protectRequest, accessToken, correlationId),
|
|
retryCount,
|
|
correlationId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Unprotect file with automatic authentication and retry
|
|
*/
|
|
async unprotect(
|
|
unprotectRequest: IUnprotectRequest,
|
|
correlationId?: string,
|
|
retryCount?: number
|
|
): Promise<IUnprotectResponse> {
|
|
return this.executeWithRetry(
|
|
(accessToken) => this.apiService.unprotect(unprotectRequest, accessToken, correlationId),
|
|
retryCount,
|
|
correlationId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Upload file with automatic authentication and retry
|
|
*/
|
|
async uploadFile(
|
|
fileBuffer: Uint8Array,
|
|
fileName: string,
|
|
correlationId?: string,
|
|
retryCount?: number
|
|
): Promise<IFileUploadResponse> {
|
|
return this.executeWithRetry(
|
|
(accessToken) => this.apiService.uploadFile(fileBuffer, fileName, accessToken, correlationId),
|
|
retryCount,
|
|
correlationId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Download file with automatic authentication and retry
|
|
* NOTE: Files whose fileStorageId has 'DL_' prefix will be deleted from the file storage after download.
|
|
*/
|
|
async downloadFile(
|
|
fileStorageId: string,
|
|
correlationId?: string,
|
|
retryCount?: number
|
|
): Promise<Uint8Array> {
|
|
return this.executeWithRetry(
|
|
(accessToken) => this.apiService.downloadFile(fileStorageId, accessToken, correlationId),
|
|
retryCount,
|
|
correlationId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get current access token (for debugging/monitoring)
|
|
*/
|
|
getAccessToken(): string | undefined {
|
|
return this.accessToken;
|
|
}
|
|
|
|
/**
|
|
* Check if currently authenticated
|
|
*/
|
|
isAuthenticated(): boolean {
|
|
return !!this.accessToken && (!this.tokenExpiry || new Date() < this.tokenExpiry);
|
|
}
|
|
|
|
}
|