From 69d3ff4f7bc1e0a0c83a55f796fda50864d50bdf Mon Sep 17 00:00:00 2001 From: Constantin Graf Date: Tue, 15 Oct 2024 10:59:53 +0200 Subject: [PATCH] Stricter validation for uuid and integer --- app/Actions/Fortify/CreateNewUser.php | 2 +- .../Fortify/UpdateUserProfileInformation.php | 2 +- .../Jetstream/AddOrganizationMember.php | 4 +- .../Resources/OrganizationResource.php | 1 + .../Resources/ProjectMemberResource.php | 1 + app/Filament/Resources/ProjectResource.php | 1 + .../Requests/V1/Client/ClientIndexRequest.php | 1 + .../Requests/V1/Client/ClientStoreRequest.php | 4 +- .../V1/Client/ClientUpdateRequest.php | 4 +- .../V1/Invitation/InvitationStoreRequest.php | 4 +- .../V1/Member/MemberUpdateRequest.php | 1 + .../OrganizationUpdateRequest.php | 1 + .../V1/Project/ProjectIndexRequest.php | 1 + .../V1/Project/ProjectStoreRequest.php | 10 ++-- .../V1/Project/ProjectUpdateRequest.php | 10 ++-- .../ProjectMemberStoreRequest.php | 6 +-- .../ProjectMemberUpdateRequest.php | 1 + app/Http/Requests/V1/Tag/TagStoreRequest.php | 4 +- app/Http/Requests/V1/Tag/TagUpdateRequest.php | 4 +- .../Requests/V1/Task/TaskIndexRequest.php | 5 +- .../Requests/V1/Task/TaskStoreRequest.php | 9 ++-- .../Requests/V1/Task/TaskUpdateRequest.php | 5 +- .../TimeEntry/TimeEntryAggregateRequest.php | 35 +++++------- .../V1/TimeEntry/TimeEntryIndexRequest.php | 31 +++++------ .../V1/TimeEntry/TimeEntryStoreRequest.php | 25 ++++----- .../TimeEntryUpdateMultipleRequest.php | 24 ++++----- .../V1/TimeEntry/TimeEntryUpdateRequest.php | 24 ++++----- .../Import/Importers/DefaultImporter.php | 2 + composer.lock | 12 ++--- .../Endpoint/Api/V1/ProjectEndpointTest.php | 54 +++++++++++++++++++ .../Endpoint/Api/V1/TimeEntryEndpointTest.php | 21 ++++++++ 31 files changed, 187 insertions(+), 122 deletions(-) diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 32cd588e..c9bd1886 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -45,7 +45,7 @@ class CreateNewUser implements CreatesNewUsers 'string', 'email', 'max:255', - new UniqueEloquent(User::class, 'email', function (Builder $builder): Builder { + UniqueEloquent::make(User::class, 'email', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->where('is_placeholder', '=', false); }), diff --git a/app/Actions/Fortify/UpdateUserProfileInformation.php b/app/Actions/Fortify/UpdateUserProfileInformation.php index b5f2e495..748d0e8c 100644 --- a/app/Actions/Fortify/UpdateUserProfileInformation.php +++ b/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -35,7 +35,7 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation 'required', 'email', 'max:255', - (new UniqueEloquent(User::class, 'email'))->ignore($user->id)->query(function (Builder $query) { + UniqueEloquent::make(User::class, 'email')->ignore($user->id)->query(function (Builder $query) { /** @var Builder $query */ return $query->where('is_placeholder', '=', false); }), diff --git a/app/Actions/Jetstream/AddOrganizationMember.php b/app/Actions/Jetstream/AddOrganizationMember.php index 508900f3..75574c3a 100644 --- a/app/Actions/Jetstream/AddOrganizationMember.php +++ b/app/Actions/Jetstream/AddOrganizationMember.php @@ -71,10 +71,10 @@ class AddOrganizationMember implements AddsTeamMembers 'email' => [ 'required', 'email', - (new ExistsEloquent(User::class, 'email', function (Builder $builder) { + ExistsEloquent::make(User::class, 'email', function (Builder $builder) { /** @var Builder $builder */ return $builder->where('is_placeholder', '=', false); - }))->withMessage(__('We were unable to find a registered user with this email address.')), + })->withMessage(__('We were unable to find a registered user with this email address.')), ], 'role' => [ 'required', diff --git a/app/Filament/Resources/OrganizationResource.php b/app/Filament/Resources/OrganizationResource.php index 4ce80391..5561cb53 100644 --- a/app/Filament/Resources/OrganizationResource.php +++ b/app/Filament/Resources/OrganizationResource.php @@ -70,6 +70,7 @@ class OrganizationResource extends Resource 'nullable', 'integer', 'gt:0', + 'max:2147483647', ]) ->numeric(), Forms\Components\DateTimePicker::make('created_at') diff --git a/app/Filament/Resources/ProjectMemberResource.php b/app/Filament/Resources/ProjectMemberResource.php index feb58ca4..c1b9f8e0 100644 --- a/app/Filament/Resources/ProjectMemberResource.php +++ b/app/Filament/Resources/ProjectMemberResource.php @@ -29,6 +29,7 @@ class ProjectMemberResource extends Resource 'nullable', 'integer', 'gt:0', + 'max:2147483647', ]) ->numeric(), Forms\Components\Select::make('user_id') diff --git a/app/Filament/Resources/ProjectResource.php b/app/Filament/Resources/ProjectResource.php index 56e3aca7..d1ef89ae 100644 --- a/app/Filament/Resources/ProjectResource.php +++ b/app/Filament/Resources/ProjectResource.php @@ -45,6 +45,7 @@ class ProjectResource extends Resource 'nullable', 'integer', 'gt:0', + 'max:2147483647', ]) ->numeric(), Forms\Components\Select::make('organization_id') diff --git a/app/Http/Requests/V1/Client/ClientIndexRequest.php b/app/Http/Requests/V1/Client/ClientIndexRequest.php index e7947dd1..0b241c44 100644 --- a/app/Http/Requests/V1/Client/ClientIndexRequest.php +++ b/app/Http/Requests/V1/Client/ClientIndexRequest.php @@ -20,6 +20,7 @@ class ClientIndexRequest extends FormRequest 'page' => [ 'integer', 'min:1', + 'max:2147483647', ], 'archived' => [ 'string', diff --git a/app/Http/Requests/V1/Client/ClientStoreRequest.php b/app/Http/Requests/V1/Client/ClientStoreRequest.php index 611d427f..f0c760f1 100644 --- a/app/Http/Requests/V1/Client/ClientStoreRequest.php +++ b/app/Http/Requests/V1/Client/ClientStoreRequest.php @@ -29,10 +29,10 @@ class ClientStoreRequest extends FormRequest 'string', 'min:1', 'max:255', - (new UniqueEloquent(Client::class, 'name', function (Builder $builder): Builder { + UniqueEloquent::make(Client::class, 'name', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }))->withCustomTranslation('validation.client_name_already_exists'), + })->withCustomTranslation('validation.client_name_already_exists'), ], ]; } diff --git a/app/Http/Requests/V1/Client/ClientUpdateRequest.php b/app/Http/Requests/V1/Client/ClientUpdateRequest.php index 800dd70c..3951a459 100644 --- a/app/Http/Requests/V1/Client/ClientUpdateRequest.php +++ b/app/Http/Requests/V1/Client/ClientUpdateRequest.php @@ -31,10 +31,10 @@ class ClientUpdateRequest extends FormRequest 'string', 'min:1', 'max:255', - (new UniqueEloquent(Client::class, 'name', function (Builder $builder): Builder { + UniqueEloquent::make(Client::class, 'name', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }))->ignore($this->client?->getKey())->withCustomTranslation('validation.client_name_already_exists'), + })->ignore($this->client?->getKey())->withCustomTranslation('validation.client_name_already_exists'), ], 'is_archived' => [ 'boolean', diff --git a/app/Http/Requests/V1/Invitation/InvitationStoreRequest.php b/app/Http/Requests/V1/Invitation/InvitationStoreRequest.php index de7de748..0830cd93 100644 --- a/app/Http/Requests/V1/Invitation/InvitationStoreRequest.php +++ b/app/Http/Requests/V1/Invitation/InvitationStoreRequest.php @@ -29,10 +29,10 @@ class InvitationStoreRequest extends FormRequest 'email' => [ 'required', 'email', - (new UniqueEloquent(OrganizationInvitation::class, 'email', function (Builder $builder): Builder { + UniqueEloquent::make(OrganizationInvitation::class, 'email', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }))->withCustomTranslation('validation.invitation_already_exists'), + })->withCustomTranslation('validation.invitation_already_exists'), ], 'role' => [ 'required', diff --git a/app/Http/Requests/V1/Member/MemberUpdateRequest.php b/app/Http/Requests/V1/Member/MemberUpdateRequest.php index 9f4ca23c..52b5c40f 100644 --- a/app/Http/Requests/V1/Member/MemberUpdateRequest.php +++ b/app/Http/Requests/V1/Member/MemberUpdateRequest.php @@ -31,6 +31,7 @@ class MemberUpdateRequest extends FormRequest 'nullable', 'integer', 'min:0', + 'max:2147483647', ], ]; } diff --git a/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php b/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php index 994a184a..babe43fb 100644 --- a/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php +++ b/app/Http/Requests/V1/Organization/OrganizationUpdateRequest.php @@ -30,6 +30,7 @@ class OrganizationUpdateRequest extends FormRequest 'nullable', 'integer', 'min:0', + 'max:2147483647', ], 'employees_can_see_billable_rates' => [ 'boolean', diff --git a/app/Http/Requests/V1/Project/ProjectIndexRequest.php b/app/Http/Requests/V1/Project/ProjectIndexRequest.php index 44f96e8a..eba503f5 100644 --- a/app/Http/Requests/V1/Project/ProjectIndexRequest.php +++ b/app/Http/Requests/V1/Project/ProjectIndexRequest.php @@ -20,6 +20,7 @@ class ProjectIndexRequest extends FormRequest 'page' => [ 'integer', 'min:1', + 'max:2147483647', ], 'archived' => [ 'string', diff --git a/app/Http/Requests/V1/Project/ProjectStoreRequest.php b/app/Http/Requests/V1/Project/ProjectStoreRequest.php index 167bd5cf..5a4795a6 100644 --- a/app/Http/Requests/V1/Project/ProjectStoreRequest.php +++ b/app/Http/Requests/V1/Project/ProjectStoreRequest.php @@ -32,10 +32,10 @@ class ProjectStoreRequest extends FormRequest 'string', 'min:1', 'max:255', - (new UniqueEloquent(Project::class, 'name', function (Builder $builder): Builder { + UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }))->withCustomTranslation('validation.project_name_already_exists'), + })->withCustomTranslation('validation.project_name_already_exists'), ], 'color' => [ 'required', @@ -51,20 +51,22 @@ class ProjectStoreRequest extends FormRequest 'nullable', 'integer', 'min:0', + 'max:2147483647', ], // ID of the client 'client_id' => [ 'nullable', - new ExistsEloquent(Client::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Estimated time in seconds 'estimated_time' => [ 'nullable', 'integer', 'min:0', + 'max:2147483647', ], ]; } diff --git a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php index 51fc829d..e50fdb34 100644 --- a/app/Http/Requests/V1/Project/ProjectUpdateRequest.php +++ b/app/Http/Requests/V1/Project/ProjectUpdateRequest.php @@ -32,10 +32,10 @@ class ProjectUpdateRequest extends FormRequest 'required', 'string', 'max:255', - (new UniqueEloquent(Project::class, 'name', function (Builder $builder): Builder { + UniqueEloquent::make(Project::class, 'name', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }))->ignore($this->project?->getKey())->withCustomTranslation('validation.project_name_already_exists'), + })->ignore($this->project?->getKey())->withCustomTranslation('validation.project_name_already_exists'), ], 'color' => [ 'required', @@ -52,21 +52,23 @@ class ProjectUpdateRequest extends FormRequest ], 'client_id' => [ 'nullable', - new ExistsEloquent(Client::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], 'billable_rate' => [ 'nullable', 'integer', 'min:0', + 'max:2147483647', ], // Estimated time in seconds 'estimated_time' => [ 'nullable', 'integer', 'min:0', + 'max:2147483647', ], ]; } diff --git a/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php b/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php index de3e8cf4..d76ed1c0 100644 --- a/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php +++ b/app/Http/Requests/V1/ProjectMember/ProjectMemberStoreRequest.php @@ -26,16 +26,16 @@ class ProjectMemberStoreRequest extends FormRequest return [ 'member_id' => [ 'required', - 'uuid', - new ExistsEloquent(Member::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], 'billable_rate' => [ 'nullable', 'integer', 'min:0', + 'max:2147483647', ], ]; } diff --git a/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php b/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php index 664a2d56..190cfbac 100644 --- a/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php +++ b/app/Http/Requests/V1/ProjectMember/ProjectMemberUpdateRequest.php @@ -25,6 +25,7 @@ class ProjectMemberUpdateRequest extends FormRequest 'nullable', 'integer', 'min:0', + 'max:2147483647', ], ]; } diff --git a/app/Http/Requests/V1/Tag/TagStoreRequest.php b/app/Http/Requests/V1/Tag/TagStoreRequest.php index d4d1dda0..ddb6112e 100644 --- a/app/Http/Requests/V1/Tag/TagStoreRequest.php +++ b/app/Http/Requests/V1/Tag/TagStoreRequest.php @@ -29,10 +29,10 @@ class TagStoreRequest extends FormRequest 'string', 'min:1', 'max:255', - (new UniqueEloquent(Tag::class, 'name', function (Builder $builder): Builder { + UniqueEloquent::make(Tag::class, 'name', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }))->withCustomTranslation('validation.tag_name_already_exists'), + })->withCustomTranslation('validation.tag_name_already_exists'), ], ]; } diff --git a/app/Http/Requests/V1/Tag/TagUpdateRequest.php b/app/Http/Requests/V1/Tag/TagUpdateRequest.php index 0eeb4f9f..7e7f8673 100644 --- a/app/Http/Requests/V1/Tag/TagUpdateRequest.php +++ b/app/Http/Requests/V1/Tag/TagUpdateRequest.php @@ -30,10 +30,10 @@ class TagUpdateRequest extends FormRequest 'string', 'min:1', 'max:255', - (new UniqueEloquent(Tag::class, 'name', function (Builder $builder): Builder { + UniqueEloquent::make(Tag::class, 'name', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }))->ignore($this->tag?->getKey())->withCustomTranslation('validation.tag_name_already_exists'), + })->ignore($this->tag?->getKey())->withCustomTranslation('validation.tag_name_already_exists'), ], ]; } diff --git a/app/Http/Requests/V1/Task/TaskIndexRequest.php b/app/Http/Requests/V1/Task/TaskIndexRequest.php index 1b2afec4..a69ef116 100644 --- a/app/Http/Requests/V1/Task/TaskIndexRequest.php +++ b/app/Http/Requests/V1/Task/TaskIndexRequest.php @@ -27,8 +27,7 @@ class TaskIndexRequest extends FormRequest { return [ 'project_id' => [ - 'uuid', - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ $builder = $builder->whereBelongsTo($this->organization, 'organization'); @@ -37,7 +36,7 @@ class TaskIndexRequest extends FormRequest } return $builder; - }), + })->uuid(), ], 'done' => [ 'string', diff --git a/app/Http/Requests/V1/Task/TaskStoreRequest.php b/app/Http/Requests/V1/Task/TaskStoreRequest.php index 7c60ba1a..6fa4c96f 100644 --- a/app/Http/Requests/V1/Task/TaskStoreRequest.php +++ b/app/Http/Requests/V1/Task/TaskStoreRequest.php @@ -31,23 +31,24 @@ class TaskStoreRequest extends FormRequest 'string', 'min:1', 'max:255', - (new UniqueEloquent(Task::class, 'name', function (Builder $builder): Builder { + UniqueEloquent::make(Task::class, 'name', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->where('project_id', '=', $this->input('project_id')); - }))->withCustomTranslation('validation.task_name_already_exists'), + })->withCustomTranslation('validation.task_name_already_exists'), ], 'project_id' => [ 'required', - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Estimated time in seconds 'estimated_time' => [ 'nullable', 'integer', 'min:0', + 'max:2147483647', ], ]; } diff --git a/app/Http/Requests/V1/Task/TaskUpdateRequest.php b/app/Http/Requests/V1/Task/TaskUpdateRequest.php index b9211eb7..4eb90ace 100644 --- a/app/Http/Requests/V1/Task/TaskUpdateRequest.php +++ b/app/Http/Requests/V1/Task/TaskUpdateRequest.php @@ -30,10 +30,10 @@ class TaskUpdateRequest extends FormRequest 'string', 'min:1', 'max:255', - (new UniqueEloquent(Task::class, 'name', function (Builder $builder): Builder { + UniqueEloquent::make(Task::class, 'name', function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->where('project_id', '=', $this->task->project_id); - }))->ignore($this->task?->getKey())->withCustomTranslation('validation.task_name_already_exists'), + })->ignore($this->task?->getKey())->withCustomTranslation('validation.task_name_already_exists'), ], 'is_done' => [ 'boolean', @@ -43,6 +43,7 @@ class TaskUpdateRequest extends FormRequest 'nullable', 'integer', 'min:0', + 'max:2147483647', ], ]; } diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php index 8d400817..c36d7e92 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php @@ -45,11 +45,10 @@ class TimeEntryAggregateRequest extends FormRequest // Filter by member ID 'member_id' => [ 'string', - 'uuid', - new ExistsEloquent(Member::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter 'member_ids' => [ @@ -58,21 +57,19 @@ class TimeEntryAggregateRequest extends FormRequest ], 'member_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Member::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by user ID 'user_id' => [ 'string', - 'uuid', - new ExistsEloquent(User::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(User::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->belongsToOrganization($this->organization); - }), + })->uuid(), ], // Filter by project IDs, project IDs are OR combined 'project_ids' => [ @@ -81,11 +78,10 @@ class TimeEntryAggregateRequest extends FormRequest ], 'project_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by client IDs, client IDs are OR combined 'client_ids' => [ @@ -94,11 +90,10 @@ class TimeEntryAggregateRequest extends FormRequest ], 'client_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Client::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by tag IDs, tag IDs are AND combined 'tag_ids' => [ @@ -107,11 +102,10 @@ class TimeEntryAggregateRequest extends FormRequest ], 'tag_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by task IDs, task IDs are OR combined 'task_ids' => [ @@ -120,10 +114,9 @@ class TimeEntryAggregateRequest extends FormRequest ], 'task_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z) 'start' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php index f877eb3f..9de7fb06 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php @@ -31,11 +31,10 @@ class TimeEntryIndexRequest extends FormRequest // Filter by member ID 'member_id' => [ 'string', - 'uuid', - new ExistsEloquent(Member::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter 'member_ids' => [ @@ -44,11 +43,10 @@ class TimeEntryIndexRequest extends FormRequest ], 'member_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Member::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by client IDs, client IDs are OR combined 'client_ids' => [ @@ -57,11 +55,10 @@ class TimeEntryIndexRequest extends FormRequest ], 'client_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Client::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by project IDs, project IDs are OR combined 'project_ids' => [ @@ -70,11 +67,10 @@ class TimeEntryIndexRequest extends FormRequest ], 'project_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by tag IDs, tag IDs are AND combined 'tag_ids' => [ @@ -83,11 +79,10 @@ class TimeEntryIndexRequest extends FormRequest ], 'tag_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter by task IDs, task IDs are OR combined 'task_ids' => [ @@ -96,11 +91,10 @@ class TimeEntryIndexRequest extends FormRequest ], 'task_ids.*' => [ 'string', - 'uuid', - new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z) 'start' => [ @@ -135,6 +129,7 @@ class TimeEntryIndexRequest extends FormRequest 'offset' => [ 'integer', 'min:0', + 'max:2147483647', ], // Filter makes sure that only time entries of a whole date are returned 'only_full_dates' => [ diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php index 062e1358..ecd8449b 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryStoreRequest.php @@ -31,36 +31,33 @@ class TimeEntryStoreRequest extends FormRequest 'member_id' => [ 'required', 'string', - 'uuid', - new ExistsEloquent(Member::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], 'project_id' => [ 'nullable', 'string', - 'uuid', 'required_with:task_id', - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // ID of the task that the time entry should belong to 'task_id' => [ 'nullable', 'string', - 'uuid', - new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), - (new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { + })->uuid(), + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization') ->where('project_id', $this->input('project_id')); - }))->withMessage(__('validation.task_belongs_to_project')), + })->uuid()->withMessage(__('validation.task_belongs_to_project')), ], // Start of time entry (ISO 8601 format, UTC timezone) 'start' => [ @@ -90,12 +87,10 @@ class TimeEntryStoreRequest extends FormRequest 'array', ], 'tags.*' => [ - 'string', - 'uuid', - new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], ]; } diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php index d4d27767..06b824a8 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateMultipleRequest.php @@ -42,37 +42,34 @@ class TimeEntryUpdateMultipleRequest extends FormRequest // ID of the organization member that the time entry should belong to 'changes.member_id' => [ 'string', - 'uuid', - new ExistsEloquent(Member::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // ID of the project that the time entry should belong to 'changes.project_id' => [ 'nullable', 'string', - 'uuid', 'required_with:task_id', - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // ID of the task that the time entry should belong to 'changes.task_id' => [ 'nullable', 'string', - 'uuid', - new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), - (new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { + })->uuid(), + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization') ->where('project_id', $this->input('changes.project_id')); - }))->withMessage(__('validation.task_belongs_to_project')), + })->uuid()->withMessage(__('validation.task_belongs_to_project')), ], // Whether time entry is billable 'changes.billable' => [ @@ -91,11 +88,10 @@ class TimeEntryUpdateMultipleRequest extends FormRequest ], 'changes.tags.*' => [ 'string', - 'uuid', - new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], ]; } diff --git a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php index 8aa821a1..7a703af7 100644 --- a/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php +++ b/app/Http/Requests/V1/TimeEntry/TimeEntryUpdateRequest.php @@ -30,37 +30,34 @@ class TimeEntryUpdateRequest extends FormRequest // ID of the organization member that the time entry should belong to 'member_id' => [ 'string', - 'uuid', - new ExistsEloquent(Member::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // ID of the project that the time entry should belong to 'project_id' => [ 'nullable', 'string', - 'uuid', 'required_with:task_id', - new ExistsEloquent(Project::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], // ID of the task that the time entry should belong to 'task_id' => [ 'nullable', 'string', - 'uuid', - new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), - (new ExistsEloquent(Task::class, null, function (Builder $builder): Builder { + })->uuid(), + ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization') ->where('project_id', $this->input('project_id')); - }))->withMessage(__('validation.task_belongs_to_project')), + })->uuid()->withMessage(__('validation.task_belongs_to_project')), ], // Start of time entry (ISO 8601 format, UTC timezone) 'start' => [ @@ -89,11 +86,10 @@ class TimeEntryUpdateRequest extends FormRequest ], 'tags.*' => [ 'string', - 'uuid', - new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder { + ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder { /** @var Builder $builder */ return $builder->whereBelongsTo($this->organization, 'organization'); - }), + })->uuid(), ], ]; } diff --git a/app/Service/Import/Importers/DefaultImporter.php b/app/Service/Import/Importers/DefaultImporter.php index f525abe1..195a42ed 100644 --- a/app/Service/Import/Importers/DefaultImporter.php +++ b/app/Service/Import/Importers/DefaultImporter.php @@ -112,6 +112,7 @@ abstract class DefaultImporter implements ImporterContract 'billable_rate' => [ 'nullable', 'integer', + 'max:2147483647', ], ], beforeSave: function (Project $project): void { if ($project->billable_rate === 0) { @@ -125,6 +126,7 @@ abstract class DefaultImporter implements ImporterContract 'billable_rate' => [ 'nullable', 'integer', + 'max:2147483647', ], ], beforeSave: function (ProjectMember $projectMember): void { if ($projectMember->billable_rate === 0) { diff --git a/composer.lock b/composer.lock index a6ec0f2e..022cc1f8 100644 --- a/composer.lock +++ b/composer.lock @@ -3480,16 +3480,16 @@ }, { "name": "korridor/laravel-model-validation-rules", - "version": "3.1.0", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/korridor/laravel-model-validation-rules.git", - "reference": "49370c70cc2b25d1b80252160ba3ab435828c95a" + "reference": "e7c27b976eefd383de10a5195e8c986815ac43a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/korridor/laravel-model-validation-rules/zipball/49370c70cc2b25d1b80252160ba3ab435828c95a", - "reference": "49370c70cc2b25d1b80252160ba3ab435828c95a", + "url": "https://api.github.com/repos/korridor/laravel-model-validation-rules/zipball/e7c27b976eefd383de10a5195e8c986815ac43a4", + "reference": "e7c27b976eefd383de10a5195e8c986815ac43a4", "shasum": "" }, "require": { @@ -3538,9 +3538,9 @@ ], "support": { "issues": "https://github.com/korridor/laravel-model-validation-rules/issues", - "source": "https://github.com/korridor/laravel-model-validation-rules/tree/3.1.0" + "source": "https://github.com/korridor/laravel-model-validation-rules/tree/3.2.0" }, - "time": "2024-02-28T15:07:15+00:00" + "time": "2024-10-14T16:58:38+00:00" }, { "name": "lab404/laravel-impersonate", diff --git a/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php b/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php index 072c25a4..cde9112a 100644 --- a/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/ProjectEndpointTest.php @@ -284,6 +284,60 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract $response->assertForbidden(); } + public function test_store_endpoint_highest_possible_billable_rate_can_be_stored_in_database(): void + { + // Arrange + $billableRate = 2147483647; + $data = $this->createUserWithPermission([ + 'projects:create', + ]); + $projectFake = Project::factory()->forOrganization($data->organization)->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [ + 'name' => $projectFake->name, + 'color' => $projectFake->color, + 'is_billable' => $projectFake->is_billable, + 'billable_rate' => $billableRate, + ]); + + // Assert + $response->assertStatus(201); + $this->assertDatabaseHas(Project::class, [ + 'name' => $projectFake->name, + 'color' => $projectFake->color, + 'organization_id' => $projectFake->organization_id, + 'is_billable' => $projectFake->is_billable, + 'billable_rate' => $billableRate, + ]); + } + + public function test_store_endpoint_fails_if_billable_rate_is_too_high(): void + { + // Arrange + $billableRate = 2147483647 + 1; + $data = $this->createUserWithPermission([ + 'projects:create', + ]); + $projectFake = Project::factory()->forOrganization($data->organization)->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->postJson(route('api.v1.projects.store', [$data->organization->getKey()]), [ + 'name' => $projectFake->name, + 'color' => $projectFake->color, + 'is_billable' => $projectFake->is_billable, + 'billable_rate' => $billableRate, + ]); + + // Assert + $response->assertStatus(422); + $response->assertJsonValidationErrors([ + 'billable_rate' => 'The billable rate field must not be greater than 2147483647.', + ]); + } + public function test_store_endpoint_creates_new_project(): void { // Arrange diff --git a/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php b/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php index 6209a797..0d0c53f9 100644 --- a/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php +++ b/tests/Unit/Endpoint/Api/V1/TimeEntryEndpointTest.php @@ -958,6 +958,27 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract ]); } + public function test_store_endpoint_fails_gracefully_if_non_uuid_text_is_in_uuid_validated_field_in_body(): void + { + // Arrange + $data = $this->createUserWithPermission([ + 'time-entries:create:own', + ]); + $timeEntryFake = TimeEntry::factory()->withTask($data->organization)->forOrganization($data->organization)->make(); + Passport::actingAs($data->user); + + // Act + $response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [ + 'member_id' => 'non-uuid-text', + 'tags' => ['non-uuid-text', 1], + 'project_id' => 'non-uuid-text', + 'task_id' => 'non-uuid-text', + ]); + + // Assert + $response->assertStatus(422); + } + public function test_store_endpoints_sets_billable_rate(): void { // Arrange