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; | |