diff --git a/packages/api/src/middleware/validation/checkAnimalOrBatch.js b/packages/api/src/middleware/validation/checkAnimalOrBatch.js index 9cc5fa9faf..cbd78a307a 100644 --- a/packages/api/src/middleware/validation/checkAnimalOrBatch.js +++ b/packages/api/src/middleware/validation/checkAnimalOrBatch.js @@ -32,6 +32,7 @@ import CustomAnimalBreedModel from '../../models/customAnimalBreedModel.js'; import AnimalUseModel from '../../models/animalUseModel.js'; import AnimalOriginModel from '../../models/animalOriginModel.js'; import AnimalIdentifierType from '../../models/animalIdentifierTypeModel.js'; +import { ANIMAL_CREATE_LIMIT } from '../../util/animal.js'; const AnimalOrBatchModel = { animal: AnimalModel, @@ -450,6 +451,9 @@ export function checkCreateAnimalOrBatch(animalOrBatchKey) { checkIsArray(req.body, 'Request body'); + if (req.body.length > ANIMAL_CREATE_LIMIT) { + return res.status(400).send(`Animal creation limit (${ANIMAL_CREATE_LIMIT}) exceeded.`); + } for (const animalOrBatch of req.body) { const { type_name, breed_name } = animalOrBatch; diff --git a/packages/api/src/util/animal.js b/packages/api/src/util/animal.js index c33e392d76..310241a018 100644 --- a/packages/api/src/util/animal.js +++ b/packages/api/src/util/animal.js @@ -25,6 +25,7 @@ import AnimalBatchModel from '../models/animalBatchModel.js'; import { checkIsArray, customError } from './customErrors.js'; export const ANIMAL_TASKS = ['animal_movement_task']; +export const ANIMAL_CREATE_LIMIT = 1000; /** * Assigns internal identifiers to records. diff --git a/packages/api/tests/animal.test.js b/packages/api/tests/animal.test.js index 4156b8844d..1aa3540a17 100644 --- a/packages/api/tests/animal.test.js +++ b/packages/api/tests/animal.test.js @@ -40,6 +40,7 @@ import mocks from './mock.factories.js'; import CustomAnimalTypeModel from '../src/models/customAnimalTypeModel.js'; import CustomAnimalBreedModel from '../src/models/customAnimalBreedModel.js'; import AnimalUseRelationshipModel from '../src/models/animalUseRelationshipModel.js'; +import { ANIMAL_CREATE_LIMIT } from '../src/util/animal.js'; describe('Animal Tests', () => { let farm; @@ -387,6 +388,38 @@ describe('Animal Tests', () => { expect(res.status).toBe(400); }); + test('Should not be able to add >1000 animals', async () => { + const roles = [1, 2, 5]; + + for (const role of roles) { + const { mainFarm, user } = await returnUserFarms(role); + + const [animalBreed] = await mocks.custom_animal_breedFactory({ + promisedFarm: [mainFarm], + }); + + const animals = []; + for (let i = 0; i < ANIMAL_CREATE_LIMIT + 1; i++) { + animals.push( + mocks.fakeAnimal({ + custom_type_id: animalBreed.custom_type_id, + custom_breed_id: animalBreed.id, + }), + ); + } + + const res = await postRequest( + { + user_id: user.user_id, + farm_id: mainFarm.farm_id, + }, + animals, + ); + + expect(res.status).toBe(400); + } + }); + describe('Create new types and/or breeds while creating animals', () => { let farm; let owner; diff --git a/packages/api/tests/animal_batch.test.js b/packages/api/tests/animal_batch.test.js index 95a3bd69f4..9f911c49ba 100644 --- a/packages/api/tests/animal_batch.test.js +++ b/packages/api/tests/animal_batch.test.js @@ -39,6 +39,7 @@ import CustomAnimalTypeModel from '../src/models/customAnimalTypeModel.js'; import CustomAnimalBreedModel from '../src/models/customAnimalBreedModel.js'; import AnimalBatchSexDetailModel from '../src/models/animalBatchSexDetailModel.js'; import AnimalBatchUseRelationshipModel from '../src/models/animalBatchUseRelationshipModel.js'; +import { ANIMAL_CREATE_LIMIT } from '../src/util/animal.js'; describe('Animal Batch Tests', () => { let farm; @@ -294,6 +295,37 @@ describe('Animal Batch Tests', () => { } }); + test('Users should not be able to create animal batch beyond ANIMAL_CREATE_LIMIT', async () => { + const roles = [1, 2, 5]; + for (const role of roles) { + const { mainFarm, user } = await returnUserFarms(role); + const [animalBreed] = await mocks.custom_animal_breedFactory({ + promisedFarm: [mainFarm], + }); + + const animalBatches = []; + + for (let i = 0; i < ANIMAL_CREATE_LIMIT + 1; i++) { + animalBatches.push( + mocks.fakeAnimalBatch({ + custom_type_id: animalBreed.custom_type_id, + custom_breed_id: animalBreed.id, + count: 6, + }), + ); + } + + const res = await postRequest( + { + user_id: user.user_id, + farm_id: mainFarm.farm_id, + }, + animalBatches, + ); + expect(res.status).toBe(400); + } + }); + test('Non-admin users should not be able to create animal batch', async () => { const roles = [3]; diff --git a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AddAnimalsFormCard.tsx b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AddAnimalsFormCard.tsx index 060c7b3332..e3d2d95398 100644 --- a/packages/webapp/src/components/Animals/AddAnimalsFormCard/AddAnimalsFormCard.tsx +++ b/packages/webapp/src/components/Animals/AddAnimalsFormCard/AddAnimalsFormCard.tsx @@ -66,6 +66,8 @@ export default function AddAnimalsFormCard({ resetField, formState: { errors }, } = useFormContext(); + + const ANIMAL_COUNT_LIMIT = 1000; const { t } = useTranslation(); const watchAnimalCount = watch(`${namePrefix}${BasicsFields.COUNT}`); const watchAnimalType = watch(`${namePrefix}${BasicsFields.TYPE}`); @@ -112,9 +114,11 @@ export default function AddAnimalsFormCard({ onIndividualProfilesCheck?.((e.target as HTMLInputElement).checked)} + onChange={(e) => { + onIndividualProfilesCheck?.((e.target as HTMLInputElement).checked); + if ((e.target as HTMLInputElement).checked && watchAnimalCount > ANIMAL_COUNT_LIMIT) { + setValue(`${namePrefix}${BasicsFields.COUNT}`, ANIMAL_COUNT_LIMIT, { + shouldValidate: true, + }); + } + }} /> {shouldCreateIndividualProfiles ? ( // @ts-ignore diff --git a/packages/webapp/src/components/Form/NumberInput/index.tsx b/packages/webapp/src/components/Form/NumberInput/index.tsx index ee5c4314bf..e490c13ed3 100644 --- a/packages/webapp/src/components/Form/NumberInput/index.tsx +++ b/packages/webapp/src/components/Form/NumberInput/index.tsx @@ -36,6 +36,8 @@ export type NumberInputProps = UseControllerProps & showStepper?: boolean; className?: string; + + value?: number; }; export default function NumberInput({ @@ -57,6 +59,7 @@ export default function NumberInput({ className, onChange, onBlur, + value, ...props }: NumberInputProps) { const { field, fieldState, formState } = useController({ name, control, rules, defaultValue }); @@ -87,6 +90,7 @@ export default function NumberInput({ className={className} error={fieldState.error?.message} onResetIconClick={reset} + value={value} leftSection={currencySymbol} rightSection={ <>