import { S3Client, PutObjectCommand, HeadBucketCommand, } from "@aws-sdk/client-s3"; import { config, getValidationUrl, getGenerationUrl, debugLog, encryptApiKey, } from "./config"; // Initialize S3 client const getS3Client = () => { debugLog("Getting S3 client with config:", { region: config.awsRegion, bucket: config.s3BucketName, hasAccessKey: !!config.awsAccessKeyId, hasSecretKey: !!config.awsSecretAccessKey, }); if (!config.awsAccessKeyId || !config.awsSecretAccessKey) { const error = new Error( "AWS credentials not configured. Please set REACT_APP_AWS_ACCESS_KEY_ID and REACT_APP_AWS_SECRET_ACCESS_KEY" ); throw error; } try { const client = new S3Client({ region: config.awsRegion, credentials: { accessKeyId: config.awsAccessKeyId, secretAccessKey: config.awsSecretAccessKey, }, }); debugLog("S3 client created successfully"); return client; } catch (error) { debugLog("Error creating S3 client:", error); throw error; } }; // API utility functions export class ApiService { // Check AWS S3 connection status static async checkAwsConnection() { debugLog("Checking AWS S3 connection status"); debugLog("AWS Configuration:", { region: config.awsRegion, bucket: config.s3BucketName, hasAccessKey: !!config.awsAccessKeyId, hasSecretKey: !!config.awsSecretAccessKey, }); try { // Check if AWS credentials are configured if (!config.awsAccessKeyId || !config.awsSecretAccessKey) { // In development mode, allow bypassing AWS connection requirement if (process.env.NODE_ENV === "development") { debugLog( "Development mode: AWS credentials not configured, but allowing bypass" ); return { connected: true, // Allow development without AWS status: "warning", message: "Development mode: AWS configuration bypassed", details: "AWS credentials not configured - using development mode", development: true, debug: { hasAccessKey: !!config.awsAccessKeyId, hasSecretKey: !!config.awsSecretAccessKey, region: config.awsRegion, bucket: config.s3BucketName, }, }; } return { connected: false, status: "error", message: "AWS credentials not configured", details: "Missing AWS Access Key ID or Secret Access Key", debug: { hasAccessKey: !!config.awsAccessKeyId, hasSecretKey: !!config.awsSecretAccessKey, region: config.awsRegion, bucket: config.s3BucketName, }, }; } // Validate AWS Access Key format if (!config.awsAccessKeyId.startsWith("AKIA")) { // Access Key validation warning removed } // Validate Secret Key length (should be 40 characters) if (config.awsSecretAccessKey.length !== 40) { // Secret Key validation warning removed } if (!config.s3BucketName) { return { connected: false, status: "error", message: "S3 bucket not configured", details: "Missing S3 bucket name configuration", debug: { bucket: config.s3BucketName, region: config.awsRegion, }, }; } debugLog("Initializing S3 client with credentials..."); // Initialize S3 client and test connection const s3Client = getS3Client(); debugLog("Testing S3 connection with HeadBucket operation..."); // Use HeadBucket operation to test connectivity and permissions const headBucketCommand = new HeadBucketCommand({ Bucket: config.s3BucketName, }); const startTime = Date.now(); try { await s3Client.send(headBucketCommand); const endTime = Date.now(); debugLog("AWS S3 connection successful", { bucket: config.s3BucketName, region: config.awsRegion, responseTime: `${endTime - startTime}ms`, }); } catch (headBucketError) { // If HeadBucket fails due to CORS, it might still work for file uploads // Let's check if it's a CORS error specifically if ( headBucketError.message && headBucketError.message.includes("CORS") ) { debugLog( "CORS error detected - this is common for browser S3 access" ); return { connected: true, // We'll mark as connected but with a warning status: "warning", message: "AWS S3 accessible with CORS limitations", details: "HeadBucket operation blocked by CORS, but file uploads should work", bucket: config.s3BucketName, region: config.awsRegion, corsWarning: true, }; } // Re-throw if it's not a CORS issue throw headBucketError; } return { connected: true, status: "success", message: "AWS S3 connected successfully", details: `Connected to bucket: ${config.s3BucketName} in ${config.awsRegion}`, bucket: config.s3BucketName, region: config.awsRegion, }; } catch (error) { debugLog("AWS S3 connection failed", error); debugLog("Error details:", { name: error.name, message: error.message, code: error.code, statusCode: error.$metadata?.httpStatusCode, requestId: error.$metadata?.requestId, }); let message = "AWS S3 connection failed"; let details = error.message; // Provide more specific error messages based on error type if (error.name === "CredentialsProviderError") { message = "Invalid AWS credentials"; details = "Check your AWS Access Key ID and Secret Access Key"; } else if (error.name === "NoSuchBucket") { message = "S3 bucket not found"; details = `Bucket '${config.s3BucketName}' does not exist or is not accessible`; } else if (error.name === "AccessDenied" || error.name === "Forbidden") { message = "Access denied to S3 bucket"; details = "Check your AWS permissions for S3 operations"; } else if ( error.name === "NetworkingError" || error.message.includes("fetch") || error.name === "TypeError" || error.message.includes("CORS") || error.code === "NetworkingError" ) { message = "Network/CORS connection failed"; details = "This is likely a CORS issue. The bucket exists but browser access is restricted. File uploads might still work."; } else if (error.name === "TimeoutError") { message = "Connection timeout"; details = "AWS S3 connection timed out"; } else if (error.code === "InvalidAccessKeyId") { message = "Invalid AWS Access Key ID"; details = "The AWS Access Key ID you provided does not exist"; } else if (error.code === "SignatureDoesNotMatch") { message = "Invalid AWS Secret Access Key"; details = "The AWS Secret Access Key you provided is incorrect"; } return { connected: false, status: "error", message, details, error: error.name || "Unknown error", debug: { errorCode: error.code, errorName: error.name, httpStatusCode: error.$metadata?.httpStatusCode, requestId: error.$metadata?.requestId, bucket: config.s3BucketName, region: config.awsRegion, }, }; } } // Retry mechanism for API requests static async retryRequest(requestFunction, maxRetries = config.maxRetries) { let lastError = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { debugLog(`API request attempt ${attempt}/${maxRetries}`); const result = await requestFunction(); return result; } catch (error) { lastError = error; debugLog(`API request attempt ${attempt} failed`, error); if (attempt < maxRetries) { // Wait before retrying (exponential backoff) const waitTime = Math.pow(2, attempt - 1) * 1000; debugLog(`Retrying in ${waitTime}ms...`); await new Promise((resolve) => setTimeout(resolve, waitTime)); } } } throw lastError; } static async validateApiKey(apiKey) { debugLog("Validating API key", { keyPrefix: apiKey.substring(0, 8) + "...", }); try { // Encrypt the API key before sending for validation const encryptedApiKey = encryptApiKey(apiKey); const response = await fetch(getValidationUrl(), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiKey: encryptedApiKey }), signal: AbortSignal.timeout(config.apiTimeout * 1000), // Convert to milliseconds }); const result = await response.json(); debugLog("API key validation result", result); // Handle the new API response format if (response.ok && result.status === "success") { // If validation is successful, store the encrypted key for future use if (result.data && result.data.isValid) { sessionStorage.setItem("encryptedApiKey", encryptedApiKey); return { success: true, valid: true, isValid: true, message: result.message || "Api Credentials Validated Successfully", data: result.data, }; } } // Handle error responses or invalid API keys if (result.status === "error") { // Remove any stored invalid key sessionStorage.removeItem("encryptedApiKey"); return { success: false, valid: false, isValid: false, message: result.message || "Invalid or revoked API key", error: true, }; } // Fallback for unexpected response format throw new Error( result.message || `Validation failed: ${response.status} ${response.statusText}` ); } catch (error) { debugLog("API key validation error", error); // Handle network errors - for development, allow bypass with proper format if (error.name === "TypeError" && error.message.includes("fetch")) { debugLog( "Network error detected, using fallback validation for development" ); // Simple validation for development - check if it's a valid sync_ token format if (apiKey.startsWith("sync_") && apiKey.length > 20) { const encryptedApiKey = encryptApiKey(apiKey); sessionStorage.setItem("encryptedApiKey", encryptedApiKey); return { success: true, valid: true, isValid: true, message: "API key format valid (offline validation)", data: { isValid: true }, }; } else { // Remove any stored invalid key sessionStorage.removeItem("encryptedApiKey"); return { success: false, valid: false, isValid: false, message: "Invalid API key format. Key must start with 'sync_' and be at least 20 characters long.", error: true, }; } } // Handle other network errors for development if ( error.message.includes("Failed to fetch") || error.name === "TypeError" ) { debugLog( "Network connection error, using fallback validation for development" ); // Simple validation for development - check if it's a valid sync_ token format if (apiKey.startsWith("sync_") && apiKey.length > 20) { const encryptedApiKey = encryptApiKey(apiKey); sessionStorage.setItem("encryptedApiKey", encryptedApiKey); return { success: true, valid: true, isValid: true, message: "API key format valid (offline validation - server not available)", data: { isValid: true }, }; } else { // Remove any stored invalid key sessionStorage.removeItem("encryptedApiKey"); return { success: false, valid: false, isValid: false, message: "Invalid API key format. Key must start with 'sync_' and be at least 20 characters long.", error: true, }; } } // Network or other errors if (error.name === "AbortError") { throw new Error("Request timeout: API validation took too long"); } throw error; } } static async uploadFileToS3(file) { debugLog("Uploading file to S3", { fileName: file.name, size: file.size }); // Check file size const maxSizeBytes = config.maxFileSizeMB * 1024 * 1024; if (file.size > maxSizeBytes) { throw new Error( `File size exceeds maximum allowed size of ${config.maxFileSizeMB}MB` ); } // In development mode, if AWS credentials are not configured, simulate upload if ( process.env.NODE_ENV === "development" && (!config.awsAccessKeyId || !config.awsSecretAccessKey) ) { debugLog( "Development mode: Simulating S3 upload without actual AWS credentials" ); // Generate a mock S3 URL for development const timestamp = Date.now(); const fileName = `uploads/${timestamp}-${file.name}`; const mockUrl = `https://mock-bucket.s3.mock-region.amazonaws.com/${fileName}`; // Simulate upload delay await new Promise((resolve) => setTimeout(resolve, 1000)); return { success: true, s3_link: mockUrl, link: mockUrl, publicUrl: mockUrl, url: mockUrl, s3Key: fileName, etag: `"mock-etag-${timestamp}"`, bucket: "mock-bucket", region: "mock-region", development: true, message: "Development mode: Upload simulated successfully", }; } try { // Initialize S3 client const s3Client = getS3Client(); // Generate unique filename const timestamp = Date.now(); const fileName = `uploads/${timestamp}-${file.name}`; // Convert file to ArrayBuffer for compatibility with AWS SDK const fileBuffer = await file.arrayBuffer(); // Create upload command const uploadCommand = new PutObjectCommand({ Bucket: config.s3BucketName, Key: fileName, Body: fileBuffer, ContentType: file.type, ACL: "public-read", // Make the uploaded file publicly accessible Metadata: { "original-name": file.name, "upload-timestamp": timestamp.toString(), }, }); debugLog("Starting S3 upload", { bucket: config.s3BucketName, key: fileName, contentType: file.type, fileSize: file.size, bufferSize: fileBuffer.byteLength, }); // Upload to S3 const result = await s3Client.send(uploadCommand); // Construct public URL const publicUrl = `https://${config.s3BucketName}.s3.${config.awsRegion}.amazonaws.com/${fileName}`; debugLog("File upload result", { etag: result.ETag, publicUrl: publicUrl, }); return { success: true, s3_link: publicUrl, link: publicUrl, publicUrl: publicUrl, url: publicUrl, s3Key: fileName, etag: result.ETag, bucket: config.s3BucketName, region: config.awsRegion, }; } catch (error) { debugLog("File upload error", error); // Provide more specific error messages if (error.name === "CredentialsProviderError") { throw new Error( "AWS credentials are invalid or not configured properly" ); } else if (error.name === "NoSuchBucket") { throw new Error( `S3 bucket '${config.s3BucketName}' does not exist or is not accessible` ); } else if (error.name === "AccessDenied") { throw new Error( "Access denied. Check your AWS permissions for S3 operations" ); } else { throw new Error(`Upload failed: ${error.message || "Unknown error"}`); } } } static async verifyStoredApiKey() { try { const encryptedApiKey = sessionStorage.getItem("encryptedApiKey"); if (!encryptedApiKey) { return { valid: false, message: "No stored API key found", }; } // Actually verify the stored API key by making a validation call debugLog("Verifying stored API key"); try { const validationResponse = await fetch(getValidationUrl(), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ encryptedApiKey: encryptedApiKey, }), }); const result = await validationResponse.json(); if (result.success && result.data && result.data.isValid) { return { valid: true, message: "Stored API key is valid", encryptedKey: encryptedApiKey, }; } else { // Remove invalid stored key sessionStorage.removeItem("encryptedApiKey"); return { valid: false, message: "Stored API key is invalid", }; } } catch (validationError) { debugLog("Error validating stored API key", validationError); // On validation error, assume key might be invalid and remove it sessionStorage.removeItem("encryptedApiKey"); return { valid: false, message: "Could not validate stored API key", error: validationError.message, }; } } catch (error) { debugLog("Error verifying stored API key", error); return { valid: false, message: "Error verifying stored API key", error: error.message, }; } } static async generateSyntheticData(apiKey, s3Link, generationConfig) { debugLog("Generating synthetic data", { s3Link, config: generationConfig }); try { // Get encrypted API key from session storage or encrypt the provided key let encryptedApiKey = sessionStorage.getItem("encryptedApiKey"); if (!encryptedApiKey) { encryptedApiKey = encryptApiKey(apiKey); } const response = await fetch(getGenerationUrl(), { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": encryptedApiKey, }, body: JSON.stringify({ fileUrl: s3Link, type: "Tabular", numberOfRows: generationConfig.numRows || config.defaultNumRecords, targetColumn: generationConfig.targetColumn, fileSizeBytes: generationConfig.fileSizeBytes || 0, sourceFileRows: generationConfig.sourceFileRows || 0, }), signal: AbortSignal.timeout(config.apiTimeout * 1000), }); if (!response.ok) { throw new Error( `Generation failed: ${response.status} ${response.statusText}` ); } const result = await response.json(); debugLog("Data generation result", result); return result; } catch (error) { debugLog("Data generation error", error); throw error; } } // Check AWS credentials - equivalent to Python check_aws_credentials function static async checkAwsCredentials() { /** * Check if AWS credentials are valid * * Returns: * Object: Status dictionary with 'valid' boolean and 'message' string */ debugLog("Checking AWS credentials validity"); // Check if credentials are configured if (!config.awsAccessKeyId || !config.awsSecretAccessKey) { // In development mode, allow bypassing AWS credentials requirement if (process.env.NODE_ENV === "development") { debugLog( "Development mode: AWS credentials not configured, but allowing bypass" ); return { valid: true, connected: true, message: "Development mode: AWS configuration bypassed", development: true, }; } return { valid: false, connected: false, message: "Cloud storage credentials not configured.", }; } // Check if bucket is configured if (!config.s3BucketName) { return { valid: false, connected: false, message: "Cloud storage not configured.", }; } // Try to get S3 client let s3Client; try { s3Client = getS3Client(); } catch (error) { return { valid: false, connected: false, message: "Cloud storage connection unavailable.", }; } // Check if bucket exists and is accessible try { const headBucketCommand = new HeadBucketCommand({ Bucket: config.s3BucketName, }); await s3Client.send(headBucketCommand); return { valid: true, connected: true, message: "Cloud storage connected", }; } catch (error) { debugLog("HeadBucket operation failed:", error); // Handle different error types similar to Python ClientError handling if ( error.name === "NoSuchBucket" || error.$metadata?.httpStatusCode === 404 ) { return { valid: false, connected: false, message: "Storage location not found", error: "Storage not found", }; } else if ( error.name === "Forbidden" || error.$metadata?.httpStatusCode === 403 ) { return { valid: false, connected: false, message: "Storage access denied", error: "Access denied", }; } else if ( error.message && error.message.toLowerCase().includes("cors") ) { // Handle CORS errors specially - this is common in browser environments debugLog("CORS error detected, but credentials may still be valid"); return { valid: true, connected: true, message: "Cloud storage connected (CORS limitations)", warning: "CORS restrictions apply in browser environment", }; } else { return { valid: false, connected: false, message: "Storage connection error", error: "Connection error", }; } } } } export default ApiService;