206 lines
5.8 KiB
TypeScript
206 lines
5.8 KiB
TypeScript
// ===========================
|
|
// NewScanForm.tsx
|
|
// ©AngelaMos | 2025
|
|
// ===========================
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Button } from '@/components/common/Button';
|
|
import { Input } from '@/components/common/Input';
|
|
import { LoadingOverlay } from '@/components/common/LoadingOverlay';
|
|
import { useCreateScan } from '@/hooks/useScan';
|
|
import { useUIStore } from '@/store/uiStore';
|
|
import {
|
|
SCAN_TEST_TYPES,
|
|
TEST_TYPE_LABELS,
|
|
type ScanTestType,
|
|
} from '@/config/constants';
|
|
import { scanSchema } from '@/lib/validation';
|
|
import './ScanForm.css';
|
|
|
|
export const NewScanForm = (): React.ReactElement => {
|
|
const scanFormState = useUIStore((state) => state.scanForm);
|
|
const setScanFormField = useUIStore((state) => state.setScanFormField);
|
|
const clearScanForm = useUIStore((state) => state.clearScanForm);
|
|
const clearExpiredData = useUIStore((state) => state.clearExpiredData);
|
|
|
|
const [targetUrl, setTargetUrl] = useState<string>('');
|
|
const [authToken, setAuthToken] = useState<string>('');
|
|
const [selectedTests, setSelectedTests] = useState<ScanTestType[]>([]);
|
|
const [maxRequests, setMaxRequests] = useState<string>('50');
|
|
const [errors, setErrors] = useState<{
|
|
targetUrl?: string;
|
|
authToken?: string;
|
|
testsToRun?: string;
|
|
maxRequests?: string;
|
|
}>({});
|
|
|
|
const { mutate: createScan, isPending } = useCreateScan();
|
|
|
|
useEffect(() => {
|
|
clearExpiredData();
|
|
setTargetUrl(scanFormState.targetUrl);
|
|
setAuthToken(scanFormState.authToken);
|
|
setSelectedTests(
|
|
scanFormState.selectedTests.length > 0
|
|
? scanFormState.selectedTests
|
|
: [SCAN_TEST_TYPES.RATE_LIMIT],
|
|
);
|
|
setMaxRequests(scanFormState.maxRequests);
|
|
}, [
|
|
clearExpiredData,
|
|
scanFormState.targetUrl,
|
|
scanFormState.authToken,
|
|
scanFormState.selectedTests,
|
|
scanFormState.maxRequests,
|
|
]);
|
|
|
|
const validateForm = (): boolean => {
|
|
const maxReq = parseInt(maxRequests);
|
|
|
|
const result = scanSchema.safeParse({
|
|
targetUrl: targetUrl.trim(),
|
|
authToken: authToken.trim().length > 0 ? authToken.trim() : undefined,
|
|
testsToRun: selectedTests,
|
|
maxRequests: maxReq,
|
|
});
|
|
|
|
if (!result.success) {
|
|
const newErrors: {
|
|
targetUrl?: string;
|
|
authToken?: string;
|
|
testsToRun?: string;
|
|
maxRequests?: string;
|
|
} = {};
|
|
|
|
result.error.issues.forEach((err) => {
|
|
const field = err.path[0] as keyof typeof newErrors;
|
|
if (field !== null && field !== undefined) {
|
|
newErrors[field] = err.message;
|
|
}
|
|
});
|
|
|
|
setErrors(newErrors);
|
|
return false;
|
|
}
|
|
|
|
setErrors({});
|
|
return true;
|
|
};
|
|
|
|
const handleTargetUrlChange = (value: string): void => {
|
|
setTargetUrl(value);
|
|
setScanFormField('targetUrl', value);
|
|
};
|
|
|
|
const handleAuthTokenChange = (value: string): void => {
|
|
setAuthToken(value);
|
|
setScanFormField('authToken', value);
|
|
};
|
|
|
|
const handleMaxRequestsChange = (value: string): void => {
|
|
setMaxRequests(value);
|
|
setScanFormField('maxRequests', value);
|
|
};
|
|
|
|
const handleTestToggle = (test: ScanTestType): void => {
|
|
const newTests = selectedTests.includes(test)
|
|
? selectedTests.filter((t) => t !== test)
|
|
: [...selectedTests, test];
|
|
|
|
setSelectedTests(newTests);
|
|
setScanFormField('selectedTests', newTests);
|
|
};
|
|
|
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) {
|
|
return;
|
|
}
|
|
|
|
const maxReq = parseInt(maxRequests);
|
|
|
|
createScan(
|
|
{
|
|
target_url: targetUrl.trim(),
|
|
auth_token: authToken.trim().length > 0 ? authToken.trim() : null,
|
|
tests_to_run: selectedTests,
|
|
max_requests: maxReq,
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
clearScanForm();
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{isPending ? <LoadingOverlay tests={selectedTests} /> : null}
|
|
<form className="scan-form" onSubmit={handleSubmit}>
|
|
<div className="scan-form__fields">
|
|
<Input
|
|
label="Target URL"
|
|
type="url"
|
|
value={targetUrl}
|
|
onChange={(e) => handleTargetUrlChange(e.target.value)}
|
|
error={errors.targetUrl}
|
|
placeholder="https://api.example.com/endpoint"
|
|
required
|
|
/>
|
|
|
|
<Input
|
|
label="Auth Token (Optional)"
|
|
type="text"
|
|
value={authToken}
|
|
onChange={(e) => handleAuthTokenChange(e.target.value)}
|
|
error={errors.authToken}
|
|
placeholder="Bearer token or API key"
|
|
/>
|
|
|
|
<div className="scan-form__field">
|
|
<label className="scan-form__label">
|
|
Select Tests
|
|
{errors.testsToRun !== null && errors.testsToRun !== undefined ? (
|
|
<span className="scan-form__error" role="alert">
|
|
{errors.testsToRun}
|
|
</span>
|
|
) : null}
|
|
</label>
|
|
<div className="scan-form__checkboxes">
|
|
{Object.values(SCAN_TEST_TYPES).map((test) => (
|
|
<label key={test} className="scan-form__checkbox-label">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedTests.includes(test)}
|
|
onChange={() => handleTestToggle(test)}
|
|
className="scan-form__checkbox"
|
|
/>
|
|
<span>{TEST_TYPE_LABELS[test]}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<Input
|
|
label="Max Requests"
|
|
type="number"
|
|
value={maxRequests}
|
|
onChange={(e) => handleMaxRequestsChange(e.target.value)}
|
|
error={errors.maxRequests}
|
|
placeholder="50"
|
|
min="1"
|
|
max="50"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<Button type="submit" isLoading={isPending} disabled={isPending}>
|
|
{isPending ? 'Running Scan...' : 'Start Scan'}
|
|
</Button>
|
|
</form>
|
|
</>
|
|
);
|
|
};
|