Skip to content

Commit

Permalink
Added user and organization deletion system; Added coverage annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
korridor committed Jun 9, 2024
1 parent 8857bef commit 86f5ea4
Show file tree
Hide file tree
Showing 65 changed files with 2,651 additions and 135 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ APP_URL=https://solidtime.test

SUPER_ADMINS=[email protected]

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_CHANNEL=single
LOG_DEPRECATIONS_CHANNEL=deprecation
LOG_LEVEL=debug

DB_CONNECTION=pgsql
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ yarn-error.log
/k8s
/_ide_helper.php
/.phpstorm.meta.php
/.rnd
5 changes: 3 additions & 2 deletions app/Actions/Jetstream/DeleteOrganization.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
namespace App\Actions\Jetstream;

use App\Models\Organization;
use App\Service\DeletionService;
use Laravel\Jetstream\Contracts\DeletesTeams;

class DeleteOrganization implements DeletesTeams
{
/**
* Delete the given team.
*/
public function delete(Organization $team): void
public function delete(Organization $organization): void
{
$team->purge();
app(DeletionService::class)->deleteOrganization($organization);
}
}
46 changes: 10 additions & 36 deletions app/Actions/Jetstream/DeleteUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,51 +4,25 @@

namespace App\Actions\Jetstream;

use App\Models\Organization;
use App\Exceptions\Api\ApiException;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Laravel\Jetstream\Contracts\DeletesTeams;
use App\Service\DeletionService;
use Illuminate\Validation\ValidationException;
use Laravel\Jetstream\Contracts\DeletesUsers;

class DeleteUser implements DeletesUsers
{
/**
* The team deleter implementation.
*
* @var \Laravel\Jetstream\Contracts\DeletesTeams
*/
protected $deletesTeams;

/**
* Create a new action instance.
*/
public function __construct(DeletesTeams $deletesTeams)
{
$this->deletesTeams = $deletesTeams;
}

/**
* Delete the given user.
*/
public function delete(User $user): void
{
DB::transaction(function () use ($user) {
$this->deleteTeams($user);
$user->deleteProfilePhoto();
$user->tokens->each->delete();
$user->delete();
});
}

/**
* Delete the teams and team associations attached to the user.
*/
protected function deleteTeams(User $user): void
{
$user->teams()->detach();

$user->ownedTeams->each(function (Organization $team) {
$this->deletesTeams->delete($team);
});
try {
app(DeletionService::class)->deleteUser($user);
} catch (ApiException $exception) {
throw ValidationException::withMessages([
'password' => $exception->getTranslatedMessage(),
]);
}
}
}
28 changes: 28 additions & 0 deletions app/Actions/Jetstream/ValidateOrganizationDeletion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace App\Actions\Jetstream;

use App\Models\Organization;
use App\Models\User;
use App\Service\PermissionStore;
use Illuminate\Auth\Access\AuthorizationException;

class ValidateOrganizationDeletion
{
/**
* Validate that the team can be deleted by the given user.
*
* @param User $user Authenticated user
* @param Organization $organization Organization to be deleted
*
* @throws AuthorizationException
*/
public function validate(User $user, Organization $organization): void
{
if (! app(PermissionStore::class)->userHas($organization, $user, 'organizations:delete')) {
throw new AuthorizationException();
}
}
}
59 changes: 59 additions & 0 deletions app/Console/Commands/Admin/DeleteOrganizationCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace App\Console\Commands\Admin;

use App\Models\Organization;
use App\Service\DeletionService;
use Illuminate\Console\Command;
use Illuminate\Support\Str;

class DeleteOrganizationCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'admin:delete-organization
{ organization : The ID of the organization to delete }';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete a organization.';

/**
* Execute the console command.
*/
public function handle(DeletionService $deletionService): int
{
$organizationId = $this->argument('organization');

if (! Str::isUuid($organizationId)) {
$this->error('Organization ID must be a valid UUID.');

return self::FAILURE;

}

/** @var Organization|null $organization */
$organization = Organization::find($organizationId);
if ($organization === null) {
$this->error('Organization with ID '.$organizationId.' not found.');

return self::FAILURE;
}

$this->info('Deleting organization with ID '.$organization->getKey());

$deletionService->deleteOrganization($organization);

$this->info('Organization with ID '.$organization->getKey().' has been deleted.');

return self::SUCCESS;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Illuminate\Support\Str;
use phpseclib3\Crypt\RSA;

class SelfHostGenerateKeys extends Command
class SelfHostGenerateKeysCommand extends Command
{
/**
* The name and signature of the console command.
Expand Down
20 changes: 20 additions & 0 deletions app/Events/BeforeOrganizationDeletion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Events;

use App\Models\Organization;
use Illuminate\Foundation\Events\Dispatchable;

class BeforeOrganizationDeletion
{
use Dispatchable;

public Organization $organization;

public function __construct(Organization $organization)
{
$this->organization = $organization;
}
}
5 changes: 5 additions & 0 deletions app/Exceptions/Api/ApiException.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ abstract class ApiException extends Exception
{
public const string KEY = 'api_exception';

public function __construct()
{
parent::__construct(static::KEY);
}

/**
* Render the exception into an HTTP response.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace App\Exceptions\Api;

class CanNotDeleteUserWhoIsOwnerOfOrganizationWithMultipleMembers extends ApiException
{
public const string KEY = 'can_not_delete_user_who_is_owner_of_organization_with_multiple_members';
}
2 changes: 1 addition & 1 deletion app/Exceptions/Api/EntityStillInUseApiException.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class EntityStillInUseApiException extends ApiException

public function __construct(string $modelToDelete, string $modelInUse)
{
parent::__construct('', 0, null);
parent::__construct();
$this->modelToDelete = $modelToDelete;
$this->modelInUse = $modelInUse;
}
Expand Down
1 change: 0 additions & 1 deletion app/Filament/Resources/OrganizationResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ public static function table(Table $table): Table
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\OrganizationResource\Actions;

use App\Exceptions\Api\ApiException;
use App\Models\Organization;
use App\Service\DeletionService;
use Filament\Actions\DeleteAction;
use Throwable;

class DeleteOrganization extends DeleteAction
{
protected function setUp(): void
{
parent::setUp();
// TODO: check why setting the icon is necessary
$this->icon('heroicon-m-trash');
$this->action(function (): void {
$result = $this->process(function (Organization $record): bool {
try {
$deletionService = app(DeletionService::class);
$deletionService->deleteOrganization($record);

return true;
} catch (ApiException $exception) {
$this->failureNotificationTitle($exception->getTranslatedMessage());
report($exception);
} catch (Throwable $exception) {
$this->failureNotificationTitle(__('exceptions.unknown_error_in_admin_panel'));
report($exception);
}

return false;
});

if (! $result) {
$this->failure();

return;
}

$this->success();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace App\Filament\Resources\OrganizationResource\Pages;

use App\Filament\Resources\OrganizationResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;

class EditOrganization extends EditRecord
Expand All @@ -15,7 +14,7 @@ class EditOrganization extends EditRecord
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
OrganizationResource\Actions\DeleteOrganization::make(),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace App\Filament\Resources\OrganizationResource\Pages;

use App\Filament\Resources\OrganizationResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;

Expand All @@ -18,8 +17,6 @@ protected function getHeaderActions(): array
return [
EditAction::make('edit')
->icon('heroicon-s-pencil'),
DeleteAction::make('delete')
->icon('heroicon-s-trash'),
];
}
}
46 changes: 46 additions & 0 deletions app/Filament/Resources/UserResource/Actions/DeleteUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\UserResource\Actions;

use App\Exceptions\Api\ApiException;
use App\Models\User;
use App\Service\DeletionService;
use Filament\Actions\DeleteAction;
use Throwable;

class DeleteUser extends DeleteAction
{
protected function setUp(): void
{
parent::setUp();
$this->icon('heroicon-m-trash');
$this->action(function (): void {
$result = $this->process(function (User $record): bool {
try {
$deletionService = app(DeletionService::class);
$deletionService->deleteUser($record);

return true;
} catch (ApiException $exception) {
$this->failureNotificationTitle($exception->getTranslatedMessage());
report($exception);
} catch (Throwable $exception) {
$this->failureNotificationTitle(__('exceptions.unknown_error_in_admin_panel'));
report($exception);
}

return false;
});

if (! $result) {
$this->failure();

return;
}

$this->success();
});
}
}
Loading

0 comments on commit 86f5ea4

Please sign in to comment.