Fix bulk CSV processing and improve user registration

- Fix bulk CSV upload functionality that was returning HTML errors
- Implement proper project/organization handling for logged-in vs anonymous users
- Update user registration to create unique Default Organization and Default Project
- Fix frontend API URL configuration for bulk upload endpoints
- Resolve foreign key constraint violations in bulk processing
- Update BulkProcessorService to use in-memory processing instead of Redis
- Fix redirect-tracker service to handle missing project IDs properly
- Update Prisma schema for optional project relationships in bulk jobs
- Improve registration form UI with better password validation and alignment
This commit is contained in:
Andrei
2025-08-23 21:30:06 +00:00
parent df3ad8b194
commit e867f98da3
25 changed files with 682 additions and 205 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -15,7 +15,7 @@
gtag('config', 'G-ZDZ26XYN2P');
</script>
<script type="module" crossorigin src="/assets/index-DL4PyATX.js"></script>
<script type="module" crossorigin src="/assets/index-BpAHoLvP.js"></script>
</head>
<body>
<div id="root"></div>

View File

@@ -60,6 +60,9 @@ import {
import { useAuth } from '../contexts/AuthContext';
// Get API base URL from environment
const API_BASE_URL = (import.meta as any).env?.VITE_API_URL || 'http://localhost:3334';
const bulkUploadSchema = z.object({
projectId: z.string().min(1, 'Project ID is required'),
enableSSLAnalysis: z.boolean(),
@@ -146,7 +149,7 @@ export function BulkUploadPage() {
} = useQuery({
queryKey: ['bulkJobs'],
queryFn: async () => {
const response = await fetch('/api/v2/bulk/jobs', {
const response = await fetch(`${API_BASE_URL}/api/v2/bulk/jobs`, {
credentials: 'include',
});
if (!response.ok) throw new Error('Failed to fetch jobs');
@@ -169,7 +172,7 @@ export function BulkUploadPage() {
formData.append('maxHops', data.maxHops.toString());
formData.append('timeout', data.timeout.toString());
const response = await fetch('/api/v2/bulk/upload', {
const response = await fetch(`${API_BASE_URL}/api/v2/bulk/upload`, {
method: 'POST',
credentials: 'include',
body: formData,
@@ -184,7 +187,7 @@ export function BulkUploadPage() {
onSuccess: (result) => {
toast({
title: 'Upload successful',
description: `Bulk job created: ${result.data.job.id}`,
description: `Bulk job created: ${result.data.jobId}`,
status: 'success',
duration: 5000,
isClosable: true,

View File

@@ -44,8 +44,7 @@ const registerSchema = z.object({
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
.regex(/[0-9]/, 'Password must contain at least one number'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
@@ -91,9 +90,23 @@ export function RegisterPage() {
// Navigate to dashboard after successful registration
navigate('/dashboard', { replace: true });
} catch (error: any) {
setError('root', {
message: error.response?.data?.message || 'Registration failed. Please try again.',
});
console.error('Registration error:', error);
let errorMessage = 'Registration failed. Please try again.';
if (error.response?.data?.message) {
errorMessage = error.response.data.message;
} else if (error.response?.data?.details) {
// Handle validation errors
const details = error.response.data.details;
if (Array.isArray(details) && details.length > 0) {
errorMessage = details[0].message || errorMessage;
}
} else if (error.message) {
errorMessage = error.message;
}
setError('root', { message: errorMessage });
} finally {
setIsLoading(false);
}
@@ -109,11 +122,10 @@ export function RegisterPage() {
{ label: 'One uppercase letter', valid: /[A-Z]/.test(password || '') },
{ label: 'One lowercase letter', valid: /[a-z]/.test(password || '') },
{ label: 'One number', valid: /[0-9]/.test(password || '') },
{ label: 'One special character', valid: /[^A-Za-z0-9]/.test(password || '') },
];
return (
<Container maxW="lg" py={12}>
<Container maxW="6xl" py={12}>
<VStack spacing={8}>
<Box textAlign="center">
<Heading as="h1" size="xl" mb={4}>
@@ -124,9 +136,9 @@ export function RegisterPage() {
</Text>
</Box>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={8} w="full">
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing={8} w="full" alignItems="start">
{/* Registration Form */}
<Card bg={cardBg} border="1px solid" borderColor={borderColor}>
<Card bg={cardBg} border="1px solid" borderColor={borderColor} h="fit-content">
<CardBody p={8}>
{errors.root && (
<Alert status="error" mb={6} borderRadius="md">
@@ -161,17 +173,18 @@ export function RegisterPage() {
<FormControl isInvalid={!!errors.password}>
<FormLabel>Password</FormLabel>
<InputGroup>
<Input
type={showPassword ? 'text' : 'password'}
placeholder="Create a strong password"
autoComplete="new-password"
{...register('password')}
/>
<Input
type={showPassword ? 'text' : 'password'}
placeholder="Create a strong password"
autoComplete="new-password"
{...register('password')}
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? 'Hide password' : 'Show password'}
icon={showPassword ? <FiEyeOff /> : <FiEye />}
variant="ghost"
size="sm"
onClick={() => setShowPassword(!showPassword)}
/>
</InputRightElement>
@@ -193,6 +206,7 @@ export function RegisterPage() {
aria-label={showConfirmPassword ? 'Hide password' : 'Show password'}
icon={showConfirmPassword ? <FiEyeOff /> : <FiEye />}
variant="ghost"
size="sm"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
/>
</InputRightElement>
@@ -207,6 +221,7 @@ export function RegisterPage() {
w="full"
isLoading={isLoading}
loadingText="Creating account..."
isDisabled={!password || passwordValidations.some(v => !v.valid)}
>
Create Account
</Button>
@@ -245,35 +260,35 @@ export function RegisterPage() {
</Card>
{/* Benefits & Password Requirements */}
<VStack spacing={6} align="stretch">
<VStack spacing={6} align="stretch" h="fit-content">
{/* Account Benefits */}
<Card bg={cardBg} border="1px solid" borderColor={borderColor}>
<CardBody>
<Heading size="md" mb={4}>Account Benefits</Heading>
<List spacing={2}>
<ListItem>
<CardBody p={8}>
<Heading size="md" mb={6}>Account Benefits</Heading>
<List spacing={4}>
<ListItem display="flex" alignItems="center">
<ListIcon as={FiCheck} color="green.500" />
Higher rate limits (1000/hour)
<Text>Higher rate limits (1000/hour)</Text>
</ListItem>
<ListItem>
<ListItem display="flex" alignItems="center">
<ListIcon as={FiCheck} color="green.500" />
Saved tracking history
<Text>Saved tracking history</Text>
</ListItem>
<ListItem>
<ListItem display="flex" alignItems="center">
<ListIcon as={FiCheck} color="green.500" />
Analysis dashboards
<Text>Analysis dashboards</Text>
</ListItem>
<ListItem>
<ListItem display="flex" alignItems="center">
<ListIcon as={FiCheck} color="green.500" />
Organization management
<Text>Organization management</Text>
</ListItem>
<ListItem>
<ListItem display="flex" alignItems="center">
<ListIcon as={FiCheck} color="green.500" />
API key access
<Text>API key access</Text>
</ListItem>
<ListItem>
<ListItem display="flex" alignItems="center">
<ListIcon as={FiCheck} color="green.500" />
Bulk URL processing
<Text>Bulk URL processing</Text>
</ListItem>
</List>
</CardBody>
@@ -282,17 +297,16 @@ export function RegisterPage() {
{/* Password Requirements */}
{password && (
<Card bg={cardBg} border="1px solid" borderColor={borderColor}>
<CardBody>
<Heading size="md" mb={4}>Password Requirements</Heading>
<List spacing={2}>
<CardBody p={8}>
<Heading size="md" mb={6}>Password Requirements</Heading>
<List spacing={4}>
{passwordValidations.map((validation, index) => (
<ListItem key={index}>
<ListItem key={index} display="flex" alignItems="center">
<ListIcon
as={FiCheck}
color={getPasswordValidationColor(validation.valid)}
/>
<Text
as="span"
color={getPasswordValidationColor(validation.valid)}
fontSize="sm"
>