Added user and organization deletion system; Added coverage annotations

This commit is contained in:
Constantin Graf
2024-06-07 19:17:40 +02:00
committed by Constantin Graf
parent 8857befc6c
commit 86f5ea47bb
65 changed files with 2651 additions and 135 deletions

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Enums\Role;
use App\Events\BeforeOrganizationDeletion;
use App\Exceptions\Api\CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers;
use App\Models\Client;
use App\Models\Member;
use App\Models\Organization;
use App\Models\OrganizationInvitation;
use App\Models\Project;
use App\Models\ProjectMember;
use App\Models\Tag;
use App\Models\Task;
use App\Models\TimeEntry;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class DeletionService
{
private UserService $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function deleteOrganization(Organization $organization, bool $inTransaction = true): void
{
if ($inTransaction) {
DB::transaction(function () use ($organization) {
$this->deleteOrganization($organization, false);
});
return;
}
Log::debug('Start deleting organization', [
'organization_id' => $organization->getKey(),
'name' => $organization->name,
'owner_id' => $organization->user_id,
]);
BeforeOrganizationDeletion::dispatch($organization);
// Delete all organization invitations
OrganizationInvitation::query()->whereBelongsTo($organization, 'organization')->delete();
// Delete all time entries
TimeEntry::query()->whereBelongsTo($organization, 'organization')->delete();
// Delete all tags
Tag::query()->whereBelongsTo($organization, 'organization')->delete();
// Delete all tasks
Task::query()->whereBelongsTo($organization, 'organization')->delete();
// Delete all project members
ProjectMember::query()->whereBelongsToOrganization($organization)->delete();
// Delete all projects
Project::query()->whereBelongsTo($organization, 'organization')->delete();
// Delete all clients
Client::query()->whereBelongsTo($organization, 'organization')->delete();
// Reset the current organization
$organization->owner()
->where('current_team_id', $organization->getKey())
->update(['current_team_id' => null]);
$organization->users()
->where('current_team_id', $organization->getKey())
->update(['current_team_id' => null]);
// Delete all members
$users = $organization->users()
->with([
'currentOrganization',
])
->get();
$organization->users()->sync([]);
// Make sure all users have at least one organization
foreach ($users as $user) {
if ($user->is_placeholder) {
$user->delete();
} else {
$this->userService->makeSureUserHasAtLeastOneOrganization($user);
$this->userService->makeSureUserHasCurrentOrganization($user);
}
}
// Delete organization
$organization->delete();
Log::debug('Finished deleting organization', [
'organization_id' => $organization->getKey(),
'name' => $organization->name,
'owner_id' => $organization->user_id,
]);
}
/**
* @throws CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers
*/
public function deleteUser(User $user, bool $inTransaction = true): void
{
if ($inTransaction) {
DB::transaction(function () use ($user) {
$this->deleteUser($user, false);
});
return;
}
Log::debug('Start deleting user', [
'id' => $user->getKey(),
'name' => $user->name,
'email' => $user->email,
]);
$members = Member::query()->whereBelongsTo($user, 'user')
->with([
'organization',
'user',
])
->get();
foreach ($members as $member) {
if ($member->role === Role::Owner->value && $member->organization->users()->count() > 1) {
throw new CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers();
}
}
/** @var Member $member */
foreach ($members as $member) {
if ($member->role === Role::Owner->value) {
// Note: The member needs to be deleted first, otherwise the organization delete function will recreate a new personal organization for the user
$member->delete();
$this->deleteOrganization($member->organization, false);
} else {
$this->userService->makeMemberToPlaceholder($member);
}
}
// Note: Since the deletion of the profile photo is not reversible via a database rollback this needs to be done last
$user->deleteProfilePhoto();
$user->delete();
Log::debug('Finished deleting user', [
'id' => $user->getKey(),
'name' => $user->name,
'email' => $user->email,
]);
}
}

View File

@@ -30,6 +30,11 @@ class PermissionStore
return false;
}
return $this->userHas($organization, $user, $permission);
}
public function userHas(Organization $organization, User $user, string $permission): bool
{
if (! isset($this->permissionCache[$user->getKey().'|'.$organization->getKey()])) {
if (! $user->belongsToTeam($organization)) {
return false;

View File

@@ -28,6 +28,11 @@ class UserService
throw new \InvalidArgumentException('User is not a member of the organization');
}
$this->assignOrganizationEntitiesToDifferentMember($organization, $fromUser, $toUser, $toMember);
}
private function assignOrganizationEntitiesToDifferentMember(Organization $organization, User $fromUser, User $toUser, Member $toMember): void
{
// Time entries
TimeEntry::query()
->whereBelongsTo($organization, 'organization')
@@ -47,6 +52,53 @@ class UserService
]);
}
public function makeMemberToPlaceholder(Member $member): void
{
$user = $member->user;
$placeholderUser = $user->replicate();
$placeholderUser->is_placeholder = true;
$placeholderUser->save();
$member->user()->associate($placeholderUser);
$member->role = Role::Placeholder->value;
$member->save();
$this->assignOrganizationEntitiesToDifferentMember($member->organization, $user, $placeholderUser, $member);
$this->makeSureUserHasAtLeastOneOrganization($user);
}
public function makeSureUserHasAtLeastOneOrganization(User $user): void
{
if ($user->organizations()->count() > 0) {
return;
}
// Create a new organization
$organization = new Organization();
$organization->name = $user->name."'s Organization";
$organization->personal_team = true;
$organization->user_id = $user->id;
$organization->save();
// Attach the user to the organization
$organization->users()->attach($user, ['role' => Role::Owner->value]);
// Set the organization as the user's current organization
$user->currentOrganization()->associate($organization);
$user->save();
}
public function makeSureUserHasCurrentOrganization(User $user): void
{
if ($user->currentOrganization !== null) {
return;
}
$organization = $user->organizations()->first();
$user->currentOrganization()->associate($organization);
$user->save();
}
/**
* Change the ownership of an organization to a new user.
* The previous owner will be demoted to an admin.