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

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