Spaces:
Running
Running
| 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; | |