Khushal-kreeda
fix: ui enhancements
826a975
raw
history blame
22.9 kB
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;