Skip to content

Commit 47aa65d

Browse files
committed
Add checks for placeholder invitation; Fixed bug in member deletion
1 parent b0e638c commit 47aa65d

File tree

11 files changed

+186
-16
lines changed

11 files changed

+186
-16
lines changed

app/Console/Kernel.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ protected function schedule(Schedule $schedule): void
2828

2929
$schedule->command('self-host:database-consistency')
3030
->when(fn (): bool => config('scheduling.tasks.self_hosting_database_consistency'))
31-
->twiceDaily();
31+
->everySixHours();
3232
}
3333

3434
/**
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Exceptions\Api;
6+
7+
class InvitationForTheEmailAlreadyExistsApiException extends ApiException
8+
{
9+
public const string KEY = 'invitation_for_the_email_already_exists';
10+
}

app/Http/Controllers/Api/V1/InvitationController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace App\Http\Controllers\Api\V1;
66

7+
use App\Exceptions\Api\InvitationForTheEmailAlreadyExistsApiException;
78
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
89
use App\Http\Requests\V1\Invitation\InvitationIndexRequest;
910
use App\Http\Requests\V1\Invitation\InvitationStoreRequest;
@@ -50,6 +51,7 @@ public function index(Organization $organization, InvitationIndexRequest $reques
5051
*
5152
* @throws AuthorizationException
5253
* @throws UserIsAlreadyMemberOfOrganizationApiException
54+
* @throws InvitationForTheEmailAlreadyExistsApiException
5355
*
5456
* @operationId invite
5557
*/

app/Http/Controllers/Api/V1/MemberController.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use App\Exceptions\Api\ChangingRoleOfPlaceholderIsNotAllowed;
1111
use App\Exceptions\Api\ChangingRoleToPlaceholderIsNotAllowed;
1212
use App\Exceptions\Api\EntityStillInUseApiException;
13+
use App\Exceptions\Api\InvitationForTheEmailAlreadyExistsApiException;
1314
use App\Exceptions\Api\OnlyOwnerCanChangeOwnership;
1415
use App\Exceptions\Api\OnlyPlaceholdersCanBeMergedIntoAnotherMember;
1516
use App\Exceptions\Api\OrganizationNeedsAtLeastOneOwner;
@@ -173,6 +174,7 @@ public function mergeInto(Organization $organization, Member $member, MemberMerg
173174
* @throws UserNotPlaceholderApiException
174175
* @throws UserIsAlreadyMemberOfOrganizationApiException
175176
* @throws ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException
177+
* @throws InvitationForTheEmailAlreadyExistsApiException
176178
*
177179
* @operationId invitePlaceholder
178180
*/

app/Http/Controllers/Controller.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ protected function member(Organization $organization): Member
4343
/** @var Member|null $member */
4444
$member = Member::query()->whereBelongsTo($organization, 'organization')->whereBelongsTo($user, 'user')->first();
4545
if ($member === null) {
46-
Log::error('This function should only be called in authenticated context after checking the user is a member of the organization');
46+
Log::error('This function should only be called in authenticated context after checking the user is a member of the organization', [
47+
'user' => $user->getKey(),
48+
'organization' => $organization->getKey(),
49+
]);
4750
throw new AuthorizationException;
4851
}
4952

app/Http/Requests/V1/Invitation/InvitationStoreRequest.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,8 @@
77
use App\Enums\Role;
88
use App\Http\Requests\V1\BaseFormRequest;
99
use App\Models\Organization;
10-
use App\Models\OrganizationInvitation;
1110
use Illuminate\Contracts\Validation\ValidationRule;
12-
use Illuminate\Database\Eloquent\Builder;
1311
use Illuminate\Validation\Rule;
14-
use Korridor\LaravelModelValidationRules\Rules\UniqueEloquent;
1512

1613
/**
1714
* @property Organization $organization
@@ -29,10 +26,6 @@ public function rules(): array
2926
'email' => [
3027
'required',
3128
'email',
32-
UniqueEloquent::make(OrganizationInvitation::class, 'email', function (Builder $builder): Builder {
33-
/** @var Builder<OrganizationInvitation> $builder */
34-
return $builder->whereBelongsTo($this->organization, 'organization');
35-
})->withCustomTranslation('validation.invitation_already_exists'),
3629
],
3730
'role' => [
3831
'required',

app/Service/InvitationService.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace App\Service;
66

77
use App\Enums\Role;
8+
use App\Exceptions\Api\InvitationForTheEmailAlreadyExistsApiException;
89
use App\Exceptions\Api\UserIsAlreadyMemberOfOrganizationApiException;
910
use App\Mail\OrganizationInvitationMail;
1011
use App\Models\Member;
@@ -16,7 +17,7 @@
1617
class InvitationService
1718
{
1819
/**
19-
* @throws UserIsAlreadyMemberOfOrganizationApiException
20+
* @throws UserIsAlreadyMemberOfOrganizationApiException|InvitationForTheEmailAlreadyExistsApiException
2021
*/
2122
public function inviteUser(Organization $organization, string $email, Role $role): OrganizationInvitation
2223
{
@@ -28,6 +29,13 @@ public function inviteUser(Organization $organization, string $email, Role $role
2829
throw new UserIsAlreadyMemberOfOrganizationApiException;
2930
}
3031

32+
if (OrganizationInvitation::query()
33+
->where('email', $email)
34+
->whereBelongsTo($organization, 'organization')
35+
->exists()) {
36+
throw new InvitationForTheEmailAlreadyExistsApiException;
37+
}
38+
3139
InvitingTeamMember::dispatch($organization, $email, $role->value);
3240

3341
$invitation = new OrganizationInvitation;

app/Service/MemberService.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ public function removeMember(Member $member, Organization $organization, bool $w
6767
throw new CanNotRemoveOwnerFromOrganization;
6868
}
6969

70+
$user = $member->user;
71+
$isPlaceholder = $user->is_placeholder;
72+
73+
if (! $isPlaceholder && $user->current_team_id === $member->organization_id) {
74+
$user->currentTeam()->disassociate();
75+
$user->save();
76+
}
77+
7078
if ($withRelations) {
7179
TimeEntry::query()->where('user_id', $member->user_id)->whereBelongsTo($organization, 'organization')->delete();
7280
ProjectMember::query()->whereBelongsToOrganization($organization)->where('user_id', $member->user_id)->delete();
@@ -80,6 +88,14 @@ public function removeMember(Member $member, Organization $organization, bool $w
8088
}
8189

8290
$member->delete();
91+
92+
if ($isPlaceholder) {
93+
$user->delete();
94+
} else {
95+
$this->userService->makeSureUserHasAtLeastOneOrganization($user);
96+
$this->userService->makeSureUserHasCurrentOrganization($user);
97+
}
98+
8399
MemberRemoved::dispatch($member, $organization);
84100
}
85101

lang/en/exceptions.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Exceptions\Api\EntityStillInUseApiException;
1010
use App\Exceptions\Api\FeatureIsNotAvailableInFreePlanApiException;
1111
use App\Exceptions\Api\InactiveUserCanNotBeUsedApiException;
12+
use App\Exceptions\Api\InvitationForTheEmailAlreadyExistsApiException;
1213
use App\Exceptions\Api\OnlyOwnerCanChangeOwnership;
1314
use App\Exceptions\Api\OnlyPlaceholdersCanBeMergedIntoAnotherMember;
1415
use App\Exceptions\Api\OrganizationHasNoSubscriptionButMultipleMembersException;
@@ -45,6 +46,7 @@
4546
ChangingRoleOfPlaceholderIsNotAllowed::KEY => 'Changing role of placeholder is not allowed',
4647
OnlyPlaceholdersCanBeMergedIntoAnotherMember::KEY => 'Only placeholders can be merged into another member',
4748
ThisPlaceholderCanNotBeInvitedUseTheMergeToolInsteadException::KEY => 'This placeholder can not be invited use the merge tool instead',
49+
InvitationForTheEmailAlreadyExistsApiException::KEY => 'The email has already been invited to the organization. Please wait for the user to accept the invitation or resend the invitation email.',
4850
],
4951
'unknown_error_in_admin_panel' => 'An unknown error occurred. Please check the logs.',
5052
];

tests/Unit/Endpoint/Api/V1/InvitationEndpointTest.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,26 +129,31 @@ public function test_store_fails_if_user_invites_user_who_is_already_member_of_o
129129
$response->assertJsonPath('message', 'User is already a member of the organization');
130130
}
131131

132-
public function test_store_fails_if_user_invites_user_who_is_already_invited_to_organization(): void
132+
public function test_store_fails_if_an_invitation_with_the_same_email_already_exists(): void
133133
{
134134
// Arrange
135135
$data = $this->createUserWithPermission([
136136
'invitations:create',
137137
]);
138138
Passport::actingAs($data->user);
139-
$invitation = OrganizationInvitation::factory()->forOrganization($data->organization)->create();
139+
$email = '[email protected]';
140+
$invitation = OrganizationInvitation::factory()->forOrganization($data->organization)->create([
141+
'email' => $email,
142+
]);
140143

141144
// Act
142145
$response = $this->postJson(route('api.v1.invitations.store', $data->organization->getKey()), [
143-
'email' => $invitation->email,
146+
'email' => $email,
144147
'role' => Role::Employee->value,
145148
]);
146149

147150
// Assert
148-
$response->assertInvalid([
149-
'email' => 'The email has already been invited to the organization. Please wait for the user to accept the invitation or resend the invitation email.',
151+
$response->assertStatus(400);
152+
$response->assertExactJson([
153+
'error' => true,
154+
'key' => 'invitation_for_the_email_already_exists',
155+
'message' => 'The email has already been invited to the organization. Please wait for the user to accept the invitation or resend the invitation email.',
150156
]);
151-
$response->assertStatus(422);
152157
}
153158

154159
public function test_store_works_if_user_invites_user_who_is_also_a_placeholder(): void

0 commit comments

Comments
 (0)