fix(profile): member delete must not destroy the shared account (closes #50)
Members joining via workspace invite share the owner's account_id. ProfileController::destroy was unconditionally calling $account->delete() in every profile-deletion path, so any member could wipe the whole organization (cascade: workspaces, posts, social accounts, signatures, labels) just by clicking Delete on their own profile. Gate the account/subscription teardown behind isAccountOwner(). For members the path now only detaches them from workspaces and deletes the user row — owner's data is untouched. Tested in both SELF_HOSTED=true and false.
This commit is contained in:
parent
48bf94e137
commit
1660187067
2 changed files with 70 additions and 10 deletions
|
|
@ -94,15 +94,7 @@ public function destroy(ProfileDeleteRequest $request): RedirectResponse
|
|||
$user->update(['current_workspace_id' => null]);
|
||||
|
||||
$account = $user->account;
|
||||
|
||||
// Cancel account subscription if exists
|
||||
if ($account && $account->subscribed(Account::SUBSCRIPTION_NAME)) {
|
||||
$account->subscription(Account::SUBSCRIPTION_NAME)->cancelNow();
|
||||
}
|
||||
|
||||
if ($account) {
|
||||
$account->subscriptions()->delete();
|
||||
}
|
||||
$isOwner = $user->isAccountOwner();
|
||||
|
||||
$ownedWorkspaces = Workspace::where('user_id', $user->id)->get();
|
||||
|
||||
|
|
@ -126,7 +118,12 @@ public function destroy(ProfileDeleteRequest $request): RedirectResponse
|
|||
|
||||
$user->workspaces()->detach();
|
||||
|
||||
if ($account) {
|
||||
if ($account && $isOwner) {
|
||||
if ($account->subscribed(Account::SUBSCRIPTION_NAME)) {
|
||||
$account->subscription(Account::SUBSCRIPTION_NAME)->cancelNow();
|
||||
}
|
||||
|
||||
$account->subscriptions()->delete();
|
||||
$account->delete();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Models\Account;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
|
@ -250,3 +251,65 @@
|
|||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('member deleting profile does NOT destroy the shared account', function (bool $selfHosted) {
|
||||
config()->set('trypost.self_hosted', $selfHosted);
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$member = User::factory()->create(['account_id' => $owner->account_id]);
|
||||
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $owner->account_id,
|
||||
'user_id' => $owner->id,
|
||||
]);
|
||||
$owner->workspaces()->attach($workspace->id, ['role' => Role::Member->value]);
|
||||
$member->workspaces()->attach($workspace->id, ['role' => Role::Member->value]);
|
||||
|
||||
$this->actingAs($member)->delete(route('app.profile.destroy'), [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
expect($member->fresh())->toBeNull();
|
||||
expect(Account::find($owner->account_id))->not->toBeNull();
|
||||
expect(Workspace::find($workspace->id))->not->toBeNull();
|
||||
expect($owner->fresh())->not->toBeNull();
|
||||
})->with([true, false]);
|
||||
|
||||
test('member deleting profile detaches them from workspaces', function (bool $selfHosted) {
|
||||
config()->set('trypost.self_hosted', $selfHosted);
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$member = User::factory()->create(['account_id' => $owner->account_id]);
|
||||
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $owner->account_id,
|
||||
'user_id' => $owner->id,
|
||||
]);
|
||||
$member->workspaces()->attach($workspace->id, ['role' => Role::Member->value]);
|
||||
|
||||
$this->actingAs($member)->delete(route('app.profile.destroy'), [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
expect($workspace->fresh()->members()->where('users.id', $member->id)->exists())->toBeFalse();
|
||||
})->with([true, false]);
|
||||
|
||||
test('owner deleting profile destroys the account and cascades', function (bool $selfHosted) {
|
||||
config()->set('trypost.self_hosted', $selfHosted);
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$accountId = $owner->account_id;
|
||||
|
||||
$workspace = Workspace::factory()->create([
|
||||
'account_id' => $accountId,
|
||||
'user_id' => $owner->id,
|
||||
]);
|
||||
$owner->workspaces()->attach($workspace->id, ['role' => Role::Member->value]);
|
||||
|
||||
$this->actingAs($owner)->delete(route('app.profile.destroy'), [
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
expect(Account::find($accountId))->toBeNull();
|
||||
expect(Workspace::find($workspace->id))->toBeNull();
|
||||
})->with([true, false]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue